Skip to main content

Entity Component System (ECS)

This project uses an Entity–Component–System (ECS) architecture to implement a multiplayer R-Type-like game. The ECS is designed to run both:

  • Server-side (headless): Contains no rendering or input systems. Only gameplay logic, networking, and simulation.

  • Client-side: Adds rendering, animation, and local input prediction.

The authoritative simulation runs on the server. Clients receive replicated states and send player actions.

Components

Components are pure data containers with no logic. Below is the list of components currently defined.

1. Controllable

Entities whose movement or actions are driven by user input or AI.

FieldTypeDescription
speedfloatBase movement speed modifier

2. Transform

Spatial properties.

FieldTypeDescription
x, y, zfloatPosition
rx, ry, rzfloatRotation
sx, sy, szfloatScale

3. Velocity

Spatial properties.

FieldTypeDescription
vx, vy, vzfloatMovement speed
vrx, vry, vrzfloatRotation speed

4. Drawable

How an entity should be rendered (client-side only).

FieldTypeDescription
meshstringIdentifier of the visual model

5. Hitbox

Collision bounds.

FieldTypeDescription
x, y, zfloatPosition
w, h, dfloatSize
collisionLayerCollisionLayerThe layer in which the collision box lives in
enum CollisionLayer {
Ally, // players ship
Enemy, // enemy ships
AllyProjectile, // bullets fired by allies
EnemyProjectile,// bullets fired by enemies
Pickup, // power-ups, bonuses
Environment // walls, boundaries, obstacles
};

6. Weapon

Ability to create other entities.

FieldTypeDescription
cooldownintActual cooldown left in ms
firerateintCooldown in ms between shots in ms
dmgintDamage inflicted (recovered if negative)
vx, vyfloatInitial velocity
meshstringIdentifier of the visual model

7. Health

Tracks hitpoints & damaging/healing events.

FieldTypeDescription
hpintCurrent health points
maxHpintMax amount of health point
changeQueuevector<int>Incoming health points changes queued this frame
warning

ChangeQueue must be of a fixed size or there will be problems !

The alternative would be to use a global variable like this:

std::vector<int targetEntity, int amount> g_damage_events;

8. Stats

Ability to influence current game statistics.

FieldTypeDescription
scoreintScore obtained
dmgintDamage inflicted
killsintKill count

9. Replicated

Indicates that the entity is present both server & clients.

FieldTypeDescription
tagstringUnique identifier

Systems

Systems operate on entities containing the required set of components.

Dependencies listed for each system represent the minimal component set an entity must have to be processed by that system.

Core Gameplay Systems

These systems simulate the game world. When running on the client, they run in prediction mode (no authority).

1. MoveSystem

Dependencies: Transform, Velocity

Applies velocities to transforms every tick.

2. CollisionSystem

Dependencies: Transform, Hitbox, Health, Stats

Checks collisions between entities based on CollisionLayer rules and registers damage events.

3. UpdateHealthSystem

Dependencies: Health

Consumes all entries in changeQueue (or global damage events) and updates hp. Triggers death/despawn events.

4. WeaponFireSystem

Dependencies: Weapon, Transform

Handles cooldown logic

Spawns projectile entities

Assigns Velocity + Hitbox + Damage payload

B. Player Interaction Systems

Used to turn player input into entity behavior.

6. PlayerControlSystem

Dependencies: Controllable, Velocity

Consumes input commands from the Input Buffer (not a component). Adjusts velocity.

C. Networking Systems (Server or Client)

Autoritative server sends state → client receives.

8. NetworkReceiveInputSystem (Server-only)

Dependencies: None

Receives input packets from network and stores them in the global InputBuffer.

9. NetworkReplicationSystem (Server-only)

Dependencies: Replicated, (any components allowed)

Serializes updated components

Sends deltas to clients

Sends spawn/despawn events

10. NetworkReceiveStateSystem (Client-only)

