Skip to content

Collision Detection

🧩 The problem

A collision system detects that two entities overlap β€” e.g.:

  • rocket hits enemy
  • player touches health pickup
  • enemy walks into wall

…but the collision system itself shouldn’t decide what happens next (damage, pickup, bounce, sound, etc).
That logic belongs to higher-level systems.


βœ… Common pattern: Event queue + observer-like dispatch

You’re right β€” the cleanest solution is a hybrid of the Observer pattern and an event queue.

1. The Collision system

Detects overlaps and pushes collision events into a global or local queue.

typedef struct {
    int a, b;   // entity IDs
} CollisionEvent;

#define MAX_EVENTS 128
CollisionEvent collision_events[MAX_EVENTS];
int num_collision_events = 0;

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

            if (aabb_overlap(&colliders[i], &positions[i],
                             &colliders[j], &positions[j])) {
                if (num_collision_events < MAX_EVENTS)
                    collision_events[num_collision_events++] = (CollisionEvent){i, j};
            }
        }
    }
}

So now, your collision detection is purely mechanical.


2. The Collision response system

Another system runs after collision detection.
It processes the queue and dispatches the events to whichever systems need them.

void collision_response_system(void) {
    for (int e = 0; e < num_collision_events; e++) {
        int a = collision_events[e].a;
        int b = collision_events[e].b;

        // Example reactions
        if (projectiles[a].active && health[b].active) {
            health[b].hp -= projectiles[a].damage;
            destroy_entity(a); // remove projectile
        }
        else if (pickup[a].active && player[b].active) {
            player[b].score += pickup[a].value;
            destroy_entity(a);
        }
    }
}

Each β€œreaction” is just a conditional test of which components are active on each entity.
This means you don’t need direct access between objects.

Look at Organising entities{: .ezlinks_not_found} here. The pickup and player arrays are components.


3. Optional: decouple reactions completely

If you want to make it even cleaner, you can turn the collision_response_system() into an event dispatcher and let other systems subscribe:

typedef enum { EVENT_COLLISION } EventType;

typedef struct {
    EventType type;
    int a, b;
} Event;

void emit_event(Event e);
void register_listener(EventType type, void (*callback)(Event *));

That gives you a pure Observer pattern β€” each system registers callbacks, and the event manager calls them.
It’s a little more code, but very modular.


🧠 Design summary

Pattern Responsibility
ECS systems Process component data (detect overlaps, etc.)
Event queue Buffers interactions between systems
Observers / listeners React to events in a decoupled way

This keeps collision detection, collision resolution, and entity logic independent, so you can later add new event types (EVENT_DAMAGED, EVENT_PICKUP_COLLECTED, EVENT_ENTITY_DIED) without hard-wiring everything together.


🧱 Practical organisation

You can structure it like this:

src/
β”œβ”€ ecs/
β”‚  β”œβ”€ entity_manager.c
β”‚  β”œβ”€ components/
β”‚  β”‚  β”œβ”€ position.h
β”‚  β”‚  β”œβ”€ collider.h
β”‚  β”‚  β”œβ”€ health.h
β”‚  β”‚  └─ projectile.h
β”‚  β”œβ”€ systems/
β”‚  β”‚  β”œβ”€ physics_system.c
β”‚  β”‚  β”œβ”€ render_system.c
β”‚  β”‚  β”œβ”€ collision_system.c
β”‚  β”‚  └─ collision_response_system.c
β”‚  └─ events/
β”‚     β”œβ”€ event_queue.c
β”‚     └─ event_queue.h

Each system includes the event queue to push or read events.
Later, if you want scripting or data-driven behavior, you can add a behavior_system.c that also listens to events.