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
| Priority | System | Description |
|---|---|---|
| 5 | EnemySpawnerSystem | Emits enemy spawn events |
| 10 | MovementSystem | Updates positions |
| 15 | PlayerCooldownSystem | Updates player shoot cooldowns |
| 20 | EnemyShootingSystem | Makes enemies emit bullet spawn events |
| 50 | CollisionSystem | Detects and handles collisions |
| 90 | BulletCleanupSystem | Removes off-screen bullets |
| 95 | EnemyCleanupSystem | Removes off-screen enemies |
| 100 | LifetimeSystem | Destroys 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:
- Player Bullets ↔ Enemies: Damages enemy, destroys bullet
- Enemy Bullets ↔ Players: Damages player, destroys bullet
- 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
- Cache Efficiency: Components are stored contiguously in memory
- Fast Iteration: Systems iterate linearly through arrays
- Memory Efficiency: No fragmentation from pointer indirection
Trade-offs
| Aspect | Benefit | Cost |
|---|---|---|
| Iteration | ⚡ Very fast (linear scan) | |
| Add Component | Slow (may require reallocation) | |
| Remove Component | Slow (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:
| System | Typical Time | % of Frame |
|---|---|---|
| Movement | ~0.5ms | 3% |
| Collision | ~2-3ms | 15% |
| Spawning | ~0.1ms | <1% |
| Other | ~1ms | 6% |
| Total | ~5ms | 30% |
Plenty of headroom for future features!
Optimization Tips
- Batch Operations: Process all entities of a type together
- Minimize Lookups: Cache component pointers in systems
- Avoid String Operations: Use enums/IDs instead
- 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:
- ENTRY (Phase 0): Boss enters from right, moves to x=1400
- PHASE_1: Vertical sine wave + spread shots (3 bullets)
- PHASE_2: Circular movement + circular bursts (6 bullets) - at 60% HP
- ENRAGED: Aggressive movement + spiral patterns (8 bullets) - at 30% HP
- 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:
- Phase changes to
DEATH destructionStarted = true,deathTimer = 2.5s- Spawns 15 explosions (type 7 entities) at random offsets around boss
- Each explosion has 0.5s lifetime, uses sprite sheets (6 or 8 frames)
- Client detects explosions → triggers screen shake
- After 2.5s, boss and all parts are destroyed
- 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
- Networking Documentation: Learn how ECS integrates with network synchronization
- Tutorials: Step-by-step guide to adding new entity types