Skip to content

Understanding Modern Game Engine Architecture with ECS

How ColumbaEngine's Entity Component System Makes Game Development Simple

ecscppgame-engine

Reading time: ~5 minutes | Level: Intermediate | Next: Building Complete Games with ColumbaEngine

Your Game Architecture Probably Sucks (And That’s Not Your Fault)

Let’s be honest. If you’ve built games the “traditional” way—with inheritance hierarchies, GameObject base classes, and virtual functions everywhere—you’ve probably hit that moment. You know the one. Where adding a simple feature means refactoring half your codebase. Where your “Player” class is 2,000 lines long. Where making a treasure chest that can be destroyed requires multiple inheritance gymnastics that would make a C++ compiler cry.

You’re not alone. I’ve been there. The entire industry has been there. That’s why Unity rewrote their entire engine with DOTS. Why Unreal built Mass Entity. Why id Software moved to ECS for DOOM Eternal. The old way of building games is fundamentally broken.

But here’s the thing: there’s a better way. It’s called Entity Component System (ECS), and it’s not just another programming pattern—it’s a completely different way of thinking about game architecture. Today, I’ll show you exactly how it works using ColumbaEngine, our production-ready ECS engine, with real code you can compile and run right now.

No theory. No abstract concepts. Just practical, working examples that will change how you build games forever.

Why Traditional Game Programming Is Broken

Let’s say you’re building an action RPG. You start with a reasonable class hierarchy:

class GameObject {
public:
    virtual void update(float deltaTime) = 0;
    virtual void render() = 0;
};

class Character : public GameObject {
protected:
    float health;
    float x, y;
    Sprite sprite;
public:
    void takeDamage(float amount) { health -= amount; }
    void move(float dx, float dy) { x += dx; y += dy; }
};

class Player : public Character { /* ... */ };
class Enemy : public Character { /* ... */ };

Looks good, right? Then reality hits:

“We need treasure chests” - They render and have position, but don’t move or have health. Where do they fit?

“We need moving platforms” - They move but aren’t characters. Another branch?

“We need ghosts that pass through walls” - Enemies that don’t collide normally. Special case?

“We need destructible walls” - Have health but don’t move. Now what?

Soon your clean hierarchy becomes a nightmare:

class GameObject { };
class PhysicalObject : public GameObject { };
class AnimatedObject : public GameObject { };
class AnimatedPhysicalObject : public PhysicalObject, public AnimatedObject { };
class Character : public AnimatedPhysicalObject { };
// ... endless combinations leading to chaos

I’ve seen real game projects with 147 GameObject subclasses, 23 levels of inheritance, and 40% of development time spent refactoring this mess. Adding a simple enemy type—something that should take hours—takes days of carefully threading it through the inheritance maze.

Enter ECS: A Radically Simple Solution

Entity Component System (ECS) completely reimagines game architecture. Instead of asking “what IS this object?” (inheritance), ECS asks “what DATA does this object have?” and “what SYSTEMS operate on that data?”

The Three Pillars of ECS

Entities: Just an ID

In ColumbaEngine, an entity is remarkably simple:

class Entity {
    _unique_id id;                    // Unique identifier
    std::list<EntityHeld> components;  // What components this entity has
    std::string name;                  // For debugging
};

That’s it. An entity is just a unique ID that groups components together—a container, or even simpler, a sticky note saying “these components belong together.”

Components: Pure Data

Components hold your game’s state with zero logic:

struct Position : public Component {
    float x, y;
    DEFAULT_COMPONENT_MEMBERS(Position)
};

struct Velocity : public Component {
    float dx, dy;
    DEFAULT_COMPONENT_MEMBERS(Velocity)
};

struct Health : public Component {
    float current, max;
    DEFAULT_COMPONENT_MEMBERS(Health)
};

struct Sprite : public Component {
    std::string texturePath;
    int width, height;
    DEFAULT_COMPONENT_MEMBERS(Sprite)
};

No methods. No behavior. Just data. This separation is powerful—components are trivial to serialize, network, and modify with tools.

Systems: Pure Logic

Systems contain all your game logic, operating on entities with specific component combinations:

// Movement system - processes entities with Position AND Velocity
class MovementSystem : public System<Own<Position>, Own<Velocity>> {
public:
    void execute() override {
        float deltaTime = timer.getDeltaTime();

        // Process ALL entities that have both Position and Velocity
        for (auto [entity, pos, vel] : getEntities()) {
            pos->x += vel->dx * deltaTime;
            pos->y += vel->dy * deltaTime;
        }
    }
};

// Render system - draws entities with Position AND Sprite
class RenderSystem : public System<Own<Position>, Own<Sprite>> {
public:
    void execute() override {
        for (auto [entity, pos, sprite] : getEntities()) {
            drawSprite(sprite->texturePath, pos->x, pos->y);
        }
    }
};

