How Jolt Physics Implements the ContactListener for Collision Callbacks
Jolt Physics implements collision callbacks through an abstract ContactListener class that is registered once with the PhysicsSystem via SetContactListener, where the internal ContactConstraintManager forwards narrow-phase contact events to four virtual methods: OnContactValidate, OnContactAdded, OnContactPersisted, and OnContactRemoved.
Jolt Physics provides a robust callback system for handling collision events through the ContactListener interface. This mechanism allows developers to filter contacts, respond to impacts, and modify solver properties in real-time according to the jrouwe/JoltPhysics source code. Understanding how the ContactListener for collision callbacks is architected is essential for implementing custom physics behaviors without compromising simulation stability.
Core Architecture of the ContactListener System
The ContactListener system in Jolt Physics operates on a pure-virtual interface pattern that separates collision detection from collision response logic.
The Abstract Listener Interface
The ContactListener class is defined in Jolt/Physics/Collision/ContactListener.h and declares four pure-virtual callback methods that cover the complete contact lifecycle:
OnContactValidate– Decides whether a newly detected contact should be processed. ReturnValidateResult::AcceptContactto keep the contact orValidateResult::RejectContactto discard it.OnContactAdded– Called the first time a contact point appears between two bodies.OnContactPersisted– Called each subsequent simulation step while the contact remains active.OnContactRemoved– Called when the contact disappears, typically when bodies separate.
All callbacks receive references to the colliding bodies and, for lifecycle events, a mutable ContactSettings object that allows modification of friction, restitution, and mass scaling.
Registration and Event Forwarding
The PhysicsSystem class exposes SetContactListener in Jolt/Physics/PhysicsSystem.h to install a user-defined listener. Internally, the ContactConstraintManager (defined in Jolt/Physics/Constraints/ContactConstraintManager.h) stores this pointer and forwards every contact-related event immediately after the narrow-phase collision detection step. Only one listener can be active at a time; registering a new listener replaces the previous instance.
Implementing a Custom ContactListener
Creating a functional collision callback handler requires deriving from the base class and implementing the desired virtual methods.
Basic Implementation Pattern
Derive your class from ContactListener, override the callbacks you need, and register the instance with the physics system before starting the simulation:
// Custom listener implementation
class MyContactListener : public JPH::ContactListener
{
public:
ValidateResult OnContactValidate(const Body &inBody1,
const Body &inBody2,
RVec3Arg inBaseOffset,
const CollideShapeResult &inCollisionResult) override
{
// Reject contacts involving sensor bodies
if (inBody1.IsSensor() || inBody2.IsSensor())
return ValidateResult::RejectContact;
return ValidateResult::AcceptContact;
}
void OnContactAdded(const Body &inBody1,
const Body &inBody2,
const ContactManifold &inManifold,
ContactSettings &ioSettings) override
{
// Log contact position using non-locking interface
auto &interface = PhysicsSystem::GetInstance()->GetBodyInterfaceNoLock();
RVec3 p1 = inManifold.GetWorldSpaceContactPointOn1(0);
RVec3 p2 = inManifold.GetWorldSpaceContactPointOn2(0);
printf("Contact added between %u and %u\n",
inBody1.GetID().GetIndex(),
inBody2.GetID().GetIndex());
// Modify solver properties
ioSettings.mCombinedFriction = 0.5f * ioSettings.mCombinedFriction;
}
void OnContactPersisted(const Body &inBody1,
const Body &inBody2,
const ContactManifold &inManifold,
ContactSettings &ioSettings) override
{
// Process ongoing contact
}
void OnContactRemoved(const SubShapeIDPair &inSubShapePair) override
{
// Cleanup cached data for this pair
}
};
// Registration
PhysicsSystem *physics = PhysicsSystem::GetInstance();
MyContactListener *listener = new MyContactListener();
physics->SetContactListener(listener);
Contact Validation and Filtering
The OnContactValidate callback executes during the narrow-phase before the contact is added to the constraint manager. This is the appropriate location for filtering collisions based on body properties, layers, or collision groups without the overhead of creating a full contact constraint.
Handling Contact Lifecycle Events
Use OnContactAdded to trigger one-time responses like impact sounds or damage calculations. The OnContactPersisted callback fires every simulation step while bodies remain in contact, suitable for continuous effects like friction heat or sustained force application. Finally, OnContactRemoved provides cleanup opportunities when the manifold is destroyed.
Thread Safety and API Constraints
The ContactListener callbacks may be issued from many threads simultaneously, though all callbacks concerning the same body pair are serialized to prevent race conditions. Critical constraint: callbacks are invoked while physics bodies are locked, meaning you cannot use standard locking APIs like GetBodyInterface or GetBodyLockInterface inside these methods.
Instead, you must use the non-locking variants:
GetBodyInterfaceNoLock– For read-only body access during callbacks.GetBodyLockInterfaceNoLock– For acquiring body locks when necessary.
Attempting to use the locking interfaces from within a callback will result in deadlock or undefined behavior.
Modifying Contact Properties
The ContactSettings structure passed to OnContactAdded and OnContactPersisted allows real-time modification of how the solver handles the contact:
mCombinedFriction– Adjust the effective friction coefficient.mCombinedRestitution– Modify bounciness.mInvMassScale1andmInvMassScale2– Scale the effective mass of bodies for this contact only.
These modifications affect only the current contact manifold and persist for the duration of the contact.
Key Source Files
Reference these files in the jrouwe/JoltPhysics repository to explore the implementation:
Jolt/Physics/Collision/ContactListener.h– Abstract listener interface with all callback definitions.Jolt/Physics/PhysicsSystem.h– ContainsSetContactListenerfor registration.Jolt/Physics/Constraints/ContactConstraintManager.h– Forwards events to the registered listener.Samples/Utils/ContactListenerImpl.h– Reference implementation used by the sample tests and unit tests.UnitTests/Physics/ContactListenerTests.cpp– Demonstrates verification patterns for custom listeners.
Summary
- Jolt Physics implements collision callbacks through the abstract
ContactListenerclass registered viaPhysicsSystem::SetContactListener. - The four callback methods (
OnContactValidate,OnContactAdded,OnContactPersisted,OnContactRemoved) cover the complete contact lifecycle from detection to removal. - Thread safety requires using non-locking body interfaces (
GetBodyInterfaceNoLock) inside callbacks, as bodies are already locked during invocation. - ContactSettings allows modification of friction, restitution, and mass scaling to influence the solver behavior for specific contacts.
- The
ContactConstraintManagerhandles internal forwarding of events after narrow-phase detection, ensuring callbacks are serialized per body pair.
Frequently Asked Questions
How do I register a ContactListener in Jolt Physics?
Register your derived class instance by calling PhysicsSystem::SetContactListener and passing a pointer to your listener. This stores the pointer in the internal ContactConstraintManager, which will immediately begin forwarding collision events. Only one listener can be active at a time, and registration should occur before the simulation begins to avoid missing early contact events.
Can I modify collision properties like friction inside the callback?
Yes. The OnContactAdded and OnContactPersisted callbacks receive a mutable ContactSettings reference allowing you to adjust mCombinedFriction, mCombinedRestitution, and mass scaling factors (mInvMassScale1, mInvMassScale2). These modifications affect only the specific contact manifold and persist until OnContactRemoved is called.
Is the ContactListener thread-safe?
Callbacks may be issued from multiple threads, but Jolt guarantees that callbacks for the same body pair are serialized. You must ensure your listener implementation is thread-safe for different body pairs. Crucially, you must use non-locking body interfaces (GetBodyInterfaceNoLock) inside callbacks because the physics bodies are already locked during the callback invocation.
What is the difference between OnContactAdded and OnContactPersisted?
OnContactAdded fires once when a new contact point is first detected between two bodies, making it ideal for impact events or one-time responses. OnContactPersisted fires every subsequent simulation step while the contact remains active, suitable for continuous effects like sustained audio or cumulative damage. Both callbacks provide access to the ContactManifold and mutable ContactSettings.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →