use avian2d::{math::*, prelude::*}; use bevy::{ecs::query::Has, 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, apply_gravity, movement, apply_movement_damping, ) .chain(), ) .add_systems( // Run collision handling after collision detection. // // NOTE: The collision implementation here is very basic and a bit buggy. // A collide-and-slide algorithm would likely work better. PostProcessCollisions, kinematic_controller_collisions, ); } } /// An event sent for a movement input action. #[derive(Event)] pub enum MovementAction { Move(Scalar), 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 gravitational acceleration used for a character controller. #[derive(Component)] pub struct ControllerGravity(Vector); /// 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, gravity: ControllerGravity, 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, gravity: Vector) -> 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::Kinematic, collider, ground_caster: ShapeCaster::new(caster_shape, Vector::ZERO, 0.0, Dir2::NEG_Y) .with_max_time_of_impact(10.0), gravity: ControllerGravity(gravity), 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 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 direction = horizontal as Scalar; if direction != 0.0 { 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, }; if let Some(x) = axes.get(axis_lx) { movement_event_writer.send(MovementAction::Move(x as Scalar)); } 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 * -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