Chapter 21 · Project · ~27 min read

Animated Character (Inheritance)

Chapter 19's runner has the landscape to herself. In this chapter, she gets company: three rivals, tinted red, green, and blue, and see-through, like ghosts, who run round and round the window on their own. Each has a pace of her own, one slower than the runner, one faster, and one slower still, and each jumps whenever she feels like it. Whatever the player does, they keep going, running off one side of the window and back on at the other.

A ghost does almost everything the player does. She runs, turns to face the way she's going, animates her legs, jumps, falls, and lands, all drawn from the same sprite sheet. Copying Chapter 19's Player into a new class would work, but that's exactly the repetition Chapter 20's inheritance was invented to avoid. So we'll lift what the runners share out of Player, into a base class called Entity, and then make Player and a new Ghost class derived classes of it, each adding only what makes her different.

Project folder: SDL3 Projects/Animated Character Inheritance — the complete source for this chapter lives here, with its thirteen files and the assets folder.

In this chapter, we will:

  • Start a new project from Chapter 19's files
  • Give Texture a tint, so one sprite sheet can draw runners of different colors
  • Lift the runner's code out of Player, into a base class called Entity
  • Rebuild Player as a derived class that adds only what's hers: staying in the window
  • Build a Ghost that runs on her own, wraps around the window, and jumps at random
  • Keep the player and the ghosts in variables and containers of their own types
  • 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 19's, the way Chapter 14 started from Chapter 12's, so the first job is to copy it into a new project. Here's the checklist:

  1. Choose File > New > Project, pick Empty Project (the one tagged C++, Windows, and Console), name it Animated Character Inheritance, and click Create. If you exported a template, as Chapter 11 suggested, use that instead.
  2. Right-click the project, choose Open Folder in File Explorer, and copy these into that folder from your Chapter 19 project folder: the nine files ending in .h and .cpp, the assets folder, SDL3.dll, and SDL3_image.dll.
  3. In Solution Explorer, right-click Source Files, choose Add > Existing Item, and pick the five .cpp files 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 four .h files the same way.
  4. 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 Chapter 14 warned, Add > Existing Item doesn't copy files: it adds them where they are. So add the copies in the new folder, not Chapter 19's own files, or every change in this chapter would land in Chapter 19's project. If you skipped Chapter 19, its finished files are in the book's repository, in SDL3 Projects/Animated Character Classes.

Checkpoint: Press F5. You have Chapter 19's program again, in a new project: the runner on her landscape, running, turning, and jumping, 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
Entity.h, Entity.cpp New: the base class, with most of Chapter 19's Player inside it
Ghost.h, Ghost.cpp New: the rival runners
Player.h, Player.cpp Changed: rewritten as a small derived class of Entity
Texture.h, Texture.cpp Changed: draw gets a tint
main.cpp Changed: makes the ghosts, updates them, and draws them
Animator.h, Animator.cpp, HUD.h, HUD.cpp Unchanged

The big move is from Player to Entity, and Figure 21.1 shows it. Almost everything Chapter 19's player could do, a ghost can do too: run, turn, jump, animate, and draw herself. All of that goes into Entity.

What's left over is what makes each of them different. The player can't leave the window, so Player keeps the check that stops her at each side. A ghost runs by herself, jumps whenever she likes, and wraps around from one side of the window to the other, so that's what Ghost adds.

Lifting the runner into a base class. Most of Chapter 19's Player moves up into Entity: the sprite sheet, the animator, the position, and running, jumping, and drawing. Player keeps only what's hers, staying inside the window, and the new Ghost adds only what's hers: running by herself, jumping at random, and wrapping around.
Figure 21.1 — Lifting the runner into a base class. Most of Chapter 19's Player moves up into Entity: the sprite sheet, the animator, the position, and running, jumping, and drawing. Player keeps only what's hers, staying inside the window, and the new Ghost adds only what's hers: running by herself, jumping at random, and wrapping around.

Two new things go into Entity on the way up. A ghost can run faster or slower than the player, so Entity gets a pace: 1 for the usual speed, 2 for twice as fast, and 0.5 for half. And a ghost is drawn in a color of her own, and see-through, so Entity gets a tint, which is where we'll start.

A Tint for Texture

The ghosts share the runner's sprite sheet, so they can't each have a picture in their own color. Instead, each one is drawn with a tint, Chapter 11's color modulation, and its alpha modulation makes it see-through. The texture is the one that draws, so the tint goes into Texture’s draw. In Texture.h, find these lines in the public part:

// Draw the part of the picture in src into dst, mirrored if flip says
// so. A nullptr for src means the whole picture, and for dst, the
// whole window
void draw(SDL_Renderer* renderer, const SDL_FRect* src,
          const SDL_FRect* dst, SDL_FlipMode flip = SDL_FLIP_NONE) const;

And change them to this:

// Draw the part of the picture in src into dst, mirrored if flip says
// so, and tinted. A nullptr for src means the whole picture, and for
// dst, the whole window, and the tint's alpha makes it see-through
void draw(SDL_Renderer* renderer, const SDL_FRect* src,
          const SDL_FRect* dst, SDL_FlipMode flip = SDL_FLIP_NONE,
          SDL_Color tint = { 255, 255, 255, 255 }) const;

In the preceding code, draw has a new parameter at the end: a tint, as an SDL_Color, with the four numbers from Chapter 1, red, green, blue, and alpha. Its default value is white, and fully solid, which changes nothing, so every call we already have, like the scenery's and the digits', draws exactly as before. A default value can be a whole struct, in braces, just as a struct variable can be given its value in braces.

Tip

A new parameter with a default value, added at the end of the list, is the gentlest way to grow a function. Every call you already have still compiles, and still means exactly what it did, which is why the scenery and the HUD need no changes at all. Put the tint first, or leave off its default, and every call in the program would need editing before the game would build again.

Now the definition. In Texture.cpp, find the whole of draw:

void Texture::draw(SDL_Renderer* renderer, const SDL_FRect* src,
                   const SDL_FRect* dst, SDL_FlipMode flip) const
{
    SDL_RenderTextureRotated(renderer, texture_, src, dst, 0.0, nullptr, flip);
}

And change it to this:

void Texture::draw(SDL_Renderer* renderer, const SDL_FRect* src,
                   const SDL_FRect* dst, SDL_FlipMode flip,
                   SDL_Color tint) const
{
    // Every drawing sets its own tint, so none is left over from the last
    SDL_SetTextureColorMod(texture_, tint.r, tint.g, tint.b);
    SDL_SetTextureAlphaMod(texture_, tint.a);
    SDL_RenderTextureRotated(renderer, texture_, src, dst, 0.0, nullptr, flip);
}

In the preceding code, the definition takes the tint too, without its default, which stays in the header, as Chapter 19 explained. Then SDL_SetTextureColorMod tints the picture, and SDL_SetTextureAlphaMod makes it as see-through as the tint's alpha says, exactly as Chapter 11 flashed a mole red and faded its stars, before the picture is drawn.

Chapter 11 set the modulation back to 255 after every drawing, because the modulation belongs to the texture, and any other drawing of it would have come out tinted too. Here, there's no need, and that's encapsulation at work. The SDL_Texture is private, so the only way anything can draw it is through draw, and draw always sets the tint first, white for the scenery and the player. No drawing can ever inherit a tint that was left over from the last one.

Setting SDL's modulation changes the SDL texture, not any member of the Texture object, so draw can stay const.

The Entity Class

Now the base class. Add a header called Entity.h and a source file called Entity.cpp, as in Chapter 19.

Entity.h

Below the #pragma once in Entity.h, type the outline of the class, so that the file reads like this:

#pragma once
#include <SDL3/SDL.h>
#include "Animator.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
class Entity
{
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();
    void update(float delta);
    void draw(SDL_Renderer* renderer) const;
};

In the preceding code, the public part is Chapter 19's Player, under a new name: the same four functions, run, jump, update, and draw. The constructor is new, though. It takes the sprite sheet, and where the entity starts, across the window, and the height of the ground, and then a pace and a tint, each with a default value. So an ordinary runner can be made with the first three alone, at the usual pace, in her own colors, and a ghost can ask for something different.

Now the members. Add this below void draw(SDL_Renderer* renderer) const;, with a blank line in between:

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 members are Chapter 19's, with pace_ and tint_ beside them, and they're split between two sections. Only x_ is protected, because it's the only member the derived classes need: the player to keep inside the window, and a ghost to wrap around it. Everything else stays private, so neither of them can change it except through run, jump, and update. That's Chapter 20's advice, to make protected only what the family genuinely needs.

