use bevy::{ecs::query::Has, prelude::*}; use bevy_xpbd_3d::{math::*, prelude::*}; pub struct CharacterControllerPlugin; impl Plugin for CharacterControllerPlugin { fn build(&self, app: &mut App) { app.add_event::().add_systems( Update, ( keyboard_input, gamepad_input, update_grounded, movement, apply_movement_damping, ) .chain(), ); } } /// An event sent for a movement input action. #[derive(Event)] pub enum MovementAction { Move(Vector2), Jump, } /// A marker component indicating that an entity is using a character controller. #[derive(Component)] pub struct CharacterController; /// A marker component indicating that an entity is on the ground. #[derive(Component)] #[component(storage = "SparseSet")] pub struct Grounded; /// The acceleration used for character movement. #[derive(Component)] pub struct MovementAcceleration(Scalar); /// The damping factor used for slowing down movement. #[derive(Component)] pub struct MovementDampingFactor(Scalar); /// The strength of a jump. #[derive(Component)] pub struct JumpImpulse(Scalar); /// The maximum angle a slope can have for a character controller /// to be able to climb and jump. If the slope is steeper than this angle, /// the character will slide down. #[derive(Component)] pub struct MaxSlopeAngle(Scalar); /// A bundle that contains the components needed for a basic /// kinematic character controller. #[derive(Bundle)] pub struct CharacterControllerBundle { character_controller: CharacterController, rigid_body: RigidBody, collider: Collider, ground_caster: ShapeCaster, locked_axes: LockedAxes, movement: MovementBundle, } /// A bundle that contains components for character movement. #[derive(Bundle)] pub struct MovementBundle { acceleration: MovementAcceleration, damping: MovementDampingFactor, jump_impulse: JumpImpulse, max_slope_angle: MaxSlopeAngle, } impl MovementBundle { pub const fn new( acceleration: Scalar, damping: Scalar, jump_impulse: Scalar, max_slope_angle: Scalar, ) -> Self { Self { acceleration: MovementAcceleration(acceleration), damping: MovementDampingFactor(damping), jump_impulse: JumpImpulse(jump_impulse), max_slope_angle: MaxSlopeAngle(max_slope_angle), } } } impl Default for MovementBundle { fn default() -> Self { Self::new(30.0, 0.9, 7.0, PI * 0.45) } } impl CharacterControllerBundle { pub fn new(collider: Collider) -> Self { // Create shape caster as a slightly smaller version of collider let mut caster_shape = collider.clone(); caster_shape.set_scale(Vector::ONE * 0.99, 10); Self { character_controller: CharacterController, rigid_body: RigidBody::Dynamic, collider, ground_caster: ShapeCaster::new( caster_shape, Vector::ZERO, Quaternion::default(), Dir3::NEG_Y, ) .with_max_time_of_impact(0.2), locked_axes: LockedAxes::ROTATION_LOCKED, movement: MovementBundle::default(), } } pub fn with_movement( mut self, acceleration: Scalar, damping: Scalar, jump_impulse: Scalar, max_slope_angle: Scalar, ) -> Self { self.movement = MovementBundle::new(acceleration, damping, jump_impulse, max_slope_angle); self } } /// Sends [`MovementAction`] events based on keyboard input. fn keyboard_input( mut movement_event_writer: EventWriter, keyboard_input: Res>, ) { let up = keyboard_input.any_pressed([KeyCode::KeyW, KeyCode::ArrowUp]); let down = keyboard_input.any_pressed([KeyCode::KeyS, KeyCode::ArrowDown]); let left = keyboard_input.any_pressed([KeyCode::KeyA, KeyCode::ArrowLeft]); let right = keyboard_input.any_pressed([KeyCode::KeyD, KeyCode::ArrowRight]); let horizontal = right as i8 - left as i8; let vertical = up as i8 - down as i8; let direction = Vector2::new(horizontal as Scalar, vertical as Scalar).clamp_length_max(1.0); if direction != Vector2::ZERO { movement_event_writer.send(MovementAction::Move(direction)); } if keyboard_input.just_pressed(KeyCode::Space) { movement_event_writer.send(MovementAction::Jump); } } /// Sends [`MovementAction`] events based on gamepad input. fn gamepad_input( mut movement_event_writer: EventWriter, gamepads: Res, axes: Res>, buttons: Res>, ) { for gamepad in gamepads.iter() { let axis_lx = GamepadAxis { gamepad, axis_type: GamepadAxisType::LeftStickX, }; let axis_ly = GamepadAxis { gamepad, axis_type: GamepadAxisType::LeftStickY, }; if let (Some(x), Some(y)) = (axes.get(axis_lx), axes.get(axis_ly)) { movement_event_writer.send(MovementAction::Move( Vector2::new(x as Scalar, y as Scalar).clamp_length_max(1.0), )); } let jump_button = GamepadButton { gamepad, button_type: GamepadButtonType::South, }; if buttons.just_pressed(jump_button) { movement_event_writer.send(MovementAction::Jump); } } } /// Updates the [`Grounded`] status for character controllers. fn update_grounded( mut commands: Commands, mut query: Query< (Entity, &ShapeHits, &Rotation, Option<&MaxSlopeAngle>), With, >, ) { for (entity, hits, rotation, max_slope_angle) in &mut query { // The character is grounded if the shape caster has a hit with a normal // that isn't too steep. let is_grounded = hits.iter().any(|hit| { if let Some(angle) = max_slope_angle { rotation.rotate(-hit.normal2).angle_between(Vector::Y).abs() <= angle.0 } else { true } }); if is_grounded { commands.entity(entity).insert(Grounded); } else { commands.entity(entity).remove::(); } } } /// Responds to [`MovementAction`] events and moves character controllers accordingly. fn movement( time: Res