Every program you've written so far has been a collection of variables and functions sitting side by side in one file, usually main.cpp. That approach works for small programs. But try to picture a real game written that way — a Player with their position, health, ammo, score, a dozen other values, plus the functions that move them, damage them, heal them, draw them. Now add enemies. Now add bullets, pickups, tiles, particles. Before long you've got hundreds of variables and functions all touching each other, and any change to one corner threatens to break something on the other side of the file.
This chapter is about a better way. Object-oriented programming, or OOP, is a way of organizing code so that each thing in your game becomes a self-contained unit with its own data and its own behaviour.
In this chapter, we will:
- Meet classes and the idea of an object
- Write member variables and member functions
- Use
publicandprivateto control access to a class's internals - Write constructors and destructors to set up and tear down objects
- Meet member initializer lists and
constmember functions - Split a class across a header file and a source file
- Meet the
thispointer, static members, and RAII in brief - Sketch small
Player,Enemy, andBulletclasses - Try an AI exercise on designing a small class
What Is a Class?
A class is a blueprint. An object is a thing built from that blueprint. You write the class once. You create as many objects from it as you need.
class Player {
int health;
int score;
void takeDamage(int amount) {
health -= amount;
}
};
Player now has two member variables, health and score, and one member function, takeDamage. Member variables describe what a player has. Member functions describe what a player can do.
public and private
By default, everything inside a class is private. Private means only code inside the class itself can touch it. To expose a member to the outside world, mark it as public:
class Player {
private:
int health;
int score;
public:
void takeDamage(int amount) {
health -= amount;
}
int getHealth() {
return health;
}
};
Now health and score are private — nothing outside the class can read or change them directly. takeDamage and getHealth are public — any code can call them.
int main() {
Player p;
p.takeDamage(20);
std::cout << p.getHealth() << std::endl;
return 0;
}
This pattern — private data, public functions that mediate access — is called encapsulation. When a member variable is private, the only way to change it is through a function you wrote. That function can check for nonsense. takeDamage could refuse to reduce health below zero. If health is private and only accessed via getHealth() and takeDamage(), those two functions are the only places that need updating if you ever change the internal representation.
A Note on protected
There's a third access specifier called protected. It behaves like private except for classes that inherit from this one, which can still see protected members. Inheritance is a Chapter 18 topic — flagging it here so you recognize the keyword when you see it.
Constructors
A constructor is a special member function that runs automatically whenever an object is created. Constructors have the same name as the class and no return type:
class Player {
private:
int health;
int score;
public:
Player() {
health = 100;
score = 0;
}
Player(int startingHealth) {
health = startingHealth;
score = 0;
}
};
Player p1; // Calls Player() — starts with health = 100
Player p2(50); // Calls Player(int) — starts with health = 50
Member Initializer Lists
The better way to write constructors uses a member initializer list:
Player() : health(100), score(0) {
}
Player(int startingHealth) : health(startingHealth), score(0) {
}
The colon after the parameter list introduces the initializer list. Each member is given its value directly — one step instead of two. Some members must be initialized via the initializer list: const members and reference members can't be assigned to after they're created. Use the initializer list. It's the idiomatic way to write C++ constructors.
Destructors
A destructor runs when an object is destroyed — automatically when it goes out of scope. Destructors have the same name as the class, prefixed with ~, and take no parameters:
~Player() {
std::cout << "Player destroyed." << std::endl;
}
RAII in Brief
RAII stands for Resource Acquisition Is Initialization — an ugly name for a beautiful idea: tie the lifetime of a resource to the lifetime of an object. You acquire the resource (open a file, allocate memory) in the constructor, you release it in the destructor. Because C++ guarantees the destructor runs when the object goes out of scope, you can never forget to clean up. You'll meet RAII in action in Chapter 17.
const Member Functions
int getHealth() const {
return health;
}
The const after the parameter list means this function promises not to modify any member variables. const member functions can be called on const objects; non-const ones can't. Make it a habit to mark any member function that doesn't modify the object as const.
Header Files and Source Files
Real programs split each class into two files: a header file (.h) that declares what the class looks like, and a source file (.cpp) that defines what the functions actually do.
Here's Player.h:
#pragma once
class Player {
private:
int health;
int score;
public:
Player();
Player(int startingHealth);
void takeDamage(int amount);
int getHealth() const;
int getScore() const;
};
#pragma once at the top is an include guard — it prevents the header from being processed twice in the same translation unit. Here's Player.cpp:
#include "Player.h"
Player::Player() : health(100), score(0) {
}
Player::Player(int startingHealth) : health(startingHealth), score(0) {
}
void Player::takeDamage(int amount) {
health -= amount;
}
int Player::getHealth() const {
return health;
}
int Player::getScore() const {
return score;
}
The Player:: prefix on every function name is the scope resolution operator. It tells the compiler "this function named takeDamage belongs to the Player class." Any file that wants to use Player just #include "Player.h".
The this Pointer
Inside a member function you sometimes have a parameter with the same name as a member variable. The this pointer resolves the ambiguity:
void setHealth(int health) {
this->health = health; // this->health = the member, health = the parameter
}
this is a pointer to the object the function was called on. The -> operator accesses members through a pointer.
Static Members
class Enemy {
private:
static int totalEnemies;
public:
Enemy() { totalEnemies++; }
~Enemy() { totalEnemies--; }
static int getTotalEnemies() {
return totalEnemies;
}
};
// One-time definition in a .cpp file:
int Enemy::totalEnemies = 0;
totalEnemies is a static member variable — there's exactly one of it, shared across every Enemy object. Static member functions belong to the class rather than to any particular object; they don't have a this pointer. Call them through the class name:
std::cout << Enemy::getTotalEnemies() << std::endl;
A Small Game OOP Example
Here's a minimal Bullet.h:
#pragma once
class Bullet {
private:
float x;
float y;
float speed;
bool active;
public:
Bullet(float startX, float startY, float bulletSpeed);
void update(float deltaTime);
void deactivate();
bool isActive() const;
float getX() const;
float getY() const;
};
And Bullet.cpp:
#include "Bullet.h"
Bullet::Bullet(float startX, float startY, float bulletSpeed)
: x(startX), y(startY), speed(bulletSpeed), active(true) {
}
void Bullet::update(float deltaTime) {
if (!active) return;
y -= speed * deltaTime;
}
void Bullet::deactivate() {
active = false;
}
bool Bullet::isActive() const { return active; }
float Bullet::getX() const { return x; }
float Bullet::getY() const { return y; }
Player and Enemy would follow the same pattern. The main loop of the game then becomes beautifully simple:
player.update(deltaTime);
for (auto& enemy : enemies) {
enemy.update(deltaTime);
}
for (auto& bullet : bullets) {
bullet.update(deltaTime);
}
main doesn't know or care how a Player updates itself, or how an Enemy decides where to move. Each class handles its own logic. main just tells them all to update and trusts them to get on with it.
AI Exercise
"I'm a C++ beginner learning object-oriented programming. I want to design a small class called
PowerUpfor a 2D game. APowerUphas a position (x and y as floats), a type (one of: health, ammo, shield), and an activation state (active or used). It needs a constructor that takes a position and a type, a function to check if the player has touched it and activate it if so, and getters for its position and state. Please sketch this class as a header file (PowerUp.h) with public and private sections, an appropriate constructor, andconston the getters where appropriate. Don't write the .cpp file — just the header. Briefly explain any design decisions you made."
When you get the response, work through it carefully. Did the AI mark the position and state as private? Did it use const on the getters? Did it use a member initializer list? How did it represent the type — std::string, an enum, or an int? Each has trade-offs worth asking about.
Summary
You've made the biggest conceptual leap in the book. Classes give you a way to bundle data and behaviour together into self-contained units. Encapsulation — private data, public functions — keeps your code safe from accidental interference. Constructors set up your objects properly, destructors clean up after them, and RAII ties those two together. Header files and source files split each class cleanly across two files, and scope resolution with :: stitches them back together for the compiler.
Real games have families of related classes — many kinds of enemy, many kinds of pickup. Writing separate classes for each, duplicating the code they share, would be miserable. C++ has a better answer, and it's called inheritance. In the next theory chapter we'll meet it, and you'll see how one class can build on another to grow a whole family of related types without ever repeating yourself.