Character Controllers
Character controllers provide smooth player movement with collision handling, slopes, and stairs.
First-Person Player
The easiest way to get started:
#![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; } } }
Custom Character Controller
For third-person or specialized characters:
#![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 } }
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) handles WASD movement, jumping, sprinting, and crouching automatically. Call it each frame:
#![allow(unused)] fn main() { fn run_systems(&mut self, world: &mut World) { character_controller_input_system(world); } }
Ground Detection
Check if the character is grounded:
#![allow(unused)] fn main() { if let Some(controller) = world.core.get_character_controller(player) { if controller.grounded { // On ground - can jump } else { // In air } } }
Slope Handling
Controllers automatically handle slopes:
#![allow(unused)] fn main() { if let Some(controller) = world.core.get_character_controller_mut(player) { controller.config.max_slope_climb_angle = 0.8; // ~45 degrees in radians controller.config.min_slope_slide_angle = 0.5; // ~30 degrees } }
Camera Integration
First-Person Camera
The camera is a child of the player:
#![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), // Eye height ..Default::default() }); world.resources.active_camera = Some(camera); }
Third-Person Camera
Follow the character with an offset:
#![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; }; // Camera behind and above player 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
Controllers can automatically climb small steps:
#![allow(unused)] fn main() { if let Some(controller) = world.core.get_character_controller_mut(player) { controller.config.autostep_max_height = Some(0.3); // Can climb 30cm steps controller.config.autostep_min_width = Some(0.2); // Minimum step width } }
Interaction Cooldowns
Prevent rapid repeated actions:
#![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; } }