Not too long ago I created my own first game from scratch in pure C. I struggled most with program design. I'd like to share what I've learned about a proper game state manager.
A game state manager is a method of creating a highly organized main game loop, which is generalized to the point that it can apply to any situation without pre-runtime modification of code. The game should always be in a state during runtime. That state could be the main menu, a pause screen, or a specific level or scenario. A state is divided up into multiple categories, and these categories are generalized enough to apply to every state the game can be in.
The idea is to use function pointers, and a specific set of them. There are six different functions to know:
- Load
- Initialize
- Update
- Draw
- Free
- Unload
These are the six functions that will make up your main loop, and once constructed your main loop should need little to no modification throughout the development of your game. Each function represents a category of functionality during a state. Every state consists of these categories. Each different state will have six functions designed for each of the function pointers in the main loop to point to them. This way your main loop will simply point to different states with its six pointers whenever you want to switch from one state to another.
The load function takes care of loading all of a state's necessary data. Load should be called only once per state -not even if the state restarts. Load also initializes the loaded data. This function is called first when a state starts.
The initialize function prepares the state's data to be used initially. It should not load any data, only prepare it. This allows a fast restart of a state in the event a restart is required -no loading or unloading will be involved in a state restart.
The update function uses a change in time (dt) to hand off to necessary functions to update the game in realtime. dt is defined as the time elapsed since the last call to update. Input should be gathered once per update call before calculations are made. All gameplay logic should happen in this state, and all live objects should be updated here.
The draw function renders all required images onto the screen, and additionally plays any desired sound effects. A well organized program will send data off to a graphics manager in this state, allowing further decoupling of major system and logic components.
The free function is what frees any objects or data no longer required, and sets the state up for switching or restarting. No data is unloaded (image sources, sound sources, meshes, etc). The idea is to set everything up to be initialized cleanly again.
The unload function is called during state termination, and unloads all data loaded in the load state. Here is an example of a properly set up game flow of a main loop:
By analyzing the setup of the above game flow you should be able to see how it works. To change a state, you simply modify the global variables currentState and nextState. previousState is then kept on-hand automatically. GSM_Update is responsible for updating the function pointers Load, Initialize, Update, Draw, Free and Unload whenever a state is started. In the event the global variable currentState changes, these function pointers will then change to their appropriate values via a switch statement. This switch statement lies within the GSM_Update function. The switch runs on the value of currentState, and once it finds a match it assigns the function pointers in the main loop to the appropriate matching state. Here is an example of a GSM_Update function:
Initialize system components
GSM_Initialize( firstState )
while not quitting
if currentState is quitting
nextState is Quit
if currentState is restart
currentState is previousState
nextState is previousState
else
GSM_Update( )
Load( )
Initialize( )
while currentState is nextState
Update
gather input
get dt
update game logic
Draw( )
Free( )
if nextState is Restart
previousState is currentState
currentState is nextState
else
Unload( )
previousState is currentState
currentState is nextState
Unload
By analyzing the setup of the above game flow you should be able to see how it works. To change a state, you simply modify the global variables currentState and nextState. previousState is then kept on-hand automatically. GSM_Update is responsible for updating the function pointers Load, Initialize, Update, Draw, Free and Unload whenever a state is started. In the event the global variable currentState changes, these function pointers will then change to their appropriate values via a switch statement. This switch statement lies within the GSM_Update function. The switch runs on the value of currentState, and once it finds a match it assigns the function pointers in the main loop to the appropriate matching state. Here is an example of a GSM_Update function:
// Update the Game State Manager by syncing the three state indicators to their
// corresponding function pointers (all six of them).
int GSM_Update( void )
{
switch(currentState)
{
case Level_1:
Load = &Level1_Load;
Initialize = &Level1_Initialize;
Update = &Level1_Update;
Draw = &Level1_Draw;
Free = &Level1_Free;
Unload = &Level1_Unload;
break;
case Level_2:
Load = &Level2_Load;
Initialize = &Level2_Initialize;
Update = &Level2_Update;
Draw = &Level2_Draw;
Free = &Level2_Free;
Unload = &Level2_Unload;
break;
case MapEditor:
Load = &MapEditor_Load;
Initialize = &MapEditor_Initialize;
Update = &MapEditor_Update;
Draw = &MapEditor_Draw;
Free = &MapEditor_Free;
Unload = &MapEditor_Unload;
break;
case Presentation:
Load = &PresentationLoad;
Initialize = &PresentationInitialize;
Update = &PresentationUpdate;
Draw = &PresentationDraw;
Free = &PresentationFree;
Unload = &PresentationUnload;
break;
/*case Template:
Load = &Load;
Initialize = &Initialize;
Update = &Update;
Draw = &Draw;
Free = &Free;
Unload = &Unload;
break;*/
case Restart:
break;
case Quit:
break;
}
return RETURN_SUCCESS;
}
And there you have it; a proper organizational setup to allow an excellent method of managing game states. Using this sort of organization allows for each state to have a universal format which allows for modification of states, additions of states, and deletions of states during development to be very easy and time-efficient.
You could put your function pointers for each state in a struct and put all states in an an array and use it as a look up table. No more switch statement and much easier to maintain!
ReplyDeleteYeah you definitely could! Good suggestion, thanks.
ReplyDelete