Pausing and Restarting

Giving our Snake game the ability to pause itself, and adding a clickable button for restarting the game

Ryan McCombe
Updated

Now that we have our core Snake game mechanics working, it's time to enhance the user experience by adding interactive UI elements. In this lesson, we'll implement a restart button that allows players to reset the game without having to close and reopen the application.

We'll also update our GameState class so the game initially starts in a paused state, and doesn't advance until the user starts moving their snake.

Starting Point

We'll continue from where we left off in the previous lesson. Our project already has a grid of interactive cells, a controllable snake, and the ability for the snake to eat apples and grow.

To keep this lesson focused, the starting point below is a direct continuation of the previous lesson's final code.

Files

src
Select a file to view its content

Creating a Restart Button

Let's begin by adding a BUTTON_COLOR variable to our configuration file:

src/Globals.h

// ...

namespace Config{
  // Colors
  inline constexpr SDL_Color BUTTON_COLOR{
    73, 117, 46, 255};
  // ...
}
// ...

We want the button to be placed in the bottom right of our window:

Let's add some more configuration values to control this positioning. We'll add a FOOTER_HEIGHT value to control the height of our button and other footer elements we'll add later.

We'll also update our WINDOW_HEIGHT value to include enough space for this footer.

src/Globals.h

// ...

namespace Config{
  inline constexpr int FOOTER_HEIGHT{60};
  inline constexpr int WINDOW_HEIGHT{
    GRID_HEIGHT
    + FOOTER_HEIGHT
    + PADDING * 2};

  // ...
}
// ...

We'll create a RestartButton class to manage our button. We'll include the standard Render() and HandleEvent() methods:

src/Snake/RestartButton.h

#pragma once
#include <SDL3/SDL.h>

class RestartButton {
public:
  RestartButton(int x, int y, int w, int h);

  void Render(SDL_Surface* Surface);
  void HandleEvent(const SDL_Event& E);
};

Let's add an instance of this button to our SnakeUI class and hook it up to our application loop through the HandleEvent() and Render() methods:

src/SnakeUI.h

#pragma once
#include <SDL3/SDL.h>
#include "Assets.h"
#include "Snake/Grid.h"
#include "Snake/RestartButton.h"

class SnakeUI {
 public:
  SnakeUI() : Grid{AssetList} {}

  void HandleEvent(const SDL_Event& E) {
    Grid.HandleEvent(E);
    RestartBtn.HandleEvent(E);
  }

  void Tick(Uint64 DeltaTime) {
    Grid.Tick(DeltaTime);
  }

  void Render(SDL_Surface* Surface) {
    Grid.Render(Surface);
    RestartBtn.Render(Surface);
  }

 private:
  Assets AssetList;
  Grid Grid;
  RestartButton RestartBtn;
};

Button Rendering

Our RestartButton's Render() function is now being called on every frame. Let's update it to make our button visible on the window surface.

As before, we'll use SDL_FillSurfaceRect(), meaning we need an SDL_Rect to position our button, and a color value to control what color our blitted pixels will have. As a reminder, the API looks like this:

SDL_FillSurfaceRect(Surface, &Rect, Color);

Let's start by defining the SDL_Rect's x, y, w, and h values in the SnakeUI constructor and passing them to our RestartButton.

  • x: We want the horizontal position of our button to start 150 points from the right edge of the window, so we'll set x to WINDOW_WIDTH - 150.
  • y: We want the vertical position of our button to start below the grid. There is padding between the top of the window and the start of the grid, and we want additional padding between the bottom of the grid and the top of the button, so we'll set y to GRID_HEIGHT + PADDING * 2.
  • w: Our button is positioned 150 points from the right edge, and we want some padding between the button and the right edge of the window, so we'll set the width to 150 - PADDING.
  • h: We want the height of the button to be the full height of the footer, with some padding between the bottom of the button and the bottom of the window. So we'll set the height of the button to be FOOTER_HEIGHT - PADDING.

src/SnakeUI.h

// ...
class SnakeUI {
 public:
  SnakeUI()
    : Grid{AssetList},
      RestartBtn{
        Config::WINDOW_WIDTH - 150,
        Config::GRID_HEIGHT + Config::PADDING * 2,
        150 - Config::PADDING,
        Config::FOOTER_HEIGHT - Config::PADDING
      } {}
 // ...
 private:
  // ...
  RestartButton RestartBtn;
};