Dependencies: none

Receives authoritative state updates from server and stores them for reconciliation.

11. ClientReconciliationSystem (Client-only)

Dependencies: Transform, Velocity, Controllable

Replays local inputs after server updates to reduce snapping.

D. Client-Only Visual Systems

Only used on machines rendering the game.

12. RenderSystem (Client-only)

Dependencies: Transform, Drawable, Health, Stats

Handles rendering meshes, sprites, HUD.

13. AnimationSystem (Client-only)

Dependencies: Drawable

Sprite animations, particle effects, etc.

14. DebugDrawSystem (optional)

Dependencies: Transform, Hitbox

Draws hitboxes during debugging.

Use the ECS

tip

See the engine page for more information on the engine itself

Create components

Add a file in /src/game_engine/components/ containing the components you want added.

Example:

namespace engn::cpnt {

struct Bullet : ISyncComponent {
Bullet() = default;

engn::SerializedComponent serialize() const override;
void deserialize(const std::vector<std::byte>& data) override;
};
}

In this example thecomponent bullet is defined with:

  • The constructor Bullet()
  • The functions serialize() and deserialize()
  • Inherit the ISyncComponent class

The two functions serialize() and deserialize() are used when components need to be synchronised between all the players; for example, the components Star would look like this:

namespace engn::cpnt {

struct Star {
float z;
};
}

These components don't need to be synchronised between the players.

Here is how you should defines the serialize() and deserialize() function:

engn::SerializedComponent Bullet::serialize() const {
engn::SerializedComponent serialized;
serialized.type = engn::ComponentType::bullet;
serialized.data = {};
return serialized;
}

void Bullet::deserialize(const std::vector<std::byte>& data) {

}
info

For other components that have variable stored in them you should defines them like this: For a components Health with variable hp and max_hp

engn::SerializedComponent Health::serialize() const {
engn::SerializedComponent serialized;
serialized.type = engn::ComponentType::health;
std::uint32_t size = sizeof(hp) + sizeof(max_hp);
serialized.data.resize(size);

std::size_t offset = 0;
std::memcpy(serialized.data.data() + offset, &hp, sizeof(hp));
offset += sizeof(hp);
std::memcpy(serialized.data.data() + offset, &max_hp, sizeof(max_hp));
return serialized;
}

void Health::deserialize(const std::vector<std::byte>& data) {
std::uint16_t size = sizeof(hp) + sizeof(max_hp);

if (data.size() >= size) {
std::size_t offset = 0;
std::memcpy(&hp, data.data() + offset, sizeof(hp));
offset += sizeof(hp);
std::memcpy(&max_hp, data.data() + offset, sizeof(max_hp));
}
}

Add components to the scene

In each scene loader the registry is reset, so you need to use this line to have access to a new registry.

auto& my_registry = my_engine_ctx.registry;

This create a registry called my_registry

Then in your function, after creating the registry, add the components to the registry:

my_registry.register_component<cpnt::Bullet>();
my_registry.register_component<cpnt::Star>();

Create a system

At this path /src/game_engine/systems/ you can a this function to create a system called star_scroll_system

void sys::star_scroll_system(EngineContext& my_engine_ctx,ecs::SparseArray<cpnt::Transform> const& positions, ecs::SparseArray<cpnt::Star> const& stars) {
// Add your Function
}

Add a system

After creating youre system you can use this line in youre scene loader to use it:

my_engine_ctx.add_system<cpnt::Transform, cpnt::Star>(sys::star_scroll_system);

this would add a system called star_scroll_system that use the component Transform and Star.

Create an entity

To create an entity use this line:

auto star = my_registry.spawn_entity();

This add an entity called star to your registry.

Then you need to add components to the entity like this:

my_engine_ctx.registry.add_component(star, cpnt::Star{42.0f});

This add the components Star to your mob, and with how i created my Star component i needed to add the float z to the component, here equal to 42.0f.