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.
| Property | Default | Description |
|---|
Radius | 16 | Capsule radius in world units. |
Height | 64 | Capsule height in world units. |
StepHeight | 18 | Maximum step the controller can climb without jumping. |
GroundAngle | 45° | Maximum slope angle considered walkable ground. |
Acceleration | 10 | Acceleration rate applied by Accelerate(). Scaled by Time.Delta internally. |
Bounciness | 0.3 | How much velocity is preserved when running into a wall (0 = stop dead, 1 = full bounce). |
UseCollisionRules | false | When 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:
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 );
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 );
}
Apply gravity
Add gravity manually when the character is airborne.if ( !controller.IsOnGround )
{
controller.Velocity += Scene.PhysicsWorld.Gravity * Time.Delta;
}
Move
Call Move() to slide the capsule through the world, step up small obstacles, and re-detect the ground.
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.
Tag-based ignore list
Project collision rules
// Ignore objects tagged "trigger" and "noclip"
controller.IgnoreLayers = new TagSet( "trigger", "noclip" );
controller.UseCollisionRules = false;
// Use the collision rule table defined in Project Settings
controller.UseCollisionRules = true;
// Tags on the owning GameObject determine which layers to collide with
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;
}