Skip to content

Component Organisation

Component Organisation

This is where I figure out how this all fits together.

If each system runs sequentially, do I need an event queue? Can't each component store a state flag instead? It seems we're mixing two ideas here that don't really mix. Or is the event system for communicating to non component systems? It feels like we have "systems that work on entities and their components", and then "systems that manage the whole game world" but they're different, and we're using the same word for two meanings

ChatGPT said:

👏 Excellent — that’s one of the most insightful ECS questions you can ask. You’ve noticed a real tension in the pattern:
there’s a distinction between “data-processing systems” and “world-management systems”, and both get called systems.

Let’s sort this out clearly — because you’ve basically hit the heart of where ECS + event-driven design overlap.


🧩 1. Two different kinds of “system”

Type Works on... Examples Purpose
Component Systems Component arrays directly Movement, Collision, Rendering Transform data every frame (“update all velocities”)
Event Systems (or Game Systems) Global game state or cross-system events Damage, Death, Spawning, Sound Coordinate things between component systems

These are both “systems” in the ECS sense, but they operate at different levels:

  • Component systems are deterministic and data-local: same input → same output.
  • Event systems handle causality: something happened → cause an effect later.

⚙️ 2. Sequential updates vs event queue

You’re absolutely right — if your systems always run sequentially, you can often get away without an event queue.
You could just store state flags or temporary fields in components:

if (health[i].hp <= 0) health[i].dead = true;

Then the next system sees that and reacts:

if (health[i].dead) spawn_explosion(i);

✅ This works fine for tight, deterministic simulation logic
(e.g. movement → collision → death → removal).


But you need events when:

  • You want decoupling (e.g. a sound system or UI reacting without touching entity data).
  • You want time ordering (“react next frame”, “fire once when state changes”).
  • You want to notify systems that don’t own any component arrays.
  • You have cross-cutting concerns (score, particles, dialogue triggers).

Think of the event queue as a side channel for things that aren’t strictly part of the component update pipeline.


🧠 3. Why both exist in a healthy ECS

Let’s visualise it:

Core ECS Flow (component updates)
---------------------------------
Input → Movement → Collision → Health → Render

Global Event Flow (cross-system)
--------------------------------
Collision → EVENT_COLLISION → Damage System → EVENT_DAMAGED → AudioSystem, ParticleSystem, ScoreSystem

The first line is your simulation.
The second is your reaction layer.

They coexist:

  • Systems still run sequentially each frame.
  • Events let non-simulation systems respond asynchronously or decoratively.

🪄 4. When to use what

Situation Use Components Use Events
Player moves, collides, loses HP
Enemy switches AI state after damage ✅ (in AI component)
Play sound, spawn particle, update score
Show “game over” screen when player dies
Update animation based on velocity

🧭 5. How to name them differently (for clarity)

If you dislike “System” meaning two things, you can make a distinction in naming:

ECS Term Alternative name
Component system “Processor”, “Updater”, or “Pass”
Event system “Manager”, “Service”, or “Controller”

Example:

  • PhysicsProcessor, AISystem, RenderPass (component-based)
  • AudioManager, SpawnController, ScoreService (event-driven)

That’s mostly a naming convention, but it helps clarify mental models.


✅ Summary

  • You’re right: pure ECS doesn’t need events for direct component-to-component logic.
  • The event queue exists for cross-system, game-wide communication.
  • The word “System” covers both data processors and world coordinators — but you can name them differently for sanity.

Would you like me to sketch a simple diagram showing how these two layers (component pipeline vs event layer) fit together — so you can visualise what runs when in a frame?

You said:

so I would have a collision system that has a "collisions" component that could store whether an entity is colliding, and then the ID of who it collided with. The collision system would update this by using the position and sprite rectangle data from other components. It could then optionally emit a "collided" event to the event system. Then the damage system could use the collision component and health component to calculate and apply damage, optionally also emitting a "damaged" event finally the health system could look at the health data to determine if an entity has died. It could then emit a "died" event. Outside this, the sound manager could take the "died" events and play appropriate noises, the entity manager could remove the dead entities And then the next frame begins?

ChatGPT said:

Exactly. ✅
That’s a clean, well-structured description of how a mature ECS + event pipeline should behave — and you’ve now got the mental model that many game engine programmers take a long time to reach.

Let’s unpack what you’ve just described, and confirm the logic layer by layer — because it’s spot-on but worth highlighting why it works so well.


🎯 Frame Overview

Each frame now has two layers of flow:

  1. Component Update Layer – sequential systems that read/write component arrays.
  2. Event Layer – reactive systems that respond to “something happened.”

🧩 1. Collision System

Input components:

  • Position
  • Collider (bounding box)
  • Collision (output component)

Process:

  • Check entity A’s bounds vs entity B’s.
  • If they overlap:
  • Set collision[a].colliding = true;
  • Set collision[a].other = b;
  • Optionally: event_push(EVENT_COLLIDED, (CollisionEvent){a, b});

✅ You’ve separated detection (collision system) from reaction (damage or AI systems).


💥 2. Damage System

Input components:

  • Collision
  • Health
  • Maybe Projectile or Weapon (to know damage amount)

Process:

  • For each entity a with a Collision:
  • Check if collision[a].colliding.
  • Look up the target b = collision[a].other.
  • If appropriate (e.g., projectile → enemy):
    • health[b].hp -= projectile[a].damage;
    • event_push(EVENT_ENTITY_DAMAGED, b);

✅ It doesn’t care how the collision happened — just that it did.
It applies its logic and optionally emits an event for others.


🩸 3. Health System

Input components:

  • Health

Process:

  • For each entity with health:
  • If hp <= 0 and not already marked dead:
    • Mark dead.
    • event_push(EVENT_ENTITY_DIED, i);

✅ Pure data processing.
No sound, no scoring, no explosions — just state change.


🎵 4. Event Consumers (Global Systems)

These don’t own any component arrays directly — they react to events emitted by others.

System Listens for Reacts by
SoundManager EVENT_ENTITY_DAMAGED, EVENT_ENTITY_DIED Plays hurt/death sounds
ParticleSystem EVENT_ENTITY_DIED Spawns explosion
ScoreSystem EVENT_ENTITY_DIED Adds points
EntityManager EVENT_ENTITY_DIED Removes entity
AISystem EVENT_ENTITY_DAMAGED Switches behaviour/state

✅ These run after all component-updating systems for the frame, consuming the accumulated events.


🕓 5. Frame loop

Here’s what your game loop might look like, pseudocode-style:

while (game_running) {
    event_clear(); // clear last frame’s events

    InputSystem_Update();
    MovementSystem_Update();
    CollisionSystem_Update();
    DamageSystem_Update();
    HealthSystem_Update();

    // now process all events from the above
    AudioManager_Update();
    ParticleSystem_Update();
    ScoreSystem_Update();
    EntityManager_Update();

    RenderSystem_Update();
}

✅ Systems that modify world state run first.
✅ Systems that react (sound, score, cleanup) run after.
✅ Rendering always happens last.


🧠 Why this is powerful

  • Predictable order: You always know when state updates vs side effects happen.
  • Loose coupling: Systems don’t need to know each other.
  • Expandable: Adding new effects (e.g. camera shake on damage) means just subscribing to an event — no core logic changes.
  • Debuggable: You can log events and component changes to trace gameplay logic easily.

🔄 Bonus: “Next frame” thinking

Events from frame N can be processed either immediately (end of frame) or deferred to N+1, depending on how stable you want your simulation to be.

Many engines process events after all updates, so you never mutate data mid-frame that another system is still using.


You’ve basically described the canonical modern ECS loop used in engines like Bevy, Flecs, Entt, and even parts of Unity’s DOTS.
It’s the sweet spot between pure data pipeline and reactive game-world orchestration.


Would you like me to show a diagram of this exact frame loop, with arrows showing the data flow (components → events → global systems → next frame)? It’d make an excellent architecture reference for your engine.

You said:

so where in all this would fit the situation of an enemy wanting to fire a projectile at the player. The enemy behaviour system works out this needs to happen, then what? Does it emit a general "spawn entity" event with the type and other data (like angle to move, etc)?

ChatGPT said:

Exactly — you’ve hit the next natural question that separates a static ECS from a living one.

Let’s walk through how an “enemy fires projectile” flow typically fits into your event-driven ECS.


🧩 1. The Enemy AI System Decides to Fire

Let’s say you’ve got an EnemyBehavior (or Logic, or Brain) component, and a BehaviorSystem that updates it.

At some point in its update loop, you detect:

if (enemy[i].state == STATE_ATTACK && CanFire(enemy[i])) {
    event_push(EVENT_SPAWN_PROJECTILE, (SpawnEvent){
        .entityType = ENTITY_PROJECTILE,
        .x = position[i].x,
        .y = position[i].y,
        .direction = player_direction,
        .speed = 300,
        .owner = i,
    });
}

✅ This is where intent is generated — the AI doesn’t create entities directly, it just declares “I want one”.


⚙️ 2. The Event Queue Holds That Intent

The EVENT_SPAWN_PROJECTILE is now in the global queue for this frame.

At this point, the projectile doesn’t exist yet — it’s just a pending “spawn” request.


🧱 3. The Entity Manager Listens for Spawn Events

At the end of your update phase (or at the start of the next frame), the Entity Manager or Spawner System runs through all EVENT_SPAWN_* events and acts on them:

for (each event in events) {
    switch (event.type) {
        case EVENT_SPAWN_PROJECTILE: {
            int id = Entity_Create();
            position[id] = (Position){ event.x, event.y };
            velocity[id] = Velocity_FromAngle(event.direction, event.speed);
            sprite[id]   = ProjectileSprite();
            projectile[id] = (Projectile){ .owner = event.owner };
            break;
        }
    }
}

✅ Now the new entity exists, with all its components created in one place, following your ECS conventions.


🔄 4. The Projectile Joins the Regular ECS Flow

From here, the projectile behaves like any other entity:

  • The MovementSystem updates its position.
  • The CollisionSystem detects hits.
  • The DamageSystem applies damage.
  • The HealthSystem checks if something died.
  • The Sound/Particle/EntityManager systems react to events that result.

The enemy doesn’t know any of this happened — it just said “I want to shoot.”


🧠 Why This Works So Well

This approach gives you:

  • Clean decoupling: AI doesn’t touch entity creation directly.
  • Centralised spawning logic: all entities are born the same way, even bullets, explosions, or pickups.
  • Deterministic order: spawns happen at a known point in the frame, so you never get “half-updated” entities.
  • Replayability / debugging: since spawns are events, you can record them for replay or debugging logs.

💡 Optional refinements

You could even split this further:

System Role
AISystem Decides that a shot should be fired → emits event
ProjectileSpawnerSystem Listens for projectile spawn events → creates new entities
EntityManager Handles raw allocation and ID management

This keeps even spawning logic modular — for example, power-ups could be handled by a different spawner system than enemies.


🔁 Frame Flow (simplified)

AI System → emits EVENT_SPAWN_PROJECTILE
                   ↓
Event Queue → stores it
                   ↓
Spawn System → consumes event, creates new projectile entity
                   ↓
Projectile joins normal ECS loop

So yes — your intuition is exactly right.

The behaviour system emits a general “spawn entity” event with the necessary data, and a later system actually creates it.