Chapter 21's runner shares her landscape with three ghosts, but the program that runs them has a seam down the middle. The player is updated and drawn on her own, the ghosts in loops of their own, and the scenery and the HUD have lines of their own too, because through an Entity, only Entity’s functions would run. Every new kind of thing in the game would mean new lines in both halves of the game loop.
Chapter 22 took away the reason for the seam. With virtual, a call through a base pointer reaches the object's own function, and with interfaces, classes that aren't related at all can be used in the same way, as long as they keep the same promises. This chapter puts both to work. Every runner goes into one list that owns her, and everything in the game, the runners, the scenery, and the HUD, is updated by one loop and drawn by another.
Then comes the payoff. The runner gets a shadow: a dark, see-through runner who follows her wherever she goes, and jumps whenever she jumps. It's a whole new kind of runner, with behavior of its own, and adding it takes one new class, two small getters in Entity, and one new line in runGame. Neither loop changes at all.
SDL3 Projects/Animated Character Interfaces — the complete source for this chapter lives here, with its nineteen files and the assets folder.In this chapter, we will:
- Start a new project from Chapter 21's files
- Write two interfaces,
IUpdatableandIDrawable - Make the runners and the HUD keep both promises, with
override - Gather the four pictures of the landscape into a
Sceneryclass that only draws - Keep every runner in one list of
unique_ptrs, and update and draw everything through two views - Add a new kind of runner, a shadow, without touching either loop
- Play the game, experiment with it, fix the most common mistakes, and try an optional AI exercise
Let's build it.
Setting Up the Project
This project starts from Chapter 21's, just as Chapter 21 started from Chapter 19's. Here's the checklist:
- Choose File > New > Project, pick Empty Project (the one tagged C++, Windows, and Console), name it
Animated Character Interfaces, and click Create. If you exported a template, as Chapter 11 suggested, use that instead. - Right-click the project, choose Open Folder in File Explorer, and copy these into that folder from your Chapter 21 project folder: the thirteen files ending in
.hand.cpp, theassetsfolder,SDL3.dll, andSDL3_image.dll. - In Solution Explorer, right-click Source Files, choose Add > Existing Item, and pick the seven
.cppfiles in the new project's folder. You can pick several at once by holding Ctrl as you click them. Then right-click Header Files, and add the six.hfiles the same way. - If you didn't use a template, open the project's Properties and make Chapter 19's four changes, for all configurations and all platforms: the two include folders, C++20, the two library folders, and
SDL3.lib;SDL3_image.lib.
As before, add the copies in the new folder, not Chapter 21's own files, or every change in this chapter would land in Chapter 21's project. If you skipped Chapter 21, its finished files are in the book's repository, in SDL3 Projects/Animated Character Inheritance.
Checkpoint: Press F5. You have Chapter 21's program again, in a new project: the runner and her three ghosts, with the frame rate in the corner.
Planning the Change
Here's everything the project will contain when we've finished:
| File | What happens to it |
|---|---|
IUpdatable.h, IDrawable.h |
New: the two interfaces, one promise each |
Scenery.h, Scenery.cpp |
New: the landscape, which owns its four pictures, and only draws |
Shadow.h, Shadow.cpp |
New: a runner who follows the player |
Entity.h, Entity.cpp |
Changed: keeps both promises, and answers two questions for the shadow |
Player.h, Ghost.h |
Changed: each update says override |
HUD.h |
Changed: keeps both promises |
main.cpp |
Changed: one list of runners, two views, and two loops |
Texture.h, Texture.cpp, Animator.h, Animator.cpp, Player.cpp, Ghost.cpp, HUD.cpp |
Unchanged |
The idea is Chapter 22's, applied to the runner's world, and Figure 23.1 shows the shape it gives the program. There are two kinds of relationship in it. The runners are a family: a player, a ghost, and a shadow are each an Entity, and they share its code, which is the is-a relationship of Chapters 20 and 21.
The interfaces are something else. The first, IUpdatable, says "I change from frame to frame", and the second, IDrawable, says "I can draw myself". They're promises about what a class can do, not about what it is. The runners keep both promises, and so does the HUD, which is nothing like a runner, while the scenery keeps just one, because it never changes.

We'll build it in four steps, each ending with a program you can run: the interfaces and the promises first, then the scenery, then the one list and the two loops, and last, the shadow. The first three change how the program is built, but not what it does, so at each of their checkpoints, the game should look exactly as it does now.
The Two Interfaces
Start with the promises. Add a header called IUpdatable.h, and below its #pragma once, type the rest, so that the file reads like this:
#pragma once
// Anything that changes from frame to frame
class IUpdatable
{
public:
virtual ~IUpdatable() = default;
virtual void update(float delta) = 0;
};
In the preceding code, IUpdatable is exactly Chapter 22's interface: a virtual destructor, and one pure virtual function, update, which takes the delta time. Anything that changes from frame to frame can promise it. There's no .cpp file, because an interface has no code to put in one.
Now add a header called IDrawable.h, and below its #pragma once, type the rest, so that the file reads like this:
#pragma once
#include <SDL3/SDL.h>
// Anything that can draw itself
class IDrawable
{
public:
virtual ~IDrawable() = default;
virtual void draw(SDL_Renderer* renderer) const = 0;
};
In the preceding code, IDrawable promises draw, which takes the renderer, as every draw in the game already does. It's const, because drawing something doesn't change it, and the header includes SDL's, because the promise mentions SDL_Renderer.
Why two interfaces, rather than one IGameObject with both functions in it? Because not everything does both. The scenery is drawn, but never changes, and Chapter 22's wave timer changed, but was never drawn. With one big interface, each of them would have to write an empty function it doesn't need, to keep a promise it never meant to make. Small interfaces, each promising one thing, let every class promise exactly what it does.
So a class can make as many promises as it keeps, and no more. Next, the classes that keep them.
Keeping the Promises
Entity
Every runner changes from frame to frame and draws herself, so Entity keeps both promises, and every class derived from it keeps them too. In Entity.h, add these below #include "Animator.h":
#include "IDrawable.h"
#include "IUpdatable.h"
In the preceding code, the two interfaces' headers come in, in alphabetical order with the others. Now find these lines:
// Anyone drawn from the runner's sprite sheet: she runs left and right at
// her own pace, turns to face the way she's going, and jumps
class Entity
And change them to this:
// Anyone drawn from the runner's sprite sheet: she runs left and right at
// her own pace, turns to face the way she's going, and jumps. Every runner
// changes from frame to frame and draws herself, so she keeps both promises
class Entity : public IUpdatable, public IDrawable
In the preceding code, Entity gets two base classes, as Chapter 22's Enemy did, so every runner is an IUpdatable and an IDrawable. The comment says why. Next, say which functions keep the promises. Find these lines:
void update(float delta);
void draw(SDL_Renderer* renderer) const;
And change them to this:
void update(float delta) override;
void draw(SDL_Renderer* renderer) const override;
In the preceding code, update and draw now end with override, because they're Entity’s versions of the interfaces' pure virtual functions. Nothing in Entity.cpp needs to change. A definition doesn't repeat override, just as it doesn't repeat a default argument.
Player and Ghost
The player's update and the ghost's already replace Entity’s, and now that Entity’s is virtual, they override it. In Player.h, find this line:
void update(float delta);
And change it to this:
void update(float delta) override;
In the preceding code, override asks the compiler to check that the player's update really does replace one in a base class. Now do the same in Ghost.h. Find this line:
void update(float delta);
And change it to this:
void update(float delta) override;
In the preceding code, the ghost's update says override too. Both functions were written in Chapter 21, before virtual existed, and they keep exactly the same bodies.
The HUD
The HUD counts a frame every frame, and draws the count, so it can keep both promises as well, though it's nothing like a runner. In HUD.h, add these below #include <SDL3/SDL.h>:
#include "IDrawable.h"
#include "IUpdatable.h"
In the preceding code, the HUD's header includes the two interfaces, just as Entity.h does. Then find these lines:
// The frame rate, counted over each second, and drawn from a strip of
// digits in the window's top-left corner
class HUD
And change them to this:
// The frame rate, counted over each second, and drawn from a strip of
// digits in the window's top-left corner. It changes and it's drawn, like
// a runner, though it's nothing like one
class HUD : public IUpdatable, public IDrawable
In the preceding code, the HUD becomes an IUpdatable and an IDrawable. It has no base class in common with the runners, and doesn't need one. Last, find these lines:
void update(float delta);
void draw(SDL_Renderer* renderer) const;
And change them to this:
void update(float delta) override;
void draw(SDL_Renderer* renderer) const override;
In the preceding code, the HUD's two functions override the interfaces', and HUD.cpp needs no change at all.
Checkpoint: Press F5. The game plays exactly as it did. The function runGame still updates and draws everything by name, but the runners and the HUD now keep their promises, and the compiler has checked every override.
The Scenery
The four pictures of the landscape are drawn one after another every frame, and they never change. They belong together, and Chapter 19's AI exercise suggested exactly this: a Scenery class, with the four textures as its members. Now it has a reason to exist, because it can keep one of the promises. Add a header called Scenery.h and a source file called Scenery.cpp. Below the #pragma once in Scenery.h, type the rest, so that the file reads like this:
#pragma once
#include <SDL3/SDL.h>
#include "IDrawable.h"
#include "Texture.h"
// The landscape behind everything: four pictures, each filling the window,
// drawn from the back to the front. It never changes, so it's drawn, but
// never updated
class Scenery : public IDrawable
{
public:
Scenery(SDL_Renderer* renderer);
bool isLoaded() const;
void draw(SDL_Renderer* renderer) const override;
private:
Texture sky_;
Texture farHills_;
Texture nearHills_;
Texture ground_;
};
In the preceding code, Scenery is an IDrawable, and nothing else. Its four Textures are ordinary members, so the scenery owns its pictures. They're loaded when it's made, and destroyed along with it, by their own destructors, as Chapter 19's Texture promised. The constructor needs the renderer to load them, and isLoaded says whether all four made it.
Open Scenery.cpp, and start it with the constructor:
#include "Scenery.h"
// Each picture is loaded as its member is made, so the scenery owns all four
Scenery::Scenery(SDL_Renderer* renderer)
: sky_(renderer, "assets/sky.png"),
farHills_(renderer, "assets/hills_far.png"),
nearHills_(renderer, "assets/hills_near.png"),
ground_(renderer, "assets/ground.png")
{
}
In the preceding code, each texture is made in the initializer list, with the renderer and its picture's path. It has to be, because a Texture has no default constructor, just as Chapter 19's Player had to set its reference in the list. The members are made in the order they're declared, which is also the order they're drawn in.
Below the constructor, with a blank line in between, add isLoaded:
bool Scenery::isLoaded() const
{
return sky_.isLoaded() && farHills_.isLoaded() &&
nearHills_.isLoaded() && ground_.isLoaded();
}
In the preceding code, the scenery is loaded if all four of its pictures are. If one isn't, its Texture has already logged which, as the comment in runGame says.
Below isLoaded, with a blank line in between, add draw:
void Scenery::draw(SDL_Renderer* renderer) const
{
sky_.draw(renderer, nullptr, nullptr);
farHills_.draw(renderer, nullptr, nullptr);
nearHills_.draw(renderer, nullptr, nullptr);
ground_.draw(renderer, nullptr, nullptr);
}
In the preceding code, draw is the four lines from runGame, moved here: each picture fills the window, from the sky at the back to the ground at the front. The function is const, to match the promise.
Now put the scenery to work in runGame. In main.cpp, add this below #include "Texture.h":
#include "Scenery.h"
In the preceding code, main.cpp includes the new class. Then find these lines, at the start of runGame:
Texture sky(renderer, "assets/sky.png");
Texture farHills(renderer, "assets/hills_far.png");
Texture nearHills(renderer, "assets/hills_near.png");
Texture ground(renderer, "assets/ground.png");
And change them to this:
Scenery scenery(renderer);
In the preceding code, one Scenery takes the place of the four textures, and loads all four pictures itself. Then find the check below them:
if (!sky.isLoaded() || !farHills.isLoaded() || !nearHills.isLoaded() ||
!ground.isLoaded() || !runnerSheet.isLoaded() || !digits.isLoaded())
{
return false;
}
And change it to this:
if (!scenery.isLoaded() || !runnerSheet.isLoaded() || !digits.isLoaded())
return false;
In the preceding code, the check asks the scenery about its four pictures at once. It's short enough now for one line, so the return needs no braces. Last, find these lines, in the drawing:
sky.draw(renderer, nullptr, nullptr);
farHills.draw(renderer, nullptr, nullptr);
nearHills.draw(renderer, nullptr, nullptr);
ground.draw(renderer, nullptr, nullptr);
And change them to this:
scenery.draw(renderer);
In the preceding code, the scenery draws itself. This line won't last long, because the next section replaces it, along with every other line of drawing.
Checkpoint: Press F5. The game looks exactly the same, with the landscape now drawn by the scenery.
One List for Every Runner
Now for the big change. Everything in runGame that names a runner, the HUD, or the scenery one at a time is about to go, replaced by a list that owns the runners, and two views of everything. There are eleven changes to main.cpp, and Figure 23.2 shows where each one goes, lettered in the order we'll make them.

First, change A, the headers. Find this line:
#include <vector> // std::vector, for the ghosts
And change it to this:
#include <memory> // std::unique_ptr, for the runners
#include <vector> // std::vector, for the runners and the views
In the preceding code, <memory> brings in std::unique_ptr and std::make_unique, and the comment on <vector> says what the vectors are for now.
Change B: add these above #include "Texture.h":
#include "IUpdatable.h"
#include "IDrawable.h"
In the preceding code, main.cpp includes both interfaces, because runGame is about to use them by name. They'd arrive anyway, through Entity.h and the others, but a file should include what it uses, rather than rely on what other headers happen to bring along.
Change C gives the ghosts' colors names. Find this line:
const float GROUND_Y = 452.0f; // the line the runner stands on
And change it to this:
const float GROUND_Y = 452.0f; // the line the runners stand on
// The ghosts' colors, each one see-through
const SDL_Color RED_GHOST = { 255, 110, 110, 150 };
const SDL_Color GREEN_GHOST = { 110, 220, 130, 150 };
const SDL_Color BLUE_GHOST = { 120, 160, 255, 150 };
In the preceding code, the comment on GROUND_Y now says that all the runners stand on it, and the three ghosts' colors become constants, below the others. Each is an SDL_Color, given its value in braces, just as it was in Chapter 21's calls, and change E explains why they've moved out of the calls.
Change D: in runGame, find these lines:
Player player(runnerSheet, GROUND_Y, WINDOW_W);
HUD hud(digits);
And change them to this:
HUD hud(digits);
// The runners take the window's width as a float. std::make_unique
// passes its arguments on exactly as they are, so convert it here, once
const float windowWidth = static_cast<float>(WINDOW_W);
In the preceding code, the line that made the player has gone. She's about to be made on the heap, and owned by the list, along with every other runner, while the HUD stays where it was.
The constant windowWidth is the window's width as a float, which is what the runners' constructors take. Chapter 21 passed WINDOW_W straight to them, and the compiler turned the int into a float without a word, because it could see the value, and that the value fits. Making a runner with std::make_unique is different, because it takes its arguments and passes them on to the constructor exactly as they are, so the conversion would happen deep inside the standard library, where the value is out of sight, and the compiler would warn about it. Converting once, here, with Chapter 2's static_cast, says that we meant it.
Change E is the ghosts themselves. Find these lines:
// Three rivals, each with a pace and a color of her own, see-through
std::vector<Ghost> ghosts;
ghosts.push_back(Ghost(runnerSheet, 150.0f, GROUND_Y, 1, 0.8f,
{ 255, 110, 110, 150 }, WINDOW_W));
ghosts.push_back(Ghost(runnerSheet, 700.0f, GROUND_Y, -1, 1.25f,
{ 110, 220, 130, 150 }, WINDOW_W));
ghosts.push_back(Ghost(runnerSheet, 420.0f, GROUND_Y, -1, 0.6f,
{ 120, 160, 255, 150 }, WINDOW_W));
And change them to this:
// Every runner, owned by this one list, in the order they're drawn: the
// ghosts at the back, then the shadow, and the player in front
std::vector<std::unique_ptr<Entity>> runners;
runners.push_back(std::make_unique<Ghost>(
runnerSheet, 150.0f, GROUND_Y, 1, 0.8f, RED_GHOST, windowWidth));
runners.push_back(std::make_unique<Ghost>(
runnerSheet, 700.0f, GROUND_Y, -1, 1.25f, GREEN_GHOST, windowWidth));
runners.push_back(std::make_unique<Ghost>(
runnerSheet, 420.0f, GROUND_Y, -1, 0.6f, BLUE_GHOST, windowWidth));
In the preceding code, runners is a vector of std::unique_ptr<Entity>, like Chapter 22's owned list of enemies, and each ghost is made by std::make_unique<Ghost>, with the same arguments as before, and pushed straight in. Each call is split after its opening parenthesis, with the arguments on a line of their own, because they wouldn't fit on one. The colors are the constants from change C. A color in braces can't go through std::make_unique, because a list in braces has no type of its own until it knows what it's for, and std::make_unique passes its arguments on without knowing what they're for.
The list's order matters, and the comment says why: it's the order the runners will be drawn in, so the ghosts go first, at the back.
Change F comes in two parts. First, add this below the ghosts, with a blank line in between:
// The keyboard needs to reach the player, so keep a pointer to her,
// which uses her but doesn't own her. The list owns her
std::unique_ptr<Player> newPlayer =
std::make_unique<Player>(runnerSheet, GROUND_Y, windowWidth);
Player* player = newPlayer.get();
In the preceding code, the player is made in a unique_ptr<Player> of her own, newPlayer, rather than straight into the list, because the line after it needs her first: it keeps a second pointer to her. The function get asks a unique_ptr for the raw pointer inside it, without giving up ownership, so player points at the player, but doesn't own her. The keyboard needs it, because run and jump are orders for the player alone, and once she's in the list, she's one Entity among several, with nothing to say which one she is.
A pointer like this, which uses an object that something else owns, is often called an observer, and it's the pointer version of Chapter 19's "used but not owned" references. The comment says who the owner is going to be, and the second part of change F makes it so. Add this below Player* player = newPlayer.get();, with a blank line in between:
runners.push_back(std::move(newPlayer));
In the preceding code, the player is moved into the list, as Chapter 22 moved its chaser, so the list owns her, like every other runner. She goes in last, so she's drawn last, in front of everyone. Afterward, newPlayer is empty, but player still points at her, because moving a unique_ptr doesn't move the object it owns. That's why the observer was taken before the move.
An observer must never delete what it points at, or hand it to another owner. Write delete player; at the end of runGame, and the player is deleted twice, once by you and once by the list, and the game crashes with an access violation as it quits. Making a second unique_ptr from player does the same, with two owners each sure the player is theirs. The rule is simple: the owner deletes, and nobody else.
With the list in charge of the runners, the views can be built.
Change G: add this below runners.push_back(std::move(newPlayer));, with a blank line in between:
// Two views of everything in the game, which own nothing: what changes
// from frame to frame, and what's drawn, from the back to the front
std::vector<IUpdatable*> updatables;
std::vector<IDrawable*> drawables = { &scenery };
for (std::unique_ptr<Entity>& runner : runners)
{
updatables.push_back(runner.get());
drawables.push_back(runner.get());
}
updatables.push_back(&hud);
drawables.push_back(&hud);
In the preceding code, updatables and drawables are the two views, vectors of raw pointers that own nothing, like Chapter 22's two views. The scenery goes into drawables in the braces, as the vector is made, because it's drawn at the very back. Then the loop goes through the runners, by reference, because a unique_ptr can't be copied, and puts each runner into both views, as a pointer from get. An Entity* turns into an IUpdatable* or an IDrawable* on the way in, because an Entity is both. Last, the HUD goes into both views, at the end, so it's updated along with everything else, and drawn over the top of it all.
The views are built once, before the game starts, and they stay right for the whole game, thanks to the unique_ptrs. Chapter 21 warned that a vector can move its elements when it grows, but here, the elements are only unique_ptrs, and the runners they own stay exactly where std::make_unique put them. Push another runner into the list later, and every pointer in the views still points at the right runner. The new runner just needs adding to the views too.
Now the game loop can use the views, and the player's observer.
Change H is the first of two for the keyboard. Find this line:
player.jump();
And change it to this:
player->jump();
In the preceding code, player is a pointer now, so the dot becomes an arrow, as it did for Chapter 22's enemies. Change I is the same. Find this line:
player.run(direction);
And change it to this:
player->run(direction);
In the preceding code, the arrow tells the player which way to run, through the observer.
Change J is the updates. Find these lines:
player.update(delta);
for (Ghost& ghost : ghosts)
ghost.update(delta);
hud.update(delta);
And change them to this:
for (IUpdatable* thing : updatables)
thing->update(delta);
In the preceding code, one loop updates everything that promised to change, through IUpdatable pointers. Each call reaches the object's own update, by dynamic binding: the ghosts wander and wrap around, the player stays in the window, and the HUD counts its frame.
Change K is the drawing. Find these lines:
scenery.draw(renderer);
for (const Ghost& ghost : ghosts)
ghost.draw(renderer);
player.draw(renderer);
hud.draw(renderer);
And change them to this:
for (const IDrawable* thing : drawables)
thing->draw(renderer);
In the preceding code, one loop draws everything that promised to draw, in the order the view holds them: the scenery, the ghosts, the player, and the HUD. The pointers are to const, because drawing changes nothing.
Checkpoint: Press F5. The game plays exactly as it did, again, but now everything in it is updated by one loop, and drawn by another. That's the whole of the game loop's work, and neither loop knows what a ghost is.
A Runner Who Follows
Here's the payoff. The shadow is a new kind of runner: dark, see-through, and always a little behind the player, running when she runs, stopping when she stops, and jumping when she jumps. To follow her, the shadow has to know two things about the player: where she is, and whether she's in the air. Both are private to Entity, so Entity needs to answer two questions. In Entity.h, add these above void update(float delta) override;, with a blank line in between:
float getX() const;
bool isJumping() const;
In the preceding code, getX and isJumping are getters, as in Chapter 18: public and const, so any runner can be asked, and asking changes nothing. They go straight below jump, and the blank line sets the two promises apart from the orders and the questions.
You might expect a getter for x_ to be unnecessary, since it's protected, and the shadow is an Entity too. But protected only opens the door to a derived class's own members. Inside Shadow, x_ means the shadow's own position, and the leader's x_, reached through an Entity reference, is as closed to her as it is to main: try it, and the build stops with C2248: 'Entity::x_': cannot access protected member declared in class 'Entity'.
Now define them. In Entity.cpp, add these below jump, with a blank line in between:
float Entity::getX() const
{
return x_;
}
bool Entity::isJumping() const
{
return jumping_;
}
In the preceding code, each getter returns its member, and that's all.
Now the shadow herself. Add Shadow.h and Shadow.cpp. Below the #pragma once in Shadow.h, type the rest, so that the file reads like this:
#pragma once
#include "Entity.h"
// A dark, see-through runner who follows another wherever she goes, and
// jumps whenever she jumps
class Shadow : public Entity
{
public:
Shadow(const Texture& sheet, const Entity& leader, float x,
float groundY);
void update(float delta) override;
private:
const Entity& leader_; // the runner she follows, used but not owned
};
In the preceding code, Shadow is a derived class of Entity, like Player and Ghost, so she keeps both promises without a word about them. Her constructor takes the sprite sheet, the runner she follows, where she starts, and the height of the ground. She overrides update, and her one member is a reference to her leader, used but not owned, like the player's sprite sheet in Chapter 19.
Open Shadow.cpp, and start it with its constants and the constructor:
#include "Shadow.h"
const float GAP = 90.0f; // how far behind she keeps
const SDL_Color SHADOW_TINT = { 40, 40, 70, 150 }; // dark, and see-through
Shadow::Shadow(const Texture& sheet, const Entity& leader, float x,
float groundY)
: Entity(sheet, x, groundY, 1.0f, SHADOW_TINT),
leader_(leader)
{
}
In the preceding code, GAP is how far behind her leader the shadow waits, and SHADOW_TINT is her color: a dark blue-gray, see-through, with the same alpha as the ghosts. The constructor passes the sheet, the start, the ground, a pace of 1, the same as the player's, and the tint up to Entity, and keeps the leader.
Below the constructor, with a blank line in between, add her update:
void Shadow::update(float delta)
{
// Run after her leader until she's close behind, then wait
float distance = leader_.getX() - x_;
if (distance > GAP)
run(1);
else if (distance < -GAP)
run(-1);
else
run(0);
// Jump when she jumps
if (leader_.isJumping())
jump();
Entity::update(delta);
}
In the preceding code, the shadow works out how far away her leader is, across the window, with getX. If the leader is more than GAP to the right, the shadow runs right. If the leader is more than GAP to the left, the shadow runs left, and otherwise, she stands and waits, still facing the way she last ran, as Chapter 19's runner did.
Then, if the leader is in the air, the shadow jumps too, and jump ignores her if she's already jumping. Last, Entity::update moves her, animates her, and moves her jump on, as it does for every runner. She runs at the same pace as the player, so while the player keeps running, the shadow keeps the same distance behind her.
Last of all, the shadow joins the game. In main.cpp, add this below #include "Ghost.h":
#include "Shadow.h"
In the preceding code, main.cpp includes the shadow's class. Then add this above runners.push_back(std::move(newPlayer));:
runners.push_back(std::make_unique<Shadow>(runnerSheet, *player, 300.0f,
GROUND_Y));
In the preceding code, the shadow is made with std::make_unique, like the ghosts, and pushed into the list after them, but before the player, so she's drawn in front of the ghosts, and behind the player. Her leader is *player, the player the observer points at, and she starts at 300 across, a little to the left of the middle of the window, where the player starts. The views are built after this line, so the shadow goes into both, and that's all it takes. Neither loop changes.
Finally, main.cpp’s opening comment still describes Chapter 21's program. Replace the whole comment with this one:
/*
Animated Character (Interfaces)
The Chapter 23 project from Learning C++ by Building Games
The runner from Chapter 21, with her three ghostly rivals, and now a
shadow of her own, who follows her wherever she goes. Hold the left
or right arrow, or A or D, to run, and press Space, W, or the Up
arrow to jump. The number in the top-left corner is the frame rate.
Escape, or the window's X, quits.
New in this project: virtual functions and interfaces. Every runner
is owned by one list, and everything in the game is updated by one
loop and drawn by another, through IUpdatable and IDrawable.
*/
In the preceding code, the comment describes the shadow, and what's new in the project.
Checkpoint: Press F5. A dark, see-through runner hurries after the player from the left, and stops a little behind her. Run, and she follows, and when you turn around, she turns too, and keeps behind you. Jump, and she jumps with you. The ghosts run on as before, and the frame rate still counts in the corner.
The Complete Files
Here are the files that are new or changed, in full, exactly as they are in the repository. The two Texture files, the two Animator files, Player.cpp, Ghost.cpp, and HUD.cpp are unchanged from Chapter 21. First, main.cpp:
/*
Animated Character (Interfaces)
The Chapter 23 project from Learning C++ by Building Games
The runner from Chapter 21, with her three ghostly rivals, and now a
shadow of her own, who follows her wherever she goes. Hold the left
or right arrow, or A or D, to run, and press Space, W, or the Up
arrow to jump. The number in the top-left corner is the frame rate.
Escape, or the window's X, quits.
New in this project: virtual functions and interfaces. Every runner
is owned by one list, and everything in the game is updated by one
loop and drawn by another, through IUpdatable and IDrawable.
*/
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <memory> // std::unique_ptr, for the runners
#include <vector> // std::vector, for the runners and the views
#include "IUpdatable.h"
#include "IDrawable.h"
#include "Texture.h"
#include "Scenery.h"
#include "Player.h"
#include "Ghost.h"
#include "Shadow.h"
#include "HUD.h"
const int WINDOW_W = 960; // window width in pixels
const int WINDOW_H = 540; // window height in pixels
const float MAX_DELTA = 0.1f; // the longest frame we'll allow, in seconds
const float GROUND_Y = 452.0f; // the line the runners stand on
// The ghosts' colors, each one see-through
const SDL_Color RED_GHOST = { 255, 110, 110, 150 };
const SDL_Color GREEN_GHOST = { 110, 220, 130, 150 };
const SDL_Color BLUE_GHOST = { 120, 160, 255, 150 };
// The whole game, from the first frame to the last. Everything made in
// here is destroyed when it returns, before main destroys the renderer
bool runGame(SDL_Renderer* renderer)
{
Scenery scenery(renderer);
Texture runnerSheet(renderer, "assets/runner.png");
Texture digits(renderer, "assets/digits.png");
// If a picture didn't load, its Texture has already said which
if (!scenery.isLoaded() || !runnerSheet.isLoaded() || !digits.isLoaded())
return false;
HUD hud(digits);
// The runners take the window's width as a float. std::make_unique
// passes its arguments on exactly as they are, so convert it here, once
const float windowWidth = static_cast<float>(WINDOW_W);
// Every runner, owned by this one list, in the order they're drawn: the
// ghosts at the back, then the shadow, and the player in front
std::vector<std::unique_ptr<Entity>> runners;
runners.push_back(std::make_unique<Ghost>(
runnerSheet, 150.0f, GROUND_Y, 1, 0.8f, RED_GHOST, windowWidth));
runners.push_back(std::make_unique<Ghost>(
runnerSheet, 700.0f, GROUND_Y, -1, 1.25f, GREEN_GHOST, windowWidth));
runners.push_back(std::make_unique<Ghost>(
runnerSheet, 420.0f, GROUND_Y, -1, 0.6f, BLUE_GHOST, windowWidth));
// The keyboard needs to reach the player, so keep a pointer to her,
// which uses her but doesn't own her. The list owns her
std::unique_ptr<Player> newPlayer =
std::make_unique<Player>(runnerSheet, GROUND_Y, windowWidth);
Player* player = newPlayer.get();
runners.push_back(std::make_unique<Shadow>(runnerSheet, *player, 300.0f,
GROUND_Y));
runners.push_back(std::move(newPlayer));
// Two views of everything in the game, which own nothing: what changes
// from frame to frame, and what's drawn, from the back to the front
std::vector<IUpdatable*> updatables;
std::vector<IDrawable*> drawables = { &scenery };
for (std::unique_ptr<Entity>& runner : runners)
{
updatables.push_back(runner.get());
drawables.push_back(runner.get());
}
updatables.push_back(&hud);
drawables.push_back(&hud);
// The time at the last frame, in milliseconds
Uint64 lastTime = SDL_GetTicks();
bool running = true;
SDL_Event event;
while (running)
{
// Handle every event that's waiting
while (SDL_PollEvent(&event))
{
if (event.type == SDL_EVENT_QUIT)
{
running = false;
}
// A key going down, but not the repeats from holding it
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat)
{
switch (event.key.key)
{
case SDLK_SPACE:
case SDLK_W:
case SDLK_UP:
player->jump();
break;
case SDLK_ESCAPE:
running = false;
break;
}
}
}
// Run left or right while an arrow key, or A or D, is held
const bool* keys = SDL_GetKeyboardState(nullptr);
int direction = 0;
if (keys[SDL_SCANCODE_LEFT] || keys[SDL_SCANCODE_A])
direction -= 1;
if (keys[SDL_SCANCODE_RIGHT] || keys[SDL_SCANCODE_D])
direction += 1;
player->run(direction);
// Delta time, never more than MAX_DELTA
Uint64 now = SDL_GetTicks();
float delta = (now - lastTime) / 1000.0f;
lastTime = now;
if (delta > MAX_DELTA)
delta = MAX_DELTA;
// Move everything on by one frame
for (IUpdatable* thing : updatables)
thing->update(delta);
// Draw the frame, from the back to the front
SDL_RenderClear(renderer);
for (const IDrawable* thing : drawables)
thing->draw(renderer);
SDL_RenderPresent(renderer);
}
return true;
}
int main(int argc, char* argv[])
{
// Start SDL, then make the window and the renderer
if (!SDL_Init(SDL_INIT_VIDEO))
{
SDL_Log("SDL_Init failed: %s", SDL_GetError());
return 1;
}
SDL_Window* window = SDL_CreateWindow("Animated Character", WINDOW_W,
WINDOW_H, 0);
if (!window)
{
SDL_Log("SDL_CreateWindow failed: %s", SDL_GetError());
SDL_Quit();
return 1;
}
SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr);
if (!renderer)
{
SDL_Log("SDL_CreateRenderer failed: %s", SDL_GetError());
SDL_DestroyWindow(window);
SDL_Quit();
return 1;
}
// Show each frame in step with the monitor's refresh
SDL_SetRenderVSync(renderer, 1);
// Play until the player quits, then clean up, in the reverse order
// we created things
bool played = runGame(renderer);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return played ? 0 : 1;
}
In the preceding code, runGame makes the scenery, the HUD, and the runners, builds the two views, and then updates and draws everything in two loops.
Next, IUpdatable.h:
#pragma once
// Anything that changes from frame to frame
class IUpdatable
{
public:
virtual ~IUpdatable() = default;
virtual void update(float delta) = 0;
};
In the preceding code, one promise: an update, and a virtual destructor.
Then IDrawable.h:
#pragma once
#include <SDL3/SDL.h>
// Anything that can draw itself
class IDrawable
{
public:
virtual ~IDrawable() = default;
virtual void draw(SDL_Renderer* renderer) const = 0;
};
In the preceding code, the other promise: a draw that changes nothing.
Then Entity.h, which keeps both promises:
#pragma once
#include <SDL3/SDL.h>
#include "Animator.h"
#include "IDrawable.h"
#include "IUpdatable.h"
#include "Texture.h"
// Anyone drawn from the runner's sprite sheet: she runs left and right at
// her own pace, turns to face the way she's going, and jumps. Every runner
// changes from frame to frame and draws herself, so she keeps both promises
class Entity : public IUpdatable, public IDrawable
{
public:
Entity(const Texture& sheet, float x, float groundY, float pace = 1.0f,
SDL_Color tint = { 255, 255, 255, 255 });
void run(int direction);
void jump();
float getX() const;
bool isJumping() const;
void update(float delta) override;
void draw(SDL_Renderer* renderer) const override;
protected:
float x_; // the middle of her body, across the window
private:
const Texture& sheet_; // the runner's sprite sheet, used but not owned
Animator animator_; // counts through her running frames
float groundTop_; // the top of her frame when she's standing
float y_; // the top of her frame
float pace_; // 1 for the usual speed, 2 for twice as fast
SDL_Color tint_; // her color, and how see-through she is
float velY_ = 0.0f; // pixels per second, and negative is up
int direction_ = 0; // -1 for left, 1 for right, 0 for standing
bool facingLeft_ = false;
bool jumping_ = false;
};
In the preceding code, the class is Chapter 21's, with two base classes, two getters, and override on the two functions it promised.
Then Entity.cpp:
#include "Entity.h"
// The runner's sprite sheet
const float FRAME_W = 112.0f; // runner.png is six frames, each
const float FRAME_H = 180.0f; // 112 by 180
const int RUN_FRAMES = 6;
const int JUMP_FRAME = 2; // her longest stride, as in Chapter 17
const int STAND_FRAME = 4; // the frame with her feet closest
const float FRAME_TIME = 0.08f; // seconds per frame
const float FEET_GAP = 2.0f; // empty pixels below her feet
const float HIPS_X = 74.0f; // her middle, from the frame's left edge
// Running and jumping
const float RUN_SPEED = 360.0f; // pixels per second, at a pace of 1
const float JUMP_SPEED = -820.0f; // pixels per second at take-off, upward
const float GRAVITY = 2200.0f; // pixels per second, per second
Entity::Entity(const Texture& sheet, float x, float groundY, float pace,
SDL_Color tint)
: x_(x),
sheet_(sheet),
animator_(RUN_FRAMES, FRAME_TIME),
groundTop_(groundY + FEET_GAP - FRAME_H),
y_(groundTop_),
pace_(pace),
tint_(tint)
{
}
// Which way to run: -1 for left, 1 for right, or 0 to stand still
void Entity::run(int direction)
{
direction_ = direction;
if (direction != 0)
facingLeft_ = direction < 0;
}
void Entity::jump()
{
if (!jumping_)
{
jumping_ = true;
velY_ = JUMP_SPEED;
}
}
float Entity::getX() const
{
return x_;
}
bool Entity::isJumping() const
{
return jumping_;
}
void Entity::update(float delta)
{
// Run at her own pace, with her legs keeping up
x_ += direction_ * RUN_SPEED * pace_ * delta;
if (direction_ != 0)
animator_.update(delta * pace_);
// Rise, fall, and land, just as in Chapter 17
if (jumping_)
{
velY_ += GRAVITY * delta;
y_ += velY_ * delta;
if (y_ >= groundTop_)
{
y_ = groundTop_; // she has landed
velY_ = 0.0f;
jumping_ = false;
}
}
}
void Entity::draw(SDL_Renderer* renderer) const
{
// In the air, running, or standing still
int frame = STAND_FRAME;
if (jumping_)
frame = JUMP_FRAME;
else if (direction_ != 0)
frame = animator_.getFrame();
// Her middle is HIPS_X from the frame's left edge, or from its right
// edge when the frame is mirrored, so the frame moves to keep her
// middle at x_
float left = x_ - HIPS_X;
SDL_FlipMode flip = SDL_FLIP_NONE;
if (facingLeft_)
{
left = x_ - (FRAME_W - HIPS_X);
flip = SDL_FLIP_HORIZONTAL;
}
SDL_FRect src = { frame * FRAME_W, 0.0f, FRAME_W, FRAME_H };
SDL_FRect dst = { left, y_, FRAME_W, FRAME_H };
sheet_.draw(renderer, &src, &dst, flip, tint_);
}
In the preceding code, the only new functions are the two getters.
Then Player.h:
#pragma once
#include "Entity.h"
// The runner you control: an Entity who can't leave the window
class Player : public Entity
{
public:
Player(const Texture& sheet, float groundY, float windowWidth);
void update(float delta) override;
private:
float maxX_; // the furthest right her middle can go
};
In the preceding code, the player's update says override.
Then Ghost.h:
#pragma once
#include "Entity.h"
// A rival runner, tinted and see-through, who runs on her own: round and
// round the window, with a jump now and then
class Ghost : public Entity
{
public:
Ghost(const Texture& sheet, float x, float groundY, int direction,
float pace, SDL_Color tint, float windowWidth);
void update(float delta) override;
private:
float windowWidth_; // where she wraps around
};
In the preceding code, so does the ghost's.
Then HUD.h:
#pragma once
#include <SDL3/SDL.h>
#include "IDrawable.h"
#include "IUpdatable.h"
#include "Texture.h"
// The frame rate, counted over each second, and drawn from a strip of
// digits in the window's top-left corner. It changes and it's drawn, like
// a runner, though it's nothing like one
class HUD : public IUpdatable, public IDrawable
{
public:
HUD(const Texture& digits);
void update(float delta) override;
void draw(SDL_Renderer* renderer) const override;
private:
void drawNumber(SDL_Renderer* renderer, int value, float x,
float y) const;
const Texture& digits_; // the strip of digits, used but not owned
float timer_ = 0.0f; // seconds since fps_ was last worked out
int frames_ = 0; // frames counted in that time
int fps_ = 0; // frames in the last whole second
};
In the preceding code, the HUD keeps both promises, with no base class in common with the runners.
Then Scenery.h:
#pragma once
#include <SDL3/SDL.h>
#include "IDrawable.h"
#include "Texture.h"
// The landscape behind everything: four pictures, each filling the window,
// drawn from the back to the front. It never changes, so it's drawn, but
// never updated
class Scenery : public IDrawable
{
public:
Scenery(SDL_Renderer* renderer);
bool isLoaded() const;
void draw(SDL_Renderer* renderer) const override;
private:
Texture sky_;
Texture farHills_;
Texture nearHills_;
Texture ground_;
};
In the preceding code, the scenery keeps one promise, and owns its four pictures.
Then Scenery.cpp:
#include "Scenery.h"
// Each picture is loaded as its member is made, so the scenery owns all four
Scenery::Scenery(SDL_Renderer* renderer)
: sky_(renderer, "assets/sky.png"),
farHills_(renderer, "assets/hills_far.png"),
nearHills_(renderer, "assets/hills_near.png"),
ground_(renderer, "assets/ground.png")
{
}
bool Scenery::isLoaded() const
{
return sky_.isLoaded() && farHills_.isLoaded() &&
nearHills_.isLoaded() && ground_.isLoaded();
}
void Scenery::draw(SDL_Renderer* renderer) const
{
sky_.draw(renderer, nullptr, nullptr);
farHills_.draw(renderer, nullptr, nullptr);
nearHills_.draw(renderer, nullptr, nullptr);
ground_.draw(renderer, nullptr, nullptr);
}
In the preceding code, the scenery loads its pictures in its initializer list, and draws them from the back to the front.
Then Shadow.h:
#pragma once
#include "Entity.h"
// A dark, see-through runner who follows another wherever she goes, and
// jumps whenever she jumps
class Shadow : public Entity
{
public:
Shadow(const Texture& sheet, const Entity& leader, float x,
float groundY);
void update(float delta) override;
private:
const Entity& leader_; // the runner she follows, used but not owned
};
In the preceding code, the shadow is an Entity with a leader.
And last, Shadow.cpp:
#include "Shadow.h"
const float GAP = 90.0f; // how far behind she keeps
const SDL_Color SHADOW_TINT = { 40, 40, 70, 150 }; // dark, and see-through
Shadow::Shadow(const Texture& sheet, const Entity& leader, float x,
float groundY)
: Entity(sheet, x, groundY, 1.0f, SHADOW_TINT),
leader_(leader)
{
}
void Shadow::update(float delta)
{
// Run after her leader until she's close behind, then wait
float distance = leader_.getX() - x_;
if (distance > GAP)
run(1);
else if (distance < -GAP)
run(-1);
else
run(0);
// Jump when she jumps
if (leader_.isJumping())
jump();
Entity::update(delta);
}
In the preceding code, the shadow follows her leader, jumps when she jumps, and lets Entity do the rest.
Playing the Game
Press F5, and play, as in Figure 23.3. Run right, and the shadow hurries after you, and stops when you stop, a little behind. Turn around, and she turns too, and follows from the other side. Jump, and she leaves the ground with you, like an echo. The ghosts go on wandering, and wrapping around, and jumping when they please.

