Chapter 19  ·  Project

Animated Character — Inheritance

Chapter 18 introduced the is-a relationship and gave us inheritance — base classes, derived classes, protected members, constructor chaining, and the slicing trap that comes with putting a derived object into a base-typed slot by value. We have everything we need to share code between classes but not, yet, the trick that lets a single loop call the right derived method on each item. That trick is virtual, and the book is saving it for Chapter 20.

This project chapter sits squarely between the two. We're going to pick up Chapter 17's Animator/Player/HUD demo and add a new class — Enemy — that shares everything Player has, then tweaks two methods. To do that cleanly, we'll lift the shared parts up into a new common base called Entity, and have both Player and Enemy derive from it. The result is a real, recognizable game-architecture pattern, written without a single virtual keyword.

In this chapter, we will:

Setting Up the Project

This project has the same multi-file structure as Chapter 17, plus two new pairs of files for Entity and Enemy. Create an empty C++ project named AnimatedCharacterInheritance and add these files to it:

If you want to save typing, copy the Chapter 17 project folder and edit it. SDL setup is identical — no add-on libraries needed.

Lifting the Shared Code Into an Entity Base

The first thing to do is recognize what's common between the Chapter 17 Player and the Enemy we want to add. If you flip back to Chapter 17's Player.h, the answer's right there: an Animator, an x/y position, a width/height, an update that delegates to the Animator, and a render that draws a tinted rectangle. That's the shared part. Both characters will need every line of it.

The Chapter 18 reflex is exactly right: put the shared part in a base class, then have both characters inherit from it.

Entity.h

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

class Entity
{
public:
    Entity(float x, float y,
           float width, float height,
           int   frameCount    = 6,
           float frameDuration = 1.0f / 8.0f);

    ~Entity() = default;

    Entity(const Entity&)            = delete;
    Entity& operator=(const Entity&) = delete;

    void update(float dt);
    void render(SDL_Renderer* renderer) const;

protected:
    Animator animator_;
    float    x_, y_;
    float    width_, height_;
};

Entity is the lift-and-shift of Chapter 17's Player. Position, size, an Animator member by composition, an update that ticks the Animator, a render that draws the colored rectangle. The constructor takes the position, size, and animation parameters and hands them through to the Animator and the member fields.

The single most important word in this file is protected. We met protected in Chapter 18: it's like private to the outside world, but derived classes can see it. We need that here because the Enemy we're about to write moves itself by adjusting its own x_ every frame. If x_ were private, Enemy couldn't touch it.

The = default destructor means we have no cleanup work to do at the Entity level — our Animator member cleans up its own heap buffer when it's destroyed, and that happens automatically. We're not making it virtual because, in this project, we never delete derived objects through a base pointer. That move requires virtual destructors, which Chapter 20 will introduce.

The = delete lines forbid copying, exactly as in Chapter 17. Our Animator member also forbids copying, so a copy of Entity would fail to compile anyway — the explicit = delete is documentation that we intended this.

Entity.cpp

#include "Entity.h"

Entity::Entity(float x, float y,
               float width, float height,
               int frameCount, float frameDuration)
    : animator_(frameCount, frameDuration)
    , x_(x), y_(y)
    , width_(width), height_(height)
{
}

void Entity::update(float dt)
{
    animator_.update(dt);
}

void Entity::render(SDL_Renderer* renderer) const
{
    SDL_Color tint = animator_.currentTint();
    SDL_SetRenderDrawColor(renderer, tint.r, tint.g, tint.b, tint.a);

    SDL_FRect rect { x_, y_, width_, height_ };
    SDL_RenderFillRect(renderer, &rect);
}

The implementation is the same code Chapter 17's Player had, just relocated. The constructor body is empty — everything is set up in the initializer list. update ticks the Animator. render reads the Animator's current tint and fills a rectangle.

Deriving a Player

Now we re-derive Player from this new base. Player is going to be deeply unimpressive — that's the point. Inheritance means we get all of Entity's behavior for free; Player only needs to pick its own position and size.

Player.h

#pragma once
#include "Entity.h"

class Player : public Entity
{
public:
    Player(float screenWidth, float screenHeight);
};

The inheritance declaration is the punchline: class Player : public Entity. That single line says "Player is a publicly-inherited derived class of Entity." From the outside world's view, every Player is also an Entity. Anything you can do with an Entity (call update, call render), you can also do with a Player.

Notice what's not in this class. No member variables of its own. No update. No render. No destructor. We don't need any of it — every part is inherited from Entity. The only thing Player has to provide is a constructor that picks the right position and size for this kind of character, and chains up to Entity's constructor with those values.

Player.cpp

#include "Player.h"

Player::Player(float screenWidth, float screenHeight)
    : Entity( (screenWidth  - 120.0f) * 0.5f,
              (screenHeight - 120.0f) * 0.5f,
              120.0f, 120.0f )
{
}

The constructor's initializer list starts with Entity(...) — Chapter 18's base-class constructor call. Before any Player-specific code runs, the Entity part of this object gets built using the values we hand up. The four arguments are the centered x, the centered y, the width, and the height (all 120-pixel square, centered in the window). Once Entity's constructor finishes, we have a fully-formed Entity with its Animator, position, and size all ready.

The body is empty. There's nothing Player-specific to do. The whole Player.cpp is six lines. If Player.h and Player.cpp feel too short to be doing real work, that's the gift of inheritance — the work is done elsewhere, exactly once, in Entity.

Deriving an Enemy

Enemy is where the work shows up. An enemy looks different from the player, moves on its own, and there are three of them on screen at different sizes, speeds, and colors.

Enemy.h

#pragma once
#include "Entity.h"

class Enemy : public Entity
{
public:
    Enemy(float startX, float y,
          float width, float height,
          float speed,
          float screenWidth,
          SDL_Color baseColor);

    void update(float dt);
    void render(SDL_Renderer* renderer) const;

private:
    float     speed_;
    float     screenWidth_;
    SDL_Color baseColor_;
};

Enemy extends Entity with three private members of its own — a speed, a width-of-screen value used for wrap-around, and a base color. The constructor takes the usual position and size plus all three new parameters.

Notice update and render here. They aren't marked override. They don't say virtual. They look, syntactically, like fresh methods that simply happen to have the same names as the base's. That's a deliberate choice we'll come back to in a moment.

Enemy.cpp

#include "Enemy.h"

Enemy::Enemy(float startX, float y,
             float width, float height,
             float speed, float screenWidth,
             SDL_Color baseColor)
    : Entity(startX, y, width, height)
    , speed_(speed)
    , screenWidth_(screenWidth)
    , baseColor_(baseColor)
{
}

The constructor's initializer list does the base call exactly the way Player's did — Entity(startX, y, width, height) builds the base part of this object. Then the three Enemy-only members get their values from the constructor arguments. Body empty, all setup in the list.

void Enemy::update(float dt)
{
    Entity::update(dt);

    x_ -= speed_ * dt;
    if (x_ + width_ < 0.0f)
        x_ = screenWidth_;
}

The pattern is "do what the base does, plus our own work." The first line, Entity::update(dt);, explicitly calls the base class's update — that ticks the Animator forward. Then the Enemy-specific lines move x leftward by speed-times-dt, and wrap to the right edge when the enemy slides off the left.

The Entity:: prefix on the base call is mandatory. Without it, the line would be update(dt);, which would call Enemy::update and recurse infinitely. Always name the base class explicitly when you want the base version.

We need to touch x_ and width_ directly, which is exactly why those members are protected in Entity rather than private. If they were private, this line wouldn't compile.

void Enemy::render(SDL_Renderer* renderer) const
{
    int   frame  = animator_.currentFrame();
    float pulse  = 1.0f + 0.05f * (float)(frame % 2);
    float w      = width_  * pulse;
    float h      = height_ * pulse;
    float drawX  = x_ - (w - width_)  * 0.5f;
    float drawY  = y_ - (h - height_) * 0.5f;

    SDL_SetRenderDrawColor(renderer,
                           baseColor_.r, baseColor_.g, baseColor_.b, baseColor_.a);
    SDL_FRect rect { drawX, drawY, w, h };
    SDL_RenderFillRect(renderer, &rect);
}

Enemy's render is different from the base's. The base draws the rectangle in the Animator's rainbow tint; this one draws in the enemy's own fixed baseColor_. It still reads the Animator's currentFrame() to add a small size pulse, and it accesses animator_ directly because the member is protected in the base.

A Brief Word About Method Hiding

We just wrote a derived-class method, Enemy::update, with the same name as the base's Entity::update, without marking either of them virtual. Strictly, this is method hiding, not method overriding, and it matters for what you can and can't do with these classes.

When you call enemy.update(dt) directly — where enemy is declared as Enemy — C++ looks at the declared type, sees Enemy, and calls Enemy::update. That's what we want, and it works.

What doesn't work is calling through a base reference or pointer. If we had a function void tick(Entity& e) { e.update(dt); } and passed an Enemy into it, the call would run the base's Entity::update, not Enemy::update. The Enemy-specific movement would silently never happen. That's the cliffhanger from Chapter 18 — without virtual, the static type of the reference decides which function runs, regardless of what's really there. Chapter 20 fixes this.

main.cpp — Two Concrete Types, Side by Side

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

#include <memory>
#include <vector>

#include "Player.h"
#include "Enemy.h"
#include "HUD.h"

const int WINDOW_W = 1024;
const int WINDOW_H = 600;

int main(int argc, char* argv[])
{
    if (!SDL_Init(SDL_INIT_VIDEO)) { return 1; }

    SDL_Window*   window   = SDL_CreateWindow(
        "Animated Character - Inheritance", WINDOW_W, WINDOW_H, 0);
    SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr);

    {
        Player player((float)WINDOW_W, (float)WINDOW_H);

        std::vector<std::unique_ptr<Enemy>> enemies;

        struct Spec { float w, h, y, speed; SDL_Color color; };
        const Spec specs[] = {
            {  80.0f,  80.0f, 100.0f, 120.0f, { 220, 100, 100, 255 } },
            {  60.0f,  60.0f, 260.0f, 220.0f, { 100, 220, 130, 255 } },
            {  40.0f,  40.0f, 420.0f, 320.0f, { 100, 170, 255, 255 } },
        };

        for (int i = 0; i < 3; ++i)
        {
            const float startX = (float)WINDOW_W + i * 200.0f;
            enemies.push_back(std::make_unique<Enemy>(
                startX, specs[i].y,
                specs[i].w, specs[i].h,
                specs[i].speed, (float)WINDOW_W,
                specs[i].color));
        }

        HUD hud;
        Uint64 lastTicks = SDL_GetTicksNS();
        bool   running   = true;

        while (running)
        {
            SDL_Event ev;
            while (SDL_PollEvent(&ev))
            {
                if (ev.type == SDL_EVENT_QUIT) running = false;
                if (ev.type == SDL_EVENT_KEY_DOWN &&
                    ev.key.scancode == SDL_SCANCODE_ESCAPE) running = false;
            }

            const Uint64 now = SDL_GetTicksNS();
            const float  dt  = (float)(now - lastTicks) / 1.0e9f;
            lastTicks = now;

            player.update(dt);
            for (auto& e : enemies) e->update(dt);

            hud.update(dt);

            SDL_SetRenderDrawColor(renderer, 30, 35, 45, 255);
            SDL_RenderClear(renderer);

            player.render(renderer);
            for (const auto& e : enemies) e->render(renderer);

            hud.render(renderer);
            SDL_RenderPresent(renderer);
        }
    }

    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}

The structure of main is almost identical to Chapter 17. The Player sits on its own as a single named variable. When we call player.update(dt) it goes straight to Player::update... which doesn't exist, so the compiler looks one level up and finds Entity::update instead. The base method runs. Animator ticks. That's exactly what we want.

The enemies live in std::vector<std::unique_ptr<Enemy>>. The vector's element type is concretely Enemy, not Entity. That's the move that sidesteps the slicing problem. Each e->update(dt) calls Enemy::update directly, because e is typed as Enemy*. All the Enemy-specific behavior runs correctly.

This works. But notice the friction: we have two loops, one for the player and one for the enemies, because we can't combine them. If we wanted a fourth kind of character — Boss, say — we'd need a third loop, with its own typed container. That's the cost of staying away from virtual.

Playing the Game

Hit F5. The window opens with a rainbow-pulsing Player square in the center, and three colored Enemy squares sliding leftward at different speeds and sizes. As each one passes the left edge of the window, it wraps to the right and starts again.

The Animated Character Inheritance demo: rainbow player in center, three solid-color enemies sliding leftward
The Animated Character Inheritance demo: rainbow player in center, three solid-color enemies (red, green, blue) at different vertical positions sliding leftward.

Understanding the Code

Step back and look at what inheritance bought us in this project.

Entity holds all the shared state and behavior — Animator, position, size, the default update and render. Player and Enemy each derive from it, getting all of that for free. Player's entire implementation is six lines (just the constructor that picks a position and size). Enemy's implementation is a constructor plus two methods that change what the base does — movement on the update side, custom color on the render side.

Without inheritance, we'd have written essentially the entire Entity body in both Player and Enemy. Twice the code, twice the chance of bugs, twice the work whenever the Animator's interface changes. Inheritance lets us write the shared code once and extend it twice.

There's also a quiet lesson in the Entity::update(dt); line inside Enemy::update. The pattern of "do what the base does, then add my own work" is one of inheritance's most common shapes. Calling the base method explicitly is how you participate in the base's behavior without throwing it away. Forget it and you lose the inherited work; remember it and you stack your work on top.

What inheritance didn't do for us is let us mix the two character types in a single container or treat them uniformly through a base reference. Without virtual, calling update through an Entity& would run the base's version, not the derived one. We worked around it by keeping the types separate. It works. It just doesn't scale once you have more than two or three character types.

Experimenting

Common Errors and Fixes

"member x_ is inaccessible" inside Enemy.cppx_ is declared private in Entity rather than protected. Change it to protected so derived classes can see it.

Linker error "unresolved external symbol Entity::Entity" — You declared Entity's constructor in Entity.h but forgot to define it in Entity.cpp (or its argument list disagrees between header and source). Match them exactly.

"no matching constructor for Entity" — You forgot the base-class call in a derived constructor's initializer list. C++ insists you initialize the base part explicitly when its constructor needs arguments. Add : Entity(...) to the derived constructor.

Enemy::update calls itself forever — You wrote update(dt); instead of Entity::update(dt); inside Enemy::update. The unqualified name calls Enemy::update again. Always prefix the base name when chaining to the base method.

AI Exercise (Optional)

"I have a small C++ SDL 3 project with an Entity base class, a Player derived class (no overrides), and an Enemy derived class that adds movement and a fixed color. I store the player as a single Player variable and the enemies in std::vector<std::unique_ptr<Enemy>>. I have learned: variables, flow, loops, functions, pointers, vectors, maps, classes, composition, and inheritance — including protected and constructor chaining. I have NOT learned virtual, polymorphism, abstract classes, or interfaces. Add a new Boss derived class that bounces vertically (uses a sine wave on y_). Add one Boss to my main loop as its own variable. Do NOT mix the Boss into the enemies vector — keep them as separate typed objects. Use only what I've learned. Paste the full code for any new or changed files."

The prompt forces the AI to respect a real constraint — no virtual — that it'll be tempted to break. Most AI suggestions for a class hierarchy reach straight for virtual, an abstract base, or both, because that's the "right answer" in production C++. For this chapter that answer is deliberately one chapter too early. Read the AI's solution carefully. If it slipped a virtual in, or made Entity abstract, push back: "I haven't learned virtual yet. Please follow the same pattern as my existing code — no virtual, no abstract base, keep the types in their own concrete containers."

Summary

You've built a small project that demonstrates the everyday value of inheritance. A new Entity base lifts every shared line out of Chapter 17's Player into one place. Both Player and Enemy derive from it — Player with no overrides at all, Enemy with new behavior layered on top of the base's. The code reuse is real; the duplication is gone.

You've also bumped, intentionally, into the wall Chapter 18 warned about. Mixing Player and Enemy in one container doesn't work yet, because we don't have virtual to make the right method get called. We worked around it by keeping the types in their own containers. The next chapter is the one that breaks through. Chapter 20 introduces virtual functions, abstract classes, and interfaces — the C++ features that turn inheritance from a code-reuse tool into a full polymorphism toolkit. Then Chapter 21 picks this same Entity/Player/Enemy structure back up and shows what the project looks like once the polymorphism is in place: one container, one loop, every character type behaving like itself.