255 lines
8.2 KiB
Rust
255 lines
8.2 KiB
Rust
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::<MovementSpeedFactor>();
|
|
|
|
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<Controls>,
|
|
trigger: Res<TriggerStateRes>,
|
|
player: Single<(&Grounding, &HasCharacterAnimations), With<Player>>,
|
|
) {
|
|
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<ControllerSwitchEvent>,
|
|
controller: Res<SelectedController>,
|
|
mut rig_transform_q: Option<Single<&mut Transform, With<PlayerBodyMesh>>>,
|
|
mut velocity: Single<&mut KinematicVelocity, With<Character>>,
|
|
character: Single<Entity, With<Character>>,
|
|
) {
|
|
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,
|
|
},
|
|
};
|