Note

Some programmers never make data protected. They keep every member private, and give the base class protected functions instead, like a setX, so the base can check every change its derived classes make, just as its public functions check changes from outside. That's worth the extra code when a base class has many derived classes, written by many people. For a family of three in one project, one protected member is simpler, and easy to keep track of.

The order of the declarations matters again, just as Chapter 19's warning said. Members are initialized in the order they're declared, whichever section they're in, so x_ comes first, and groundTop_ is still declared above y_, which is worked out from it.

Entity.cpp

Open Entity.cpp, and start with its constants:

#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

In the preceding code, the constants are the ones from Chapter 19's Player.cpp, and they've moved here with the code that uses them. Only one has changed: its comment now says that RUN_SPEED is the speed at a pace of 1. The EDGE constant hasn't come across, because only the player stays inside the window, so it will stay in Player.cpp.

Below the constants, with a blank line in between, add the constructor:

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

In the preceding code, the initializer list gives every member without a default its starting value, in the order they're declared, so x_ comes first this time, because it's declared first, in the protected section. The rest are as Chapter 19's player had them, with the pace and the tint stored as they were given. Notice that the definition leaves out the two defaults, which live in the header.

Below the constructor, add run and jump:

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

In the preceding code, both functions are Chapter 19's, with Entity:: in front of their names instead of Player::. If you still have Chapter 19's Player.cpp open, you can copy them across, and change the names. Everything that can run can be told which way to go, and everything that can jump can be told to jump.

Below jump, add update:

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

In the preceding code, the jump is Chapter 19's, and the running has changed in two small ways. The distance run is multiplied by pace_, so a runner with a pace of 2 covers twice the ground. And the time given to the Animator is multiplied by the pace too, which makes her legs go around twice as fast, so they keep up with her, as the runner's did with the scrolling ground in Chapter 17. The Animator doesn't need to change at all: it just counts whatever time it's given.

What's gone is the check that kept her inside the window. That was the player's, and it will go back into Player.

Below update, add draw:

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, draw is Chapter 19's, apart from its last line, which passes the entity's tint along to the sprite sheet. For the player, that's white and solid, and she looks exactly as she did. For a ghost, it's her own color, and see-through.

Checkpoint: With Entity.cpp open, press Ctrl+F7, and if the Error List stays empty, Entity is right. The game still runs, too, exactly as before, because nothing uses Entity yet: Player still has all of Chapter 19's code, until the next section.

The Player, Derived

With Entity in place, Player can shrink to what's hers. Open Player.h, delete everything in it below its first line, #pragma once, and type the rest, so that the file reads like this:

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

private:
    float maxX_;                // the furthest right her middle can go
};

In the preceding code, : public Entity makes Player a derived class, so she has everything an Entity has. She declares only three things of her own. Her constructor takes the same three values as Chapter 19's did, so runGame doesn't need to change the line that makes her. She has an update of her own, and her one private member is maxX_, the furthest right she can go, which is the one thing Entity doesn't know about.

Now open Player.cpp, delete everything in it, and type this in its place:

#include "Player.h"

const float EDGE = 40.0f;   // how close her middle can get to a side

// She starts in the middle of the window, at the usual pace
Player::Player(const Texture& sheet, float groundY, float windowWidth)
    : Entity(sheet, windowWidth / 2.0f, groundY),
      maxX_(windowWidth - EDGE)
{
}

// Everything an Entity does, and then keep her in the window
void Player::update(float delta)
{
    Entity::update(delta);

    if (x_ < EDGE)
        x_ = EDGE;
    if (x_ > maxX_)
        x_ = maxX_;
}

In the preceding code, EDGE has come back from Chapter 19, because it's only the player's. The constructor passes the sprite sheet, the middle of the window, and the ground's height up to Entity’s constructor, as Chapter 20's chaser passed its values to Enemy’s. It leaves out the pace and the tint, so she gets the defaults: the usual pace, and her own colors. Then it works out maxX_, just as Chapter 19 did.

The player's update is Chapter 20's pattern, from the shooter: do what the base does, and then add your own. First, Entity::update(delta) runs, jumps, and animates her, and then the two ifs keep her middle inside the window, using x_, which she can reach because it's protected. The Entity:: in front matters, as Chapter 20 warned: without it, update would call itself, forever.

