This varies wildly by game kind, but there are some standard patterns.
A game loop of any kind typically has three phases:
- Receive input (controller/keyboard/mouse/network)
- Update simulation
- Render/present/play sounds – this may in turn be two steps, “update animation state” and “draw stuff.”
Typically, there will be a stack of active … things, that receive these callbacks. Thus, you may have a main game mode, and then a character-equipment menu on top of that, and then an in-game settings menu on top of that. Each of them would receive each callback, but have the ability to mutate the arguments passed along to the next state. For example, an in-game menu might set the “time to advance physics by” to 0, to pause the game while in the menu. Also, it may consume and zero out any input events, so they only go to control the menu, not the underlying game.
Additionally, these receivers will typically want to know when they “become active” and “become inactive.”
So, the “major game state manager” API might look something like:
class IGameState {
public:
virtual void OnBecomeActive(IGameState *parent) = 0;
virtual void OnBecomeTopmost() = 0;
virtual void OnBecomeObscured() = 0;
virtual void OnBecomeInactive() = 0;
virtual void OnReceiveInput(KeyboardInput &kb, MouseInput &mi, ControllerInput &ci, NetworkInput &ni) = 0;
virtual void OnUpdateSimulation(Time &simDeltaTime) = 0;
virtual void OnUpdateAnimations(Time &animDeltaTime) = 0;
virtual void OnIssueRenderingCommands(Time renderDeltaTime) = 0;
virtual ~IGameState() {}
};
class GameStateStack {
public:
// pop each current state on the stack, calling its OnBecomeInactive(), then
// push this state at the bottom, calling its OnBecomeActive() with no parent.
// Then call OnBecomeTopmost() on it.
void SwitchToBaseState(IGameState *newBase);
// call OnBecomeObscured() on the current top.
// call OnBecomeActive() on this new state, passing the current head as parent,
// then call OnBecomeTopmost() on it.
void PushGameState(IGameState *newTop);
// call OnBecomeInactive() on the top state, and OnBecomeTopmost() on the state
// below it on the stack, which becomes the top.
void PopGameState();
// Call OnReceiveInput() on each state, starting at the topmost
void SendReceiveInput(KeyboardInput &kb, MouseInput &mi, ControllerInput &ci, NetworkInput &ni);
// Call OnUpdateSimulation() on each state, starting at the topmost
void SendUpdateSimulation(Time &simDeltaTime);
// Call OnUpdateAnimations() on each state, starting at the topmost
void SendUpdateAnimations(Time &animDeltaTime);
// Call OnIssueRenderingCommands() on each state, STARTING AT THE BOTTOM-MOST
void SendIssueRenderingCommands(Time renderDeltaTime);
std::vector<IGameState *> stateStack_;
};
Typically, you create one of each kind of screen ("main menu," “settings menu,” “matchmaking menu,” “main gameplay,” “character configuration,” …)
Then you'd push/pop the appropriate state at the appropriate time, and they'd each take care of doing the right thing. You might also want to add some asserts that a state that's already on the stack, can't be pushed on the stack again, and such.
When switching “major mode” (such as selecting “exit game” from the in-game pause menu,) you'll use SwitchToBaseState() to nuke all the states that are on the current stack, to jump straight to the new main state.
If you want fancy transitions with slide-in/slide-out, background asynchronous loading, and so on, you might want to make this API fancier; specifically, you might want a queue of states that are transitioning out, and they'd get a special “OnRenderTransitionOut()” callback until they're done. Transition-in could be handled by the main render function, or you could have a callback for that, too. All of those bits can be done many ways, and might not be necessary for Your First Game, though!