How Box3D Implements Capsule-Based Character Collision Response
Box3D's CharacterMover uses an iterative plane-solving algorithm that queries the physics world for capsule collisions, solves up to 8 contact planes to compute penetration-free displacement, and exchanges impulses with dynamic bodies while clipping velocity to prevent sliding into surfaces.
The CharacterMover class in Erin Catto's Box3D physics engine provides robust capsule-based character collision response for navigating complex game environments without making the character a traditional rigid body. Found in samples/mover.h and samples/mover.cpp, this system handles ground adherence, obstacle sliding, and dynamic object interaction through a multi-step constraint solving approach that runs independently of the main physics simulation step.
Capsule Geometry and World Representation
The mover represents the player character as a capsule defined by the b3Capsule m_capsule member variable. This shape stores two endpoint positions and a radius, providing smooth collision detection that handles stairs and slopes better than box primitives.
The capsule's current world position is tracked through the m_transform.p field, which updates each frame as the solver computes safe displacement vectors. Unlike standard rigid bodies, the character exists as a kinematic probe that queries the world rather than existing as a simulated body within the broad-phase structure.
The Five-Step Collision Response Pipeline
Each simulation frame, the CharacterMover::Step method executes a sophisticated collision resolution sequence that iteratively resolves contacts while respecting geometric constraints.
1. Gathering Collision Planes
The system begins by collecting collision data using b3World_CollideMover, passing the capsule geometry and a moverFilter callback to exclude self-collisions or specific shape categories. The engine invokes PlaneResultFcn for each detected contact, storing up to 8 contact planes in the b3CollisionPlane m_planes[] array.
Each plane stores:
- A surface normal vector
- A penetration offset
- Push limits that constrain how far the solver can move the character along that normal
2. Solving Penetration Constraints
The collected planes feed into b3SolvePlanes, which computes a displacement vector (b3PlaneSolverResult::delta) that resolves all penetrations simultaneously while respecting each plane's push limit. This constraint solver ensures the character doesn't tunnel through thin geometry or over-correct into adjacent surfaces.
3. Iterative Movement Casting
Rather than applying the full displacement immediately, the mover performs conservative advancement using b3World_CastMover. The returned collision fraction scales the delta vector, allowing the character to slide along surfaces. This process repeats up to five iterations, re-collecting planes each time to handle new contacts that emerge during the movement.
// Simplified iteration logic from samples/mover.cpp
for (int i = 0; i < 5; ++i) {
// Collect planes at current position
b3World_CollideMover(world, m_capsule, moverFilter, this);
// Solve for penetration-free displacement
b3PlaneSolverResult result = b3SolvePlanes(m_planes, planeCount);
// Cast to find safe movement fraction
float fraction = b3World_CastMover(world, m_capsule, result.delta, moverFilter);
// Update position
m_transform.p += fraction * result.delta;
if (fraction >= 1.0f) break; // No collision, movement complete
}
4. Impulse Exchange with Dynamic Bodies
When the capsule contacts dynamic rigid bodies, the mover calculates response impulses based on relative normal velocity at the contact point. The system calls b3Body_ApplyLinearImpulse to push the struck object, while simultaneously adjusting the character's own velocity to conserve momentum. This allows players to push crates, affect ragdolls, or ride moving platforms while maintaining kinematic control.
5. Velocity Clipping for Surface Stability
After position resolution completes, the mover optionally performs velocity clipping using b3ClipVector. This removes any velocity component that would push the character into a collision surface within the next frame, eliminating jitter and preventing the character from sliding down gentle slopes when standing still. Pass true to the clipVelocity parameter in Step() to enable this stabilization.
Practical Implementation Example
Integrating the CharacterMover requires initialization with a starting position and per-frame stepping with input vectors. The following pattern from samples/sample_character.cpp demonstrates typical usage:
void SampleCharacter::Step()
{
// Calculate world-space movement vectors from camera orientation
b3Vec3 forward = -m_camera->GetForward();
b3Vec3 right = m_camera->GetRight();
forward.y = 0.0f;
forward = b3Normalize(forward);
// Convert WASD input to throttle values
b3Vec2 throttle = {0.0f, 0.0f};
if (IsKeyDown(KEY_W)) throttle.x += 1.0f;
if (IsKeyDown(KEY_S)) throttle.x -= 1.0f;
if (IsKeyDown(KEY_A)) throttle.y -= 1.0f;
if (IsKeyDown(KEY_D)) throttle.y += 1.0f;
// Step the mover with collision response enabled
m_mover.Step(nullptr, 0, true);
}
// Initialization elsewhere
void SampleCharacter::Initialize()
{
b3Pos startPos = b3MakePos(0.0f, 1.0f, 0.0f);
m_mover.Initialize(this, startPos);
}
The Step method signature accepts an optional array of shapes to ignore (for custom collision filtering), a count, and the clipVelocity boolean that toggles post-solve velocity clipping.
Summary
- Capsule Geometry: The
CharacterMoverusesb3Capsulefor smooth collision detection against world geometry, stored insamples/mover.h. - Plane-Based Solving: Up to 8 collision planes are collected via
b3World_CollideMoverand resolved usingb3SolvePlanesto compute penetration-free displacement. - Iterative Casting: The solver runs up to 5 iterations, using
b3World_CastMoverto slide along surfaces and handle complex contact manifolds. - Dynamic Interaction: Impulses are exchanged with hit bodies through
b3Body_ApplyLinearImpulse, allowing the character to affect the physics world. - Velocity Stability: Optional
b3ClipVectorpass removes residual penetration velocity, preventing jitter on slopes and against walls.
Frequently Asked Questions
Why does Box3D use a capsule shape for character collision?
The capsule provides mathematically smooth collision detection that handles edge cases better than boxes or cylinders. As implemented in samples/mover.cpp, the rounded ends prevent catching on terrain seams, allow natural sliding along walls, and provide consistent normal vectors for the plane solver when navigating stairs or slopes.
How does the CharacterMover prevent tunneling through thin geometry?
The system uses iterative conservative advancement through b3World_CastMover. Rather than teleporting the full computed displacement, the mover casts the capsule along the delta vector and scales the movement by the collision fraction. If the cast hits a surface at 20% of the distance, the mover only moves 20% and re-collects planes, ensuring the capsule never jumps through thin collision surfaces.
What limits how many surfaces the character can collide with simultaneously?
The implementation stores contact planes in a fixed-size array b3CollisionPlane m_planes[8], limiting the solver to 8 simultaneous collision constraints. This balances performance and stability; in practice, this handles standing in corners or sliding along complex geometry without requiring dynamic memory allocation during the physics step.
How does the mover handle standing on moving platforms?
When b3World_CollideMover detects contact with a dynamic body, the system calculates an impulse proportional to the relative velocity at the contact point. The mover applies this impulse to the body via b3Body_ApplyLinearImpulse while adjusting its own velocity to match the platform's movement. This implicit coupling allows the character to ride elevators and moving objects while maintaining independent kinematic control.
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 →