It's a great question. I'm not sure I have a general solution for this, but here are two ways I handle this.
1. When a modal has to be shown (and these can come at arbitrary times), I set the modal state as a child of whatever State is currently the leaf.
// This state shows a modal dialog
public BuyGriftCoinState(Singletons singletons, Action onExit) : base(singletons) {
this.onExit = onExit;
}
// The thing that triggers the modal, it can happen
// anywhere
private void OnBuyGriftCoin() {
State leafNode = this;
while (leafNode.Substate != null) {
leafNode = leafNode.Substate;
}
leafNode.SetSubstate(new BuyGriftCoinState(singletons, leafNode.ExitAllSubstates));}
This only works because the modal blocks out all other player interactions since it's a full screen overlay, so I know that nothing can happen until the modal state exists.
2. States that are very similar and used in more than two places. This is tricky because the states are SO close but also different enough where it can be hard to reuse.
It's possible to solve this, the key thing being that the State that is reused cannot make any assumptions about:
- what its parent state is
- what happens when it closes
To put it more clearly, the reusable state has N different configurations (based on where it's used). Likewise, there are N different things that may need to happen once the state finishes.
The responsibly for configuring the state and handling what happens once the state finishes gets pushed to the parent class. In a way this is similar to encapsulation in object oriented programming.
I configure the child using constructors. This isn't a great example, but here's a state that can get configured in two different ways, as it's used in two different places.
public NarrativeOverlayState(Singletons singletons, MarketingLevelGmt level, string analyticsName, Action onComplete) : base(singletons)
public NarrativeOverlayState(Singletons singletons, List<StoryGmt> storyGmts, string analyticsName, Action onComplete) : base(singletons)
The parent state is also responsible for handling when the state finishes; that's why I have callbacks in the constructor of the child state. Apart from ExitState() which cleans itself up, the child itself is not responsible for figuring out what is supposed to happen when it closes. This responsibility falls on the parent states, so they can handle it
Hope this makes sense!