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.