Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/facepunch/sbox-public/llms.txt

Use this file to discover all available pages before exploring further.

The CharacterController component provides swept collision-constrained movement without a rigidbody. It is not affected by physics forces; instead you write code that sets Velocity and calls Move() each frame. This page covers the full workflow: configuring the component, accelerating and decelerating the character, detecting and leaving the ground, and implementing jumping.
CharacterController is in the Physics category in the component browser. Add it to the same GameObject as your character mesh or at its root.

Component properties

Configure the controller in the editor or in code. All properties are exposed as [Property] so they appear in the inspector.
PropertyDefaultDescription
Radius16Capsule radius in world units.
Height64Capsule height in world units.
StepHeight18Maximum step the controller can climb without jumping.
GroundAngle45°Maximum slope angle considered walkable ground.
Acceleration10Acceleration rate applied by Accelerate(). Scaled by Time.Delta internally.
Bounciness0.3How much velocity is preserved when running into a wall (0 = stop dead, 1 = full bounce).
UseCollisionRulesfalseWhen true, use the project’s collision rules for the GameObject’s tags. When false, use IgnoreLayers.
IgnoreLayers(empty)Tags to ignore during movement traces. Useful for ignoring trigger volumes or team-specific geometry.

Bounding box

The controller traces a BBox derived from Radius and Height:
BBox bounds = controller.BoundingBox;
// Equivalent to: new BBox( new Vector3(-Radius, -Radius, 0), new Vector3(Radius, Radius, Height) )

Synced state

Two properties are marked [Sync] and replicated across the network automatically:
// Current velocity — set this to control movement direction and speed
controller.Velocity = new Vector3( 100, 0, 0 );

// Whether the character is on solid ground
bool grounded = controller.IsOnGround;
Additional ground information is available locally (not synced):
GameObject groundObj     = controller.GroundObject;   // the GameObject underfoot
Collider   groundCollider = controller.GroundCollider; // the specific collider

Typical per-frame movement loop

The standard pattern is:
1

Apply acceleration from input

Call Accelerate() with the desired direction and speed. It applies Acceleration * Time.Delta smoothly.
var wishVelocity = Input.AnalogMove.Normal * 200.0f; // world-space move direction
controller.Accelerate( wishVelocity );
2

Apply friction

Slow the character down when no input is given, or always on ground.
if ( controller.IsOnGround )
{
    controller.ApplyFriction( frictionAmount: 8.0f, stopSpeed: 140.0f );
}
3

Apply gravity

Add gravity manually when the character is airborne.
if ( !controller.IsOnGround )
{
    controller.Velocity += Scene.PhysicsWorld.Gravity * Time.Delta;
}
4

Move

Call Move() to slide the capsule through the world, step up small obstacles, and re-detect the ground.
controller.Move();

Full example

using Sandbox;

public class PlayerMovement : Component
{
    [RequireComponent] CharacterController _controller;

    float MoveSpeed = 200.0f;
    float JumpForce = 400.0f;
    float Gravity   = 800.0f;
    float Friction  = 8.0f;

    protected override void OnUpdate()
    {
        // Build wish velocity from input (XY plane only)
        var wishDir = Input.AnalogMove;
        var wishVelocity = WorldRotation * new Vector3( wishDir.x, wishDir.y, 0 ) * MoveSpeed;

        // Ground movement
        if ( _controller.IsOnGround )
        {
            _controller.Accelerate( wishVelocity );
            _controller.ApplyFriction( Friction );

            if ( Input.Pressed( "Jump" ) )
            {
                _controller.Punch( Vector3.Up * JumpForce );
            }
        }
        else
        {
            // Air acceleration (reduced control)
            _controller.Accelerate( wishVelocity * 0.1f );
            _controller.Velocity += Vector3.Down * Gravity * Time.Delta;
        }

        _controller.Move();
    }
}

Accelerate

public void Accelerate( Vector3 vector )
Adds velocity toward vector at a rate of Acceleration * Time.Delta. Calling it repeatedly each frame produces smooth acceleration toward the target velocity rather than a teleport. You do not need to multiply by Time.Delta yourself.
// Move toward camera-relative forward at 200 units/sec
var forward = Scene.Camera.WorldRotation.Forward.WithZ( 0 ).Normal;
controller.Accelerate( forward * 200.0f );

ApplyFriction

public void ApplyFriction( float frictionAmount, float stopSpeed = 140.0f )
Decelerates Velocity by frictionAmount per second. The stopSpeed sets the minimum effective speed for the friction calculation — it prevents the character from wiggling back and forth at near-zero velocity.
// Strong friction on ground, none in air
if ( controller.IsOnGround )
    controller.ApplyFriction( 8.0f );

Jumping with Punch

public void Punch( in Vector3 amount )
Punch clears ground state and adds the given vector directly to Velocity. It is the canonical way to make a character leave the ground.
// Standard jump
controller.Punch( Vector3.Up * 400.0f );

// Jump-pad — add forward velocity as well
controller.Punch( Vector3.Up * 600.0f + forward * 300.0f );
Punch sets IsOnGround = false immediately. If you call it in the same frame as Move(), the character will be treated as airborne for that frame, preventing repeated jumps.

Ground detection

IsOnGround is updated automatically by Move() every frame. The controller probes StepHeight downward to decide whether to snap to the floor. A surface is considered ground if its angle to Vector3.Up is less than GroundAngle.
if ( controller.IsOnGround )
{
    // Character is on walkable ground
}
else
{
    // Character is airborne
}

Manual direction trace

Use TraceDirection to probe the collision volume in an arbitrary direction without moving the character. This is useful for ledge detection, ceiling checks, or ladder detection.
// Check for a ceiling above
var ceiling = controller.TraceDirection( Vector3.Up * 10.0f );
if ( ceiling.Hit )
{
    // Hit a ceiling — kill upward velocity
    _controller.Velocity = _controller.Velocity.WithZ( 0 );
}

// Check for a wall ahead
var wall = controller.TraceDirection( wishDirection * 4.0f );
if ( wall.Hit && !controller.IsOnGround )
{
    // Wall-running logic here
}

Moving to a target position

MoveTo slides the capsule from its current position to targetPosition in one call. Useful for scripted movement, ladders, or mount animations.
// Move to a target point, stepping over small obstacles
controller.MoveTo( ladderTopPosition, useStep: true );

Collision filtering

By default the controller ignores its own GameObject hierarchy and respects IgnoreLayers. Switch to the project’s collision rule table by enabling UseCollisionRules.
// Ignore objects tagged "trigger" and "noclip"
controller.IgnoreLayers = new TagSet( "trigger", "noclip" );
controller.UseCollisionRules = false;

Common patterns

Crouching

Change Height at runtime to shrink the capsule. The controller adjusts automatically on the next Move() call.
bool _crouching = false;

void ToggleCrouch()
{
    _crouching = !_crouching;
    controller.Height = _crouching ? 32.0f : 64.0f;
}
When standing back up, always check for headroom first using TraceDirection( Vector3.Up * (fullHeight - crouchHeight) ) before restoring Height. If there is no room, keep the character crouched.

Slope-based speed reduction

if ( controller.IsOnGround && controller.GroundCollider is not null )
{
    var tr = controller.TraceDirection( Vector3.Down * 4.0f );
    if ( tr.Hit )
    {
        var slopeAngle = tr.Normal.Angle( Vector3.Up );
        var speedScale = 1.0f - (slopeAngle / controller.GroundAngle) * 0.5f;
        controller.Velocity *= speedScale;
    }
}

Preventing bunny-hopping

Zero out vertical velocity when landing to stop accumulated jump velocity:
bool _wasOnGround;

protected override void OnUpdate()
{
    bool justLanded = !_wasOnGround && controller.IsOnGround;

    if ( justLanded )
    {
        controller.Velocity = controller.Velocity.WithZ( 0 );
    }

    _wasOnGround = controller.IsOnGround;
}

Build docs developers (and LLMs) love