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. Return ValidateResult::AcceptContact to keep the contact or ValidateResult::RejectContact to 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.
  • mInvMassScale1 and mInvMassScale2 – 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:

Summary

  • Jolt Physics implements collision callbacks through the abstract ContactListener class registered via PhysicsSystem::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 ContactConstraintManager handles 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →