I'm not sure what sort of hierarchical FSM do you have in mind, so I'm sorry if this does not directly answer your question, but I'd really like to take the chance and add some input from my own experience using a stack based screen manager.
Since you need the ability to open a popup screen but still be able to see the screens below, having a stack based implementation will make implementing it pretty easy - you can just push the popup screen into the stack, and pop it again when you're done.
You can also easily wire it so that all screens on the stack will be drawn at all times (from top to bottom), but only the topmost screen will be updated. But here's a few extra tips that I find useful in my managers.
Tip 1 - Provide a Switch operation
Pushing a new state is useful, but I find that most of the time you won't be creating popup windows but rather transitioning between larger states. So I like to add a third operation besides the push and pop that allows me to swap states in one go. Something like:
Push(State)
- Pushes a new state into the stack.
Switch(State)
- Replaces the current topmost state in the stack with a new one.
Pop(Amount)
- Removes the N topmost states from the stack.
Tip 2 - Registering states and addressing them by ID
I've seen some state manager implementations that expect you to pass the actual state object when pushing a new state. This means that you will either be using it as Push(new TitleScreen())
and creating a new screen object everytime, or you will have to create and store references to your screens and reuse those references.
Usually I don't want to recreate the same state everytime, but I don't want to have to manage the references myself either, so I always add a a method such as Register(Name, State)
to my state manager which stores the states in a Dictionary
and then I do all push or switch operations using the state name instead (e.g. Push("PauseScreen")
instead of Push(new PauseScreen()
).
I tend to create and register all my states at once when the application starts, although most of them hold little state until they are actually added to the stack.
Tip 3 - Transition callbacks
It's also useful to add a few callbacks to your states to be called whenever the state is pushed or popped. For instance, I use these four callbacks:
OnEnter
- Called when the state is pushed into the stack.
OnExit
- Called when the state is removed from the stack.
OnSuspend
- Called when a new state is pushed on top of the current one.
OnWakeup
- Called when the state on top of this one is popped.
I tend to put little or no processing in my states' constructors, and leave all the actual initialization to the OnEnter
method. This way I can reuse my state object multiple times without having to create a new one.
You can also define a duration for your transitions, so that when you push or pop a state, the operation does not take place automatically, but only after a small transitioning phase, which you can use for fading or other animations.
Tip 4 - Delayed operations
Pushing or popping states right in the middle of the update phase can have unpredictable results. In order to address this, I delay my operations and queue them to be executed at the end of the update phase instead.
So when I issue a push/switch/pop operation I simply instantiate a new struct object with the parameters and type of operation that was performed and add it to a list.
Then after all states have been updates, I go over the list and apply any operation that have been queued. I use something like this inside my screen manager class:
private enum ScreenManagerCommandType
{
Switch,
Push,
Pop
}
private struct ScreenManagerCommand
{
public readonly ScreenManagerCommandType Type;
public readonly Screen Screen;
public ScreenManagerCommand(ScreenManagerCommandType type, Screen screen)
{
Type = type;
Screen = screen;
}
}