I’ve been thinking about a good state design system lately and how to address common problems in other games.
The problem in most other games is that states are designed to be modular, but don’t turn out to be, both in functions and in data.
Functionally, a game may have an options state, and a game state, and may later decide to show the options state while running the game state. If this is not defined in advance, the likely fix is just to call the render function of the options state from the game state. It works, but it’s poor design that can easily cause bugs. For example, the render function may rely on data setup by the enter function of the state. If not now, then maybe later when someone adds it and didn’t know about that dependency.
The same holds true for data in non-obvious ways. When I was working for nFusion we had to access game-specific data from the main menu screen, in order to determine if the continue button should show up or not.
You could just make the data global, or essentially global, such as putting it in the “Game” or “Application” class. However, this is bad design. You want to make data accessible to exactly the classes that need it, no more and no less. One solution is to put the data into a higher level shared class. For example, at my job we have an AI class and various controllers that interact with it. CONTROLLER SPECIFIC data sometimes needs to be shared between controllers that were designed to be independent. This generally goes in the AI class, which is bad design. We now have this huge AI class with a ton of data members, half of which serve some special purpose.
The solution I came up with is pretty simple but I think solves these problems with a minimum of complexity:
The application class contains a state manager
The state manager contains a stack of states, which are instantiated or deleted as needed.
States are unique in the state manager. No more than one instance of a particular state can run at the same time
States are independent and contain implementation functions and capability functions.
States refer to each other through enumerations, rather than pointers.
When a state manager gets an event (keypress, render, update), it goes to the state manager, which then processes it to each state in reverse order. For each of these, a state can trap an event so it does not progress to higher level states.
Data that is shared between states is held in a reference counted pointer.
How this works is easiest to show in an example:
Game states, calls state manager to create the main menu
The main menu does not already exist. It is created and put on top of the stack.
Query, update, and render calls to go the state manager, which sends the events to the main menu, which handles the events.
The user presses options. The main menu state tells the state manager to create the options state. As it does not already exist, it is created and put on top of the stack.
Since events are processed in reverse order, the main menu state renders and updates first. However, the main menu state queries the state manager and finds it is not at the top of the stack. Therefore, it renders a dimmed version of the background. All events are passed through to the options screen except the keypress “alt-z” which plays a main menu specific easter egg sound.
The options screen updates as usual.
Now the user presses esc. The options screen interprets this as “Remove myself from the state manager” and the user is back at the main menu.
The user presses “Start game” which tells the state manager to remove the main menu from the stack and create the game state.
In the game state, the user now brings up customize ship menu (which was also available from the main menu). The game continues to run in the background fully, but keypresses and mouse presses are not processed. The user then brings up the options screen while the customize ship menu is open.
The customize ship menu is not designed to run unless it has primary focus (top of stack). Therefore, the allow render and allow update calls return false. The options screen shows up over the game screen. The user presses esc, and as before the options screen is removed. Since the customize ship menu is next on the stack, it shows up again and none of the user’s changes were lost.
The user presses esc again, and goes back to the game.
Shared data will be in singletons that are referenced counted and delete themselves when they are no longer used. States will get a pointer to the singleton when they are created, and release the pointer on free.