Building the Score Display

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

Ryan McCombe
Published

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 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:

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 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:

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.

Next Lesson
Lesson 65 of 129

Animating Snake Movement

Learn to animate the snake's movement across cells for a smooth, dynamic visual effect.

Have a question about this lesson?
Purchase the course to ask your own questions