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