Now, inside the RestartButton class, we'll store this geometry in a private ButtonRect member. We'll also store its color in a CurrentColor member.

src/Snake/RestartButton.h

#pragma once
#include <SDL3/SDL.h>
#include "Globals.h"

class RestartButton {
public:
  RestartButton(int x, int y, int w, int h)
    : ButtonRect{x, y, w, h},
      CurrentColor{Config::BUTTON_COLOR} {}

  void Render(SDL_Surface* Surface);
  void HandleEvent(const SDL_Event& E);

private:
  SDL_Rect ButtonRect;
  SDL_Color CurrentColor;
};

In our button's Render() function, we'll use SDL_FillSurfaceRect() in conjunction with this rectangle and color to render our button onto the surface:

src/Snake/RestartButton.h

#pragma once
#include <SDL3/SDL.h>
#include "Globals.h"

class RestartButton {
public:
  // ...
  void Render(SDL_Surface* Surface) {
    SDL_FillSurfaceRect(Surface, &ButtonRect,
      SDL_MapRGB(
        SDL_GetPixelFormatDetails(Surface->format),
        nullptr,
        CurrentColor.r,
        CurrentColor.g,
        CurrentColor.b
      )
    );
  }
  // ...
};

Running our code, we should now see the button positioned correctly in the bottom right of our window:

Text Rendering

Let's add the "RESTART" text to our button. We have a Text class defined in our Engine/Text.h file, so let's add an instance of this class to our RestartButton. The Text constructor accepts our desired content and font size.

We'll also add a TextRect member to control the positioning of our text:

src/Snake/RestartButton.h

#pragma once
#include <SDL3/SDL.h>
#include "Globals.h"
#include "Engine/Text.h"

class RestartButton {
public:
  RestartButton(int x, int y, int w, int h)
    : ButtonRect{x, y, w, h},
      Text{"RESTART", 20} {}

  void Render(SDL_Surface* Surface) override {
    SDL_FillSurfaceRect(
      Surface, &ButtonRect,
      SDL_MapRGB(
        SDL_GetPixelFormatDetails(Surface->format),
        nullptr,
        CurrentColor.r,
        CurrentColor.g,
        CurrentColor.b
      )
    );
    Text.Render(Surface, &ButtonRect);
  }
  
  void HandleEvent(const SDL_Event& E);

private:
  Engine::Text Text;
  SDL_Rect ButtonRect;
  SDL_Color CurrentColor;
};

The base Button::Render() will draw the background, and then we call Text.Render() to draw the text on top of it. Our Engine::Text class automatically centers the text within the provided rectangle.

Running our program, we should now see our text rendered in the correct position:

Button Clicking

Now that our button is visually complete, we need to make it functional. First, let's define a new user event type in our Globals.h file:

src/Globals.h

// ...

namespace UserEvents{
  // ...
  inline const Uint32 RESTART_GAME{
    SDL_RegisterEvents(1)};
}

// ...

This custom event will signal that the game should be restarted. Next, let's update the RestartButton class to detect clicks. It can do this by checking for SDL_MOUSEBUTTONDOWN events and performing a bounds check.

src/Snake/RestartButton.h

// ...
class RestartButton {
public:
  // ...
  void HandleEvent(const SDL_Event& E) {
    if (E.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
      HandleClick(E.button);
    }
  }

private:
  void HandleClick(const SDL_MouseButtonEvent& E) {}
  // ...
};

Our HandleClick method checks if the mouse coordinates are within the button's rectangle. If they are, it creates and dispatches our custom RESTART_GAME event:

// ...
class RestartButton {
  // ...
private:
  void HandleClick(const SDL_MouseButtonEvent& E) {
    if (
      E.x >= ButtonRect.x &&
      E.x <= ButtonRect.x + ButtonRect.w &&
      E.y >= ButtonRect.y &&
      E.y <= ButtonRect.y + ButtonRect.h
    ) {
      SDL_Event RestartEvent{
        .type = UserEvents::RESTART_GAME
      };
      SDL_PushEvent(&RestartEvent);
    }
  }
  // ...
};

Restarting the Game

Now that our button is dispatching a RESTART_GAME event, we need to update the relevant components to implement this restarting behavior. Let's first update our GameState class to detect these events and invoke a new RestartGame() method when they occur:

src/GameState.h

// ...