On the outside, the shadow is the only new thing. On the inside, the game loop has shrunk to two loops that don't know what's in them, and the shadow cost one class, two getters, and one line in runGame.
Understanding the Code
Is-a and Can-Do
The program uses both of Act 3's big relationships, each for its own job. Inheritance joins the runners, because they really are one family: they share a sprite sheet, an animator, running, jumping, and drawing, all written once, in Entity. The interfaces join things that share no code at all.
The HUD counts frames, and the scenery draws four pictures, and neither has anything in common with a runner, except what they can do. So they don't inherit from Entity. They make the same promises, and that's enough for the loops.
That's the rule of thumb for the rest of your programming life: inherit to share code, within a family that really is one, and use interfaces to share promises, between classes that just happen to do the same things.
Owners and Observers
Every object in runGame has exactly one owner, and Figure 23.4 shows who. The scenery, the two textures, and the HUD are ordinary local variables, owned by runGame itself. The runners are on the heap, each owned by one unique_ptr in runners, and runners is owned by runGame. Everything else only looks: player is an observer of the player, the shadow's leader_ is a reference to her, and the two views are lists of observers of everything.

When runGame returns, everything goes in the reverse order it was made, as Chapter 19 showed. The views go first, and deleting a vector of observers deletes nothing but the vector. Then runners goes, and each of its unique_ptrs deletes a runner, through an Entity*. That works, because Entity’s destructor is virtual, even though it never says so: a destructor is virtual whenever a base class's is, and IUpdatable’s is.
The HUD goes next, then the digits and the sprite sheet, which the HUD and the runners used, and last, the scenery, with its four pictures. All of it is gone before main destroys the renderer, and there isn't a delete anywhere in the program.
Two Loops
Figure 23.5 puts Chapter 21's game loop beside this one. Chapter 21 named every object, and every new kind of object meant another line to update it and another to draw it. This chapter's two loops never change. The one call thing->update(delta) runs four different functions, depending on what thing points at: Ghost::update, Shadow::update, Player::update, or HUD::update. The one call thing->draw(renderer) runs three: Scenery::draw, Entity::draw, or HUD::draw.

