Building the Score Display
Build a dynamic score counter that tracks the player's progress and displays it with custom graphics and text.
In this lesson, we'll implement a score counter for our Snake game to help players track their progress.
We'll build a UI component that displays the current score alongside the maximum possible score, complete with a custom background and an apple icon.

Creating a ScoreCounter
Class
Let's begin by creating a ScoreCounter
class to manage our UI element. To get access to the apple image, we'll accept a reference to the Assets
manager as a constructor.
Our score counter will also need to be notified of events and render itself, so let's add HandleEvent()
and Render()
methods:
// ScoreCounter.h
#pragma once
#include <SDL.h>
#include "Assets.h"
class ScoreCounter {
public:
ScoreCounter(Assets& Assets) : Assets{Assets} {}
void HandleEvent(SDL_Event& E) {}
void Render(SDL_Surface* Surface) {}
private:
Assets& Assets;
};
Over in our GameUI
class, we'll create an instance of our ScoreCounter
, pass the Assets
to the constructor, and hook it up to our HandleEvent()
and Render()
pipelines:
// GameUI.h
// ...
#include "ScoreCounter.h"
class GameUI {
public:
GameUI()
: Grid{Assets},
ScoreCounter{Assets}
{}
void HandleEvent(SDL_Event& E) {
Grid.HandleEvent(E);
ScoreCounter.HandleEvent(E);
RestartButton.HandleEvent(E);
}
void Tick(Uint32 DeltaTime) {
Grid.Tick(DeltaTime);
}
void Render(SDL_Surface* Surface) {
Grid.Render(Surface);
ScoreCounter.Render(Surface);
RestartButton.Render(Surface);
}
private:
ScoreCounter ScoreCounter;
// ...
};
Background Rendering
To render our ScoreCounter
, we'll start by rendering a solid rectangle, acting as the background of our element. We'll add a configuration variable to GameConfig.h
to control what color this background should be:
// GameConfig.h
// ...
namespace Config{
// ...
// Colors
inline constexpr SDL_Color SCORE_BACKGROUND_COLOR{
73, 117, 46, 255};
}
// ...
To control where our ScoreCounter
is positioned within the window, we'll need to define an SDL_Rect
, with the usual x
, y
, w
, and h
values.
In this case, the width (w
) for our rectangle depends on how much text is going to be rendered on top of it. If our maximum possible score contains 3 digits, we need more space than if it contains only 2 - for example, "12/75" requires more space than "123/520".
To help with this, we'll calculate a MaxScore
variable from the maximum length of our snake, and we'll subtract 2 because our snake starts with a length of 2:
// ScoreCounter.h
// ...
#include "GameConfig.h"
class ScoreCounter {
// ...
private:
int MaxScore{Config::MAX_LENGTH - 2};
// ...
};
Next, we'll define an SDL_Rect
scaled in the following way:
x
: We want the score counter aligned to the left of our window with some padding to offset it from the edge, so our horizontal position will simply beConfig::PADDING
.y
: We want the vertical position to be below the grid. The grid's bottom edge is located atConfig::PADDING + Config::GRID_HEIGHT
. We want additional padding between our grid and score counter, so we'll set they
position to beConfig::GRID_HEIGHT + Config::PADDING * 2
.w
: We'll set the width of our score counter based on ourMaxScore
variable. For example:MaxScore > 99 ? 250 : 190
will adopt a size of 250 pixels if we need to support 3 digits or 190 otherwise.h
: We'll set our score counter to match the full height of the footer, with some padding below. So we'll sety
toConfig::FOOTER_HEIGHT - Config::PADDING
Let's add this to our class. Note that because it depends on the MaxScore
variable, we need to initialize it after MaxScore
:
// ScoreCounter.h
// ...
class ScoreCounter {
// ...
private:
// Snake's initial length is 2
int MaxScore{Config::MAX_LENGTH - 2};
SDL_Rect BackgroundRect{
Config::PADDING,
Config::GRID_HEIGHT + Config::PADDING * 2,
MaxScore > 99 ? 250 : 190,
Config::FOOTER_HEIGHT - Config::PADDING
};
// ...
};
We can now update our Render()
function to use these BackgroundRect
and Config::SCORE_BACKGROUND
to render our background rectangle in the correct position:
// ScoreCounter.h
// ...
class ScoreCounter {
public:
// ...
void Render(SDL_Surface* Surface) {
using namespace Config;
SDL_FillRect(Surface, &BackgroundRect,
SDL_MapRGB(Surface->format,
SCORE_BACKGROUND_COLOR.r,
SCORE_BACKGROUND_COLOR.g,
SCORE_BACKGROUND_COLOR.b
));
}
// ...
};
Running our program, we should now see our score counter's background rendered in the correct location:

