New blog
This commit is contained in:
18
src/content/posts/relearning-programming-introduction.md
Normal file
18
src/content/posts/relearning-programming-introduction.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: 'Relearning Programming Introduction'
|
||||
pubDate: '2025-11-21'
|
||||
tag: 'study'
|
||||
---
|
||||
|
||||

|
||||
|
||||
# Foreword
|
||||
Hey,
|
||||
I suck at coding - and that's why I decided to relearn programming. My language of choice is C++. I've been working with this language for a long time now, and it just grew on me like no other language did.
|
||||
|
||||
# What Led Me Here
|
||||
I've been struggling with my problem solving + the core concepts of C++ itself. Even though I am aware of the fact that it's totally common to
|
||||
make mistakes, I've had the overwhelming feeling of frustration and "imposter syndrome". I tolerated it for too long.
|
||||
|
||||
# My Plan and Goals
|
||||
My goals with relearning programming and C++ is to thoroughly study and apply my knowledge with a modern approach to C++.
|
||||
@@ -6,4 +6,4 @@ topic: 'general'
|
||||
|
||||

|
||||
|
||||
WELCOME!!!!
|
||||
WELCOME!!!!
|
||||
|
||||
@@ -1,342 +0,0 @@
|
||||
---
|
||||
title: 'Game Engine Series I - The Entity Component System'
|
||||
pubDate: '2025-07-19'
|
||||
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.
|
||||
|
||||
Since I want to teach you the principles of an ECS and make it
|
||||
beginner-friendly, I purposely stripped the code down and removed things like multithreading and more.
|
||||
These more advanced topics will be discussed in the future.
|
||||
|
||||
# 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 value = 100; };
|
||||
|
||||
struct Attack { int value = 1; };
|
||||
|
||||
struct Defense { int value = 1; };
|
||||
|
||||
struct Level { uint8_t value = 1; };
|
||||
|
||||
struct Position { Vector2 value = {0.0f, 0.0f}; };
|
||||
|
||||
struct Velocity { Vector2 value = {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 :)
|
||||
Reference in New Issue
Block a user