Sol's Graphics for Beginners

32 - Menus

Now that we have some IMGUI widgets, we can create menus. We'll only create the main menu and the pause menu at this point.

First, go to main.cpp changestate() function, and find the STATE_MAINMENU and STATE_PAUSEMENU cases, and replace their contents with imgui_open(), like so:

case STATE_MAINMENU:
    imgui_open();
    break;
  case STATE_PAUSEMENU:
    imgui_open();
    break;

Also add the following cases to the things that need to be done when leaving a state:

case STATE_MAINMENU:
    imgui_close();
    break;
  case STATE_PAUSEMENU:
    imgui_close();
    break;

After these changes the IMGUI is set up and cleaned up when entering and leaving these states.

Next we'll need to render the main menu. Go to render() and add the following case:

case STATE_MAINMENU:
    rendermainmenu();
    break;

and naturally we'll need to add the main menu rendering as well. Add the following function before the render() function:

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

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

  // Clear screen to mid-gray
  drawrect(0, 0, WIDTH, HEIGHT, 0x7f7f7f);

  // Set up IMGUI block
  imgui_begin();

  if (imgui_button(GEN_ID, (WIDTH - 90) / 2,(HEIGHT / 2) - 60, 90, 20, &quot;Start game&quot;))
  {
    gNextState = STATE_STAGESELECT;
  }

  if (imgui_button(GEN_ID, (WIDTH - 60) / 2,(HEIGHT / 2) - 30, 60, 20, &quot;Credits&quot;))
  {
    gNextState = STATE_CREDITS;
  }

  if (imgui_slider(GEN_ID, (WIDTH - 255) / 2, (HEIGHT / 2), 255, 20, 10, gVolume, &quot;Volume&quot;))
  {
    // adjust volume
  }

  if (imgui_button(GEN_ID, (WIDTH - 80) / 2,(HEIGHT / 2) + 60, 80, 20, &quot;Quit game&quot;))
  {
    exit(0);
  }

  imgui_end();

  // 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);
}

This won't compile due to the lack of the variable gVolume. So let's add it. Add a new global variable (still in main.cpp):

// Audio volume
int gVolume;

and set it to 5 in init():

gVolume = 5;

The reason for setting the volume already in init() is that we might be starting to play audio even before the splash screen starts.

Compile and run. This new functionality reveals one irritating thing: we can't skip the splash screen with mouse. Let's fix that. In main(), add the following to SDL_MOUSEBUTTONUP case:

if (gGameState == STATE_SPLASH)
    {
      gNextState = STATE_MAINMENU;
    }

Next, let's add the pause menu. First a way to get to the pause menu; we'll change the way the ESC key functions. In main(), the SDLK_ESCAPE case handling changes to this:

case SDLK_ESCAPE:
      switch(gGameState)
      {
      case STATE_SPLASH:
        gNextState = STATE_MAINMENU;
        break;
      case STATE_MAINMENU:
        return 0;
      case STATE_CREDITS:
        gNextState = STATE_MAINMENU;
        break;
      case STATE_STAGESELECT:
        gNextState = STATE_MAINMENU;
        break;
      case STATE_INGAME:
        gNextState = STATE_PAUSEMENU;
        break;
      case STATE_PAUSEMENU:
        gNextState = STATE_INGAME;
        break;
      case STATE_GAMEOVER:
        gNextState = STATE_MAINMENU;
        break;
      case STATE_ENDSTAGE:
        gNextState = STATE_STAGESELECT;
        break;
      }
      break;

After this, the ESC key function more or less logically depending on the state we're in. Some transition states do not respond to ESC after this change.

Next we'll render the pause menu. The same moves again, i.e., add the following case to render()..

case STATE_PAUSEMENU:
    renderpausemenu();
    break;

..and the pause menu render code:

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

  // Clear screen to mid-gray
  drawrect(0, 0, WIDTH, HEIGHT, 0x7f7f7f);

  // Set up IMGUI block
  imgui_begin();

  if (imgui_button(GEN_ID, (WIDTH - 90) / 2,(HEIGHT / 2) - 30, 90, 20, &quot;Resume game&quot;))
  {
    gNextState = STATE_INGAME;
  }

  if (imgui_slider(GEN_ID, (WIDTH - 255) / 2, (HEIGHT / 2), 255, 20, 10, gVolume, &quot;Volume&quot;))
  {
    // adjust volume
  }

  if (imgui_button(GEN_ID, (WIDTH - 100) / 2,(HEIGHT / 2) + 30, 100, 20, &quot;Return to main&quot;))
  {
    gNextState = STATE_MAINMENU;
  }

  imgui_end();

  // 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);
}

After this the pause menu will work.. mostly. There are a couple of irritations with it that we'll want to fix. First off, it doesn't look too pretty, and the more important problem is that it messes up the game clock.

Let's fix the first one first. Add a new surface (main.cpp):

// Pause menu background
SDL_Surface *gPauseBg;

Then, in changestate(), change the case block for entering the pause menu state to:

case STATE_PAUSEMENU:
    gPauseBg = SDL_ConvertSurface(gScreen, gScreen->format, SDL_SWSURFACE);
    imgui_open();
    break;

and we'll need to clean the surface up as well, so the leaving state case block changes to:

case STATE_PAUSEMENU:
    SDL_FreeSurface(gPauseBg);
    gPauseBg = NULL;
    imgui_close();
    break;

Finally, go to render_pausemenu() and replace the screen-filling drawrect with:

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

  // Render
  int i, j, screenofs, imgofs;
  screenofs = 0;
  for (i = 0; i < HEIGHT; i++)
  {
    imgofs = i * (gPauseBg->pitch / 4);
    for (j = 0; j < WIDTH; j++)
    {
      ((unsigned int*)gScreen->pixels)[screenofs + j] = 
        blend_mul(((unsigned int*)gPauseBg->pixels)[imgofs + j], 0x7f7fff);
    }
    screenofs += PITCH;
  }
  
  // Unlock if needed
  if (SDL_MUSTLOCK(gPauseBg)) 
    SDL_UnlockSurface(gPauseBg);

There. Much prettier. Now, what shall we do with the game clock?

Entering the ingame state starts the clock off from beginning, so the easiest way to solve the time problem is to adjust the level's time limit when entering the pause state. Change the STATE_PAUSEMENU case block in changestate again, this time to:

case STATE_PAUSEMENU:
    gLevelTime -= (gLastTick - gLevelStartTick) / 1000;
    gPauseBg = SDL_ConvertSurface(gScreen, gScreen->format, SDL_SWSURFACE);
    imgui_open();
    break;

The negative thing here is that this will let the user cheat by tapping escape all the time - the clock won't move onwards, since its granularity is set to one second. We'll postpone fixing of this to the next gameplay adjusting pass.

Next we'll go through some effort to create a 33. Prettier Main Menu..

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

Any comments etc. can be emailed to me.