Skip to content

Collision Response

đź§© 1. Why infer type instead of using a type enum?

âś… Advantages of inference (pure ECS approach)

  1. No rigid hierarchy or coupling

    • You don’t have to maintain a giant switch(type) anywhere.

    • Systems just care about the data they need — e.g., RenderSystem doesn’t care if an entity is a player or a rocket; it just needs a Position and a Sprite.

  2. Emergent behaviour through composition

    • If you add a Health component to a door, suddenly the door can take damage — without changing any code that assumes what a “door” is.

    • Add a Projectile component to a “rock” entity, and now it behaves like a throwable weapon.
      No inheritance changes, no new enum values — just add/remove components.

  3. Extremely flexible for prototyping

    • You can define new entity “types” entirely in data (e.g., JSON or level scripts) by combining components in new ways.

    • Designers (or future-you) can mix and match components freely.

  4. Systems stay generic

    • Your CollisionSystem, RenderSystem, HealthSystem all operate on aspects of entities.

    • You can add new entity kinds without touching those systems.

  5. Fewer dependencies

    • No central enum header that every file has to include.

    • Less recompilation churn when you add a new entity type.


đźš« Downsides

  1. You lose an explicit label

    • Debugging is trickier — it’s nice to log Entity 5: TYPE_PLAYER instead of “entity with position, velocity, sprite, input.”

    • You can always add an optional “debug type” component purely for human readability.

  2. It can be less obvious what an entity is

    • When reading code, “components as type” feels less intuitive than if (entity.type == PLAYER) at first.
  3. You can overcomplicate systems if you go too abstract

    • If your game’s entity types are fixed and few, a type enum may be simpler and faster to reason about.

⚖️ When to use which

Situation Better Approach
Small, fixed cast of entities Type enum (simpler)
Large / data-driven / moddable world Inferred from components (ECS purity)
You want both simplicity and flexibility Hybrid: type enum + component checks
## đź§® 2. Figuring out collision response rules

There’s no universal formula, but there is a general design pattern.

âś… Typical pattern

You define collision logic based on what components are present, not on the type enum.

For example: for each collision (a, b): if (a has Projectile && b has Health) damage b destroy a else if (a has Player && b has Pickup) collect b else if (a has Velocity && b has Solid) resolve physics

Each rule describes what combination of components causes what response.


đź’ˇ How to organize that

There are a few good ways:

  1. Central “collision resolver” system

    • A big if/else or table that handles the combinations.

    • Clear and direct, but can grow large.

  2. Event + listener system

    • The collision system emits EVENT_COLLISION(a, b).

    • Other systems (damage, pickup, sound) subscribe and check whether they care.

    • Keeps logic modular and avoids a central monster function.

  3. Per-component callback (data-driven)

    • Each component type (e.g., Projectile, Pickup) can register a function pointer for collision handling.

    • When a collision happens, the system checks if either side has a handler and calls it.

if (components[a].on_collision) components[a].on_collision(a, b); if (components[b].on_collision) components[b].on_collision(b, a);

  1. This is elegant, especially in C.

âś… General rule:
→ The collision system’s only job is to say “these two things touched.”
→ Everything else — damage, destruction, pickup, bounce — should be handled by systems or callbacks that care about those entities’ components.

That way, adding new entity types or effects doesn’t require editing the collision system.


If you’d like, I can show you a small, idiomatic C version of this — where the collision system just emits events, and different systems (damage, pickup, physics) each listen for and react to the ones they care about. It makes it much easier to grow the game later.

Would you like me to write that version out?

Perfect — let’s walk through a clean, idiomatic C ECS design for collision handling using an event queue + listeners pattern.

This gives you:

  • clear separation between collision detection and response,

  • modular per-system logic (damage, pickups, etc.),

  • and no spaghetti of cross-references between entities.


đź§± 1. Event system

event.h

#ifndef EVENT_H
#define EVENT_H

typedef enum {
    EVENT_COLLISION,
    EVENT_DAMAGE,
    EVENT_ENTITY_DIED,
    // add more later
} EventType;

typedef struct {
    EventType type;
    int a, b;  // entity IDs involved
} Event;

#define MAX_EVENTS 256

void event_push(Event e);
int  event_count(void);
Event *event_get_all(void);
void event_clear(void);

#endif

event.c

#include "event.h"

static Event events[MAX_EVENTS];
static int num_events = 0;

void event_push(Event e) {
    if (num_events < MAX_EVENTS)
        events[num_events++] = e;
}

int event_count(void) {
    return num_events;
}

Event *event_get_all(void) {
    return events;
}

void event_clear(void) {
    num_events = 0;
}

đź§© 2. Collision detection system

collision_system.c

#include "components.h"
#include "event.h"
#include "collision.h"  // aabb_overlap()

void collision_system(void) {
    event_clear();  // start fresh each frame

    for (int i = 0; i < MAX_ENTITIES; i++) {
        if (!colliders[i].active || !positions[i].active) continue;

        for (int j = i + 1; j < MAX_ENTITIES; j++) {
            if (!colliders[j].active || !positions[j].active) continue;

            if (aabb_overlap(&positions[i], &colliders[i],
                             &positions[j], &colliders[j])) {
                event_push((Event){ EVENT_COLLISION, i, j });
            }
        }
    }
}

Responsibility: only detects collisions — does not decide what happens.


đź§  3. Collision response systems

Now we add individual systems that react to events.


damage_system.c