That's what Act 3 has been building toward. The game loop doesn't know what a ghost is, and it doesn't need to. It knows what everything in the game has promised, and that's all it asks of them.
Experimenting
A few things to try:
- A shadow for a ghost. Make the red ghost in a
unique_ptr<Ghost>of her own, keep an observer of her withget, as for the player, and push her into the list. Then give a second shadow that ghost as her leader. Watch the new shadow chase the ghost off the edge of the window, and run all the way back when the ghost wraps around. - A shadow in front. Push the shadow after the player, instead of before her, and the shadow is drawn in front of her. The order of the list is the order of the drawing.
- A HUD that's only drawn. Take the line that pushes
&hudintoupdatablesout. The HUD is still drawn, but it's never updated, so it shows 0 forever. - Scenery that changes. Try pushing
&sceneryintoupdatables, and the build stops, becauseScenerynever promised to update. Promises are checked by the compiler, not taken on trust. - Chapter 21's pacer. If you wrote Chapter 21's
Pacerin its AI exercise, addoverrideto herupdate, and push her into the list. She joins both loops without a single change to either.
Common Errors and Fixes
C3668: 'Entity::draw': method with override specifier 'override' did not override any base class methods, followed by more errors in Entity.cpp. The draw declared in Entity.h doesn't match the promise exactly. Most often, its const is missing. The interface promises a draw that's const, and a draw that isn't is a different function, which is also why the definition in Entity.cpp no longer matches anything. Put the const back, and all the errors go.
C2243: 'type cast': conversion from 'HUD *' to '_Ty' exists, but is inaccessible, on the line that pushes the HUD into a view. The HUD's class line is missing a public, as in class HUD : public IUpdatable, IDrawable. Without public, a class's base is private, so the rest of the program isn't allowed to treat a HUD as an IDrawable. The _Ty is the vector's own name for the type it holds. Write public in front of every base.
Warning C4244: 'argument': conversion from 'const int' to 'float', possible loss of data, pointing into a file called memory. An int, such as WINDOW_W, has been passed through std::make_unique to a constructor that takes a float. The conversion happens inside the standard library, where it's warned about. Pass windowWidth, which is already a float.
C2672: 'std::make_unique': no matching overloaded function found. A color, or some other struct, has been passed to std::make_unique as a list in braces, such as { 255, 110, 110, 150 }. The braces have no type until something says what they're for, and std::make_unique doesn't. Use one of the named colors, or write the type in front of the braces, as SDL_Color{ 255, 110, 110, 150 }.
C2665: 'push_back': no overloaded function could convert all the argument types, on the line that pushes the player into the list. The std::move is missing, as in runners.push_back(newPlayer);. That would copy the unique_ptr, and a unique_ptr can't be copied, as Chapter 10 found, so none of push_back’s versions will take it. (The error's full text spells out the vector's whole type, which is long.) Write std::move(newPlayer).
The game crashes as it quits, with an access violation, 0xC0000005. Something has deleted a runner that the list still owns, most likely with delete player;, or a second unique_ptr has been made from an observer. When the list deletes the runner again, it reaches into memory that's already been given back. Take the delete or the second owner out: the list owns the runners, and only the owner deletes.
The shadow never appears. She was pushed into the list after the views were built, so neither view has her: she's owned, but never updated or drawn. Push her before the loop that builds the views.
AI Exercise (Optional)
If you'd like to add something of your own to the runner's world with an AI's help, here's a challenge that's all about keeping promises. As always, it's optional.
Open your AI chatbot of choice and try a prompt like this:
"I have a C++ SDL 3 program with two interfaces: IUpdatable, with a pure virtual void update(float delta), and IDrawable, with a pure virtual void draw(SDL_Renderer* renderer) const. Both have virtual destructors. In runGame, everything that changes is in a std::vector<IUpdatable*> called updatables, and everything drawn is in a std::vector<IDrawable*> called drawables, and one loop calls update on each, and another calls draw. I have a Texture class with a constructor Texture(SDL_Renderer* renderer, const char* path), a bool isLoaded() const, and a draw(SDL_Renderer* renderer, const SDL_FRect* src, const SDL_FRect* dst) const, and a strip of digits, digits.png, ten digits each 26 by 40 pixels. Write a Stopwatch class, in Stopwatch.h and Stopwatch.cpp, that implements both interfaces: it counts the whole seconds since the game started, and draws them in the window's top-right corner from the digits strip, which it uses but doesn't own. Use override on every function that overrides, put each curly brace on its own line, and show me the lines to add to runGame, without changing either loop."
Notice what the preceding prompt does. It describes the two interfaces exactly, down to the const on draw, so the AI knows what promises it's keeping. It describes the views and the loops, so it knows where a new class plugs in, and it says the loops mustn't change, which is the whole point of the design. And it describes Texture and the digits, so the answer can use what you already have.
When the answer comes back, check it against this chapter. Does Stopwatch inherit publicly from both interfaces? Does its draw have the const, and do both functions say override? Does it keep a const Texture& to the digits, rather than loading a picture of its own? And does runGame push it into both views, before the loops, without touching the loops themselves?
If the stopwatch counts by adding up the delta time, and shows the whole seconds, it's doing its job. If anything is missing, ask about it.
Then add it to your project, and watch the seconds count up in the corner, updated and drawn by the same two loops as everything else.
Summary — and the End of Act 3
You've finished the runner's world. Two interfaces, IUpdatable and IDrawable, say what can be updated and what can be drawn. The runners keep both promises through Entity, the HUD keeps both on its own, and the scenery, which owns its pictures, keeps just the one. Every runner is owned by one list of unique_ptrs, the player is also reached through an observer, and two views of observers feed one loop that updates everything and another that draws it. Then a whole new kind of runner, a shadow who follows the player, cost one class, two getters, and one line, and neither loop changed.
That's the end of Act 3, and of a long climb. Chapter 18 bundled data and functions into classes, and Chapter 19 rebuilt the runner from them, with objects that clean up after themselves. Chapter 20 built families of classes, and ran into the wall of static binding, and Chapter 21 lived with it. Chapter 22 knocked the wall down with virtual, and gave you interfaces, and this chapter put it all together.
Act 4 turns to what comes next: Chapter 24 heads out into the C++ wilderness, to the features this book hasn't needed yet, Chapter 25 shows how to program well with an AI, Chapters 26 and 27 build two games that way, and Chapters 28 and 29 look at two patterns that real games are built on, including the Component pattern, which takes this chapter's ideas even further. After that comes the final project, a whole game of its own.
