Sol's Graphics for Beginners

30 - New States and Splash Screens

The part E revolves mostly around things that happen outside the game. First off, we're going to expand the state system of the game a bit. This could be done in an iterative manner, i.e. add a feature, add a state, add a feature and so on, but to save time and to force ourselves to think a bit ahead, we'll be adding a bunch of states on one go.

So in this chapter, We'll be changing our little state machine from this

into this:

Don't panic, it's only 7 new states, and it'll take a few chapters to fill in all the new functionality.

Let's get to work. Starting off from where we left off from the last chapter (remembering backups and so on), open gp.h and add the following lines to the gamestates enum:

STATE_SPLASH,
  STATE_MAINMENU,
  STATE_CREDITS,
  STATE_STAGESELECT,
  STATE_PAUSEMENU,
  STATE_GAMEOVER,
  STATE_ENDSTAGE

Remember to add a comma after STATE_ENDLEVEL if it doesn't have one yet.

Next, we'll change the starting state. Instead of starting off from STATE_ENTRY, we want to start from START_SPLASH. Around the middle of init() function in main.cpp..

gNextState = STATE_SPLASH;

This will leave the game in a non-working state. Let's fix that in main.cpp changestate(), add the following cases to the second switch structure ('things that need to be done when entering a state')..

case STATE_SPLASH:
    gNextState = STATE_MAINMENU;
    break;
  case STATE_CREDITS:
    gNextState = STATE_MAINMENU;
    break;
  case STATE_MAINMENU:
    gNextState = STATE_STAGESELECT;
    break;
  case STATE_STAGESELECT:
    gNextState = STATE_ENTRY;
    break;
  case STATE_ENDSTAGE:
    gNextState = STATE_STAGESELECT;
    break;
  case STATE_GAMEOVER:
    gNextState = STATE_MAINMENU;
    break;
  case STATE_PAUSEMENU:
    exit(0);
    break;

Now the game should work more or less like before. The states above are set so that when we fill in the gaps, the rest of the system should keep working. The only problematic case is the pause menu, which will have to act like the termination command for the time being, or else we'll end up in a situation where we can't quit at all.

Let's add the splash screen. Go to the changestate() function and remove the STATE_SPLASH case so that we don't automatically skip to the next state.

In main.cpp, add a new variable after the other surfaces:

// Splash screen
SDL_Surface *gSplash;

Still in main.cpp, add the following after the loading of texture17.bmp:

temp = SDL_LoadBMP("splash.bmp");
  gSplash = SDL_ConvertSurface(temp, gScreen->format, SDL_SWSURFACE);
  SDL_FreeSurface(temp);

You'll need the splash screen image as well - splash.bmp - right click, save as.

Next we'll need to render the splash screen. In main.cpp, find render() and add a new case to the switch structure:

case STATE_SPLASH:
    rendersplash();
    break;

We'll naturally need this new function as well. Add it above the render() function in main.cpp:

void rendersplash()
{
  // Ask SDL for the time in milliseconds
  int tick = SDL_GetTicks();

  // Go to the main menu if 4 seconds have passed
  if (tick - gStateStartTick > 4000)
    gNextState = STATE_MAINMENU;

  // Lock surface if needed
  if (SDL_MUSTLOCK(gScreen))
    if (SDL_LockSurface(gScreen) < 0) 
      return;

  // Lock surface if needed
  if (SDL_MUSTLOCK(gSplash))
    if (SDL_LockSurface(gSplash) < 0) 
      return;

  // Calculate effect value
  float v = (tick - gStateStartTick) / 2000.0f;  
  if (v > 1) v = 1;
  v = pow(v, 0.1f);
  int value = (int)floor((HEIGHT+1) - HEIGHT * v);

  // Render
  int i, j, screenofs, imgofs;
  screenofs = 0;
  for (i = 0; i < HEIGHT; i++)
  {
    int ypos = ((i - (HEIGHT / 2)) / value) * value + (HEIGHT / 2);
    imgofs = ypos * (gSplash->pitch / 4);
    for (j = 0; j < WIDTH; j++)
    {
      int xpos = ((j - (WIDTH / 2)) / value) * value + (WIDTH / 2);
      ((unsigned int*)gScreen->pixels)[screenofs + j] = 
        ((unsigned int*)gSplash->pixels)[imgofs + xpos];
    }
    screenofs += PITCH;
  }

  // Unlock if needed
  if (SDL_MUSTLOCK(gSplash)) 
    SDL_UnlockSurface(gSplash);

  // Unlock if needed
  if (SDL_MUSTLOCK(gScreen)) 
    SDL_UnlockSurface(gScreen);

  // Tell SDL to update the whole gScreen
  SDL_UpdateRect(gScreen, 0, 0, WIDTH, HEIGHT);    

  // Don't hog all the CPU power
  SDL_Delay(5);
}

The above code first checks if four seconds have passed since the state start. If yes, we're ready to move to the next state.

Next we're locking the display as well as the splash screen image surface in a familiar manner. Next up are a few cryptic lines of calculating effect value. We're first calculating an interpolation value between 0 and 1 in two seconds, and then calculating 0.1th power of said value. Finally the value is scaled to (width + 1)..1 range.

The resulting ramp looks like this:

Next up is the rendering code, which goes through all of the display's pixels, and fetches pixels from the splash screen based on the effect value, generating a reverse 'pixelation' effect.

The pixelation effect is produced by taking the desired pixel coordinate, peforming an integer divide on it by the effect value, and multiplying it back, resulting in tossing out the modulo of the pixel coordinate and the effect value. The half of the coordinates are added to cause the pixelation to offset at the middle of the big pixels, instead of the top left corner.

Easiest way to understand the above is to remove the WIDTH / 2 and HEIGHT / 2 bits of the xpos and ypos calculations and to see what happens.

The 0.1th power scaling of the effect value makes the effect slow down towards the end, making it look better. Try removing the power value, or experiment with different values.

The rest of the function should be quite familiar, as we're unlocking the surfaces and updating the screen.

There are a couple of things we must take care of regarding the splash screen. First off, having a mandatory 4-second splash screen in the beginning of the game is rather irritating, so we'll need to add ways to skip it. Second, most of the initialization of the game, which may take a considerable time on some target platforms, is currently performed before the splash screen. Third, we don't need the splash screen image after showing it, so we might as well free it.

First, in main.cpp, the escape handling in main() function changes to:

case SDLK_ESCAPE:
          if (gGameState == STATE_SPLASH)
          {
            gNextState = STATE_MAINMENU;
          }
          else
          {
            // If escape is pressed, return (and thus, quit)
            return 0;
          }
          break;

and the space/enter handling changes to:

case SDLK_RETURN:
        case SDLK_SPACE:
          if (gGameState == STATE_SPLASH)
          {
            gNextState = STATE_MAINMENU;
          }
          if (gGameState == STATE_ENDLEVEL)
          {
            gNextState = STATE_ENTRY;
          }
          break;

Save, compile, try it out.

Next we'll need to refactor the init() function. Rename the init() function to loadresources(), and create a new function init() above it. The new init() function, with the bare minimum, looks like this:

void init()
{
  SDL_Surface *temp = SDL_LoadBMP(&quot;splash.bmp&quot;);
  gSplash = SDL_ConvertSurface(temp, gScreen->format, SDL_SWSURFACE);
  SDL_FreeSurface(temp);

  gGameState = STATE_NONE;
  gNextState = STATE_SPLASH;
}

Remove the corresponding code lines from loadresources(), and add the following lines to the beginning of the function:

SDL_FreeSurface(gSplash);
  gSplash = NULL;

Finally, go to changestate() and add the following case to the things that need to be done when leaving a state:

case STATE_SPLASH:
    loadresources();
    break;

Compile and run; things should work again.

Next we'll lay groundwork for menus by plugging in the 31. IMGUI..

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

Any comments etc. can be emailed to me.