Apple Rendering
Let's add the apple image to our score counter. We already have access to the apple image through the Assets
member, so we just need to define an SDL_Rect
to control where the image is rendered.
The x
and y
positions will match the x
and y
positions of our background rectangle, with some additional padding to move the apple away from the edge.
Our apple image is square, so we can set both the w
and h
of our image to match the background rectangle's height. We'll subtract Config::PADDING * 2
from these values, to add spacing on both sides of each dimension:
// ScoreCounter.h
// ...
class ScoreCounter {
// ...
private:
// ...
SDL_Rect AppleRect{
BackgroundRect.x + Config::PADDING,
BackgroundRect.y + Config::PADDING,
BackgroundRect.h - Config::PADDING * 2,
BackgroundRect.h - Config::PADDING * 2
};
};
We can now update our Render()
function to render the apple image in this rectangle:
// ScoreCounter.h
// ...
class ScoreCounter {
public:
// ...
void Render(SDL_Surface* Surface) {
using namespace Config;
SDL_FillRect(Surface, &BackgroundRect,
SDL_MapRGB(Surface->format,
SCORE_BACKGROUND_COLOR.r,
SCORE_BACKGROUND_COLOR.g,
SCORE_BACKGROUND_COLOR.b
));
Assets.Apple.Render(Surface, &AppleRect);
}
// ...
};

Score Tracking
We want to render text in the format "12/34" where "12" is our current score, and "34" is the maximum possible score. We already have the MaxScore
variable, so we just need to add the CurrentScore
, which we'll initialize to 0
:
// ScoreCounter.h
// ...
class ScoreCounter {
// ...
private:
int CurrentScore{0};
int MaxScore{Config::MAX_LENGTH - 2};
// ...
};
In our HandleEvent()
function, we'll increment this score every time an apple is eaten, and set it back to 0
every time our game restarts:
// ScoreCounter.h
// ...
class ScoreCounter {
public:
// ...
void HandleEvent(SDL_Event& E) {
if (E.type == UserEvents::APPLE_EATEN) {
++CurrentScore;
} else if (E.type == UserEvents::RESTART_GAME) {
CurrentScore = 0;
}
}
// ...
};
To create the CurrentScore/MaxScore
string, we'll create a private GetScoreString()
helper:
// ScoreCounter.h
// ...
class ScoreCounter {
// ...
private:
std::string GetScoreString() {
return std::to_string(CurrentScore) + "/"
+ std::to_string(MaxScore);
}
// ...
};
Text Rendering
To manage our text, we'll create an instance of our Text
class from Engine/Text.h
. For the constructor, we pass in the initial content of our text, and the font size we want to use:
// ScoreCounter.h
// ...
#include "Engine/Text.h"
class ScoreCounter {
// ...
private:
// ...
Text Text{GetScoreString(), 40};
};
We need to define another SDL_Rect
to control where our text should be rendered.
- Horizontally, we want the text to be after our apple image, with some additional padding. So we'll set the
x
position to beAppleRect.x + AppleRect.w + Config::PADDING
. - Vertically, we want the text aligned with the apple image, so we'll set
y
to be the same asAppleRect.y
.
The Render()
method within Text
uses SDL_BlitSurface()
, which doesn't require the SDL_Rect
to have a width and height. As such, we can just set these to 0
.
Let's create an SDL_Rect
object called TextRect
that uses these values. Note that because TextRect
depends on AppleRect
, we should ensure AppleRect
is initialized first:
// ScoreCounter.h
// ...
class ScoreCounter {
// ...
private:
// ...
SDL_Rect TextRect{
AppleRect.x + AppleRect.w + Config::PADDING,
AppleRect.y,
0, 0};
};
We now have what we need to render our Text
object to the Surface
provided to ScoreCounter::Render()
:
// ScoreCounter.h
// ...
class ScoreCounter {
public:
// ...
void Render(SDL_Surface* Surface) {
using namespace Config;
SDL_FillRect(Surface, &BackgroundRect,
SDL_MapRGB(Surface->format,
SCORE_BACKGROUND_COLOR.r,
SCORE_BACKGROUND_COLOR.g,
SCORE_BACKGROUND_COLOR.b
));
Assets.Apple.Render(Surface, &AppleRect);
Text.Render(Surface, &TextRect);
}
// ...
};
Running our program, we should now see our score rendered in the correct position:

Finally, we need to update the content our Text
object is rendering any time the score changes. We can do this in our HandleEvent()
function:
// ScoreCounter.h
// ...
#include "Engine/Text.h"
class ScoreCounter {
public:
// ...
void HandleEvent(SDL_Event& E) {
if (E.type == UserEvents::APPLE_EATEN) {
++CurrentScore;
Text.SetText(GetScoreString());
} else if (E.type == UserEvents::RESTART_GAME) {
CurrentScore = 0;
Text.SetText(GetScoreString());
}
}
// ...
};
Complete Code
Complete versions of the files we changed in this part are available below
Files not listed above have not been changed since the previous section.
Summary
In this lesson, we built a score counter UI component for our Snake game. We implemented background rendering, image display, and text updates to create a polished user interface element that tracks the player's progress.
In the final lesson, we'll update the visual rendering of our snake, letting it slide between cells using frame-by-frame animation.
Animating Snake Movement
Learn to animate the snake's movement across cells for a smooth, dynamic visual effect.