ffdgfg
This commit is contained in:
@@ -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
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|

|
||||||
|
- 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 aren’t the only problems you’ll 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 you’ll 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
|
||||||
|
You’ve probably encountered this already, but it’s 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 :)
|
||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user