The system doesn’t care what entity it’s processing—player, enemy, projectile—if it has the required components, it gets processed.

The Power of Composition

Here’s how those problematic game objects look in ColumbaEngine:

// Player - moves, renders, takes damage, player-controlled
Entity player = ecs->createEntity("Player");
ecs->attach<Position>(player, {100, 100});
ecs->attach<Velocity>(player, {0, 0});
ecs->attach<Sprite>(player, {"player.png", 32, 32});
ecs->attach<Health>(player, {100, 100});
ecs->attach<PlayerController>(player);

// Enemy - same as player but AI-controlled
Entity enemy = ecs->createEntity("Goblin");
ecs->attach<Position>(enemy, {300, 100});
ecs->attach<Velocity>(enemy, {0, 0});
ecs->attach<Sprite>(enemy, {"goblin.png", 32, 32});
ecs->attach<Health>(enemy, {50, 50});
ecs->attach<AIController>(enemy);  // Only difference!

// Treasure chest - renders, has position, but doesn't move
Entity chest = ecs->createEntity("TreasureChest");
ecs->attach<Position>(chest, {200, 200});
ecs->attach<Sprite>(chest, {"chest.png", 32, 32});
ecs->attach<Container>(chest, {{"gold", 100}});

// Ghost - enemy that passes through walls
Entity ghost = ecs->createEntity("Ghost");
ecs->attach<Position>(ghost, {400, 400});
ecs->attach<Velocity>(ghost, {0, 0});
ecs->attach<Sprite>(ghost, {"ghost.png", 32, 32});
ecs->attach<Health>(ghost, {30, 30});
ecs->attach<AIController>(ghost);
// Note: No Collider component = passes through walls!

// Moving platform - moves but no health or AI
Entity platform = ecs->createEntity("MovingPlatform");
ecs->attach<Position>(platform, {150, 300});
ecs->attach<Velocity>(platform, {50, 0});
ecs->attach<Sprite>(platform, {"platform.png", 64, 16});
ecs->attach<Path>(platform, {{150, 300}, {250, 300}});

No inheritance hierarchies. No virtual functions. No “is-a” relationships. Just entities with exactly the components they need.

Want the chest to be destructible? Add a Health component. Want the ghost to leave a trail? Add a ParticleEmitter. Each change is one line that doesn’t affect anything else.

Real-World Example: Building Tetris with ECS

Let’s build something real. Here’s how Tetris works in ColumbaEngine:

// Tetris-specific components
struct GridPosition : public Component {
    int x, y;  // Position in game grid (not pixels)
    DEFAULT_COMPONENT_MEMBERS(GridPosition)
};

struct TetrisPiece : public Component {
    enum Type { I, O, T, S, Z, J, L } type;
    int rotation = 0;
    DEFAULT_COMPONENT_MEMBERS(TetrisPiece)
};

struct FallTimer : public Component {
    float timeUntilFall;
    float fallSpeed;
    DEFAULT_COMPONENT_MEMBERS(FallTimer)
};

// Create a falling piece
Entity piece = ecs->createEntity("ActivePiece");
ecs->attach<GridPosition>(piece, {5, 0});        // Start at top
ecs->attach<TetrisPiece>(piece, {Type::T, 0});   // T-shaped piece
ecs->attach<FallTimer>(piece, {1.0f, 1.0f});     // Fall every second
ecs->attach<Sprite>(piece, {"t_piece.png", 30, 30});
ecs->attach<PlayerControllable>(piece);          // Player can control

Now the systems that make it work:

// Handles piece falling
class TetrisFallSystem : public System<Own<GridPosition>,
                                       Own<TetrisPiece>,
                                       Own<FallTimer>> {
public:
    void execute() override {
        float deltaTime = timer.getDeltaTime();

        for (auto [entity, gridPos, piece, fallTimer] : getEntities()) {
            fallTimer->timeUntilFall -= deltaTime;

            if (fallTimer->timeUntilFall <= 0) {
                fallTimer->timeUntilFall = fallTimer->fallSpeed;

                if (canMoveTo(gridPos->x, gridPos->y + 1, piece)) {
                    gridPos->y += 1;
                } else {
                    lockPiece(entity);
                }
            }
        }
    }

private:
    void lockPiece(Entity entity) {
        ecs->detach<PlayerControllable>(entity);
        ecs->detach<FallTimer>(entity);
        ecs->sendEvent(PieceLocked{entity});
        ecs->sendEvent(CheckForLines{});
        ecs->sendEvent(SpawnNewPiece{});
    }
};

