Building the Score Display

Build a dynamic score counter that tracks the player's progress and displays it with custom graphics and text.
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Digital art showing a retro snake game
Ryan McCombe
Ryan McCombe
Posted

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.

Screenshot showing the final state of the snake game

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 be Config::PADDING.
  • y: We want the vertical position to be below the grid. The grid’s bottom edge is located at Config::PADDING + Config::GRID_HEIGHT. We want additional padding between our grid and score counter, so we’ll set the y position to be Config::GRID_HEIGHT + Config::PADDING * 2.
  • w: We’ll set the width of our score counter based on our MaxScore 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 set y to Config::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:

Screenshot showing our score counter background in the bottom left of the UI

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);
  }
  // ...
};
Screenshot showing our score counter background with an apple icon in the bottom left of the UI

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 be AppleRect.x + AppleRect.w + Config::PADDING.
  • Vertically, we want the text aligned with the apple image, so we’ll set y to be the same as AppleRect.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:

Screenshot showing the final state of the snake game

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

#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 GAME_WON =
    SDL_RegisterEvents(1);
  inline Uint32 GAME_LOST =
    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");

  inline constexpr int MAX_LENGTH{
    GRID_COLUMNS * GRID_ROWS};

  // 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 SNAKE_LOST_COLOR{
    227, 67, 97, 255};
  inline constexpr SDL_Color SNAKE_VICTORY_COLOR{
    255, 140, 0, 255};
  inline constexpr SDL_Color BUTTON_COLOR{
    73, 117, 46, 255};
  inline constexpr SDL_Color BUTTON_HIGHLIGHT_COLOR{
    67, 117, 234, 255};
  inline constexpr SDL_Color SCORE_BACKGROUND_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 "Engine/Text.h"
#include "Assets.h"
#include "GameConfig.h"

class ScoreCounter {
public:
  ScoreCounter(Assets& Assets) : Assets{Assets} {}

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

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

private:
  std::string GetScoreString() {
    return std::to_string(CurrentScore) + "/"
      + std::to_string(MaxScore);
  }

  Assets& Assets;
  int CurrentScore{0};
  int MaxScore{Config::MAX_LENGTH - 2};
  Text Text{GetScoreString(), 40};

  SDL_Rect BackgroundRect{
    Config::PADDING,
    Config::GRID_HEIGHT + Config::PADDING * 2,
    MaxScore > 99 ? 250 : 190,
    Config::FOOTER_HEIGHT - Config::PADDING};

  SDL_Rect AppleRect{
    BackgroundRect.x + Config::PADDING,
    BackgroundRect.y + Config::PADDING,
    BackgroundRect.h - Config::PADDING * 2,
    BackgroundRect.h - Config::PADDING * 2};

  SDL_Rect TextRect{
    AppleRect.x + AppleRect.w + Config::PADDING,
    AppleRect.y,
    0, 0};
};
#pragma once
#include <SDL.h>
#include "Grid.h"
#include "Assets.h"
#include "ScoreCounter.h"
#include "RestartButton.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:
  Grid Grid;
  Assets Assets;
  ScoreCounter ScoreCounter;
  RestartButton RestartButton;
};

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.

Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Ryan McCombe
Ryan McCombe
Posted
Lesson Contents

Building the Score Display

Build a dynamic score counter that tracks the player's progress and displays it with custom graphics and text.

sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Project: Snake

    68.
    Building the Score Display

    Build a dynamic score counter that tracks the player's progress and displays it with custom graphics and text.


sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

This course includes:

  • 84 Lessons
  • 92% Positive Reviews
  • Regularly Updated
  • Help and FAQs
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved