# How Jolt Physics Implements the ContactListener for Collision Callbacks

> Discover how Jolt Physics uses the ContactListener for collision callbacks. Learn about OnContactValidate, OnContactAdded, OnContactPersisted, and OnContactRemoved with detailed explanations.

- Repository: [Jorrit Rouwe/JoltPhysics](https://github.com/jrouwe/JoltPhysics)
- Tags: internals
- Published: 2026-07-17

---

**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`](https://github.com/jrouwe/JoltPhysics/blob/main/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`](https://github.com/jrouwe/JoltPhysics/blob/main/Jolt/Physics/PhysicsSystem.h) to install a user-defined listener. Internally, the `ContactConstraintManager` (defined in [`Jolt/Physics/Constraints/ContactConstraintManager.h`](https://github.com/jrouwe/JoltPhysics/blob/main/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:

```cpp
// 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:

- **[`Jolt/Physics/Collision/ContactListener.h`](https://github.com/jrouwe/JoltPhysics/blob/main/Jolt/Physics/Collision/ContactListener.h)** – Abstract listener interface with all callback definitions.
- **[`Jolt/Physics/PhysicsSystem.h`](https://github.com/jrouwe/JoltPhysics/blob/main/Jolt/Physics/PhysicsSystem.h)** – Contains `SetContactListener` for registration.
- **[`Jolt/Physics/Constraints/ContactConstraintManager.h`](https://github.com/jrouwe/JoltPhysics/blob/main/Jolt/Physics/Constraints/ContactConstraintManager.h)** – Forwards events to the registered listener.
- **[`Samples/Utils/ContactListenerImpl.h`](https://github.com/jrouwe/JoltPhysics/blob/main/Samples/Utils/ContactListenerImpl.h)** – Reference implementation used by the sample tests and unit tests.
- **[`UnitTests/Physics/ContactListenerTests.cpp`](https://github.com/jrouwe/JoltPhysics/blob/main/UnitTests/Physics/ContactListenerTests.cpp)** – Demonstrates verification patterns for custom listeners.

## 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`.