This commit is contained in:
2025-07-19 19:34:31 +02:00
parent e62bedd191
commit d791348c97
3 changed files with 337 additions and 3 deletions

View File

@@ -16,7 +16,7 @@ export const themeConfig: ThemeConfig = {
centeredLayout: true, // Use centered layout (false for left-aligned) centeredLayout: true, // Use centered layout (false for left-aligned)
themeToggle: false, // Show theme toggle button (uses system theme by default) themeToggle: false, // Show theme toggle button (uses system theme by default)
postListDottedDivider: false, // Show dotted divider in post list postListDottedDivider: false, // Show dotted divider in post list
footer: false, // Show footer footer: true, // Show footer
fadeAnimation: true // Enable fade animations fadeAnimation: true // Enable fade animations
}, },

View File

@@ -1,5 +1,338 @@
--- ---
title: 'Game Engine Series I - The Entity Component System' title: 'Game Engine Series I - Entity Component System'
pubDate: '2025-07-19' pubDate: '2025-07-19'
tag: 'games' tag: 'games'
--- ---
#
Welcome to the Game Engine Series.
In this series of blog posts, I'll be diving into one of my favorite topics:<br/>
**Game Engine Development**
The idea came up when I was in the middle on developing my own game engine. While
this blog serves as a purpose to teach and explain what I've learned
to programmers, this blog is also a way for my to track my progress and refine my
own understanding along the way.
Whether you're just curious about how game engines work under the hood, or you're
building one yourself. I hope this series gives you useful insights, ideas and
maybe even some inspiration for your future projects, or even your own game engine.
# Core Concepts Explained
ECS stands for <mark>Entity Component System</mark>. It is a data-oriented
design pattern that separates data (_components_) from behavior (_systems_), using
entities as simple IDs to associate them.
- **Entity** Consists of nothing but an ID. Think of it like a dumb label:</br>
`Player = 1`, `Tree = 2`, `Boss1 = 523`</br>
- **Component** - Components are attached to entities and represent what that entity
is made of.</br>
`Position { float x, y }`, `Health { int hp }`, `Level { uint8_t level }`
- **System** - This can be a `physics system`, `rendering system`, or whatever your game
needs. These systems iterate over your stored entites and apply the systems to them.
# Why use ECS over OOP?
Don't get me wrong. ECS takes advantage of OOP as well, but the difference lies in the
use of OOP. Traditionally, beginners learn one thing - inheritance (_including polymorphism_).
I would say that it's quite primitive to have one base class called `Entity`,
and another class called `Player`, which is derived from the base class `Entity`,
so it would look something like this:
```cpp
class Entity {
public:
Entity();
virtual ~Entity();
virtual void update() = 0;
virtual void render() = 0;
}
class Player : public Entity {
private:
// Some vars, objects or whatever
public:
Player() = default;
virtual ~Player() = default;
void update() override { /* Update logic of player */ }
void render() override { /* Render player */ }
}
```
Would you notice what's wrong there? Imagine this shape:
![](./_assets/oop-diamond-inheritance.png)
- Class `B` and class `C` both inherit from `A`.
- Class `D` inherits from both `B` and `C`.
So now, if `D` tries to access a member from `A`, which copy does it get?
Here, let's look at another example:
```cpp
class Enemy {
public:
void update() { /* generic update stuff */ }
};
class FlyingEnemy : public Enemy {
public:
void update() { /* flying logic */ }
};
class ShootingEnemy : public Enemy {
public:
void update() { /* shooting logic */ }
};
class FlyingShootingEnemy : public FlyingEnemy, public ShootingEnemy {
public:
void update() {
FlyingEnemy::update();
ShootingEnemy::update();
// shit's messy
}
};
```
- `FlyingShootingEnemy` now has two instances of `Enemy` in its inheritance tree.
- If `Enemy` has members or state, they're being duplicated, which leads to ambiguity.
The compiler gets confused, and so will you. Your project will end up in a virtual hell.
But these arent the only problems youll face.
As your project scales, this architecture will slam you into several brick walls:
- Virtual dispatches are relatively slow
- Every virtual call to `update()` or `render()` goes through a so-called <mark>vtable</mark>.
A vtable or virtual table in C++ is a lookup table of function pointers maintained
by the compiler for each class that has virtual functions, meaning heavy use of
virtual functions will end up with a bunch of overhead, that can be avoided by ditching that
programming pattern.
- The diamond inheritance problem mentioned above.
- Poor caching
- OOP often scatters data all over memory because each object carries it's own data + vptr.
- Hard to Extend Beheavior Dynamically
- Would you like to add or remove abilities (e.g.: canShoot(), canWalk()) at runtime?
- And much more...
See? I could go on about the issues with this approach, but these are the biggest brick walls youll hit.
# The Solution
It means moving away from traditional Object-Oriented Programming (OOP) patterns like inheritance and polymorphism,
and instead starting to think in Data-Oriented design / programming (DOD).
Instead of organizing your game logic around what objects are (e.g.: `class Player : public Entity`),
data-oriented design focuses on _what data you operate on_ and _how to structure that data to make the CPU
cache happy_. It's less about entities and more about the systems and data layout.
Here is roughly what I came up with:
1. First I created a Component.hpp file, exclusively for the components:
```cpp
#include <cstdint>
#include "raylib.h"
namespace DREAM {
struct Health { int health = 100; };
struct Attack { int attack = 1; };
struct Defense { int defense = 1; };
struct Level { uint8_t level = 1; };
struct Position { Vector2 position = {0.0f, 0.0f}; };
struct Velocity { Vector2 velocity = {0.0f, 0.0f}; };
}
```
Then I implemented following in the ComponentManager:
```cpp
using Entity = std::uint32_t;
class InterfaceComponentArray {
public:
virtual ~InterfaceComponentArray() = default;
virtual void entityDestroyed(Entity entity) = 0;
};
```
You might get a rough understanding on what I am trying to achieve here. Do you see the assignment of
Entity just being a simple ID/number?
The `InterfaceComponentArray` only knows about the method `entityDestroyed()`. It will make much more sense
in a bit.
After this we define the `ComponentArray` class:
```cpp
template<typename T>
class ComponentArray : public InterfaceComponentArray {
private:
std::unordered_map<Entity, T> m_componentMap;
public:
ComponentArray() = default;
~ComponentArray() override = default;
void insertData(Entity entity, const T& component) {
// Insert new data / component for entity
}
void removeData(Entity entity) {
// Remove data
}
T& getData(Entity entity) {
// Return data
}
bool hasData(Entity entity) const {
// Check if entity has any data
}
void entityDestroyed(Entity entity) override {
// Destroy entity's data
}
};
```
This provides a dedicated storage and dedicated API for each `ComponentArray` we create (e.g.: `ComponentArray<Position>`).
Theoretically, the `ComponentManager` would work like this, but the user might have a hard time using the API.
That's why we will create a `ComponentManager` class and wrap the `ComponentArray` methods in it:
```cpp
class ComponentManager {
private:
std::unordered_map<std::type_index, std::shared_ptr<InterfaceComponentArray>> m_componentArrays;
template<typename T>
std::shared_ptr<ComponentArray<T>> getComponentArray() {
const auto typeId = std::type_index(typeid(T));
auto it = m_componentArrays.find(typeId);
if (it == m_componentArrays.end()) {
fmt::print(
fmt::emphasis::bold | fmt::fg(fmt::color::red),
"[Error] ComponentManager::getComponentArray -> Component type {} not registered.\n",
typeId.name()
);
throw std::runtime_error("Component type not registered");
}
return std::static_pointer_cast<ComponentArray<T>>(it->second);
}
public:
ComponentManager() = default;
~ComponentManager() = default;
template<typename T>
void registerComponent() {
const auto typeId = std::type_index(typeid(T));
if (m_componentArrays.contains(typeId)) {
fmt::print(
fmt::emphasis::bold | fmt::fg(fmt::color::yellow),
"[Warning] ComponentManager::registerComponent -> Component type {} already registered.\n",
typeId.name()
);
return;
}
m_componentArrays.emplace(typeId, std::make_shared<ComponentArray<T>>());
}
template<typename T>
void addComponent(Entity entity, const T& component) {
getComponentArray<T>()->insertData(entity, component);
}
template<typename T>
void removeComponent(Entity entity) {
getComponentArray<T>()->removeData(entity);
}
template<typename T>
T& getComponent(Entity entity) {
return getComponentArray<T>()->getData(entity);
}
template<typename T>
bool hasComponent(Entity entity) {
return getComponentArray<T>()->hasData(entity);
}
void destroyEntity(Entity entity) {
for (auto& [_, componentArray] : m_componentArrays) {
componentArray->entityDestroyed(entity);
}
}
};
```
In this step we simply wrapped the `ComponentArray` methods around methods from the `ComponentManager`.
After calling `registerComponent()`, a ComponentArray for the component of your need gets created.
This map then gets stored in another map, which is located in the `ComponentManager` class.
Using this API could look something like this:
```cpp
int main() {
ComponentManager componentManager;
// Register needed component types:
componentManager.registerComponent<Position>();
componentManager.registerComponent<Velocity>();
componentManager.registerComponent<Health>();
// Create some entities (For now just IDs, unless you have an EntityManager)
Entity player = 1;
Entity enemy = 2;
// Attach components to your entities
componentManager.addComponent(player, Position{ 100.0f, 200.0f });
componentManager.addComponent(player, Velocity{ 1.5f, 0.0f });
componentManager.addComponent(player, Health{ 50 });
componentManager.addComponent(enemy, Position{ 150.0f, 100.0f });
componentManager.addComponent(enemy, Velocity{ 1.5f, 0.0f });
componentManager.addComponent(enemy, Health{ 20 });
// Simple movement "system"
// Imagine you have a list of all entities with both Position & Velocity
for (Entity e : { player, enemy }) {
if (componentManager.hasComponent<Position>(e) &&
componentManager.hasComponent<Velocity>(e)) {
auto& pos = componentManager.getComponent<Position>(e);
auto& vel = componentManager.getComponent<Velocity>(e);
pos.position.x += vel.velocity.x;
pos.position.y += vel.velocity.y;
}
}
componentManager.removeComponent<Velocity>(enemy);
componentManager.destroyEntity(enemy);
}
```
That's quite a lot to read and grasp, right? Don't worry. ECS design can be tricky at first, but once
you grasp the full concept, it will stick in your mind and I will guarantee you that it's awesome.
All this code made it possible to work with entities in a flexible & drastically improved way.
Also keep in mind that this code only should be used for reference and not blatant copying,
because I left some parts out on purpose to avoid blasting you a couple of hundred lines of code
in your face.
# Common Pitfalls
Youve probably encountered this already, but its worth repeating.
Plenty of developers lose their way in the jungle by over-engineering their code and end up abusing
ECS like they do with inheritance or polymorphism.
Also - don't overuse components. Too many tiny ones can bloat your systems with excessive iterations
and seriously hurt performance.
# Conclusion
There we are at the end of today's post. You probably need to digest the amount of information you've
just obtained, but that's totally fine. As mentioned previously, even though I showed you the basics
of an ECS, learning a topic like this is messy at first, especially since it requires 'rethinking'
how code can be structured so generically.
Hopefully, I will see you next time on future posts :)

View File

@@ -107,6 +107,7 @@
height: auto; height: auto;
display: block; display: block;
margin: 2em 0; margin: 2em 0;
border-radius: 5px;
} }
.img-placeholder { .img-placeholder {