class GameState {
public:
  void HandleEvent(const SDL_Event& E) {
    if (E.type == SDL_EVENT_KEY_DOWN) {
      HandleKeyEvent(E.key);
    } else if (E.type == UserEvents::APPLE_EATEN) {
      ++Snake.Length;
    } else if (E.type == UserEvents::RESTART_GAME) {
      RestartGame();
    }
  }
  // ...

private:
  void RestartGame() {}
  // ...
};

Our RestartGame() method will reset our member variables to their initial values:

src/GameState.h

// ...

class GameState {
  // ...
private:
  void RestartGame() {
    ElapsedTime = 0;
    Snake = {
      .HeadRow = Config::GRID_ROWS / 2,
      .HeadCol = 3,
      .Length = 2,
      .Direction = Right
    };
    NextDirection = Right;
  }
  // ...
};

Finally, our Cell objects need to react to the player restarting the game. We already have Initialize() and HandleEvent() functions in that class, so we'll just update HandleEvent() to call Initialize() in response to the event. Let's also rename Initialize() to Reset() for clarity.

src/Snake/Cell.h

// ...
class Cell {
public:
  //...
  void Reset();
private:
  void Initialize();
  //...
};

src/Snake/Cell.cpp

// ...
void Cell::Reset() { // Renamed from Initialize
  //...
}

void Cell::HandleEvent(const SDL_Event& E) {
  if (E.type == UserEvents::ADVANCE) {
    Advance(E.user);
  } else if (E.type == UserEvents::APPLE_EATEN) {
    if (State == CellState::Snake) {
      ++SnakeDuration;
    }
  } else if (E.type == UserEvents::RESTART_GAME) {
    Reset();
  }
}
//...

Pausing the Game

Currently, when the player starts (or restarts) the game, the snake immediately starts moving. This isn't a great experience, so we'll give our game the capability to pause itself.

We'll add an IsPaused member to the GameState class. When this is true, the Tick() function won't do anything except immediately return:

src/GameState.h

// ...

class GameState {
public:
  // ...
  void Tick(Uint64 DeltaTime) {
    if (IsPaused) return;

    ElapsedTime += DeltaTime;
    if (ElapsedTime >= Config::ADVANCE_INTERVAL) {
      ElapsedTime = 0;
      UpdateSnake();
    }
  }
  // ...

private:
  // ...
  bool IsPaused; 
};

We'll set IsPaused to true when the game starts, and any time it is restarted:

src/GameState.h

// ...

class GameState {
  // ...
private:
  void RestartGame() {
    IsPaused = true;
    ElapsedTime = 0;
    Snake = {
      .HeadRow = Config::GRID_ROWS / 2,
      .HeadCol = 3,
      .Length = 2,
      .Direction = Right
    };
    NextDirection = Right;
  }
  //...
  bool IsPaused{true};
};

Starting the Game

We'll let the user unpause the game by moving the snake right. As we're only pausing at the start of the game, and the first apple always spawns to the right, this should feel like an intuitive way to start the game.

We'll also call UpdateSnake() when the game is unpaused, thereby advancing the game state immediately when the user presses the key. This is optional, but makes the game feel more responsive compared to waiting for the advance interval to pass before the snake starts moving:

src/GameState.h

// ...
class GameState {
// ...
private:
  void HandleKeyEvent(const SDL_KeyboardEvent& E) {
    switch (E.key) {
case SDLK_RIGHT: case SDLK_D: if (IsPaused) { IsPaused = false; NextDirection = Right; UpdateSnake(); } else if (Snake.Direction != Left) { NextDirection = Right; } break; } } // ... };

Running our game, we should now see that it starts in the paused state, and reverts to this paused state any time it is restarted.

Complete Code

Complete versions of the files we changed in this part are available below.

Files

src
Select a file to view its content

Summary

We've implemented a restart button, a game state management system, and a pause/resume mechanism, improving the game flow and user experience.

The key things we did in this lesson include:

  • Restart Button: Created a functional restart button with positioning, text rendering, and event handling.
  • Game State Management: Centralized game state in a GameState class, allowing for easy resets and state transitions.
  • Event Handling: Used custom events to trigger game restarts.
  • Pause/Resume: Implemented a pause mechanism that enhances the game's start and restart flow, making it more user-friendly.
Next Lesson
Lesson 63 of 69

Win/Loss Logic for Snake

Add game-ending logic with custom events, state management, and visual cues for player feedback.

Have a question about this lesson?
Answers are generated by AI models and may not be accurate