Skip to main content

Systems & Components

Overview

The R-Type server uses an Entity Component System (ECS) architecture where:

  • Entities are simple identifiers (IDs)
  • Components are pure data structures
  • Systems contain the logic that operates on components

This design promotes data-oriented programming for better cache performance and maintainability.


Entity Component System Fundamentals

Entity

An entity is nothing more than a unique identifier:

using EntityId = uint32_t;

class Entity {
EntityId _id;
// No data, no behavior - just an ID
};

Purpose: Acts as a handle to associate multiple components together.

Component

A component is a plain data structure with no behavior:

struct Position {
float x;
float y;
};

struct Velocity {
float vx;
float vy;
};

Key Principle: Components contain only data, never logic.

System

A system contains pure logic that operates on entities with specific components:

class MovementSystem : public ISystem {
void update(float deltaTime, EntityManager& entityManager) override {
// Get all entities with Position AND Velocity
auto entities = entityManager.getEntitiesWith<Position, Velocity>();

for (auto& entity : entities) {
auto* pos = entityManager.getComponent<Position>(entity);
auto* vel = entityManager.getComponent<Velocity>(entity);

// Update position based on velocity
pos->x += vel->vx * deltaTime;
pos->y += vel->vy * deltaTime;
}
}
};

Key Principle: Systems operate on components, not entities directly.


Component Reference

Core Components

Position

struct Position {
float x; // X coordinate
float y; // Y coordinate
};

Used by: All visible entities (players, enemies, bullets)

Velocity

struct Velocity {
float vx; // X velocity (pixels/second)
float vy; // Y velocity (pixels/second)
};

Used by: Moving entities (players, enemies, bullets)

Health

struct Health {
float current;
float max;

void takeDamage(float amount);
void heal(float amount);
bool isAlive() const;
};

Used by: Entities that can take damage (players, enemies)

BoundingBox

struct BoundingBox {
float width;
float height;
float offsetX; // Offset from Position
float offsetY;
};

Used by: Entities involved in collision detection

Entity-Specific Components

Player

struct Player {
uint32_t clientId; // Network client ID
float shootCooldown; // Time until can shoot again

static constexpr float SHOOT_COOLDOWN_DURATION = 0.2f;
static constexpr float PLAYER_SPEED = 400.0f;
};

Used by: Player entities only

Enemy

struct Enemy {
enum class Type : uint8_t {
BASIC = 0,
KAMIKAZE = 1,
TANK = 2
};

Type type;
};

Used by: Enemy entities

Bullet

struct Bullet {
uint32_t ownerId; // Entity that fired the bullet
float damage;
bool fromPlayer; // True if player bullet, false if enemy
};

Used by: Projectile entities

Network Components

NetworkEntity

struct NetworkEntity {
uint32_t entityId; // Network-wide unique ID
uint8_t entityType; // Type identifier for clients
bool needsSync; // Flag for network synchronization

// Some Entity types
static constexpr uint8_t PLAYER = 1;
static constexpr uint8_t PLAYER_MISSILE = 2;
static constexpr uint8_t BASIC = 10;
// ...
};

Used by: All entities that need network synchronization

Lifetime

struct Lifetime {
float remaining; // Seconds until entity is destroyed
};

Used by: Temporary entities (e.g., bullets with max range)


System Reference

Systems are executed in priority order every frame. Lower priority number = executes first.

System Execution Order

PrioritySystemDescription
5EnemySpawnerSystemEmits enemy spawn events
10MovementSystemUpdates positions
15PlayerCooldownSystemUpdates player shoot cooldowns
20EnemyShootingSystemMakes enemies emit bullet spawn events
50CollisionSystemDetects and handles collisions
90BulletCleanupSystemRemoves off-screen bullets
95EnemyCleanupSystemRemoves off-screen enemies
100LifetimeSystemDestroys expired entities

System Details

1. EnemySpawnerSystem

