
I. Foreword
Whether to use procedural or object-oriented programming is indeed a problem, especially for beginners. Procedural code is straightforward and simple in its flow. It also doesn’t require a lot of prior design; you can just do things as you think of them. However, as a project expands rapidly, the drawbacks of procedural programming begin to show. The two biggest pitfalls I have encountered are as follows:
- Code reuse primarily relies on copy-pasting, which often leads to forgetting to modify other related functionalities when changes are needed, and many bugs are generated this way.
- Variable scope confusion, which leads to logic errors that are more difficult to trace.
However, some people are exceptionally talented and possess a strong structured code mindset, allowing them to avoid these two major pitfalls. If that’s the case, please continue to enjoy your “free” structured programming journey.
While I’ve mentioned some of the drawbacks, is procedural programming completely useless? It is not. Structured programming is more suitable for quickly validating a function, process, or method.
II. Mainstream Programming Paradigms and Their Application in Games
1. Procedural Programming
- Features: Function-driven, clear structure, suitable for small projects
- Application Scenarios: Input handling, initialization functions, rapid prototyping
- Drawbacks: Lack of modularity, difficult to maintain
- Example Code:
C++
void handleInput() {
// Simple input handling logic
}
void update() {
// Update game state
}
void render() {
// Render the screen
}
int main() {
while (running) {
handleInput();
update();
render();
}
return 0;
}
This structure can indeed speed up development when the project is small. However, when a project grows to a certain scale (e.g., a 3A console game), this design becomes a nightmare for the development team.
2. Object-Oriented Programming (OOP)
- Features: Classes and inheritance, encapsulated behavior, supports reuse
- Application Scenarios: Character classes, skill classes, UI components like widgets
- Drawbacks: Deep inheritance hierarchies can make maintenance difficult
- Example Code:
C++
// Base class: GameObject is an abstraction for all game entities
class GameObject {
public:
GameObject(std::string name) : name_(name) {}
virtual ~GameObject() = default;
virtual void Update(float deltaTime) = 0;
virtual void Render() const = 0;
std::string GetName() const { return name_; }
protected:
std::string name_;
};
// A character is a GameObject
class Character : public GameObject {
public:
Character(std::string name, int hp)
: GameObject(name), health_(hp) {}
void TakeDamage(int amount) {
health_ -= amount;
std::cout << name_ << " took " << amount << " damage. HP: " << health_ << "n";
}
void Update(float deltaTime) override {
// Implement character-specific logic updates
std::cout << name_ << " is thinking...n";
}
void Render() const override {
std::cout << name_ << " is being drawn on screen.n";
}
protected:
int health_;
};
// An enemy is a Character
class Enemy : public Character {
public:
Enemy(std::string name, int hp, int atk)
: Character(name, hp), attack_(atk) {}
void Attack(Character& target) {
std::cout << name_ << " attacks " << target.GetName() << " for " << attack_ << " damage.n";
target.TakeDamage(attack_);
}
void Update(float deltaTime) override {
// Enemy-specific AI logic
std::cout << name_ << " is hunting...n";
}
private:
int attack_;
};
Explanation:
GameObject
is an abstract class (pure virtual functions) used to define an interface.Character
andEnemy
both use the “is-a” relationship for inheritance, which is semantically clear.- Subclasses override the base class’s virtual functions (Update, Render) to achieve polymorphism.
- The logic responsibilities are clear and do not violate the Single Responsibility Principle (SRP).
Improper Example: Inheriting a Behavior Class as a Base Class
C++
#include <iostream>
// Assume a general movement logic class is designed
class MovementSystem {
public:
void Move(float x, float y) {
std::cout << "Moving to (" << x << ", " << y << ")n";
}
};
// Error: Player inherits MovementSystem
class Player : public MovementSystem {
public:
void Update() {
// Input handling logic...
std::cout << "Player update...n";
// Call the method inherited from MovementSystem
Move(1.0f, 2.0f);
}
};
int main() {
Player player;
player.Update();
}
Problem Analysis:
- A
Player
is not aMovementSystem
; the inheritance violates the is-a principle. - Movement is just a “capability” of the player, not their “essence”.
- If multiple objects (like Enemy, NPC) need movement logic, they must inherit repeatedly, leading to rigid and difficult-to-extend code.
MovementSystem
has no polymorphic interface and no abstract purpose; it is merely an encapsulation of behavior.
Correct Approach: Using Composition to Implement a “has-a” Relationship
C++
class MovementSystem {
public:
void Move(float x, float y) {
std::cout << "Moving to (" << x << ", " << y << ")n";
}
};
class Player {
public:
Player() : movement_(std::make_shared<MovementSystem>()) {}
void Update() {
std::cout << "Player update...n";
// Use the composed object to handle behavior
movement_->Move(1.0f, 2.0f);
}
private:
std::shared_ptr<MovementSystem> movement_;
};
int main() {
Player player;
player.Update();
}
Analysis of Advantages:
- A
Player
has aMovementSystem
, making the composition relationship clearer. - Behavior can be reused: Enemies, NPCs, etc., can all be composed with different
MovementSystem
subclasses. - Easier to test, extend, and replace (e.g., an aerial movement system, a pathfinding system).
- Satisfies the object-oriented design principle of “Favor Composition over Inheritance”.
In game development, behavior modules such as input control, movement, AI, and animation are better designed as composable components rather than using inheritance. Component-based design helps improve the flexibility and maintainability of the system.
3. Data-Driven Design
- Features: Separates “behavior” and “configuration,” suitable for on-the-fly adjustments
- Application Scenarios: Skill lists, AI behaviors, scene libraries
- Advantages: No compilation required, supports hot-reloading, easy to maintain
- Example JSON Configuration:
JSON
{
"id": 101,
"name": "Fireball",
"damage": 50,
"cooldown": 2.0
}
- Parsing Example (using nlohmann::json, project available at: https://github.com/nlohmann/json):
C++
json j = json::parse(config_str);
std::string name = j["name"];
int damage = j["damage"];
double cd = j["cooldown"];
4. Component-Based Design
- Features: Composition over inheritance, dynamic loading/unloading of components
- Application Scenarios: ECS architecture, command skills, enemy AI
- Advantages: Highly extensible, reusable components, easier for scene switching
- Simplified UML Structure:

Example Code: C++
// === Abstract Component Base Class ===
class Component {
public:
virtual ~Component() = default;
virtual void update() = 0;
};
// === Concrete Component: Health ===
class Health : public Component {
public:
explicit Health(int hp) : hp_(hp) {}
void update() override {
std::cout << "[Health] Current HP: " << hp_ << "n";
hp_ += 1; // Assume 1 HP is recovered per frame
}
private:
int hp_;
};
// === Concrete Component: Position ===
class Position : public Component {
public:
Position(float x, float y) : x_(x), y_(y) {}
void update() override {
std::cout << "[Position] Position: (" << x_ << ", " << y_ << ")n";
x_ += 1.0f; // Move to the right
}
private:
float x_, y_;
};
// === Entity Class: Supports adding, getting, and checking for components ===
class Entity {
public:
// Add a component (overwrites an old component of the same type)
template<typename T, typename... Args>
void addComponent(Args&&... args) {
auto component = std::make_shared<T>(std::forward<Args>(args)...);
components_[typeid(T)] = component;
}
// Get a component (type-safe)
template<typename T>
std::shared_ptr<T> getComponent() {
auto it = components_.find(typeid(T));
if (it != components_.end()) {
return std::dynamic_pointer_cast<T>(it->second);
}
return nullptr;
}
// Check if the entity has a component of a specific type
template<typename T>
bool hasComponent() const {
return components_.find(typeid(T)) != components_.end();
}
// Update all components
void update() {
for (auto& [type, comp] : components_) {
comp->update();
}
}
private:
std::unordered_map<std::type_index, std::shared_ptr<Component>> components_;
};
int main() {
Entity player;
// Add components
player.addComponent<Health>(100);
player.addComponent<Position>(10.0f, 20.0f);
// Check if a component exists
if (player.hasComponent<Health>()) {
std::cout << "Player has health.n";
}
std::cout << "n== Frame 1 ==n";
player.update();
std::cout << "n== Frame 2 ==n";
player.update();
// Access a specific component
auto health = player.getComponent<Health>();
if (health) {
std::cout << "n[Debug] Accessed Health component via getComponent<>()n";
}
return 0;
}
III. Common Programming Models in Game Development
1. Game Loop
- Fixed logic steps vs. variable rendering frames
- Handling
deltaTime
: unifying motion, animation, and frame rate - General flow: Input → Logic → Render → Sound
- Pseudocode:
C++
while (running) {
processInput();
updateLogic(deltaTime);
renderFrame();
}
2. Layered Module Thinking
- Common layers: Input, Logic, Render, Scene, UI
- Each module has an independent interface and lifecycle
IV. C++ Code Mindset Shifts
1. From “Writing a Function” to “Writing a Structure”
- Not just completing a single behavior, but creating a design that supports a series of behaviors
- Case study: Skill base class + implementation classes, using an interface to maintain a unified usage method
C++
class Character {};
class Player : public Character {};
class Enemy : public Character {};
class Skill {
public:
virtual void cast(Character* target) = 0;
};
class Fireball : public Skill {
public:
void cast(Character* target) override {
// Fireball implementation
}
};
2. From “Direct Connection” to “Decoupling”
- System A does not directly depend on System B; communication is done through events or messages
- Case study: Attack behavior → send event → health system → UI refresh
- Example Code:
C++
class Event {
public:
std::string type;
// Other parameters
};
class EventBus {
std::unordered_map<std::string, std::vector<std::function<void(const Event&)>>> listeners;
public:
void subscribe(const std::string& type, std::function<void(const Event&)> cb) {
listeners[type].push_back(cb);
}
void publish(const Event& e) {
for (auto& cb : listeners[e.type]) {
cb(e);
}
}
};
3. From “One-Time Script” to “Reusable Module”
- Abstract functional structures: combat logic, action management, state machines, etc.
- Use templates, the Strategy Pattern, and composite class design
V. Common Mindset Traps in Development
Common Trap | Suggested Alternative Thinking |
Over-reliance on inheritance | Use composition, Strategy Pattern |
Global variable overflow | Singleton pattern, Service Locator |
Hardcoding logic | Configuration + handler function registration |
Disorganized, scattered logic | Modular design, interface protocols |
导出到 Google 表格
VI. Recommended Practice Path
- Start with small modules: event system, skill system, state management
- Prioritize composition, decoupling, and reusability
- Study open-source projects: entt, sol2, rpg_maker, miniaudio
VII. Conclusion
The focus of C++ programming in game development is not just about “making it work,” but about “making it easy to use, maintain, and extend”. Building a structured mindset, distinguishing between modules, and designing interfaces are the foundations for writing code that can truly run a heavy-duty game.