use super::{ControllerSet, ControllerSwitchEvent}; use crate::{ GameState, abilities::TriggerStateRes, animation::AnimationFlags, character::HasCharacterAnimations, control::{Controls, SelectedController, controls::ControllerSettings}, heads_database::HeadControls, player::{Player, PlayerBodyMesh}, }; use avian3d::{math::*, prelude::*}; use bevy::prelude::*; use happy_feet::prelude::{ Character, CharacterDrag, CharacterGravity, CharacterMovement, CharacterPlugins, GroundFriction, Grounding, GroundingConfig, KinematicVelocity, MoveInput, SteppingBehaviour, SteppingConfig, }; pub fn plugin(app: &mut App) { app.add_plugins(CharacterPlugins::default()); app.register_type::(); app.add_systems( PreUpdate, reset_upon_switch .run_if(in_state(GameState::Playing)) .before(ControllerSet::ApplyControlsRun) .before(ControllerSet::ApplyControlsFly), ); app.add_systems( PreUpdate, set_animation_flags .run_if(in_state(GameState::Playing)) .after(ControllerSet::ApplyControlsRun) .after(ControllerSet::ApplyControlsFly), ); app.add_systems( FixedPreUpdate, decelerate.run_if(in_state(GameState::Playing)), ); } fn set_animation_flags( mut flags: Query<&mut AnimationFlags>, controls: Res, trigger: Res, player: Single<(&Grounding, &HasCharacterAnimations), With>, ) { let mut direction = controls.keyboard_state.move_dir; let deadzone = 0.2; let (grounding, has_anims) = *player; let mut flags = flags.get_mut(*has_anims.collection()).unwrap(); if let Some(gamepad) = controls.gamepad_state { direction += gamepad.move_dir; } if flags.any_direction { if direction.length_squared() < deadzone { flags.any_direction = false; } } else if direction.length_squared() > deadzone { flags.any_direction = true; } if flags.shooting != trigger.is_active() { flags.shooting = trigger.is_active(); } // `apply_controls` sets the jump flag when the player actually jumps. // Unset the flag on hitting the ground if grounding.is_grounded() { flags.jumping = false; } } /// Reset the pitch and velocity of the character if the controller was switched. pub fn reset_upon_switch( mut c: Commands, mut event_controller_switch: EventReader, controller: Res, mut rig_transform_q: Option>>, mut velocity: Single<&mut KinematicVelocity, With>, character: Single>, ) { for _ in event_controller_switch.read() { velocity.0 = Vec3::ZERO; // Reset pitch but keep yaw the same if let Some(ref mut rig_transform) = rig_transform_q { let euler_rot = rig_transform.rotation.to_euler(EulerRot::YXZ); let yaw = euler_rot.0; rig_transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, 0.0, 0.0); } match controller.0 { ControllerSet::ApplyControlsFly => { c.entity(*character).insert(FLYING_MOVEMENT_CONFIG); } ControllerSet::ApplyControlsRun => { c.entity(*character).insert(RUNNING_MOVEMENT_CONFIG); } _ => unreachable!(), } } } /// Decelerates the player in the directions of "undesired velocity"; velocity that is not aligned /// with the movement input direction. This makes it quicker to reverse direction, and prevents /// sliding around, even with low friction, without slowing down the player globally like high /// friction or drag would. fn decelerate( mut character: Query<( &mut KinematicVelocity, &MoveInput, Option<&Grounding>, &ControllerSettings, )>, ) { for (mut velocity, input, grounding, settings) in character.iter_mut() { let direction = input.value.normalize(); let ground_normal = grounding .and_then(|it| it.normal()) .unwrap_or(Dir3::Y) .as_vec3(); let velocity_within_90_degrees = direction.dot(velocity.0) > 0.0; let desired_velocity = if direction != Vec3::ZERO && velocity_within_90_degrees { // project velocity onto direction to extract the component directly aligned with direction velocity.0.project_onto(direction) } else { // if velocity isn't within 90 degrees of direction then the projection would be in the // exact opposite direction of `direction`; so just zero it Vec3::ZERO }; let undesired_velocity = velocity.0 - desired_velocity; let vertical_undesired_velocity = undesired_velocity.project_onto(ground_normal); // only select the velocity along the ground plane; that way the character can't decelerate // while falling or jumping, but will decelerate along slopes properly let undesired_velocity = undesired_velocity - vertical_undesired_velocity; let deceleration = Vec3::ZERO.move_towards(undesired_velocity, settings.deceleration_factor); velocity.0 -= deceleration; } } #[derive(Component, Reflect)] #[reflect(Component)] pub struct MovementSpeedFactor(pub f32); /// A bundle that contains the components needed for a basic /// kinematic character controller. #[derive(Bundle)] pub struct CharacterControllerBundle { character_controller: Character, collider: Collider, move_input: MoveInput, movement_factor: MovementSpeedFactor, collision_events: CollisionEventsEnabled, movement_config: MovementConfig, interpolation: TransformInterpolation, } impl CharacterControllerBundle { pub fn new(collider: Collider, controls: HeadControls) -> 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); let config = match controls { HeadControls::Plane => FLYING_MOVEMENT_CONFIG, HeadControls::Walk => RUNNING_MOVEMENT_CONFIG, }; Self { character_controller: Character, collider, move_input: MoveInput::default(), movement_factor: MovementSpeedFactor(1.0), collision_events: CollisionEventsEnabled, movement_config: config, interpolation: TransformInterpolation, } } } #[derive(Bundle)] struct MovementConfig { movement: CharacterMovement, step: SteppingConfig, ground: GroundingConfig, gravity: CharacterGravity, friction: GroundFriction, drag: CharacterDrag, settings: ControllerSettings, } const RUNNING_MOVEMENT_CONFIG: MovementConfig = MovementConfig { movement: CharacterMovement { target_speed: 15.0, acceleration: 40.0, }, step: SteppingConfig { max_vertical: 0.25, max_horizontal: 0.4, behaviour: SteppingBehaviour::Grounded, max_substeps: 8, }, ground: GroundingConfig { max_angle: PI / 4.0, max_distance: 0.2, snap_to_surface: true, up_direction: Dir3::Y, max_iterations: 2, }, gravity: CharacterGravity(Some(vec3(0.0, -60.0, 0.0))), friction: GroundFriction(10.0), drag: CharacterDrag(0.0), settings: ControllerSettings { jump_force: 25.0, deceleration_factor: 1.0, }, }; const FLYING_MOVEMENT_CONFIG: MovementConfig = MovementConfig { movement: CharacterMovement { target_speed: 20.0, acceleration: 300.0, }, step: SteppingConfig { max_vertical: 0.25, max_horizontal: 0.4, behaviour: SteppingBehaviour::Never, max_substeps: 8, }, ground: GroundingConfig { max_angle: 0.0, max_distance: -1.0, snap_to_surface: false, up_direction: Dir3::Y, max_iterations: 2, }, gravity: CharacterGravity(Some(Vec3::ZERO)), friction: GroundFriction(0.0), drag: CharacterDrag(10.0), settings: ControllerSettings { jump_force: 0.0, deceleration_factor: 0.0, }, };