use super::{ControllerSet, Controls, collisions::kinematic_controller_collisions}; use crate::{GameState, player::PlayerBodyMesh}; use avian3d::{math::*, prelude::*}; use bevy::{ecs::query::Has, prelude::*}; pub struct CharacterControllerPlugin; impl Plugin for CharacterControllerPlugin { fn build(&self, app: &mut App) { app.init_resource::(); app.register_type::(); app.register_type::(); app.register_type::(); app.add_systems( Update, (set_movement_flag, update_grounded, apply_gravity, movement) .chain() .in_set(ControllerSet::ApplyControls) .run_if(in_state(GameState::Playing)), ); app.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.run_if(in_state(GameState::Playing)), ); } } /// 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; #[derive(Resource, Default)] pub struct PlayerMovement { pub any_direction: bool, } /// The acceleration used for character movement. #[derive(Component, Reflect)] #[reflect(Component)] pub struct MovementAcceleration(Scalar); #[derive(Component, Reflect)] #[reflect(Component)] pub struct MovementSpeedFactor(pub f32); /// The strength of a jump. #[derive(Component, Reflect)] #[reflect(Component)] pub struct JumpImpulse(Scalar); /// The gravitational acceleration used for a character controller. #[derive(Component, Reflect)] #[reflect(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, Reflect)] #[reflect(Component)] pub struct MaxSlopeAngle(pub 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, jump_impulse: JumpImpulse, max_slope_angle: MaxSlopeAngle, factor: MovementSpeedFactor, } impl MovementBundle { pub const fn new(acceleration: Scalar, jump_impulse: Scalar, max_slope_angle: Scalar) -> Self { Self { acceleration: MovementAcceleration(acceleration), jump_impulse: JumpImpulse(jump_impulse), max_slope_angle: MaxSlopeAngle(max_slope_angle), factor: MovementSpeedFactor(1.), } } } impl CharacterControllerBundle { pub fn new(collider: Collider, gravity: Vector, movement: MovementBundle) -> Self { // Create shape caster as a slightly smaller version of collider let mut caster_shape = collider.clone(); caster_shape.set_scale(Vector::ONE * 0.98, 10); Self { character_controller: CharacterController, rigid_body: RigidBody::Kinematic, collider, ground_caster: ShapeCaster::new( caster_shape, Vector::ZERO, Quaternion::default(), Dir3::NEG_Y, ) .with_max_distance(0.2), gravity: ControllerGravity(gravity), movement, } } } /// 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 { debug!("grounded"); commands.entity(entity).insert(Grounded); } else { debug!("not grounded"); commands.entity(entity).remove::(); } } } /// Sets the movement flag, which is an indicator for the rig animation and the braking system. fn set_movement_flag(mut player_movement: ResMut, controls: Res) { let mut direction = controls.keyboard_state.move_dir; let deadzone = 0.2; if let Some(gamepad) = controls.gamepad_state { direction += gamepad.move_dir; } if player_movement.any_direction { if direction.length_squared() < deadzone { player_movement.any_direction = false; } } else if direction.length_squared() > deadzone { player_movement.any_direction = true; } } /// Responds to [`MovementAction`] events and moves character controllers accordingly. fn movement( time: Res