This is the last project chapter in the OOP arc and the capstone of Act 2. We have a complete OOP toolkit now — classes, encapsulation, RAII, composition, inheritance, virtual functions, abstract classes, and interfaces. This chapter takes the Chapter 19 project and finishes it properly with everything Chapter 20 added: virtual, override, virtual destructors, and a pair of pure abstract interfaces that let unrelated classes plug into the same loops.
If you skim back through Act 2, the through-line is visible. Chapter 17's project had one class shape (Player) and used composition. Chapter 19's project added inheritance and ran into the wall — the slicing trap, with two character types stuck in two separate containers. This chapter knocks the wall down. One container of entities. One update loop. One render loop. The HUD — which has no business being an Entity — joins both loops because it implements the same contracts everyone else does. This is the architecture real games use.
There is no new code to write from scratch this chapter. We're taking the Chapter 19 project and changing it in five specific places. The change is small. The payoff is huge.
In this chapter, we will:
- Write two small pure-abstract interfaces —
IUpdatableandIDrawable - Make
Entityimplement both interfaces (multiple interface inheritance) - Add
virtualandoverrideto the methods Chapter 19 left as plain functions - Make
HUDalso implement both interfaces, even though it has nothing else in common withEntity - Replace Chapter 19's two typed containers with one polymorphic vector of entities, plus two interface-typed views that the main loop iterates uniformly
- Try an optional AI exercise that pushes the design further
Setting Up the Project
Either start fresh — empty C++ project, add the files listed below from the project folder — or, the more illustrative path, copy your Chapter 19 project folder, rename the copy, and edit the relevant files inline as we go. The copy-and-modify route is more interesting because you'll see exactly which lines change and which don't.
The full file list:
Animator.h/Animator.cpp— unchanged from Chapter 17/19IUpdatable.h— new (header only)IDrawable.h— new (header only)Entity.h/Entity.cpp— small changesPlayer.h/Player.cpp— unchanged from Chapter 19Enemy.h/Enemy.cpp— addsoverrideHUD.h/HUD.cpp— implements both interfacesmain.cpp— replaces two typed containers with one polymorphic vector + two interface views
Writing the Two Interfaces
We start with the new headers — the smallest files in the whole book.
IUpdatable.h
#pragma once
class IUpdatable
{
public:
virtual ~IUpdatable() = default;
virtual void update(float dt) = 0;
};
That is the complete header. No .cpp is needed — there's nothing to implement. Every method is pure virtual (= 0), which means the class is abstract and you cannot instantiate it.
Three properties make this a C++ interface:
- All methods are pure virtual. Nothing has a body here.
- There are no data members. Pure contract.
- The destructor is virtual (and
= defaultbecause there's nothing to clean up). This is the non-negotiable rule from Chapter 20: any class meant to be deleted through a base pointer must have a virtual destructor, or only the base's destructor runs and the derived part leaks.
The naming convention is the leading I. There's nothing in the language enforcing it, but readers will see IUpdatable and know at a glance "this is a contract, not a class I'm meant to instantiate." Stick to the convention.
IDrawable.h
#pragma once
#include <SDL3/SDL.h>
class IDrawable
{
public:
virtual ~IDrawable() = default;
virtual void render(SDL_Renderer* renderer) const = 0;
};
The same shape, different verb. render takes an SDL_Renderer* and is marked const because drawing shouldn't change the object's state.
A small but interesting design choice: why two interfaces and not one combined IGameObject with both methods? Because some classes need only one. A static background needs render but not update. A silent timing helper might need update but not render. Splitting keeps the door open. This is the interface segregation principle — prefer many small contracts over a single fat one, so callers depend only on what they use.
Updating Entity to Implement the Interfaces
Open Entity.h from your Chapter 19 code. Three small changes.
The Chapter 19 declaration was:
class Entity
{
public:
Entity(...);
~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_;
};
The Chapter 21 declaration is:
#include "IUpdatable.h"
#include "IDrawable.h"
class Entity : public IUpdatable, public IDrawable
{
public:
Entity(...);
// (no explicit destructor needed — virtual by inheritance)
Entity(const Entity&) = delete;
Entity& operator=(const Entity&) = delete;
void update(float dt) override;
void render(SDL_Renderer* renderer) const override;
protected:
Animator animator_;
float x_, y_;
float width_, height_;
};
The three changes are:
1. The class line gains two bases: : public IUpdatable, public IDrawable. That's C++'s multiple inheritance syntax. Inheriting from multiple interfaces is safe and common — each interface contributes only a contract, no data, so there's nothing to clash.
2. update and render get the override keyword. The methods are still functionally the same as before, but override is the safety net from Chapter 20: it tells the compiler "I'm claiming this overrides something in a base class." If the base ever changes its signature, the build fails with a clear error.
3. The explicit ~Entity() = default; line is gone. It's still implicitly there — the compiler generates a destructor for any class that doesn't have one — but it's now virtual by inheritance. Both interfaces already declare virtual ~... = default;, and that virtual-ness propagates down.
Entity.cpp needs no changes. The methods do exactly what they did before. virtual is a property of the declaration, not the body.
Player and Enemy
Player.h and Player.cpp need no changes whatsoever from Chapter 19. Player still doesn't override anything; it inherits Entity's now-virtual update and render, and because they're virtual, calling them through any base or interface pointer still ends up at Entity's bodies. That comes for free.
Enemy.h needs just the override keyword:
class Enemy : public Entity
{
public:
Enemy(...);
void update(float dt) override;
void render(SDL_Renderer* renderer) const override;
// private members unchanged
};
The bodies in Enemy.cpp are unchanged from Chapter 19.
Making HUD Implement the Interfaces
Now the move that demonstrates what interfaces are for. HUD shares nothing structural with Entity — no Animator, no position, no inheritance relationship. But it does have an update (which writes the latest dt into a buffer) and a render (which draws the FPS bar). Two methods with the right shape are all it takes to satisfy the contracts.
The Chapter 19 declaration was:
class HUD
{
public:
explicit HUD(int sampleCount = 60);
~HUD();
void update(float dt);
void render(SDL_Renderer* renderer) const;
float smoothedFps() const;
private:
// buffer fields
};
The Chapter 21 declaration is:
#include "IUpdatable.h"
#include "IDrawable.h"
class HUD : public IUpdatable, public IDrawable
{
public:
explicit HUD(int sampleCount = 60);
~HUD() override;
void update(float dt) override;
void render(SDL_Renderer* renderer) const override;
float smoothedFps() const;
private:
// buffer fields unchanged
};
Three changes: two new bases, override on update and render, and override on the destructor too — confirming we're aware we're overriding a virtual destructor from the interfaces.
HUD.cpp is unchanged. The implementation already allocates the buffer in the constructor, delete[]s it in the destructor, updates on demand, and renders on demand.
Stop and look at what just happened. HUD and Entity are now both IUpdatable and both IDrawable. They share no parent class. They share no fields. They share no methods other than the ones promised by the interfaces. And yet a function that takes IUpdatable* can take either of them. A vector of IDrawable* can hold either of them. That's interfaces.
main.cpp — One Polymorphic Loop
This is the chapter's payoff. The Chapter 19 main had two parallel update loops and two parallel render loops because the types couldn't be mixed. The new version has one of each, driven by interface pointers.
The Owning Storage
std::vector<std::unique_ptr<Entity>> entities;
HUD hud;
The entities vector is now typed for the base: Entity. With virtual in place, this is finally safe — Entity* pointers can point at a Player or an Enemy, and any virtual call through them will reach the right derived method. unique_ptr handles deletion at the end of the block, and because the destructor chain is virtual, the correct derived destructor runs for each.
entities.push_back(std::make_unique<Player>(
(float)WINDOW_W, (float)WINDOW_H));
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;
entities.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));
}
Same Spec array, same for loop as Chapter 19. The difference is that all four entities now live in one vector, side by side, talked to through the same Entity* pointer type.
The Interface Views
std::vector<IUpdatable*> updatables;
std::vector<IDrawable*> drawables;
for (auto& e : entities)
{
updatables.push_back(e.get());
drawables.push_back(e.get());
}
updatables.push_back(&hud);
drawables.push_back(&hud);
Two new vectors of raw, non-owning pointers. These don't own anything — the unique_ptrs in entities and the hud variable on the stack are the owners. These vectors are just views over the owners through the interface lenses.
We walk the entities vector and add each entity to both interface views via e.get() (which extracts the raw pointer from a unique_ptr). Then we add &hud to both. After these few lines, each interface view has five entries: four entity pointers and one HUD pointer.
Are raw pointers in a vector a problem here? Not at all. The dangerous pattern is owning raw pointers. These pointers are observers of objects that someone else owns and won't free behind their back. As long as the entities vector and the hud variable outlive the interface views (which they do, because everything is declared in the same enclosing scope), the observer pointers stay valid.
The Loops
for (auto* u : updatables)
u->update(dt);
SDL_SetRenderDrawColor(renderer, 30, 35, 45, 255);
SDL_RenderClear(renderer);
for (const auto* d : drawables)
d->render(renderer);
SDL_RenderPresent(renderer);
That's the whole per-frame work of the game.
The update loop ticks every updatable — Player runs Entity::update, each Enemy runs Enemy::update (movement plus wrap-around), and HUD runs HUD::update. Same line of code drives all four behaviors. The loop has no idea which is which and doesn't need to.
The render loop draws every drawable in order — Player first, then the three Enemies, then the HUD on top. One line, four concrete renderings.
The fact that those four objects share no other code — Player and Enemy have an Animator and a position; HUD has a buffer of frame deltas — is the point. The interface contract is what they have in common, and the contract is enough.
Playing the Game
Hit F5. The window opens. Same visible game as Chapter 19: rainbow Player in the center, three colored Enemies sliding leftward and wrapping. FPS bar in the top-left.
The fact that the visible game is identical to Chapter 19's is the point. We re-architected the program for cleaner extensibility and ended up with the same observable behavior. That's normal. A lot of the time, design improvements aren't about adding features — they're about getting the same features through a structure that won't bite you later.
Press Escape to quit. Mentally trace the cleanup: when the enclosing block ends, the updatables and drawables vectors are destroyed first (they own no objects, so nothing happens). Then hud is destroyed, running HUD::~HUD which frees its buffer. Then the entities vector is destroyed; each unique_ptr destroys its Entity, which calls the correct derived destructor (Player or Enemy), each of which destroys its Animator, which frees its tints array. Zero leaks, no manual delete, every owned resource cleaned up by an owning class. RAII end-to-end.
Understanding the Code
Entity is what Player and Enemy are — a base class that gives them shared state (Animator, position, size) and shared default behavior (update, render).
IUpdatable and IDrawable are what Entity and HUD do — contracts that say "I can be ticked" and "I can be drawn." Anything implementing these contracts can show up in the polymorphic loops.
Those two relationships are different. Inheritance from Entity is structural — "Player and Enemy share state and code." Implementation of an interface is behavioral — "Entity and HUD both happen to know how to update themselves." A well-designed game uses both at the same time, exactly the way this project does. Inheritance for shared structure. Interfaces for shared capability.
Also worth pausing on: how small the changes from Chapter 19 to Chapter 21 actually were. Two new tiny headers (twelve lines combined). Three changes to Entity.h. Two changes to Enemy.h. Three changes to HUD.h. A rewrite of two short blocks in main.cpp. Nothing in the implementation files. That's good architecture — the right tools let you change behavior dramatically with surgical edits.
Experimenting
- Add a third character type. Derive
BossfromEntity, override itsupdateandrender, push it into the entities vector. The interface views pick it up automatically — no changes needed in the loops. - Add a
Backgroundclass. Make it implementIDrawableonly (it has nothing to update). Push&backgroundinto thedrawablesvector first so it renders behind everything else. Notice you don't have to make it inherit from Entity — the interface is enough. - Add an
IUpdatablethat isn't drawable. Maybe aTimerclass that triggers aSDL_Logmessage every 5 seconds. Push it intoupdatablesonly. The render loop never sees it, the update loop ticks it correctly. Two interfaces really do beat one combined interface.
Common Errors and Fixes
"cannot allocate an object of abstract type IUpdatable" — You wrote IUpdatable u; somewhere by accident. Pure abstract classes can't be instantiated; they only exist to be inherited from. Use IUpdatable* (or IUpdatable&) instead.
Loop calls Entity::update even for Enemy objects — You forgot virtual somewhere along the chain. The interface declares virtual void update(float) = 0;, Entity provides void update(float) override;, and Enemy provides void update(float) override;. Drop any of those virtual/override keywords and the dispatch can revert to static, with predictable wrong behavior.
Crash on exit — Almost always a missing virtual destructor. In this project the interfaces provide it for free; if you copied the wrong code somewhere and lost it, derived destructors stop running on cleanup. Re-check that IUpdatable.h and IDrawable.h both declare virtual ~... = default;.
updatables.push_back(e.get()) doesn't compile — Often a missing include of IUpdatable.h (or IDrawable.h) in main.cpp. The compiler needs to know what the interface is before it can convert the pointer.
AI Exercise (Optional)
"I have a small C++ SDL 3 project with two pure-abstract interfaces,
IUpdatableandIDrawable. AnEntitybase class implements both.PlayerandEnemyderive fromEntity.HUDimplements both interfaces directly, independent ofEntity.mainholds entities instd::vector<std::unique_ptr<Entity>>and HUD in its own variable, then builds two non-owning views:std::vector<IUpdatable*>andstd::vector<IDrawable*>. I have learned: variables, flow, loops, functions, pointers, vectors, maps, classes, composition, inheritance, virtual, override, pure virtual, abstract classes, and interfaces. I have NOT learned templates,std::function, or lambdas in depth. Add a new classParticleFieldthat implements onlyIDrawable: it renders a swarm of small bouncing colored squares, but it doesn't need updating from the outside (it tracks its own internal clock). Wire it into my drawables view so it renders BEHIND everything else. Use only the C++ features I have. Paste any new or modified files."
That prompt is testing whether the AI respects two design constraints at once: that ParticleField should implement only IDrawable, and that it should be drawn first (so it appears behind). A good answer will keep ParticleField out of the updatables vector entirely, and will push &particleField to the front of the drawables vector (or insert it before the entities are added). A weak answer might shove it into both vectors out of habit. Read carefully and push back if needed.
Summary — and the End of Act 2
You've built the last project of Act 2 and pulled together every OOP tool the book has taught: classes for encapsulating data and behavior, composition for building bigger objects out of smaller ones, inheritance for sharing code between related classes, virtual functions for letting the actual object decide which method gets called, and interfaces for letting unrelated classes share contracts the rest of the code can use. The Animated Character that started as one class in Chapter 17 ended up as a small ecosystem of classes and interfaces — and the per-frame work in main shrank along the way to just two short loops.
That's modern C++ for game architecture, written small. Real engines look more complicated, but the pieces are the same. Act 3 begins with a tour of the C++ features this book deliberately saved for last — templates, lambdas, the four named casts, exceptions, and more.