Priority: 5 (Early)
Purpose: Periodically emits enemy spawn events (does NOT create entities directly)

Architecture: Event-driven - emits SpawnEnemyEvent to spawn queue

Algorithm:

class EnemySpawnerSystem : public ISystem {
std::vector<SpawnEvent>& _spawnQueue; // Reference to GameLoop's event queue

void update(float deltaTime, EntityManager& entityManager) {
_spawnTimer += deltaTime;

if (_spawnTimer >= _spawnInterval) {
_spawnTimer = 0.0f;
spawnEnemy(); // Emit spawn event
}
}

void spawnEnemy() {
// Emit event instead of creating entity directly
_spawnQueue.push_back(SpawnEnemyEvent{type, x, y});
}
};

Enemy Types:

  • BASIC: Standard speed (-100 px/s), 30 HP, shoots bullets
  • KAMIKAZE: Follows nearest player (250 px/s), 15 HP
  • TANK: Half speed (-50 px/s), 100 HP, shoots bullets

Configuration:

  • Spawn interval: 5.0 seconds (configurable at construction)
  • Spawn position: Right side of screen (x=1900)
  • Random Y position: 50-1000 pixels
  • Random enemy type selection (33% each type)

Pure ECS Design: System only emits events. GameLoop processes events and uses GameEntityFactory to create actual entities.


2. MovementSystem

Priority: 10 (Early)
Purpose: Updates entity positions based on velocity

Components Required: Position, Velocity

Algorithm:

void update(float deltaTime, EntityManager& entityManager) {
auto entities = entityManager.getEntitiesWith<Position, Velocity>();

for (auto& entity : entities) {
pos->x += vel->vx * deltaTime;
pos->y += vel->vy * deltaTime;

// Mark for network sync (every 2 frames)
if (shouldSync) {
netEntity->needsSync = true;
}
}
}

Network Optimization: Only syncs position every 2 frames (30 Hz instead of 60 Hz) to reduce bandwidth.


3. PlayerCooldownSystem

Priority: 15
Purpose: Updates player shoot cooldowns

Components Required: Player

Algorithm:

void update(float deltaTime, EntityManager& entityManager) {
for (each player) {
if (player->shootCooldown > 0.0f) {
player->shootCooldown -= deltaTime;
player->shootCooldown = max(0.0f, player->shootCooldown);
}
}
}

Fire Rate: 0.2s cooldown = 5 shots per second maximum


4. EnemyShootingSystem

Priority: 20
Purpose: Makes enemies shoot bullets at regular intervals (event-driven)

Components Required: Enemy, Position

Architecture: Event-driven - emits SpawnEnemyBulletEvent to spawn queue

Algorithm:

class EnemyShootingSystem : public System<Enemy, Position> {
std::vector<SpawnEvent>& _spawnQueue;
const float SHOOT_INTERVAL = 2.0f;

void processEntity(float deltaTime, Entity& entity,
Enemy* enemy, Position* pos) {
// Decrement cooldown
if (enemy->shootCooldown > 0.0f) {
enemy->shootCooldown -= deltaTime;
return;
}

// Only BASIC and TANK enemies shoot (FAST doesn't)
if (enemy->type != Enemy::Type::BASIC) {
return;
}

// Emit bullet spawn event
_spawnQueue.push_back(SpawnEnemyBulletEvent{entity.getId(), *pos});
enemy->shootCooldown = SHOOT_INTERVAL;
}
};

Shooting Behavior:

  • BASIC enemies: Shoot every 2 seconds
  • TANK enemies: Never shoot (will shoot every 2 seconds)
  • FAST enemies: Never shoot
  • Bullet velocity: -300 px/s (moving left)
  • Bullet damage: 20 HP

Pure ECS Design: System only emits events. GameLoop creates bullet entities via GameEntityFactory.


5. CollisionSystem

Priority: 50 (Mid)
Purpose: Detects and resolves collisions between entities