Checkpoint: Press F5. The game plays exactly as it did at the start of the chapter. That's the sign of a good refactoring, which means changing how a program is built without changing what it does: the player now inherits most of what she does, and nobody can tell.

The Ghost Class

Now for the rivals. Add Ghost.h and Ghost.cpp.

Ghost.h

Below the #pragma once in Ghost.h, type the class, so that the file reads like this:

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

private:
    float windowWidth_;         // where she wraps around
};

In the preceding code, Ghost is a derived class of Entity, like Player, and it declares only what's its own: a constructor, an update, and the width of the window, which it needs in order to wrap around. The constructor takes more than the player's: where she starts, which way she runs, her pace, and her tint, as well as the sprite sheet, the ground's height, and the window's width. That's seven values, split over two lines.

Ghost.cpp

Open Ghost.cpp, and start it with its constants and the constructor:

#include "Ghost.h"

const float OFFSCREEN   = 120.0f;   // how far past an edge she goes, unseen
const float JUMP_CHANCE = 0.5f;     // jumps a second on the ground, on average

Ghost::Ghost(const Texture& sheet, float x, float groundY, int direction,
             float pace, SDL_Color tint, float windowWidth)
    : Entity(sheet, x, groundY, pace, tint),
      windowWidth_(windowWidth)
{
    run(direction);   // she never stops
}

In the preceding code, OFFSCREEN is how far a ghost runs past the edge of the window before she comes back on at the other side. At 120 pixels, it's more than a frame's width, so she's always completely out of sight when she wraps around to the other side. The second constant, JUMP_CHANCE, is how many times a second, on average, a ghost jumps while she's on the ground.

The constructor passes five of its values up to Entity’s constructor, including the pace and the tint, and keeps the window's width for itself. Then its body calls run, which is public in Entity, so the ghost sets off in her direction as soon as she's made, and she never stops.

Below the constructor, add the ghost's update:

void Ghost::update(float delta)
{
    // A jump now and then, at random
    if (SDL_randf() < JUMP_CHANCE * delta)
        jump();

    Entity::update(delta);

    // Off one side of the window, and back on at the other
    if (x_ > windowWidth_ + OFFSCREEN)
        x_ = -OFFSCREEN;
    else if (x_ < -OFFSCREEN)
        x_ = windowWidth_ + OFFSCREEN;
}

In the preceding code, the ghost decides for herself, and then does what every entity does. First, she might jump. The call to SDL_randf, from Chapter 9, gives a random float from 0 up to, but not including, 1. If she's already in the air, jump ignores the call.

A random number like that is less than 0.01 one time in a hundred, and less than 0.5 half the time, so the test passes with a chance of JUMP_CHANCE times the frame's delta time. At 60 frames a second, that's a chance of one in 120 each frame, and over the whole second, those 60 chances add up to one half. On average, then, a ghost spends about two seconds on the ground between one jump and the next, whatever the frame rate.

Then Entity::update runs her along, animates her, and moves her jump on, and afterward, she wraps around. If her middle is more than OFFSCREEN past the right edge, she's moved to OFFSCREEN before the left edge, and the other way around, so she runs back on from the other side. Only one of the two checks can be true at once, so the else skips the second when the first has moved her.

Ghosts in the Game

Last of all, runGame needs the ghosts. There are six small changes to make in main.cpp, and Figure 21.2 shows where each one goes, lettered in the order we'll make them.

Where the ghosts go in main.cpp. The gray lines are Chapter 19's, and the six blue slots are this section's changes, from A, a new opening comment at the top, to F, the loop that draws the ghosts behind the player.
Figure 21.2 — Where the ghosts go in main.cpp. The gray lines are Chapter 19's, and the six blue slots are this section's changes, from A, a new opening comment at the top, to F, the loop that draws the ghosts behind the player.

First, change A: main.cpp’s opening comment describes Chapter 19's program. Replace the whole comment with this one:

/*
    Animated Character (Inheritance)
    The Chapter 21 project from Learning C++ by Building Games

    The runner from Chapter 19, now with three ghostly rivals who run
    round and round the window on their own. 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: inheritance. An Entity is anyone drawn from the
    runner's sprite sheet, and the Player and the Ghost are both kinds of
    Entity, each adding only what makes her different.
*/

In the preceding code, the comment says what the program is now, and what's new in it.

Change B: the ghosts will live in a vector. Add this below #include <SDL3/SDL_main.h>, with a blank line on each side:

#include <vector>   // std::vector, for the ghosts

In the preceding code, <vector> goes in its own group, between SDL's headers and ours, with a comment saying what it's for.

Change C: add this below #include "Player.h":

#include "Ghost.h"

In the preceding code, main.cpp includes Ghost.h, so it knows what a Ghost is. That header, in turn, includes Entity.h, which brings in the rest.

Change D makes the ghosts. Add this in runGame, below HUD hud(digits);, with a blank line in between:

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

In the preceding code, ghosts is a vector of Ghost objects, with three pushed into it. Each is made with the sprite sheet, a starting point, the ground, a direction, a pace, a tint, and the window's width. The red ghost starts on the left and runs right, at 0.8 of the player's pace. The green one starts on the right and runs left, a quarter faster than the player, and the blue one starts near the middle and runs left, at a little over half the pace. Each tint's alpha is 150, so all three are a little over half solid, and the landscape shows through them.

A tint is passed in braces, straight into the argument list, which works for a struct parameter, just as it did for the default value. And WINDOW_W is an int that becomes a float on the way in, as it did for the player.

Change E updates them. Add this below player.update(delta);:

for (Ghost& ghost : ghosts)
    ghost.update(delta);

In the preceding code, the loop goes through the ghosts by reference, as a Ghost&, and tells each one to update. Because the reference is a Ghost&, it's Ghost’s own update that runs, with its jumps and its wrapping, as Chapter 20's family did.

And change F draws them. Add this below ground.draw(renderer, nullptr, nullptr);, above player.draw(renderer);:

for (const Ghost& ghost : ghosts)
    ghost.draw(renderer);

In the preceding code, the ghosts are drawn after the ground, but before the player, so they run behind her, and she's always in front. The loop uses a const reference, because drawing a ghost doesn't change her, and draw is const.

Checkpoint: Press F5. Three see-through runners, red, green, and blue, run past on their own, the green one quickest and the blue one slowest, and each jumps now and then. When one runs off the edge of the window, she comes back on at the other side a moment later. Your runner still does what you tell her, in front of them all, and still can't leave the window.

The Complete Files

Here are the files that are new or changed, in full, exactly as they are in the repository. Only the Animator and HUD files are unchanged from Chapter 19, so they aren't repeated here. First, main.cpp:

/*
    Animated Character (Inheritance)
    The Chapter 21 project from Learning C++ by Building Games

    The runner from Chapter 19, now with three ghostly rivals who run
    round and round the window on their own. 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: inheritance. An Entity is anyone drawn from the
    runner's sprite sheet, and the Player and the Ghost are both kinds of
    Entity, each adding only what makes her different.
*/

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

#include <vector>   // std::vector, for the ghosts

#include "Texture.h"
#include "Player.h"
#include "Ghost.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 runner stands on

// 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)
{
    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");
    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 (!sky.isLoaded() || !farHills.isLoaded() || !nearHills.isLoaded() ||
        !ground.isLoaded() || !runnerSheet.isLoaded() || !digits.isLoaded())
    {
        return false;
    }

    Player player(runnerSheet, GROUND_Y, WINDOW_W);
    HUD hud(digits);

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

    // 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
        player.update(delta);
        for (Ghost& ghost : ghosts)
            ghost.update(delta);
        hud.update(delta);

        // Draw the frame, from the back to the front
        SDL_RenderClear(renderer);
        sky.draw(renderer, nullptr, nullptr);
        farHills.draw(renderer, nullptr, nullptr);
        nearHills.draw(renderer, nullptr, nullptr);
        ground.draw(renderer, nullptr, nullptr);
        for (const Ghost& ghost : ghosts)
            ghost.draw(renderer);
        player.draw(renderer);
        hud.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 ghosts below the player and the HUD, updates them after the player, and draws them after the scenery, behind her. Everything else is as Chapter 19 left it.

Next, Texture.h, with the tint in draw:

#pragma once
#include <SDL3/SDL.h>

// A picture, loaded from a file when a Texture is made, and destroyed by
// its destructor, so it can't be forgotten
class Texture
{
public:
    Texture(SDL_Renderer* renderer, const char* path);
    ~Texture();

    // One picture, one owner, so a Texture can't be copied
    Texture(const Texture&) = delete;
    Texture& operator=(const Texture&) = delete;

    bool isLoaded() const;

    // Draw the part of the picture in src into dst, mirrored if flip says
    // so, and tinted. A nullptr for src means the whole picture, and for
    // dst, the whole window, and the tint's alpha makes it see-through
    void draw(SDL_Renderer* renderer, const SDL_FRect* src,
              const SDL_FRect* dst, SDL_FlipMode flip = SDL_FLIP_NONE,
              SDL_Color tint = { 255, 255, 255, 255 }) const;

private:
    SDL_Texture* texture_;
};

In the preceding code, the only change from Chapter 19 is draw’s fifth parameter, with its default.

Then Texture.cpp:

#include "Texture.h"
#include <SDL3_image/SDL_image.h>

Texture::Texture(SDL_Renderer* renderer, const char* path)
    : texture_(IMG_LoadTexture(renderer, path))
{
    if (texture_ == nullptr)
        SDL_Log("Couldn't load %s: %s", path, SDL_GetError());
}

Texture::~Texture()
{
    if (texture_ != nullptr)
        SDL_DestroyTexture(texture_);
}

bool Texture::isLoaded() const
{
    return texture_ != nullptr;
}

void Texture::draw(SDL_Renderer* renderer, const SDL_FRect* src,
                   const SDL_FRect* dst, SDL_FlipMode flip,
                   SDL_Color tint) const
{
    // Every drawing sets its own tint, so none is left over from the last
    SDL_SetTextureColorMod(texture_, tint.r, tint.g, tint.b);
    SDL_SetTextureAlphaMod(texture_, tint.a);
    SDL_RenderTextureRotated(renderer, texture_, src, dst, 0.0, nullptr, flip);
}

In the preceding code, draw sets the tint, and then draws.

Then Entity.h, the new base class:

#pragma once
#include <SDL3/SDL.h>
#include "Animator.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
class Entity
{
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();
    void update(float delta);
    void draw(SDL_Renderer* renderer) const;

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 has all three sections, public, protected, and private, in that order, and only x_ is protected.

Then Entity.cpp, which holds most of what Chapter 19's player did:

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

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 pace comes into update, and the tint into draw, and otherwise, this is Chapter 19's player, under a new name.

Then the new 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);

private:
    float maxX_;                // the furthest right her middle can go
};

In the preceding code, the player declares only her constructor, her update, and maxX_.

Then Player.cpp:

#include "Player.h"

const float EDGE = 40.0f;   // how close her middle can get to a side

// She starts in the middle of the window, at the usual pace
Player::Player(const Texture& sheet, float groundY, float windowWidth)
    : Entity(sheet, windowWidth / 2.0f, groundY),
      maxX_(windowWidth - EDGE)
{
}

// Everything an Entity does, and then keep her in the window
void Player::update(float delta)
{
    Entity::update(delta);

    if (x_ < EDGE)
        x_ = EDGE;
    if (x_ > maxX_)
        x_ = maxX_;
}

In the preceding code, the constructor hands most of its work to Entity’s, and update adds the one thing that's hers.

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

private:
    float windowWidth_;         // where she wraps around
};

In the preceding code, a ghost declares a constructor, an update, and the window's width.

And last, Ghost.cpp:

#include "Ghost.h"

const float OFFSCREEN   = 120.0f;   // how far past an edge she goes, unseen
const float JUMP_CHANCE = 0.5f;     // jumps a second on the ground, on average

Ghost::Ghost(const Texture& sheet, float x, float groundY, int direction,
             float pace, SDL_Color tint, float windowWidth)
    : Entity(sheet, x, groundY, pace, tint),
      windowWidth_(windowWidth)
{
    run(direction);   // she never stops
}

void Ghost::update(float delta)
{
    // A jump now and then, at random
    if (SDL_randf() < JUMP_CHANCE * delta)
        jump();

    Entity::update(delta);

    // Off one side of the window, and back on at the other
    if (x_ > windowWidth_ + OFFSCREEN)
        x_ = -OFFSCREEN;
    else if (x_ < -OFFSCREEN)
        x_ = windowWidth_ + OFFSCREEN;
}

In the preceding code, a ghost sets off as soon as she's made, and each frame, she might jump, runs on as every entity does, and wraps around.

Playing the Game

