Skip to content

Collision Events

Collision detection emits events when an overlapping pair starts, remains active, or separates. Each event contains every contact of that lifecycle phase detected during the fixed step.

EventEmitted when
CollisionStartedTwo colliders overlap for the first fixed step
CollisionActiveThe pair remains overlapping on a later fixed step
CollisionStoppedA previously overlapping pair no longer overlaps

Only penetrating colliders produce collision lifecycle events. Shapes that exactly touch without overlap do not produce a contact.

Run collision gameplay after PhysicsSet.Collide in Schedule.FixedPostUpdate:

import { Define, Schedule } from '@shell/ecs';
import { CollisionStarted, PhysicsSet } from '@shell/physics';
export const HandleCollisionStartedSystem = Define.system()
.after(PhysicsSet.Collide)
.update(({ events }) => {
const event = events.get(CollisionStarted);
if (!event) return;
for (const contact of event.data.contacts) {
console.log(contact.entityA, contact.entityB);
console.log(contact.depth, contact.normalX, contact.normalY);
}
});
world.registerSystem(Schedule.FixedPostUpdate, HandleCollisionStartedSystem);

The same pattern works with CollisionActive and CollisionStopped.

An observer runs immediately after collision detection triggers the event:

const OnCollisionStarted = Define.system().update(({ events }) => {
const event = events.get(CollisionStarted);
if (!event) return;
for (const contact of event.data.contacts) {
// React before the next scheduled system runs.
}
});
world.observe(CollisionStarted, OnCollisionStarted);

Use an observer for immediate reactions. Use a scheduled system when the reaction must be ordered relative to other physics system sets.

Each item in event.data.contacts has these fields:

FieldTypeDescription
entityAEntityFirst collider entity
entityBEntitySecond collider entity
depthnumberPenetration depth
normalX, normalYnumberContact normal from entityB toward entityA
separationX, separationYnumberNormal multiplied by penetration depth

The event payload also mirrors the first contact at event.data.entityA, event.data.depth, and the other contact fields. Prefer iterating contacts so multiple contacts from the same fixed step are handled.

CollisionStopped contacts identify the entities but set depth, normal, and separation fields to 0 because the shapes are no longer intersecting.

Events describe detected contacts independently of physical response. Colliders without rigid bodies still emit events. The built-in resolver only responds when both entities have RigidBody and Position, and every dynamic participant has Velocity. Response does not generate angular impulses.

The resolver consumes CollisionStarted and CollisionActive after detection. It does not consume CollisionStopped.

For general event lifetime and observer behavior, see Events.