Collision Events
When two rigid bodies touch, the physics engine reports the contact to both entities. You can respond by playing a sound, applying damage, spawning particles or changing game state. The events are fired on both the collision and the rigidbody components of each entity involved, so you can listen on whichever is more convenient.
Events
| Event | When it fires | Handler argument |
|---|---|---|
collisionstart | Once, on the physics step in which two bodies begin touching | ContactResult |
contact | Every physics step while the bodies remain in contact | ContactResult |
collisionend | Once, when the bodies stop touching | The other Entity |
Contacts are only reported when at least one of the two bodies is dynamic. Static and kinematic bodies never collide with each other. Overlaps with a trigger volume fire triggerenter and triggerleave instead of these events.
Listening for Collisions
Subscribe with on, which returns an EventHandle. Keep that handle and call its off() method when the listener is no longer needed; passing the same callback to off(eventName, callback) on the component, as described in Events, removes the listener too. Use collisionstart for one-off reactions such as an impact sound, and contact when you need the contact details refreshed every step:
const handle = entity.collision.on('collisionstart', (result) => {
console.log(`${entity.name} hit ${result.other.name}`);
});
// Later, when the entity is destroyed or the listener is no longer wanted
handle.off();
Where that code lives depends on how you build:
- Engine
- Editor
- React
- Web Components
Subscribe once the entity has its components, as above, or from a Script attached to the entity as shown in the Editor tab. Scripts are covered in the Scripting section.
Add a Script component to the entity and attach a script that subscribes in initialize and unsubscribes when it is destroyed:
import { Script } from 'playcanvas';
export class ImpactSound extends Script {
static scriptName = 'impactSound';
initialize() {
const handle = this.entity.collision.on('collisionstart', this.onCollisionStart, this);
this.on('destroy', () => handle.off());
}
onCollisionStart(result) {
// Play the 'hit' slot of a Sound component on the same entity
this.entity.sound.play('hit');
}
}
The third argument to on binds this inside the handler to the script instance.
Place a component inside the <Entity>, after its <Collision>, and subscribe in an effect once physics has loaded:
import { useEffect } from 'react';
import { useParent, usePhysics } from '@playcanvas/react/hooks';
function ImpactSound() {
const entity = useParent();
const { isPhysicsLoaded } = usePhysics();
useEffect(() => {
if (!isPhysicsLoaded || !entity.collision) return;
const handle = entity.collision.on('collisionstart', (result) => {
console.log(`${entity.name} hit ${result.other.name}`);
});
return () => handle.off();
}, [entity, isPhysicsLoaded]);
return null;
}
Attach the same ImpactSound script shown in the Editor tab with <pc-script>, or subscribe from page JavaScript through the element's entity property once the app is ready:
<script type="module">
import { whenReady } from '@playcanvas/web-components';
await whenReady('pc-app');
const { entity } = document.querySelector('pc-entity[name="crate"]');
entity.collision.on('collisionstart', (result) => {
console.log(`${entity.name} hit ${result.other.name}`);
});
</script>
See Programmatic Access for how whenReady works.
Contact Data
collisionstart and contact pass a ContactResult with two properties:
other- The entity this body collided with.contacts- An array of ContactPoint objects, one for each point where the two shapes touch.
Each contact point holds:
| Property | Description |
|---|---|
point | The contact point on this entity, in world space |
pointOther | The contact point on the other entity, in world space |
localPoint | The contact point in the local space of this entity |
localPointOther | The contact point in the local space of the other entity |
normal | The contact normal in world space. It points away from the surface of the other entity at the point of contact |
impulse | The impulse the physics engine applied to separate the bodies. Larger values mean a harder hit |
The impulse is a handy measure of how hard two bodies collided. Use it to scale a sound's volume or the damage dealt, and to ignore glancing touches:
entity.collision.on('collisionstart', (result) => {
let strongest = 0;
for (const contact of result.contacts) {
strongest = Math.max(strongest, contact.impulse);
}
if (strongest > 5) {
entity.sound.play('crash');
}
});
Scene-wide Contacts
To handle every contact in one place, for example in a central audio or damage manager, listen to the contact event on the rigid body component system instead of on individual entities. It fires once per contact point with a SingleContactResult that names both entities:
app.systems.rigidbody.on('contact', (result) => {
// result.a and result.b are the two entities
// result.pointA, result.pointB and result.normal are in world space
if (result.impulse > 5) {
console.log(`${result.a.name} and ${result.b.name} collided hard`);
}
});
contact fires every physics step for as long as two bodies touch, which for a box resting on the floor means every frame. Prefer collisionstart for one-shot effects and keep contact handlers cheap.
See Also
- Trigger Volumes - Overlap events for regions that do not block movement
- Collision and Triggers - Tutorial that plays a sound on impact
- Events - How the scripting event system works
- CollisionComponent and RigidBodyComponent - API reference for the events on each component