Press F5, and the landscape has company, as Figure 21.3 shows. The ghosts pass behind the runner at their own speeds, the green one overtaking the others, and they jump when they please, whether or not there's anything to jump over. Run alongside one, and her legs keep time with her pace, faster or slower than yours. Stand still, and watch one run off the right edge and come back on at the left.

The finished program. The runner, in front, jumps as she runs, while the three ghosts, red, green, and blue, run on at their own paces, two of them in mid-jump. They're see-through: the sun shows through the green one.
Figure 21.3 — The finished program. The runner, in front, jumps as she runs, while the three ghosts, red, green, and blue, run on at their own paces, two of them in mid-jump. They're see-through: the sun shows through the green one.

The ghosts don't chase her, or even notice her, and nothing happens when they meet. This chapter isn't about a new game. It's about four runners, of two kinds, sharing one set of code, and there's more of that code in Entity.cpp than in Player.cpp and Ghost.cpp put together.

Understanding the Code

The Family

The base class, Entity, holds everything a runner does, and its derived classes hold only what makes each kind different. The player's whole class is a constructor and a few lines that keep her in the window, and a ghost's is a constructor, a random jump, and a wraparound. If you decide tomorrow that every runner should jump higher, there's one constant to change, in Entity.cpp, and the player and all three ghosts jump higher together.

Both derived classes use the same pattern in update, the one Chapter 20's shooter used: call the base's update by its full name, and do their own work before or after it. The ghost decides whether to jump before she moves, and wraps around after, while the player only needs to act afterward. And both reach x_ directly, because it's protected, while everything else about the runner stays private to Entity.

Two Kinds, Two Containers

Look at how runGame keeps them. The player is a Player variable, on her own, and the ghosts are in a std::vector<Ghost>, and each loop goes through a reference of the ghosts' own type. That's deliberate, and it's because of Chapter 20's static binding: the update that runs is chosen by the type the compiler can see. Figure 21.4 shows what happens both ways.

Why the player and the ghosts are kept apart. Updated through their own types, the player runs Player's update, and each ghost runs Ghost's, so each does her own thing. Put them all in one list of Entity pointers, and every call is Entity's update: the player can leave the window, and the ghosts stop jumping and never come back.
Figure 21.4 — Why the player and the ghosts are kept apart. Updated through their own types, the player runs Player's update, and each ghost runs Ghost's, so each does her own thing. Put them all in one list of Entity pointers, and every call is Entity's update: the player can leave the window, and the ghosts stop jumping and never come back.

Suppose you wanted one loop instead, and kept everyone in a std::vector<Entity*>. Every call through an Entity* is Entity::update, so the player would run off the edge of the window, and the ghosts would run off too, and never wrap around, or jump again. The objects would still be whole players and ghosts, but nothing would ever call their own functions. That's the problem Chapter 22's virtual solves, and in Chapter 23, the player and the ghosts go into one list at last.

The Tint

One sprite sheet draws four runners in four colors, because each drawing sets its own tint as it's drawn. That's worth remembering for any game with a lot of similar things in it: one picture, tinted differently for each, uses a quarter of the memory of four pictures, and a new color is just a new SDL_Color.

Experimenting

A few things to try:

  • More ghosts. Push a fourth Ghost into the vector, in a color of your own, and nothing else needs to change: the loops handle it.
  • A fast one. Give a ghost a pace of 2, and watch her legs spin to keep up. Try 0.25, and she strolls.
  • Solid ghosts. Set a ghost's alpha to 255, and she's solid, tinted, but not see-through. Set it to 60, and she's barely there.
  • A jumpier ghost. Change JUMP_CHANCE to 2 in Ghost.cpp, and a ghost waits only half a second on the ground, on average, before she jumps again. Everything else about the ghosts is unchanged, because their own code is all in one place.
  • One list for everyone. In runGame, below the ghosts, make a std::vector<Entity*> everyone;, and push &player and the address of each ghost into it. Then replace the lines that update the player and the ghosts with one loop over everyone, calling update through each pointer. Run it, and watch Figure 21.4 happen: nobody jumps unless you press Space, the ghosts run off and never come back, and the player can leave the window too. Put the old lines back when you've seen it.
Warning

In the one-list experiment, push all three ghosts into their vector before you take their addresses. As Chapter 13 showed, a vector that grows can move its elements to a bigger block, and a pointer to where one used to be is left dangling. Take the addresses first, and then push another ghost, and everyone could be pointing at memory that no longer holds a ghost at all.