#include "components.h"
#include "event.h"
#include "entity_manager.h"

void damage_system(void) {
    Event *events = event_get_all();
    int count = event_count();

    for (int i = 0; i < count; i++) {
        Event *e = &events[i];
        if (e->type != EVENT_COLLISION) continue;

        int a = e->a;
        int b = e->b;

        // If a projectile hits something with health
        if (projectiles[a].active && health[b].active) {
            health[b].hp -= projectiles[a].damage;
            destroy_entity(a);
        }

        // Optional: reverse (b hitting a)
        if (projectiles[b].active && health[a].active) {
            health[a].hp -= projectiles[b].damage;
            destroy_entity(b);
        }
    }
}

đź§­ 4. Main update order

Your main game loop (simplified):

void game_update(void) {
    input_system();
    behavior_system();    // e.g. AI or player logic
    movement_system();    // update positions
    collision_system();   // detect overlaps
    damage_system();      // react to collisions
    pickup_system();
    physics_collision_response_system();
    render_system();
}

đź§  Generalisation

Later you can add more event types (e.g. EVENT_DAMAGED, EVENT_DIED, EVENT_SPAWNED) and let systems emit new events in response. That forms a chain of cause–effect that’s easy to trace and debug.


Would you like me to show a variant of this event system that uses a function-pointer-based listener registry (so systems can register handlers dynamically rather than checking events manually)? That’s a little more abstract, but elegant if you ever want modding or runtime system toggling.

Once you introduce an event system, it stops being “just for collisions” and becomes a generic message bus for your ECS. It’s the glue that lets otherwise-isolated systems react to what others do, without directly knowing about each other.

So:

  • The collision system emits EVENT_COLLISION.

  • The damage system might then emit EVENT_DAMAGED.

  • A sound system could listen for EVENT_DAMAGED and play a sound.

  • A particle system could listen for EVENT_DIED and spawn an explosion.

  • A score system could listen for EVENT_ENEMY_DESTROYED and add points.

Each of those systems only depends on the event type, not on which other system caused it.

That’s what makes the design scalable — every new feature is just another system subscribing to the flow of events.

🪦 Example event flow

[Collision System] → EVENT_COLLISION
       ↓
[Damage System] → health[b].hp -= projectile[a].damage → EVENT_ENTITY_DAMAGED
       ↓
[Health System] → detects hp <= 0 → EVENT_ENTITY_DIED
       ↓
[Death System] → plays explosion, removes entity, gives player score

🧍‍♂️ Player input and movement

[Input System]
  → reads keyboard/controller
  → sets movement component velocity
       ↓
[Physics System]
  → applies gravity, friction, collisions
       ↓
[Transform System]
  → updates entity positions based on velocity
       ↓
[Render System]
  → draws sprite at updated position

⚙️ The point of ECS — and its trap

ECS is designed to separate data from logic and make dependencies explicit.
It’s not about having a billion micro-systems for their own sake.
It’s about making sure each system has a single responsibility and no hidden side-effects.

That’s why people sometimes go overboard — they take “separation” to mean “split everything into the smallest possible atom.”

In reality, the goal is clarity of purpose, not quantity of files.


đź§© The principle

A system should exist if it manages a distinct kind of simulation or game logic that runs independently of others.

That usually means one system per domain of behaviour — not per single line of code.

For example:

Good split Over-split (too granular)
PhysicsSystem GravitySystem, FrictionSystem, VelocitySystem, GroundCheckSystem
AISystem PatrolSystem, ChaseSystem, AttackSystem, IdleSystem
HealthSystem DamageSystem, DeathSystem, RegenerationSystem (if tiny and dependent)
## 🏗️ A typical mid-sized game’s systems

Here’s what you’d realistically have in a C-style ECS engine for a 2D action game:

Category Example Systems Responsibility
Core / Engine EntityManager, ComponentManager Create, destroy, track entities/components
Game Loop TimeSystem, EventSystem Frame timing, event dispatch
Physics / Motion MovementSystem, CollisionSystem, MapCollisionSystem Movement, physics response, world boundaries
Gameplay Logic AISystem, PlayerControlSystem, CombatSystem, HealthSystem Core game rules and interactions
Rendering SpriteRenderSystem, AnimationSystem, CameraSystem Visual output only
Audio / Feedback SoundSystem, ParticleSystem Play sounds, visual effects
UI / Meta UISystem, GameStateSystem Menus, overlays, game transitions
## 🧠 Think in update phases, not “every concept = system”

You can group related systems into phases that run in order:

  1. Input phase → read controls, AI intentions

  2. Simulation phase → movement, collisions, physics, health

  3. Feedback phase → sounds, animations, particles

  4. Rendering phase → draw everything

Each phase may contain a few small systems, but they’re cohesive.

🔍 ECS vs OOP in spirit

OOP mindset ECS mindset
“Each object owns its data and logic.” “Data lives in components, logic lives in systems.”
Tends to couple data & behaviour tightly. Explicitly separates them to make behaviour composable.
Overuse leads to deep class hierarchies. Overuse leads to endless tiny systems.
You fix OOP sprawl by merging responsibilities. You fix ECS sprawl by merging systems that always operate together.
## đź§­ Practical rule of thumb

Ask yourself:

“Could I meaningfully disable this system and have the rest of the game still run (just without that feature)?”

  • If yes → it’s probably a good, independent system.

- If no → it’s probably just part of another system’s responsibility.