Components Required: Position, BoundingBox

Collision Pairs Checked:

  1. Player Bullets ↔ Enemies: Damages enemy, destroys bullet
  2. Enemy Bullets ↔ Players: Damages player, destroys bullet
  3. Players ↔ Enemies: Damages player (20 HP), destroys enemy

Algorithm:

bool checkCollision(pos1, box1, pos2, box2) {
// AABB (Axis-Aligned Bounding Box) collision
left1 = pos1.x + box1.offsetX;
right1 = left1 + box1.width;
top1 = pos1.y + box1.offsetY;
bottom1 = top1 + box1.height;

// Same for entity 2...

return !(right1 < left2 || left1 > right2 ||
bottom1 < top2 || top1 > bottom2);
}

Collision Response:

  • Bullet hits enemy → Enemy takes damage, bullet destroyed
  • Enemy health ≤ 0 → Enemy destroyed
  • Enemy hits player → Player takes 20 damage, enemy destroyed
  • Player health ≤ 0 → Player destroyed (death event fired)

5. BulletCleanupSystem

Priority: 90 (Late)
Purpose: Removes bullets that leave the screen boundaries

Boundaries:

MIN_X = -50.0f
MAX_X = 2100.0f
MIN_Y = -50.0f
MAX_Y = 1200.0f

Algorithm:

void update(float deltaTime, EntityManager& entityManager) {
auto bullets = entityManager.getEntitiesWith<Position, Bullet>();

for (auto& bullet : bullets) {
if (pos->x < MIN_X || pos->x > MAX_X ||
pos->y < MIN_Y || pos->y > MAX_Y) {
// Queue for destruction
destroyEntity(bullet);
}
}
}

Purpose: Prevents memory leaks from bullets that miss targets


6. EnemyCleanupSystem

Priority: 95 (Late)
Purpose: Removes enemies that move off-screen to the left

Boundary: MIN_X = -200.0f

Algorithm: Same pattern as BulletCleanupSystem

Purpose: Enemies that reach the left side are considered "escaped" and removed


7. LifetimeSystem

Priority: 100 (Latest)
Purpose: Destroys entities after their lifetime expires

Components Required: Lifetime

Algorithm:

void update(float deltaTime, EntityManager& entityManager) {
auto entities = entityManager.getEntitiesWith<Lifetime>();

for (auto& entity : entities) {
lifetime->remaining -= deltaTime;

if (lifetime->remaining <= 0.0f) {
destroyEntity(entity);
}
}
}

Use Case: Currently not heavily used, but useful for:

  • Temporary power-ups
  • Timed effects
  • Particle effects (future)

Entity Lifecycle

Creating an Entity

// 1. Create entity (just an ID)
Entity player = entityManager.createEntity();

// 2. Add components (data)
entityManager.addComponent<Position>(player, {100.0f, 500.0f});
entityManager.addComponent<Velocity>(player, {0.0f, 0.0f});
entityManager.addComponent<Health>(player, {100.0f});
entityManager.addComponent<Player>(player, {clientId, 0.0f});
entityManager.addComponent<BoundingBox>(player, {64.0f, 64.0f, 0.0f, 0.0f});
entityManager.addComponent<NetworkEntity>(player, {playerId, 1, true});

// 3. Systems automatically detect and process the entity

Updating an Entity

Systems automatically update entities:

// MovementSystem will process this entity every frame
// No manual update needed!

Destroying an Entity

// Mark for destruction
entityManager.destroyEntity(entityId);

// Entity and all its components are removed
// Systems will no longer see this entity

EntityManager

The EntityManager is the core of the ECS system.

Key Operations

Create Entity

Entity createEntity();

Returns a new entity with a unique ID.

Add Component

template<typename T, typename... Args>
void addComponent(Entity entity, Args&&... args);

Attaches a component to an entity.

Get Component

template<typename T>
T* getComponent(Entity entity);

