use avian3d::{math::*, prelude::*}; use bevy::prelude::*; use happy_feet::KinematicVelocity; use happy_feet::ground::{Grounding, GroundingConfig}; use happy_feet::prelude::{ Character, CharacterDrag, CharacterFriction, CharacterGravity, CharacterMovement, CharacterPlugin, MoveInput, SteppingBehaviour, SteppingConfig, }; use crate::GameState; use crate::control::SelectedController; use crate::control::controls::ControllerSettings; use crate::heads_database::HeadControls; use crate::player::PlayerBodyMesh; use super::{ControllerSet, ControllerSwitchEvent}; pub fn plugin(app: &mut App) { app.add_plugins(CharacterPlugin::default()); app.register_type::(); app.init_resource::(); app.add_systems( PreUpdate, reset_upon_switch .run_if(in_state(GameState::Playing)) .before(ControllerSet::ApplyControlsRun) .before(ControllerSet::ApplyControlsFly), ) .add_systems( FixedPreUpdate, decelerate.run_if(in_state(GameState::Playing)), ); } /// 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 &mut character { 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(Resource, Default)] pub struct PlayerMovement { pub any_direction: bool, pub shooting: bool, } #[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 { up: Dir3::Y }, 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: CharacterFriction, drag: CharacterDrag, settings: ControllerSettings, } const RUNNING_MOVEMENT_CONFIG: MovementConfig = MovementConfig { movement: CharacterMovement { target_speed: 15.0, acceleration: 40.0, }, step: SteppingConfig { max_height: 0.25, behaviour: SteppingBehaviour::Grounded, }, ground: GroundingConfig { max_angle: PI / 4.0, max_distance: 0.2, snap_to_surface: true, }, gravity: CharacterGravity(vec3(0.0, -60.0, 0.0)), friction: CharacterFriction(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_height: 0.25, behaviour: SteppingBehaviour::Never, }, ground: GroundingConfig { max_angle: 0.0, max_distance: -1.0, snap_to_surface: false, }, gravity: CharacterGravity(Vec3::ZERO), friction: CharacterFriction(0.0), drag: CharacterDrag(10.0), settings: ControllerSettings { jump_force: 0.0, deceleration_factor: 0.0, }, };