Skip to content

Character Controllers

Kinematic character controllers for predictable game feel.

Kinematic vs Dynamic

AspectKinematicDynamic
MovementCode-calculated velocityPhysics-simulated
FeelPredictable, responsiveFloaty, realistic
TuningDirect: change numbersIndirect: mass, friction
Use casePlatformers, action gamesRagdolls, realistic sims

Frond is kinematic-first. Use physics for collision detection, not movement.

Basic Structure

rust
struct KinematicController {
    velocity: Vec3,
    grounded: bool,
    config: MovementConfig,
}

struct MovementConfig {
    gravity: f32,
    max_speed: f32,
    acceleration: f32,
    friction: f32,
    jump_force: f32,
}

Movement Loop

rust
impl KinematicController {
    fn update(&mut self, input: &Input, dt: f32) {
        // Apply gravity
        if !self.grounded {
            self.velocity.y -= self.config.gravity * dt;
        }

        // Apply input
        let wish_dir = input.movement.normalize_or_zero();
        self.velocity.x = wish_dir.x * self.config.max_speed;
        self.velocity.z = wish_dir.z * self.config.max_speed;

        // Friction when grounded
        if self.grounded && input.movement.length() < 0.1 {
            self.velocity.x *= self.config.friction;
            self.velocity.z *= self.config.friction;
        }
    }

    fn move_and_collide(&mut self, physics: &mut Physics, dt: f32) {
        let displacement = self.velocity * dt;
        let result = physics.move_shape(displacement);

        self.grounded = result.grounded;
        self.velocity = result.velocity;
    }
}

Warframe-Style Movement

High-mobility movement uses chained states:

rust
enum MovementState {
    Grounded,
    Slide,       // crouch while running
    BulletJump,  // crouch + jump while sliding
    AimGlide,    // hold aim in air
    WallRun,     // contact wall while airborne
    DoubleJump,  // jump in air
}

Each state has:

  • Entry impulse - BulletJump applies large upward + forward force
  • Persistent effect - AimGlide reduces gravity
  • Chaining rules - Can double jump after wall hop but not after bullet jump

With Rapier

rust
use bevy_rapier3d::prelude::*;

fn kinematic_move(
    mut query: Query<(&mut KinematicController, &mut KinematicCharacterController)>,
    physics: Res<RapierContext>,
) {
    for (mut ctrl, mut kcc) in &mut query {
        kcc.translation = Some(ctrl.velocity * time.delta_seconds());
    }
}