Retrieves a component (returns nullptr if not found).

Query Entities

template<typename... Components>
std::vector<Entity> getEntitiesWith();

Returns all entities that have ALL specified components.

Destroy Entity

void destroyEntity(EntityId entityId);

Removes entity and all its components.


ComponentManager

Manages storage for a specific component type.

Implementation

Uses contiguous arrays for cache-friendly data access:

template<typename T>
class ComponentManager {
std::vector<T> _components; // Dense array of components
std::vector<EntityId> _entities; // Corresponding entity IDs
std::unordered_map<EntityId, size_t> _entityToIndex; // Fast lookup
};

Benefits of Dense Storage

  1. Cache Efficiency: Components are stored contiguously in memory
  2. Fast Iteration: Systems iterate linearly through arrays
  3. Memory Efficiency: No fragmentation from pointer indirection

Trade-offs

AspectBenefitCost
Iteration⚡ Very fast (linear scan)
Add ComponentSlow (may require reallocation)
Remove ComponentSlow (requires swap-and-pop)
Lookup⚡ Fast (hash map)Extra memory

Verdict: Perfect for game servers where reading >> writing.


System Interface

All systems implement the ISystem interface:

class ISystem {
public:
virtual void update(float deltaTime, EntityManager& entityManager) = 0;
virtual std::string getName() const = 0;
virtual int getPriority() const = 0; // Lower = earlier
};

Template System

For systems that operate on specific components:

template<typename... Components>
class System : public ISystem {
protected:
virtual void processEntity(float deltaTime, Entity& entity,
Components*... components) = 0;

public:
void update(float deltaTime, EntityManager& entityManager) override {
auto entities = entityManager.getEntitiesWith<Components...>();
for (auto& entity : entities) {
auto components = (entityManager.getComponent<Components>(entity), ...);
processEntity(deltaTime, entity, components...);
}
}
};

Example Usage:

class PlayerCooldownSystem : public System<Player> {
protected:
void processEntity(float dt, Entity& entity, Player* player) override {
player->shootCooldown -= dt;
}
};

Adding New Systems

Step 1: Define Component (if needed)

struct PowerUp {
enum Type { SPEED, DAMAGE, HEALTH } type;
float duration;
};

Step 2: Implement System

class PowerUpSystem : public System<PowerUp> {
protected:
void processEntity(float dt, Entity& entity, PowerUp* powerup) override {
powerup->duration -= dt;

if (powerup->duration <= 0.0f) {
// Remove power-up effect
entityManager.removeComponent<PowerUp>(entity);
}
}

public:
std::string getName() const override { return "PowerUpSystem"; }
int getPriority() const override { return 20; } // After movement
};

Step 3: Register System

In GameLoop.cpp constructor:

_gameLoop.addSystem(std::make_unique<PowerUpSystem>());

Performance Considerations

Memory Layout

ECS provides excellent cache performance:

Traditional OOP:
[Player*] -> [Player Object with all data]
↓ Cache miss on each access

ECS:
[Position Array]: [pos1][pos2][pos3][pos4]... ← Linear memory access!
[Velocity Array]: [vel1][vel2][vel3][vel4]...

Frame Budget

At 60 FPS, each frame has 16.67ms budget:

SystemTypical Time% of Frame
Movement~0.5ms3%
Collision~2-3ms15%
Spawning~0.1ms<1%
Other~1ms6%
Total~5ms30%

Plenty of headroom for future features!

Optimization Tips

  1. Batch Operations: Process all entities of a type together
  2. Minimize Lookups: Cache component pointers in systems
  3. Avoid String Operations: Use enums/IDs instead
  4. Profile First: Don't optimize without data

Testing Systems

Unit Testing Example

