From d791348c974a5e2ecc7081584e85b9eb0d6e5cb1 Mon Sep 17 00:00:00 2001 From: heaven Date: Sat, 19 Jul 2025 19:34:31 +0200 Subject: [PATCH] ffdgfg --- src/config.ts | 2 +- src/content/posts/the-ecs-system.md | 337 +++++++++++++++++++++++++++- src/styles/post.css | 1 + 3 files changed, 337 insertions(+), 3 deletions(-) diff --git a/src/config.ts b/src/config.ts index ba732e6..1d4e20f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -16,7 +16,7 @@ export const themeConfig: ThemeConfig = { centeredLayout: true, // Use centered layout (false for left-aligned) themeToggle: false, // Show theme toggle button (uses system theme by default) postListDottedDivider: false, // Show dotted divider in post list - footer: false, // Show footer + footer: true, // Show footer fadeAnimation: true // Enable fade animations }, diff --git a/src/content/posts/the-ecs-system.md b/src/content/posts/the-ecs-system.md index 01fb72e..acbf6fc 100644 --- a/src/content/posts/the-ecs-system.md +++ b/src/content/posts/the-ecs-system.md @@ -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' tag: 'games' ---- \ No newline at end of file +--- + +# +Welcome to the Game Engine Series. + +In this series of blog posts, I'll be diving into one of my favorite topics:
+**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 Entity Component System. 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:
+ `Player = 1`, `Tree = 2`, `Boss1 = 523`
+- **Component** - Components are attached to entities and represent what that entity + is made of.
+ `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 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 vtable. + 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 +#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 + class ComponentArray : public InterfaceComponentArray { + private: + std::unordered_map 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`). + +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> m_componentArrays; + + template + std::shared_ptr> 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>(it->second); + } + + public: + ComponentManager() = default; + ~ComponentManager() = default; + + template + 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>()); + } + + template + void addComponent(Entity entity, const T& component) { + getComponentArray()->insertData(entity, component); + } + + template + void removeComponent(Entity entity) { + getComponentArray()->removeData(entity); + } + + template + T& getComponent(Entity entity) { + return getComponentArray()->getData(entity); + } + + template + bool hasComponent(Entity entity) { + return getComponentArray()->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(); + componentManager.registerComponent(); + componentManager.registerComponent(); + + // 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(e) && + componentManager.hasComponent(e)) { + auto& pos = componentManager.getComponent(e); + auto& vel = componentManager.getComponent(e); + pos.position.x += vel.velocity.x; + pos.position.y += vel.velocity.y; + } + } + + componentManager.removeComponent(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 :) \ No newline at end of file diff --git a/src/styles/post.css b/src/styles/post.css index 3e8e1c8..0c7809e 100644 --- a/src/styles/post.css +++ b/src/styles/post.css @@ -107,6 +107,7 @@ height: auto; display: block; margin: 2em 0; + border-radius: 5px; } .img-placeholder {