Skip to main content
CharacterController moves a capsule collider through the scene using trace-based collision. It does not use forces or a Rigidbody — movement only happens when you call Move() or MoveTo(). This makes it predictable and easy to drive from player input.
CharacterController is not affected by physics forces. Use Punch to add a velocity impulse such as a jump, and manage gravity yourself by decreasing Velocity.z each frame.

Shape properties

Radius
float
default:"16"
Radius of the character capsule in world units. Range 0–200.
Height
float
default:"64"
Total height of the character capsule in world units. Range 0–200.
StepHeight
float
default:"18"
Maximum height of a step the character can climb without losing speed. Range 0–50.

Movement properties

GroundAngle
float
default:"45"
Maximum slope angle in degrees that is considered walkable ground. Steeper surfaces are treated as walls. Range 0–90.
Acceleration
float
default:"10"
Rate at which Accelerate() changes velocity (units per second per second). Range 0–64.
Bounciness
float
default:"0.3"
How much velocity is preserved when the character hits a wall while airborne. 0 stops dead on impact; 1 reflects fully. Range 0–1.

Collision properties

UseCollisionRules
bool
default:"false"
When true, the controller uses the project’s collision rule matrix (based on the GameObject’s tags) to determine what to collide with, instead of the IgnoreLayers tag set.
IgnoreLayers
TagSet
Tags to ignore during collision traces. Only used when UseCollisionRules is false.

State

Velocity
Vector3
Current velocity of the character in world space. Readable and writable. Synced over the network.
IsOnGround
bool
true when the character is standing on a surface within StepHeight below its feet. Synced over the network.
GroundObject
GameObject
The GameObject the character is currently standing on, or null if airborne.
GroundCollider
Collider
The specific Collider the character is standing on, or null if airborne.
BoundingBox
BBox
The bounding box of the character capsule in local space, derived from Radius and Height.

Methods

MethodDescription
Move()Moves the character by its current Velocity for this frame, handling step-up, sliding, and ground detection. Call once per OnFixedUpdate.
MoveTo(Vector3 targetPosition, bool useStep)Moves directly toward a target position using trace and sliding. Good for ladders or special movement modes.
Accelerate(Vector3 vector)Adds acceleration toward vector at the rate set by Acceleration. Time-delta is applied internally.
ApplyFriction(float amount, float stopSpeed)Bleeds off speed proportional to amount. Stops the character if speed drops below stopSpeed.
Punch(Vector3 amount)Clears ground state and adds amount directly to Velocity. Use for jumps or knockback.
TraceDirection(Vector3 direction)Runs a collision trace from the current position in direction. Returns a SceneTraceResult.

Complete FPS movement example

The component below reads WASD and mouse input to move and look, handles jumping, and applies gravity. Attach it to a GameObject alongside a CameraComponent on a child object.
using Sandbox;

public sealed class FpsController : Component
{
    [Property] public float MoveSpeed { get; set; } = 200f;
    [Property] public float JumpStrength { get; set; } = 320f;
    [Property] public GameObject Eye { get; set; }

    CharacterController _cc;
    float _eyePitch;

    protected override void OnStart()
    {
        _cc = Components.Get<CharacterController>();
    }

    protected override void OnUpdate()
    {
        // Mouse look
        var mouseDelta = Input.MouseDelta;
        _eyePitch = MathX.Clamp( _eyePitch - mouseDelta.y * 0.1f, -89f, 89f );
        Eye.LocalRotation = Rotation.FromPitch( _eyePitch );
        WorldRotation = Rotation.FromYaw( WorldRotation.Yaw() + mouseDelta.x * 0.1f );
    }

    protected override void OnFixedUpdate()
    {
        if ( _cc is null ) return;

        // Build a wish direction from WASD input
        var wishDir = new Vector3(
            Input.AnalogMove.x,
            Input.AnalogMove.y,
            0f
        ) * WorldRotation;

        // Apply friction when on the ground
        if ( _cc.IsOnGround )
        {
            _cc.ApplyFriction( 6f );
            _cc.Accelerate( wishDir * MoveSpeed );

            // Jump
            if ( Input.Pressed( "Jump" ) )
                _cc.Punch( Vector3.Up * JumpStrength );
        }
        else
        {
            // Air movement — reduced control
            _cc.Accelerate( wishDir * MoveSpeed * 0.2f );

            // Gravity
            _cc.Velocity -= Vector3.Up * 800f * Time.Delta;
        }

        _cc.Move();
    }
}
Call Move() inside OnFixedUpdate rather than OnUpdate so movement integrates at a fixed time step and stays consistent across different frame rates.

Build docs developers (and LLMs) love