TEST(MovementSystem, UpdatesPosition) {
EntityManager em;
MovementSystem system;

// Create test entity
Entity e = em.createEntity();
em.addComponent<Position>(e, {0.0f, 0.0f});
em.addComponent<Velocity>(e, {100.0f, 0.0f});

// Update for 1 second
system.update(1.0f, em);

// Verify position changed
auto* pos = em.getComponent<Position>(e);
EXPECT_FLOAT_EQ(pos->x, 100.0f);
}

Common Patterns

Pattern: Deferred Destruction

Problem: Can't destroy entity while iterating over entities.

Solution: Queue destructions, process at end:

class CollisionSystem {
std::vector<EntityId> _entitiesToDestroy;

void update(float dt, EntityManager& em) {
_entitiesToDestroy.clear();

// Detect collisions, queue destructions
for (collision) {
_entitiesToDestroy.push_back(entityId);
}

// Now safe to destroy
for (EntityId id : _entitiesToDestroy) {
em.destroyEntity(id);
}
}
};

Pattern: Entity Factory

Problem: Creating entities requires lots of boilerplate.

Solution: Factory functions:

Entity createPlayer(EntityManager& em, uint32_t clientId, float x, float y) {
Entity player = em.createEntity();
em.addComponent<Position>(player, {x, y});
em.addComponent<Velocity>(player, {0, 0});
em.addComponent<Health>(player, {100});
em.addComponent<Player>(player, {clientId, 0});
em.addComponent<BoundingBox>(player, {64, 64, 0, 0});
em.addComponent<NetworkEntity>(player, {nextId++, 1, true});
return player;
}

Boss System

Boss Components

Boss Component

Main boss entity with phase-based AI:

struct Boss : public ComponentBase<Boss> {
enum class Phase { ENTRY, PHASE_1, PHASE_2, ENRAGED, DEATH };

Phase currentPhase;
float phaseTimer;
float maxHealth; // Base HP (e.g., 1000)
float scaledMaxHealth; // Scaled: base * (1 + 0.5 * (playerCount - 1))
uint32_t playerCount;
float attackTimer;
float attackInterval;
int attackPatternIndex;
float damageFlashTimer;
bool isFlashing;
std::vector<uint32_t> partEntityIds; // IDs of boss parts (turrets, etc.)
float phase2Threshold = 0.6f; // 60% HP
float enragedThreshold = 0.3f; // 30% HP
float deathTimer = -1.0f;
bool destructionStarted = false;
float explosionTimer = 0.0f;
int explosionCount = 0;
};

HP Scaling: Boss health adapts to player count for balanced difficulty:

  • 1 player: 1000 HP
  • 2 players: 1500 HP
  • 3 players: 2000 HP
  • 4 players: 2500 HP

BossPart Component

For multi-part bosses (turrets, armor plates):

struct BossPart : public ComponentBase<BossPart> {
enum class PartType { MAIN_BODY, TURRET, TENTACLE, WEAK_POINT, ARMOR_PLATE };

uint32_t bossEntityId; // Parent boss entity
PartType partType;
float relativeX, relativeY; // Position relative to boss
float rotationSpeed;
float currentRotation;
bool canTakeDamage; // Armor plates are invulnerable
};

Animation Component

Multi-frame sprite animations:

struct Animation : public ComponentBase<Animation> {
uint8_t animationId;
uint8_t currentFrame;
uint8_t frameCount; // e.g., 4 frames
float frameTime; // Time since last frame change
float frameDuration = 0.15f; // 150ms per frame
bool loop;
bool finished;
};

Boss Systems

BossSystem (Priority: 15)

Purpose: Boss AI with state machine and attack patterns

Components: Boss, Health, Position

State Machine:

  1. ENTRY (Phase 0): Boss enters from right, moves to x=1400
  2. PHASE_1: Vertical sine wave + spread shots (3 bullets)
  3. PHASE_2: Circular movement + circular bursts (6 bullets) - at 60% HP
  4. ENRAGED: Aggressive movement + spiral patterns (8 bullets) - at 30% HP
  5. DEATH: Explosion animation (15 explosions over 2.5s), then destruction

