Character Controllers
A character controller is a kinematic body with a custom contact resolver on top. The constraint solver is what makes a stack of boxes stable, and it is also what makes a player character feel mushy when standing on those boxes. Character controllers bypass it. Movement happens in code, contacts are resolved by sweeping and sliding the capsule against the world, and the result is movement that snaps to walls without bouncing off them.
First-Person Player
The fastest way to get a working player is spawn_first_person_player. It creates the controller entity, a child camera at eye height, and wires up input.
#![allow(unused)] fn main() { use nightshade::ecs::physics::commands::spawn_first_person_player; use nightshade::ecs::physics::character_controller::character_controller_input_system; fn initialize(&mut self, world: &mut World) { let (player_entity, camera_entity) = spawn_first_person_player( world, Vec3::new(0.0, 2.0, 0.0), ); self.player = Some(player_entity); if let Some(controller) = world.core.get_character_controller_mut(player_entity) { controller.max_speed = 5.0; controller.is_sprinting = false; controller.jump_impulse = 6.0; } } }
max_speed is in meters per second. jump_impulse is the instantaneous upward velocity applied when the jump input fires.
Custom Character Controller
For third-person, NPCs, or anything that needs custom dimensions, build the entity by hand.
#![allow(unused)] fn main() { fn spawn_character(world: &mut World, position: Vec3) -> Entity { let entity = world.spawn_entities( NAME | LOCAL_TRANSFORM | GLOBAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | CHARACTER_CONTROLLER, 1, )[0]; world.core.set_name(entity, Name("Player".to_string())); world.core.set_local_transform(entity, LocalTransform { translation: position, ..Default::default() }); if let Some(controller) = world.core.get_character_controller_mut(entity) { *controller = CharacterControllerComponent::new_capsule(0.5, 0.3); controller.max_speed = 3.0; controller.acceleration = 15.0; controller.jump_impulse = 4.0; controller.is_sprinting = false; controller.is_crouching = false; } entity } }
new_capsule(0.5, 0.3) is half-height 0.5 and radius 0.3. The full character is 1 meter tall and 0.6 meters wide, roughly humanoid proportions.
Controller Properties
| Property | Description | Default |
|---|---|---|
max_speed | Walking speed | 5.0 |
is_sprinting | Sprint active | false |
acceleration | Speed up rate | 20.0 |
jump_impulse | Jump strength | 5.0 |
can_jump | Allow jumping | true |
is_crouching | Crouch active | false |
Movement Input
The built-in character_controller_input_system(world) reads WASD, space, shift, and control, and writes the resulting motion into the controller each frame. Call it from run_systems.
#![allow(unused)] fn main() { fn run_systems(&mut self, world: &mut World) { character_controller_input_system(world); } }
Skip this call and the controller will only move when something else writes to it. That is the right choice when input comes from a network packet or an AI script.
Ground Detection
The controller flags whether the capsule is currently in contact with a surface below it.
#![allow(unused)] fn main() { if let Some(controller) = world.core.get_character_controller(player) { if controller.grounded { // On ground - can jump } else { // In air } } }
Use this for jump gating, footstep timing, and switching between ground and air movement.
Slope Handling
The controller has two slope angles. Anything below max_slope_climb_angle is walkable. Anything above min_slope_slide_angle will slide the character down. Values are radians.
#![allow(unused)] fn main() { if let Some(controller) = world.core.get_character_controller_mut(player) { controller.config.max_slope_climb_angle = 0.8; controller.config.min_slope_slide_angle = 0.5; } }
0.8 rad is about 45 degrees. 0.5 rad is about 30 degrees. Tune for the world's geometry.
Camera Integration
First-Person Camera
The camera is a child entity of the player, offset to eye height.
#![allow(unused)] fn main() { let camera = world.spawn_entities( LOCAL_TRANSFORM | GLOBAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | CAMERA | PARENT, 1 )[0]; world.core.set_parent(camera, Parent(Some(player))); world.core.set_local_transform(camera, LocalTransform { translation: Vec3::new(0.0, 0.8, 0.0), ..Default::default() }); world.resources.active_camera = Some(camera); }
Parenting means the transform hierarchy handles position. Mouse look writes to the camera's local rotation, body rotation goes on the player.
Third-Person Camera
Third-person follows the character with an offset and looks at a target point on or near the body.
#![allow(unused)] fn main() { fn third_person_camera_system(world: &mut World, player: Entity, camera: Entity) { let Some(player_pos) = world.core.get_local_transform(player).map(|t| t.translation) else { return; }; let offset = Vec3::new(0.0, 3.0, 8.0); let target_pos = player_pos + offset; if let Some(pan_orbit) = world.core.get_pan_orbit_camera_mut(camera) { pan_orbit.target_focus = player_pos + Vec3::new(0.0, 1.0, 0.0); } } }
Step Climbing
Automatic step climbing lets the controller walk over small ledges without jumping. The two parameters bound what counts as a step.
#![allow(unused)] fn main() { if let Some(controller) = world.core.get_character_controller_mut(player) { controller.config.autostep_max_height = Some(0.3); controller.config.autostep_min_width = Some(0.2); } }
30 cm is roughly a typical stair height. Anything taller than autostep_max_height is treated as a wall.
Interaction Cooldowns
Player actions that fire on a button press need cooldowns so a long key press does not fire the action every frame.
#![allow(unused)] fn main() { struct PlayerState { interaction_cooldown: f32, } fn update_cooldown(state: &mut PlayerState, dt: f32) { state.interaction_cooldown = (state.interaction_cooldown - dt).max(0.0); } fn can_interact(state: &PlayerState) -> bool { state.interaction_cooldown <= 0.0 } fn set_cooldown(state: &mut PlayerState, duration: f32) { state.interaction_cooldown = duration; } }
A just_pressed check on the input also works and is simpler. Use cooldowns when the action should repeat at a fixed rate while the key is held, like an auto-fire weapon.