Sol's Graphics for Beginners

(ch28src.zip)

(prebuilt win32 exe)

28 - Game States

What we've been playing with so far is what I've learned to call the 'ingame'.

A finished game application consists of much more than just the 'ingame' - there's menus, loading screens, spash screens, pause screens, etc. The whole game application can be seen as a finite state machine, where one state might be the main menu, another might be the loading screen, and yet another would be the ingame.

At the moment, our game's state graph looks something like this:

That is, we have one state, and we can exit the state in two ways: enter the same state in the same level, or in the next level.

Once we're done with this chapter, the state graph will look like this:

What's boring about this is that the game will play exactly the same way as before, but the mechanism we're building here will be used a lot in the coming chapters.

Feel free to make a copy of the project, or continue from the last one.

Open gp.h, and add the following enumeration:

// Game states
enum gamestates
{
  STATE_NONE,
  STATE_ENTRY,
  STATE_READY,
  STATE_INGAME,
  STATE_FALLOFF,
  STATE_ENDLEVEL
};

We'll also need a couple of new variables. Add the following to main.cpp and gp.h (with the extern prefixes, as always):

// Scheduled game state
int gNextState;
// Current game state
int gGameState;

In main.cpp, go to init(), and set the states to their initial states:

gGameState = STATE_NONE;
gNextState = STATE_ENTRY;

I'll explain the 'none' state in a bit. The render() function (still in main.cpp) changes to:

void render()
{
  while (gNextState != gGameState)
    changestate();
  switch (gGameState)
  {
  case STATE_ENTRY:
  case STATE_READY:
  case STATE_INGAME:
  case STATE_FALLOFF:
  case STATE_ENDLEVEL:
    rendergame();
    break;
  }
}

First we're checking if we should change state, and we're calling the 'changestate' function to perform the change. Since the state change may change states, we're doing this until we're not changing states anymore.

Then, we're checking the current state and calling the appropriate rendering function. So far, all of our states are rendered with the same function. The 'none' state does not have a renderer.

Paste the following above the render() function:

void changestate()
{
  // Things that need to be done when leaving a state
  switch (gGameState)
  {
  case STATE_NONE: 
    break;
  }

  // Switch state
  gGameState = gNextState;

  // Things that need to be done when entering a state
  switch (gGameState)
  {
  case STATE_NONE: 
      break;
  }
}

Let's consider a splash screen. You know, those still screens in the beginning of most games these days, that display the publishers logos and stuff. We could implement one, allocate its data when entering the hypothetical PUBLISHERSPLASH state, and discard the data when leaving the state.

Let's fill out some of the easy states. Entry always goes to ready, ready always goes to game. Since we're not doing much in these states right now, we'll just make them schedule the next states. We can also set the falloff and endlevel to change to the entry state. Change the lower switch-structure to:

// Things that need to be done when entering a state
switch (gGameState)
{
case STATE_ENTRY: 
  gNextState = STATE_READY;
  break;
case STATE_READY:
  gNextState = STATE_INGAME;
  break;
case STATE_FALLOFF:
  gNextState = STATE_ENTRY;
  break;
case STATE_ENDLEVEL:
  gNextState = STATE_ENTRY;
  break;
}

We know that we'll be calling reset() when we're entering the ENTRY state, so add the call:

case STATE_ENTRY: 
  gNextState = STATE_READY;
  reset();
  break;

In game.cpp, we have three calls to the reset() function. One is when the player hits a LEVEL_DROP tile, another on LEVEL_END tile, and finally when player hits the level's borders.

Change the level drop case to:

case LEVEL_DROP:
  // player fell off
  gNextState = STATE_FALLOFF;
  break;

The level end case changes to:

case LEVEL_END:
  {
    int secondsleft = gLevelTime - (gLastTick - gLevelStartTick) / 1000;
    if (secondsleft < 0)
      secondsleft = 0;
    gScore += 100 + secondsleft * 25;
    gNextState = STATE_ENDLEVEL;
  }
  break;

Note that we're removing the gCurrentLevel++; from the level end.

Finally, the collision with the level borders changes to:

// Collision with the level borders
if (gXPos > gLevelWidth * TILESIZE || 
    gXPos < 0 || 
    gYPos > gLevelHeight * TILESIZE || 
    gYPos < 0)
{
  gNextState = STATE_FALLOFF;
}

Back in main.cpp, in changestate(), change the first switch-structure to:

// Things that need to be done when leaving a state
switch (gGameState)
{
case STATE_ENDLEVEL: 
  gCurrentLevel++;
  break;
}

The reset() is also called at the end of the init() function in main. This is no longer needed, so you can remove the call.

Now that the state mechanism is in place, we can put it to use with 29 - Transitions.

Having problems? Improvement ideas? Just want to discuss this tutorial? Try the forums!

Any comments etc. can be emailed to me.