When you've finished experimenting, put everything back as it was, because Chapter 23 starts from this project.

Common Errors and Fixes

C2512: 'Entity': no appropriate default constructor available, in Player.cpp or Ghost.cpp. A derived class's constructor doesn't call Entity’s in its initializer list, so C++ looks for a default constructor, and Entity hasn't got one. Add Entity(...), with its arguments, at the start of the list.

C2248: 'Entity::y_': cannot access private member declared in class 'Entity'. A derived class is using a member that Entity keeps private. Only x_ is protected, so a derived class that needs anything else should ask for it through Entity’s public functions, as the ghost does with jump and run.

The program crashes at once with 0xC00000FD, after warning C4717. A derived class's update calls update(delta) rather than Entity::update(delta), so it calls itself, forever, until the stack overflows. Put Entity:: back in front.

LNK2019: unresolved external symbol, naming a function of Entity, Ghost, or Player. A .cpp file isn't part of the project, so its code is never built. It happens easily in the setup, or when a file is copied into the folder rather than added. Check that all seven .cpp files are under Source Files, and add any that aren't with Add > Existing Item.

C2065: 'Ghost': undeclared identifier, followed by a flood of other errors in main.cpp. main.cpp doesn't include Ghost.h, so the compiler has never heard of Ghost when it meets std::vector<Ghost>, and every later line that mentions a ghost confuses it further. As Chapter 7 advised, fix the first error, and the flood goes away: add #include "Ghost.h" to main.cpp.

The ghosts look exactly like the player. Their tint isn't reaching the texture. The last line of Entity::draw isn't passing tint_ to sheet_.draw, so every runner gets the default, white and solid. Add tint_ as the last argument.

The ghosts are tinted, but solid. Texture::draw sets the color modulation, but not the alpha modulation, so the tint's alpha is never used. Add the call to SDL_SetTextureAlphaMod.

AI Exercise (Optional)

If you'd like to take the family further with an AI's help, here's a challenge that's all about derived classes. 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 a base class Entity, for runners drawn from one sprite sheet. Entity has public run(int direction), jump(), update(float delta), and draw(SDL_Renderer*) const functions, a protected float x_ for the middle of her body across the window, and everything else private. Its constructor is Entity(const Texture& sheet, float x, float groundY, float pace = 1.0f, SDL_Color tint = { 255, 255, 255, 255 }). Two classes derive from it: Player, whose update calls Entity::update and then keeps x_ inside the window, and Ghost, which runs on her own and wraps around. I've learned inheritance, protected members, calling a base constructor and a base function, and name hiding, but not virtual functions. Write a third derived class, Pacer, in Pacer.h and Pacer.cpp: a guard who runs back and forth between two x positions given to her constructor, turning around at each one, in a tint of her own. Use only what I've learned, put each curly brace on its own line, and show me the lines to add to runGame to make one and update and draw her."

Notice what the preceding prompt does. It describes Entity exactly, including which member is protected, so the AI knows what a derived class can and can't reach. It says you haven't learned virtual, so the answer shouldn't lean on it. And it describes the new behavior precisely, so there's less for the AI to guess.

When the answer comes back, check it against this chapter. Does Pacer’s constructor call Entity’s in its initializer list, passing the tint? Does its update call Entity::update, with the Entity:: in front, and decide which way to run by comparing x_ with her two turning points? Does it turn her with run, which is public, rather than trying to reach Entity’s private members?

Then look at runGame. Is she updated through a Pacer, not an Entity*, so that her own update runs, as Figure 21.4 showed? If any of those is missing, ask about it.

Then add her to your project, and watch her patrol.

Summary

You've built a family. The base class, Entity, holds everything a runner does: the sprite sheet, the animator, the position, and running, turning, jumping, and drawing, at a pace and in a tint of her own. Its derived classes, Player and Ghost, each add only what's theirs, the player's walls and the ghost's wandering, reaching the one protected member, x_, and calling Entity::update to do the rest. The texture learned to tint, so one sprite sheet draws four runners in four colors.

You've also met the limit Chapter 20 warned about. The player and the ghosts have to be kept apart, each in a variable or container of her own type, because through an Entity, only Entity’s functions would run. In the next chapter, virtual functions fix that, and then Chapter 23 puts the player and the ghosts in one list, with the HUD and the scenery beside them, all updated and drawn by the same two loops.