// Handles player input
class TetrisInputSystem : public System<Own<PlayerControllable>,
                                        Own<GridPosition>,
                                        Own<TetrisPiece>> {
public:
    void onKeyPressed(int key) {
        for (auto [entity, control, gridPos, piece] : getEntities()) {
            switch(key) {
                case KEY_LEFT:
                    if (canMoveTo(gridPos->x - 1, gridPos->y, piece))
                        gridPos->x -= 1;
                    break;

                case KEY_RIGHT:
                    if (canMoveTo(gridPos->x + 1, gridPos->y, piece))
                        gridPos->x += 1;
                    break;

                case KEY_UP:
                    rotatePiece(piece);
                    break;

                case KEY_DOWN:
                    // Speed up falling
                    if (auto timer = ecs->getComponent<FallTimer>(entity))
                        timer->fallSpeed = 0.05f;
                    break;
            }
        }
    }
};

Notice the magic? The FallSystem doesn’t know about input or rendering. The InputSystem doesn’t know about the board state. The RenderSystem (which we already defined) will automatically draw anything with Position and Sprite. They communicate through events without knowing about each other.

Why This Changes Everything

Performance That Scales

ECS provides massive performance benefits through data locality:

// Traditional OOP - objects scattered in memory
GameObject* objects[1000];
for (int i = 0; i < 1000; i++) {
    objects[i]->update();  // Cache miss likely for each object
}

// ECS - components stored contiguously
Position positions[1000];
Velocity velocities[1000];
for (int i = 0; i < 1000; i++) {
    positions[i].x += velocities[i].dx * deltaTime;  // Cache-friendly!
}

Real measurements show 5-10x performance improvements for systems processing many entities.

Flexibility Without Complexity

Need to add a feature? Just create a new component and system:

// Add particle effects to any entity
struct ParticleEmitter : public Component {
    std::string particleType;
    float rate;
};

class ParticleSystem : public System<Own<Position>, Own<ParticleEmitter>> {
    void execute() override {
        for (auto [entity, pos, emitter] : getEntities()) {
            spawnParticles(pos->x, pos->y, emitter->particleType, emitter->rate);
        }
    }
};

// Now ANY entity can have particles
ecs->attach<ParticleEmitter>(player, {"sparkle", 10.0f});
ecs->attach<ParticleEmitter>(treasure, {"glow", 5.0f});

Parallel Processing for Free

Systems that don’t share components can run in parallel:

// These can run simultaneously
parallel_execute(
    movementSystem,      // Modifies: Position
    animationSystem,     // Modifies: Sprite
    particleSystem,      // Modifies: ParticleEmitter
    audioSystem          // Modifies: AudioSource
);

ColumbaEngine automatically detects parallelization opportunities, giving you free performance on multi-core processors.

The Industry Is Moving to ECS

This isn’t just theory. Major engines are adopting ECS because it works:

  • Unity DOTS: Complete ECS rewrite for 100x performance gains
  • Unreal Mass Entity: ECS for massive crowds and simulations
  • Bevy: Rust engine built entirely on ECS
  • Godot: Experimenting with ECS for Godot 4.x

By learning ECS through ColumbaEngine’s clean implementation, you’re learning the future of game development.

Start Building with ColumbaEngine Today

Ready to experience ECS yourself? Here’s how to start:

1. Clone and Build:

git clone https://github.com/columbaengine/columba
cd columba && mkdir build && cd build
cmake .. && make
./examples/tetris  # Play the Tetris example

2. Create Your First Game:

auto ecs = std::make_unique<EntitySystem>();

// Create player
auto player = ecs->createEntity("Player");
ecs->attach<Position>(player, {100, 100});
ecs->attach<Velocity>(player, {0, 0});
ecs->attach<Sprite>(player, {"player.png", 32, 32});

// Create enemy
auto enemy = ecs->createEntity("Enemy");
ecs->attach<Position>(enemy, {200, 100});
ecs->attach<Velocity>(enemy, {-10, 0});
ecs->attach<Sprite>(enemy, {"enemy.png", 32, 32});

// Systems automatically process them!

3. Join the Community:

  • GitHub: github.com/columbaengine
  • Discord: Active developers building real games
  • Docs: Complete API reference and tutorials

Why ColumbaEngine?

ColumbaEngine isn’t just another engine—it’s a learning platform for modern game architecture. With clean, readable code and a pure ECS implementation, it’s perfect for:

  • Learning: Understand how modern engines really work
  • Prototyping: Build games quickly with minimal boilerplate
  • Production: Ships with WebAssembly support for browser deployment
  • Teaching: Clear architecture perfect for education

Whether you’re building your first game or your hundredth, ColumbaEngine’s ECS architecture provides the foundation you need without the complexity you don’t.

Next Steps

The best way to understand ECS is to use it. Download ColumbaEngine, run the examples, and build something. Start small—a Pong clone, a simple shooter—and watch how naturally features come together.

In future articles, we’ll explore:

  • Advanced ECS patterns and optimizations
  • Building multiplayer with component synchronization
  • WebAssembly deployment for browser games
  • Creating custom tools and editors

The game industry is moving toward data-driven architectures. Join us at the forefront with ColumbaEngine.


Ready to revolutionize how you build games? Star us on GitHub and join our Discord community.