Attack Patterns:

  • Spread: 3 bullets in cone formation
  • Circular: 6 bullets in all directions
  • Spiral: Rotating 8-bullet pattern
  • Turret Fire: Parts shoot independently

Key Methods:

void handleEntryPhase(float dt, Position* pos);
void handlePhase1(float dt, Boss* boss, Position* pos);
void handlePhase2(float dt, Boss* boss, Position* pos);
void handleEnragedPhase(float dt, Boss* boss, Position* pos);
void handleDeathPhase(float dt, Entity& entity, Boss* boss,
Health* health, Position* pos);

void shootSpreadPattern(Position* pos, float angleOffset);
void shootCircularPattern(Position* pos);
void shootSpiralPattern(Position* pos, float rotation);
void shootTurretShots(Position* bossPos);

Phase Transitions: Automatic based on HP percentage thresholds.

BossPartSystem (Priority: 14)

Purpose: Synchronizes boss part positions with main body

Components: BossPart, Position

Behavior: Updates each part's position based on parent boss position + relative offsets.

void update(float dt, EntityManager& em) {
auto parts = em.getEntitiesWith<BossPart, Position>();

for (auto& partEntity : parts) {
auto* part = em.getComponent<BossPart>(partEntity);
auto* partPos = em.getComponent<Position>(partEntity);

// Get parent boss position
Entity* boss = em.getEntity(part->bossEntityId);
auto* bossPos = em.getComponent<Position>(*boss);

// Update part position
partPos->x = bossPos->x + part->relativeX;
partPos->y = bossPos->y + part->relativeY;
}
}

AnimationSystem (Priority: 5)

Purpose: Updates sprite frame animations

Components: Animation

Behavior: Cycles through frames at specified duration, loops if enabled.

Creating a Boss

// Server-side (GameEntityFactory)
Entity createBoss(uint8_t bossType, float x, float y, uint32_t playerCount) {
Entity boss = _entityManager.createEntity();

// Scale HP based on player count
float baseHealth = 1000.0f;
float scaledHealth = baseHealth * (1.0f + 0.5f * (playerCount - 1));

_entityManager.addComponent(boss, Position(x, y));
_entityManager.addComponent(boss, Velocity(0.0f, 0.0f));
_entityManager.addComponent(boss, Boss(playerCount));
_entityManager.addComponent(boss, Health(scaledHealth));
_entityManager.addComponent(boss, BoundingBox(260.0f, 100.0f));
_entityManager.addComponent(boss, NetworkEntity(_nextEnemyId++, 5)); // Type 5
_entityManager.addComponent(boss, Animation(0, 4, 0.15f, true));

// Create turret parts
Entity turret1 = createBossPart(boss.getId(), BossPart::TURRET,
-80.0f, -60.0f, true);
Entity turret2 = createBossPart(boss.getId(), BossPart::TURRET,
-80.0f, 60.0f, true);

return boss;
}

Boss Death Animation

When boss HP reaches 0:

  1. Phase changes to DEATH
  2. destructionStarted = true, deathTimer = 2.5s
  3. Spawns 15 explosions (type 7 entities) at random offsets around boss
  4. Each explosion has 0.5s lifetime, uses sprite sheets (6 or 8 frames)
  5. Client detects explosions → triggers screen shake
  6. After 2.5s, boss and all parts are destroyed
  7. Destruction notifications sent to clients → entities removed

Explosion Spawning:

// In BossSystem::handleDeathPhase()
for (int i = 0; i < 15; i++) {
SpawnEnemyBulletEvent explosion;
explosion.ownerId = (rand() % 2) + 1; // Type 1 or 2
explosion.x = pos->x + randomOffset();
explosion.y = pos->y + randomOffset();
explosion.vx = 0.0f; // Marker: vx=vy=0 means explosion
explosion.vy = 0.0f;
_spawnQueue.push_back(explosion);
}

Next Steps