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.
Let’s begin by adding a BUTTON_COLOR
variable to our configuration file:
// GameConfig.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 in the next section.
We’ll also update our WINDOW_HEIGHT
value to include enough space for this in our footer.
// GameConfig.h
// ...
namespace Config{
// Size and Positioning
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 don’t need any ticking logic for this function, but we’ll include the Render()
and HandleEvent()
methods:
// RestartButton.h
#pragma once
#include <SDL.h>
class RestartButton {
public:
void Render(SDL_Surface* Surface) {
// ...
}
void HandleEvent(SDL_Event& E) {
// ...
}
};
Let’s add an instance of this button to our GameUI
class, and hook it up to our application loop through the HandleEvent()
and Render()
methods:
// GameUI.h
// ...
#include "RestartButton.h"
class GameUI {
public:
void HandleEvent(SDL_Event& E) {
Grid.HandleEvent(E);
RestartButton.HandleEvent(E);
}
void Render(SDL_Surface* Surface) {
Grid.Render(Surface);
RestartButton.Render(Surface);
}
private:
RestartButton RestartButton;
};
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_FillRect
, meaning we need an SDL_Rect
to position our button, and an SDL_Color
to control what color our blitted pixels will have:
SDL_FillRect(Surface, &Rect, Color);
Let’s start by defining the SDL_Rect
's x
, y
, w
, and h
values:
x
: We want the horizontal position of our button to start 150 pixels 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 some 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 pixels 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 bottom of the window. So we’ll set the height of the button to be FOOTER_HEIGHT - PADDING
.Let’s create a ButtonRect
private member to store these calculations:
// RestartButton.h
// ...
#include "GameConfig.h"
class RestartButton {
public:
// ...
private:
SDL_Rect ButtonRect{
Config::WINDOW_WIDTH - 150,
Config::GRID_HEIGHT + Config::PADDING * 2,
150 - Config::PADDING,
Config::FOOTER_HEIGHT - Config::PADDING
};
};
Later in the chapter, we’ll want to change the button color based on gameplay events, so we’ll store this color as a member variable rather than using the Config::BUTTON_COLOR
value directly in our Render()
function.
Initially, it will just be set to the same color we defined in our config file, but we’ll update this later:
// RestartButton.h
// ...
#include "GameConfig.h"
class RestartButton {
public:
// ...
private:
// ...
SDL_Color CurrentColor{Config::BUTTON_COLOR};
};
In our button’s Render()
function, we’ll use the SDL_FillRect()
function in conjunction with this rectangle and the color to render our button onto the surface:
// RestartButton.h
// ...
class RestartButton {
public:
void Render(SDL_Surface* Surface) {
SDL_FillRect(Surface, &ButtonRect,
SDL_MapRGB(
Surface->format,
CurrentColor.r,
CurrentColor.g,
CurrentColor.b
)
);
}
// ...
};
Running our code, we should now see the button positioned correctly in the bottom right of our window:
Let’s add the "Restart" text to our button. We have a 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:
// RestartButton.h
// ...
#include "Engine/Text.h"
class RestartButton {
// ...
private:
// ...
Text Text{"RESTART", 20};
};
We’ll define another SDL_Rect
to control where this text is positioned. The TextRect
variable is similar to our ButtonRect
, but we’ll add some additional padding to control the position of the text within the button.
The Text
class doesn’t require us to define the width or height of our text, so we can set those values to 0
:
// RestartButton.h
// ...
class RestartButton {
public:
// ...
private:
// ...
SDL_Rect TextRect{
ButtonRect.x + Config::PADDING * 5,
ButtonRect.y + Config::PADDING * 3,
0, 0
};
};
Finally, we’ll render our Text
within the Render()
function. The text should be blitted after the background color, to ensure it appears on top of the background:
// RestartButton.h
// ...
#include "Engine/Text.h"
class RestartButton {
public:
// ...
void Render(SDL_Surface* Surface) {
SDL_FillRect(Surface, &ButtonRect,
SDL_MapRGB(
Surface->format,
CurrentColor.r,
CurrentColor.g,
CurrentColor.b
)
);
Text.Render(Surface, &TextRect);
}
// ...
};
Running our program, we should now see our text rendered in the correct position:
Now that our button is visually complete, we need to make it functional. We'll implement a mechanism that allows the player to restart the game by clicking on our button. First, let's define a new user event type in our configuration file:
// GameConfig.h
// ...
namespace UserEvents{
// ...
inline Uint32 RESTART_GAME =
SDL_RegisterEvents(1);
}
// ...
This custom event will serve as a signal throughout our application that the game should be restarted. Now, let's update the RestartButton
class to detect clicks and dispatch this event when appropriate:
// RestartButton.h
// ...
class RestartButton {
public:
void HandleEvent(SDL_Event& E) {
using namespace UserEvents;
if (E.type == SDL_MOUSEBUTTONDOWN) {
HandleClick(E.button);
}
}
// ...
private:
// ...
void HandleClick(SDL_MouseButtonEvent& E) {}
};
Our HandleClick
method needs to perform a boundary check to determine if the mouse coordinates at the time of clicking fall within our button's rectangle. If they do, we create a new SDL_Event
with our custom type and dispatch it using SDL_PushEvent()
:
// RestartButton.h
// ...
class RestartButton {
// ...
private:
// ...
void HandleClick(SDL_MouseButtonEvent& E) {
using namespace UserEvents;
if (
E.x >= ButtonRect.x &&
E.x <= ButtonRect.x + ButtonRect.w &&
E.y >= ButtonRect.y &&
E.y <= ButtonRect.y + ButtonRect.h
) {
SDL_Event RestartEvent{RESTART_GAME};
SDL_PushEvent(&RestartEvent);
}
}
};
Other components in our application will listen for this event and respond accordingly.
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:
// GameState.h
// ...
class GameState {
public:
void HandleEvent(SDL_Event& E) {
using namespace UserEvents;
if (E.type == SDL_KEYDOWN) {
HandleKeyEvent(E.key);
} else if (E.type == APPLE_EATEN) {
++Snake.Length;
} else if (E.type == RESTART_GAME) {
RestartGame();
}
}
// ...
private:
void RestartGame() {}
// ...
};
Our RestartGame()
method will reset our member variables to their initial values:
// GameState.h
// ...
class GameState {
// ...
private:
void RestartGame() {
ElapsedTime = 0;
Snake = {Config::GRID_ROWS / 2, 3, 2, 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:
// Cell.h
// ...
class Cell {
public:
void HandleEvent(SDL_Event& E) {
using namespace UserEvents;
if (E.type == ADVANCE) {
Advance(E.user);
} else if (E.type == APPLE_EATEN) {
if (CellState == Snake) {
++SnakeDuration;
}
} else if (E.type == RESTART_GAME) {
Initialize();
}
}
// ...
};
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.
In this example, we’ll only pause the game when it first opens and after the user presses restart, but feel free to expand this if you feel comfortable. You could, for example, add a keybinding to let the user pause at any time.
To keep track of whether the game is paused, we’ll add an IsPaused
member to the GameState
class. When this is true
, the Tick()
function won’t do anything except immediately return
:
// GameState.h
// ...
class GameState {
public:
// ...
void Tick(Uint32 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:
// GameState.h
// ...
class GameState {
// ...
private:
void RestartGame() {
IsPaused = true;
ElapsedTime = 0;
Snake = {Config::GRID_ROWS / 2, 3, 2, Right};
NextDirection = Right;
}
bool IsPaused{true};
};
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:
// GameState.h
// ...
class GameState {
// ...
private:
void HandleKeyEvent(SDL_KeyboardEvent& E) {
switch (E.keysym.sym) {
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 versions of the files we changed in this part are available below
#pragma once
#define CHECK_ERRORS
#include <iostream>
#include <SDL.h>
#include <string>
namespace UserEvents{
inline Uint32 ADVANCE =
SDL_RegisterEvents(1);
inline Uint32 APPLE_EATEN =
SDL_RegisterEvents(1);
inline Uint32 RESTART_GAME =
SDL_RegisterEvents(1);
}
namespace Config{
// Game Settings
inline const std::string GAME_NAME{
"Snake"};
inline constexpr int ADVANCE_INTERVAL{200};
inline constexpr int GRID_COLUMNS{16};
static_assert(
GRID_COLUMNS >= 12,
"Grid must be at least 12 columns wide");
inline constexpr int GRID_ROWS{5};
static_assert(
GRID_ROWS >= 5,
"Grid must be at least 5 rows tall");
// Size and Positioning
inline constexpr int PADDING{5};
inline constexpr int CELL_SIZE{36};
inline constexpr int FOOTER_HEIGHT{60};
inline constexpr int GRID_HEIGHT{
CELL_SIZE * GRID_ROWS
};
inline constexpr int GRID_WIDTH{
CELL_SIZE * GRID_COLUMNS};
inline constexpr int WINDOW_HEIGHT{
GRID_HEIGHT + FOOTER_HEIGHT
+ PADDING * 2};
inline constexpr int WINDOW_WIDTH{
GRID_WIDTH + PADDING * 2};
// Colors
inline constexpr SDL_Color BACKGROUND_COLOR{
85, 138, 52, 255};
inline constexpr SDL_Color CELL_COLOR_A{
171, 214, 82, 255};
inline constexpr SDL_Color CELL_COLOR_B{
161, 208, 74, 255};
inline constexpr SDL_Color SNAKE_COLOR{
67, 117, 234, 255};
inline constexpr SDL_Color BUTTON_COLOR{
73, 117, 46, 255};
inline constexpr SDL_Color FONT_COLOR{
255, 255, 255, 255};
// Asset Paths
inline const std::string APPLE_IMAGE{
"apple.png"};
inline const std::string FONT{
"Rubik-SemiBold.ttf"};
}
inline void CheckSDLError(
const std::string& Msg){
#ifdef CHECK_ERRORS
const char* error = SDL_GetError();
if (*error != '\0') {
std::cerr << Msg << " Error: "
<< error << '\n';
SDL_ClearError();
}
#endif
}
#pragma once
#include <SDL.h>
#include "GameConfig.h"
#include "Engine/Text.h"
class RestartButton {
public:
void Render(SDL_Surface* Surface) {
SDL_FillRect(Surface, &ButtonRect,
SDL_MapRGB(
Surface->format,
CurrentColor.r,
CurrentColor.g,
CurrentColor.b
)
);
Text.Render(Surface, &TextRect);
}
void HandleEvent(SDL_Event& E) {
using namespace UserEvents;
if (E.type == SDL_MOUSEBUTTONDOWN) {
HandleClick(E.button);
}
}
private:
void HandleClick(SDL_MouseButtonEvent& E) {
using namespace UserEvents;
if (
E.x >= ButtonRect.x &&
E.x <= ButtonRect.x + ButtonRect.w &&
E.y >= ButtonRect.y &&
E.y <= ButtonRect.y + ButtonRect.h
) {
SDL_Event RestartEvent{RESTART_GAME};
SDL_PushEvent(&RestartEvent);
}
}
Text Text{"RESTART", 20};
SDL_Rect ButtonRect{
Config::WINDOW_WIDTH - 150,
Config::GRID_HEIGHT + Config::PADDING * 2,
150 - Config::PADDING,
Config::FOOTER_HEIGHT - Config::PADDING
};
SDL_Rect TextRect{
ButtonRect.x + Config::PADDING * 5,
ButtonRect.y + Config::PADDING * 3,
0, 0
};
SDL_Color CurrentColor{Config::BUTTON_COLOR};
};
#pragma once
#include <SDL.h>
#include "Grid.h"
#include "Assets.h"
#include "RestartButton.h"
class GameUI {
public:
GameUI(): Grid{Assets} {}
void HandleEvent(SDL_Event& E) {
Grid.HandleEvent(E);
RestartButton.HandleEvent(E);
}
void Tick(Uint32 DeltaTime) {
Grid.Tick(DeltaTime);
}
void Render(SDL_Surface* Surface) {
Grid.Render(Surface);
RestartButton.Render(Surface);
}
private:
Grid Grid;
Assets Assets;
RestartButton RestartButton;
};
#pragma once
#include <SDL.h>
#include "GameConfig.h"
#include "SnakeData.h"
class GameState {
public:
void HandleEvent(SDL_Event& E) {
using namespace UserEvents;
if (E.type == SDL_KEYDOWN) {
HandleKeyEvent(E.key);
} else if (E.type == APPLE_EATEN) {
++Snake.Length;
} else if (E.type == RESTART_GAME) {
RestartGame();
}
}
void Tick(Uint32 DeltaTime) {
if (IsPaused) return;
ElapsedTime += DeltaTime;
if (ElapsedTime >= Config::ADVANCE_INTERVAL) {
ElapsedTime = 0;
UpdateSnake();
}
}
private:
void HandleKeyEvent(SDL_KeyboardEvent& E) {
switch (E.keysym.sym) {
case SDLK_UP:
case SDLK_w:
if (Snake.Direction != Down) {
NextDirection = Up;
}
break;
case SDLK_DOWN:
case SDLK_s:
if (Snake.Direction != Up) {
NextDirection = Down;
}
break;
case SDLK_LEFT:
case SDLK_a:
if (Snake.Direction != Right) {
NextDirection = Left;
}
break;
case SDLK_RIGHT:
case SDLK_d:
if (IsPaused) {
IsPaused = false;
NextDirection = Right;
UpdateSnake();
} else if (Snake.Direction != Left) {
NextDirection = Right;
}
break;
}
}
void UpdateSnake() {
Snake.Direction = NextDirection;
switch (NextDirection) {
case Up:
Snake.HeadRow--;
break;
case Down:
Snake.HeadRow++;
break;
case Left:
Snake.HeadCol--;
break;
case Right:
Snake.HeadCol++;
break;
}
SDL_Event Event{UserEvents::ADVANCE};
Event.user.data1 = &Snake;
SDL_PushEvent(&Event);
}
void RestartGame() {
IsPaused = true;
ElapsedTime = 0;
Snake = {Config::GRID_ROWS / 2, 3, 2, Right};
NextDirection = Right;
}
bool IsPaused{true};
Uint32 ElapsedTime{0};
SnakeData Snake{
Config::GRID_ROWS / 2, 3, 2, Right};
MovementDirection NextDirection{Right};
};
#pragma once
#include <SDL.h>
#include "GameConfig.h"
#include "SnakeData.h"
#include "Assets.h"
enum CellState { Snake, Apple, Empty };
class Cell {
public:
Cell(int Row, int Column, Assets& Assets)
: Row(Row),
Column(Column),
Assets{Assets}
{
Initialize();
}
void HandleEvent(SDL_Event& E) {
using namespace UserEvents;
if (E.type == ADVANCE) {
Advance(E.user);
} else if (E.type == APPLE_EATEN) {
if (CellState == Snake) {
++SnakeDuration;
}
} else if (E.type == RESTART_GAME) {
Initialize();
}
}
void Tick(Uint32 DeltaTime) {}
void Render(SDL_Surface* Surface) {
SDL_FillRect(Surface, &BackgroundRect,
SDL_MapRGB(
Surface->format,
BackgroundColor.r,
BackgroundColor.g,
BackgroundColor.b
)
);
if (CellState == Apple) {
Assets.Apple.Render(Surface, &BackgroundRect);
} else if (CellState == Snake) {
SDL_FillRect(Surface, &BackgroundRect,
SDL_MapRGB(
Surface->format,
Config::SNAKE_COLOR.r,
Config::SNAKE_COLOR.g,
Config::SNAKE_COLOR.b
)
);
}
}
bool PlaceApple() {
if (CellState != Empty) return false;
CellState = Apple;
return true;
}
private:
void Initialize() {
CellState = Empty;
SnakeDuration = 0;
int MiddleRow{Config::GRID_ROWS / 2};
if (Row == MiddleRow && Column == 2) {
CellState = Snake;
SnakeDuration = 1;
} else if (Row == MiddleRow && Column == 3) {
CellState = Snake;
SnakeDuration = 2;
} else if (Row == MiddleRow && Column == 11) {
CellState = Apple;
}
}
void Advance(SDL_UserEvent& E) {
SnakeData* Data{static_cast<SnakeData*>(
E.data1)};
bool isThisCell{
Data->HeadRow == Row &&
Data->HeadCol == Column
};
if (isThisCell) {
if (CellState == Apple) {
SDL_Event Event{UserEvents::APPLE_EATEN};
SDL_PushEvent(&Event);
}
CellState = Snake;
SnakeDuration = Data->Length;
} else if (CellState == Snake) {
SnakeDuration--;
if (SnakeDuration <= 0) {
CellState = Empty;
}
}
}
int Row;
int Column;
CellState CellState;
int SnakeDuration;
SDL_Rect BackgroundRect{
Column * Config::CELL_SIZE + Config::PADDING,
Row * Config::CELL_SIZE + Config::PADDING,
Config::CELL_SIZE, Config::CELL_SIZE};
SDL_Color BackgroundColor{
(Row + Column) % 2 == 0
? Config::CELL_COLOR_A
: Config::CELL_COLOR_B};
Assets& Assets;
};
Files not listed above have not been changed since the previous section.
We've implemented UI positioning, text rendering, and mouse interaction handling to create a functional button that allows players to restart the game at any time. We've also added a pause mechanism that starts the game in a paused state and resumes when the player presses the right arrow key.
The key things we did in this lesson include:
SDL_Rect
structuresText
class to render text within our buttonRESTART_GAME
event to notify other components when the player wants to restartGiving our Snake game the ability to pause itself, and adding a clickable button for restarting the game
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games