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.

Starting Point
We'll continue from where we left off in the previous lesson. Our project already has a grid of interactive cells, win/loss detection, and a restart button.
Files
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 argument.
Our score counter will also need to be notified of events and render itself, so let's add HandleEvent() and Render() methods:
src/Snake/ScoreCounter.h
#pragma once
#include <SDL3/SDL.h>
#include "Assets.h"
class ScoreCounter {
public:
ScoreCounter(Assets& Assets) : AssetList{Assets} {}
void HandleEvent(const SDL_Event& E) {}
void Render(SDL_Surface* Surface) {}
private:
Assets& AssetList;
};Over in our SnakeUI 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:
src/SnakeUI.h
#pragma once
#include <SDL3/SDL.h>
#include "Assets.h"
#include "Snake/Grid.h"
#include "Snake/RestartButton.h"
#include "Snake/ScoreCounter.h"
class SnakeUI {
public:
SnakeUI()
: Grid{AssetList},
ScoreCounter{AssetList},
RestartBtn{
Config::WINDOW_WIDTH - 150,
Config::GRID_HEIGHT + Config::PADDING * 2,
150 - Config::PADDING,
Config::FOOTER_HEIGHT - Config::PADDING
} {}
void HandleEvent(const SDL_Event& E) {
Grid.HandleEvent(E);
ScoreCounter.HandleEvent(E);
RestartBtn.HandleEvent(E);
}
void Tick(Uint64 DeltaTime) {
Grid.Tick(DeltaTime);
}
void Render(SDL_Surface* Surface) {
Grid.Render(Surface);
ScoreCounter.Render(Surface);
RestartBtn.Render(Surface);
}
private:
Assets AssetList;
Grid Grid;
ScoreCounter ScoreCounter;
RestartButton RestartBtn;
};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 Globals.h to control what color this background should be:
src/Globals.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:
src/Snake/ScoreCounter.h
#pragma once
#include <SDL3/SDL.h>
#include "Assets.h"
#include "Globals.h"
class ScoreCounter {
public:
ScoreCounter(Assets& Assets) : AssetList{Assets} {}
void HandleEvent(const SDL_Event& E) {}
void Render(SDL_Surface* Surface) {}
private:
Assets& AssetList;
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 theyposition to beConfig::GRID_HEIGHT + Config::PADDING * 2.w: We'll set the width of our score counter based on ourMaxScorevariable. For example:MaxScore > 99 ? 250 : 190will 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 sethtoConfig::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:
src/Snake/ScoreCounter.h
#pragma once
#include <SDL3/SDL.h>
#include "Assets.h"
class ScoreCounter {
public:
ScoreCounter(Assets& Assets) : AssetList{Assets} {}
void HandleEvent(const SDL_Event& E) {}
void Render(SDL_Surface* Surface) {}
private:
Assets& AssetList;
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_COLOR to render our background rectangle in the correct position:
src/Snake/ScoreCounter.h
#pragma once
#include <SDL3/SDL.h>
#include "Assets.h"
class ScoreCounter {
public:
ScoreCounter(Assets& Assets) : AssetList{Assets} {}
void HandleEvent(const SDL_Event& E) {}
void Render(SDL_Surface* Surface) {
using namespace Config;
SDL_FillSurfaceRect(Surface, &BackgroundRect,
SDL_MapRGB(
SDL_GetPixelFormatDetails(Surface->format),
nullptr,
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 AssetList 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:
src/Snake/ScoreCounter.h
#pragma once
#include <SDL3/SDL.h>
#include "Assets.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:
src/Snake/ScoreCounter.h
#pragma once
#include <SDL3/SDL.h>
#include "Assets.h"
class ScoreCounter {
public:
// ...
void Render(SDL_Surface* Surface) {
using namespace Config;
SDL_FillSurfaceRect(Surface, &BackgroundRect,
SDL_MapRGB(
SDL_GetPixelFormatDetails(Surface->format),
nullptr,
SCORE_BACKGROUND_COLOR.r,
SCORE_BACKGROUND_COLOR.g,
SCORE_BACKGROUND_COLOR.b
)
);
AssetList.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:
src/Snake/ScoreCounter.h
#pragma once
#include <SDL3/SDL.h>
#include "Assets.h"
class ScoreCounter {
// ...
private:
int CurrentScore{0};
// ...
};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:
src/Snake/ScoreCounter.h
// ...
class ScoreCounter {
public:
// ...
void HandleEvent(const 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:
src/Snake/ScoreCounter.h
#pragma once
#include <SDL3/SDL.h>
#include <string>
#include "Assets.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:
src/Snake/ScoreCounter.h
#pragma once
#include <SDL3/SDL.h>
#include <string>
#include "Assets.h"
#include "Engine/Text.h"
class ScoreCounter {
// ...
private:
// ...
Engine::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
xposition to beAppleRect.x + AppleRect.w + Config::PADDING. - Vertically, we want the text aligned with the apple image, so we'll set
yto 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:
src/Snake/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():
src/Snake/ScoreCounter.h
// ...
class ScoreCounter {
public:
// ...
void Render(SDL_Surface* Surface) {
using namespace Config;
SDL_FillSurfaceRect(Surface, &BackgroundRect,
SDL_MapRGB(
SDL_GetPixelFormatDetails(Surface->format),
nullptr,
SCORE_BACKGROUND_COLOR.r,
SCORE_BACKGROUND_COLOR.g,
SCORE_BACKGROUND_COLOR.b
)
);
AssetList.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:
src/Snake/ScoreCounter.h
// ...
#include "Engine/Text.h"
class ScoreCounter {
public:
// ...
void HandleEvent(const 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
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 game. We implemented background rendering, image display, and text updates to create a user interface element that tracks the player's progress.
In the final lesson, we'll finish off by updating the visual rendering of our snake, letting it smoothly slide between cells using frame-by-frame animation.
Animating Snake Movement
Animate the snake's movement across cells for a smooth, dynamic visual effect.