Character Controllers
Build responsive character movement with custom controllers.
Introduction
Character controllers provide player movement that feels responsive. This guide covers kinematic controllers, ground detection, jumping, and slope handling.
Guide 12 of 25
Screenshot
Step-by-Step Guide
Use a kinematic controller
Instead of a rigidbody, use a kinematic character controller for full control without physics jitter.
Set up a capsule collider
Characters use capsules — they slide smoothly over terrain and steps. Radius 0.3-0.5m, height 1.8-2.0m.
Implement ground detection
Raycast downward to check if grounded. Set isGrounded based on raycast hit distance (< 0.1m).
Add movement smoothing
Lerp horizontal velocity towards target speed for smooth acceleration/deceleration.
Implement jumping
Apply upward velocity on jump input. Use variable jump height: hold = higher, release = shorter.
Handle slopes and stairs
Configure slope limit (45-60 degrees) and step offset (0.3-0.5m). Controller slides on steep slopes, climbs stairs.
FluxScript Example
Here is a complete FluxScript implementation for this guide:
on start {
player = scene.find("Player")
player.setCharacterController({
radius: 0.4, height: 1.8,
slopeLimit: 55, stepOffset: 0.4
})
velocity = vec3(0, 0, 0)
moveSpeed = 8
jumpForce = 12
}
on update(dt) {
player = scene.find("Player")
hit = physics.raycast(player.position, vec3(0, -1, 0), 0.15)
isGrounded = hit != null
moveDir = vec3(0, 0, 0)
if input("w") moveDir += forward
if input("s") moveDir -= forward
if input("a") moveDir -= right
if input("d") moveDir += right
if math.length(moveDir) > 0 moveDir = math.normalize(moveDir)
targetVel = moveDir * moveSpeed
velocity.x = math.lerp(velocity.x, targetVel.x, 10 * dt)
velocity.z = math.lerp(velocity.z, targetVel.z, 10 * dt)
velocity.y -= 25 * dt
if input(" ") && isGrounded {
velocity.y = jumpForce
audio.play("jump")
}
if !input(" ") && velocity.y > 5 velocity.y *= 0.5
player.move(velocity * dt)
if math.length(moveDir) > 0.1 {
targetAngle = math.atan2(moveDir.x, moveDir.z)
player.setRotation(0, math.lerpAngle(player.rotation.y, targetAngle, 10 * dt), 0)
}
}Node Graph Example
Prefer visual programming? Here is the equivalent Node Graph:
[On Start] --> [Set Character Controller: capsule, slope=55]
[On Update] --> [Raycast down] --> [Set: isGrounded]
--> [Get Input W/A/S/D] --> [Normalize moveDir]
--> [Lerp: velocity towards target] --> [Apply Gravity]
--> [Branch: jump + grounded] --> [Set: velocity.y]
--> [Player.Move] --> [Lerp Angle: face direction]Optimization Tips
Keep your implementation fast and efficient with these optimization tips:
- Use kinematic controllers instead of rigidbodies for characters. Rigidbodies cause jitter on stairs and slopes.
- Cache the raycast result for ground detection. For 50+ NPCs, cache and update every 3-5 frames.
- Use variable jump height. Holding the button longer = higher jump. Gives players precise control.
- Apply acceleration via lerp (factor 8-12). Instant velocity changes feel robotic; lerped feels smooth.
Common Mistakes
Using a rigidbody for the player
Rigidbodies cause jitter on stairs, get stuck on edges, feel floaty. Use kinematic controllers.
No coyote time
Allow jumping 0.1-0.2s after walking off a ledge. Without it, jumps feel unresponsive at edges.
