Table of Contents

Characters

Box3D.NET gives you the three engine-side primitives a kinematic character controller needs, and stops there:

// 1. What is the capsule touching?
var gather = new GatherPlanes { Planes = buffer };
world.CollideCapsule(capsule, position, ref gather);

Span<CollisionPlane> planes = buffer.AsSpan(0, gather.Count);

// 2. Where can it actually go?
PlaneSolverResult result = CharacterMover.SolvePlanes(velocity * dt, planes);
position += result.Translation;

// 3. Do not accumulate speed into a wall.
velocity = CharacterMover.ClipVelocity(velocity, planes);

Walk speed, jump height, what counts as ground, whether a slope is climbable — that is game design, and every game answers it differently. Wrapping an opinion about it here would be inventing policy Box3D deliberately left to the caller.

CharacterControllerSample builds a complete controller on these three calls in about eighty lines, with gravity, jumping, ground detection, slope limits and wall sliding. Copy it and change the parts that are yours.

Gathering contacts

CollideCapsule reports every surface the capsule is touching, through a struct callback so that gathering them every frame allocates nothing:

struct GatherPlanes : ICharacterCollisionCallback
{
    public CollisionPlane[] Planes;
    public int Count;

    public bool OnContact(in CharacterContact contact)
    {
        if (Count < Planes.Length)
        {
            Planes[Count++] = CollisionPlane.From(contact);
        }

        return true;   // false stops gathering
    }
}

The capsule is given relative to origin, which is the character's world position. A CharacterContact carries the shape, the plane normal, the offset and the contact point — relative to the query origin, not in world space, which is the detail that makes a debug marker appear in the wrong place the first time.

Solving

SolvePlanes finds the translation closest to the one requested that satisfies every plane. That is what makes a character slide along a wall instead of stopping dead against it, and what keeps it out of the corner where two walls meet.

The solver writes back into the planes it was given: each one's Push reports how far it had to move along that plane. ClipVelocity then projects the velocity onto the planes that resisted, skipping the ones with no push. Without that step a character walking into a wall keeps building velocity into it and shoots sideways the moment the wall ends.

Tuning what a plane means

CollisionPlane is where policy goes:

CollisionPlane plane = CollisionPlane.From(contact);

if (plane.Normal.Y < SlopeLimit)
{
    plane = plane with { ClipsVelocity = false };   // steep: do not slide down it
}

plane = plane with { PushLimit = maxStepHeight };   // do not be shoved further than this

PushLimit bounds how far the solver may move the character along that plane, and ClipsVelocity decides whether the plane takes part in velocity clipping at all. Between them they cover step height, slope limits and one-way surfaces.

Casting instead of colliding

float fraction = world.CastCapsule(capsule, position, move);

A specialised shape cast that slides along surfaces instead of catching on them. Good for a quick "can the character get there", poor as a source of information about what it is touching — use CollideCapsule for that.

Choosing a body

A character is usually a kinematic body: you own its position, and it pushes dynamic bodies without being pushed back. Move it with MoveTowards so that its contacts carry what stands on it, never with SetTransform.

If the character is a dynamic body instead, lock its rotation so it cannot tip over:

character.MotionLocks = MotionLocks.NoRotation;

Either way, turn off contact recycling on the character's body:

var def = BodyDefinition.Kinematic(spawn) with { EnableContactRecycling = false };

Recycling reuses contact manifolds across small movements and is a performance win, but it can produce ghost collisions. On a character a snagged step is more noticeable than the cost.