Chapter 16 was the biggest single conceptual shift in the book — classes, objects, encapsulation, constructors, destructors, RAII, composition, the lot. This chapter is where we put all of it to work in one tidy project, deliberately picked to play to OOP's strengths.
We're going to build a single animated character on a colored background, structured into three small classes. What we're building is a fixed-size colored square in the middle of the window that cycles smoothly through a rainbow of tints, with a small frame-rate bar in the top corner. Three classes do the work: an Animator that drives the tint, an HUD that smooths and draws the FPS, and a Player that owns the Animator and renders the square. Each class is small. Each class has one job. The main function ends up doing almost nothing — and that's the whole point.
In this chapter, we will:
- Split a project across multiple
.h/.cppfiles for the first time - Build an
Animatorclass that owns a heap-allocated buffer, with proper RAII - Build a
HUDclass that demonstrates the same RAII pattern a second time - Build a
Playerclass that has-a Animator (composition) - Disable copy on classes that own resources, so the compiler protects us
- See how a well-classed
mainshrinks to a few delegation calls - Try an optional AI exercise
Setting Up the Project
Create an empty C++ project named AnimatedCharacterClasses. Add these seven files:
Animator.handAnimator.cppHUD.handHUD.cppPlayer.handPlayer.cppmain.cpp
The Animator Class
The Animator's job: know, at any moment, which "frame" of an animation we're on and what color tint that frame corresponds to. It owns a heap-allocated array of SDL_Color values — one per frame — and frees that array when it's destroyed.
Animator.h
#pragma once
#include <SDL3/SDL.h>
class Animator
{
public:
Animator(int frameCount, float frameDuration);
~Animator();
Animator(const Animator&) = delete;
Animator& operator=(const Animator&) = delete;
void update(float dt);
SDL_Color currentTint() const;
int currentFrame() const { return currentFrame_; }
private:
SDL_Color* tints_; // OWNED — destructor frees it
int frameCount_;
float frameDuration_;
int currentFrame_;
float accumulator_;
};
The two = delete lines forbid copying and copy-assignment. If a class owns a heap buffer, copying it naively duplicates the pointer, and then both copies eventually try to delete the same memory — a crash. Deleting the copy operations makes the compiler refuse the bad code.
Animator.cpp
#include "Animator.h"
#include <cmath>
// Helper: hue (0..1) -> SDL_Color rainbow.
static SDL_Color hueToColor(float h)
{
float r = 0, g = 0, b = 0;
float i = std::floor(h * 6.0f);
float f = h * 6.0f - i;
float q = 1.0f - f;
switch (((int)i) % 6)
{
case 0: r = 1; g = f; b = 0; break;
case 1: r = q; g = 1; b = 0; break;
case 2: r = 0; g = 1; b = f; break;
case 3: r = 0; g = q; b = 1; break;
case 4: r = f; g = 0; b = 1; break;
case 5: r = 1; g = 0; b = q; break;
}
return SDL_Color{
(Uint8)(r * 255), (Uint8)(g * 255), (Uint8)(b * 255), 255
};
}
Animator::Animator(int frameCount, float frameDuration)
: tints_(new SDL_Color[frameCount])
, frameCount_(frameCount)
, frameDuration_(frameDuration)
, currentFrame_(0)
, accumulator_(0.0f)
{
for (int i = 0; i < frameCount_; ++i)
tints_[i] = hueToColor((float)i / (float)frameCount_);
}
Animator::~Animator()
{
delete[] tints_;
}
void Animator::update(float dt)
{
accumulator_ += dt;
while (accumulator_ >= frameDuration_)
{
accumulator_ -= frameDuration_;
currentFrame_ = (currentFrame_ + 1) % frameCount_;
}
}
SDL_Color Animator::currentTint() const
{
return tints_[currentFrame_];
}
The most important line in the constructor is tints_(new SDL_Color[frameCount]) — that's our heap allocation. The destructor calls delete[] tints_. Because we allocated with new SDL_Color[...] (the array form), we must free with delete[] (the array form). The destructor runs automatically whenever an Animator goes out of scope — the RAII guarantee.
The HUD Class
The HUD's job: display a smoothed frame-rate bar in the top-left of the window. It owns a circular buffer of recent frame-time samples. The pattern is identical in shape to Animator — own a heap buffer, allocate in the constructor, free in the destructor — and seeing it twice is what makes the pattern click.
HUD.h
#pragma once
#include <SDL3/SDL.h>
class HUD
{
public:
explicit HUD(int sampleCount = 60);
~HUD();
HUD(const HUD&) = delete;
HUD& operator=(const HUD&) = delete;
void update(float dt);
void render(SDL_Renderer* renderer) const;
float smoothedFps() const;
private:
float* samples_; // OWNED
int sampleCount_;
int nextIndex_;
int filledCount_;
};
The explicit keyword on the constructor stops C++ from using it as an implicit conversion. The default argument int sampleCount = 60 means you can write HUD hud; and get a 60-sample HUD by default.
HUD.cpp
#include "HUD.h"
HUD::HUD(int sampleCount)
: samples_(new float[sampleCount])
, sampleCount_(sampleCount)
, nextIndex_(0)
, filledCount_(0)
{
for (int i = 0; i < sampleCount_; ++i)
samples_[i] = 0.0f;
}
HUD::~HUD()
{
delete[] samples_;
}
void HUD::update(float dt)
{
samples_[nextIndex_] = dt;
nextIndex_ = (nextIndex_ + 1) % sampleCount_;
if (filledCount_ < sampleCount_) ++filledCount_;
}
float HUD::smoothedFps() const
{
if (filledCount_ == 0) return 0.0f;
float sum = 0.0f;
for (int i = 0; i < filledCount_; ++i)
sum += samples_[i];
float avgDt = sum / (float)filledCount_;
if (avgDt <= 0.0f) return 0.0f;
return 1.0f / avgDt;
}
void HUD::render(SDL_Renderer* renderer) const
{
float fps = smoothedFps();
if (fps > 120.0f) fps = 120.0f;
float fillFrac = fps / 120.0f;
SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255);
SDL_FRect frame { 10.0f, 10.0f, 200.0f, 14.0f };
SDL_RenderRect(renderer, &frame);
SDL_SetRenderDrawColor(renderer, 80, 200, 100, 255);
SDL_FRect fill { 12.0f, 12.0f, 196.0f * fillFrac, 10.0f };
SDL_RenderFillRect(renderer, &fill);
}
The Player Class — Composition
We have an Animator that drives a per-frame tint, and we want a Player that uses one to render itself. The relationship is composition — Player has-a Animator.
Player.h
#pragma once
#include <SDL3/SDL.h>
#include "Animator.h"
class Player
{
public:
Player(float screenWidth, float screenHeight,
int frameCount = 6,
float frameDuration = 1.0f / 8.0f);
Player(const Player&) = delete;
Player& operator=(const Player&) = delete;
void update(float dt);
void render(SDL_Renderer* renderer) const;
private:
Animator animator_; // HAS-A
float x_, y_;
float width_, height_;
};
The single line Animator animator_; is the entire mechanic of composition. It's an Animator by value — the actual Animator object lives inside every Player. When a Player is created, its animator_ is created at the same time. When a Player is destroyed, its animator_ is destroyed at the same time. The Animator's destructor (which frees its tints_ array) runs automatically as part of the Player being torn down. No new, no delete, no manual cleanup at the Player level.
Player.cpp
#include "Player.h"
Player::Player(float screenWidth, float screenHeight,
int frameCount, float frameDuration)
: animator_(frameCount, frameDuration)
, width_(120.0f)
, height_(120.0f)
, x_((screenWidth - width_) * 0.5f)
, y_((screenHeight - height_) * 0.5f)
{
}
void Player::update(float dt)
{
animator_.update(dt);
}
void Player::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 most interesting line is in the initializer list: : animator_(frameCount, frameDuration). That's calling the Animator constructor with our two arguments, and the resulting Animator is constructed in-place inside this Player. There's no separate "create then attach" step.
main.cpp — The Conductor
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include "Player.h"
#include "HUD.h"
const int WINDOW_W = 800;
const int WINDOW_H = 600;
int main(int argc, char* argv[])
{
if (!SDL_Init(SDL_INIT_VIDEO)) { /* error handling */ return 1; }
SDL_Window* window = SDL_CreateWindow(
"Animated Character - Classes Demo", WINDOW_W, WINDOW_H, 0);
SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr);
{
Player player((float)WINDOW_W, (float)WINDOW_H);
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);
hud.update(dt);
SDL_SetRenderDrawColor(renderer, 30, 35, 45, 255);
SDL_RenderClear(renderer);
player.render(renderer);
hud.render(renderer);
SDL_RenderPresent(renderer);
}
}
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
The extra { opens a small inner scope just for player and hud. When that block ends, both objects are destroyed before SDL_DestroyRenderer runs — a habit worth picking up early, since in the full asset-driven version the destructors free SDL resources and order absolutely matters.
Look at the update and render sections — four lines combined:
player.update(dt);
hud.update(dt);
player.render(renderer);
hud.render(renderer);
That's the entire per-frame work of the program at the main level. Every detail — the circular buffer, the rainbow tints, the rectangle drawing — is hidden inside the classes. This is what well-designed OOP buys you.
Playing the Game
Hit F5. A large square in the middle cycles slowly through the rainbow. In the top-left corner, a small green bar fills up to indicate your frame rate.
Understanding the Code
Pull back and look at the file structure. Seven files. Each header declares one class. Each source file implements that class. main.cpp includes the headers it needs (Player.h and HUD.h) and just wires the parts together.
Two classes, same RAII shape. Animator and HUD are unrelated — different jobs, different data, different methods. But the shape of how they manage their owned resource is identical: heap-allocate in the constructor, free in the destructor, disable copy. Once you've seen that pattern twice, you'll see it everywhere in real C++ code.
Composition lives in one line. Animator animator_; inside Player is, by itself, the whole composition story. The lifetime of the inner object is tied to the outer, the destruction order is correct, no special syntax needed at the call sites.
Common Errors and Fixes
"identifier 'Animator' is undefined" in Player.h — You forgot #include "Animator.h" at the top of Player.h.
Linker error "unresolved external symbol Player::update" — You declared update in Player.h but forgot to define it in Player.cpp.
Crash on exit — Almost always a destructor running on an already-freed pointer. Most likely cause: you accidentally enabled copy. Re-add the = delete lines and let the compiler help you.
AI Exercise (Optional)
"I have a small C++ SDL 3 OOP demo with three classes:
Animator,HUD, andPlayer. Each class is in its own .h/.cpp pair.PlayercomposesAnimator(has-a);HUDis independent. BothAnimatorandHUDown a heap-allocated array and free it in their destructors. I have learned: variables, flow, loops, functions, pointers, vectors, maps, classes, encapsulation, constructors and destructors, RAII, member initializer lists, and composition. I have NOT learned inheritance, polymorphism, virtual functions, or smart pointers in any depth. Add a new classBackgroundthat draws a slowly-shifting solid background color (cycling through its own colors over time, independently of Player's animator). Use the same patterns my existing code uses — own any resource in the class, free in the destructor, disable copy. Paste the full code for all updated files."
Summary
You've taken last chapter's brand-new OOP theory and used it to build a small, properly-layered SDL program. Three small classes, each one job. One composition relationship — Player has-a Animator — that demonstrates how the lifetime of a member is tied to its enclosing object with zero extra effort on your part. A main that does almost nothing because all the actual work is hidden inside the classes that do it best.
The next chapter introduces inheritance — the relationship that lets one class be a specialized version of another. After that, Chapter 19's project picks up exactly where this one leaves off: Player and Enemy will both derive from a common Entity base, and the main loop will treat them as a single collection of entities.