247 lines
7.6 KiB
Rust
247 lines
7.6 KiB
Rust
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::<PlayerMovement>();
|
|
|
|
app.register_type::<JumpImpulse>();
|
|
app.register_type::<ControllerGravity>();
|
|
app.register_type::<MovementAcceleration>();
|
|
|
|
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<CharacterController>,
|
|
>,
|
|
) {
|
|
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::<Grounded>();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sets the movement flag, which is an indicator for the rig animation and the braking system.
|
|
fn set_movement_flag(mut player_movement: ResMut<PlayerMovement>, controls: Res<Controls>) {
|
|
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<Time>,
|
|
controls: Res<Controls>,
|
|
mut controllers: Query<(
|
|
&MovementAcceleration,
|
|
&MovementSpeedFactor,
|
|
&JumpImpulse,
|
|
&mut LinearVelocity,
|
|
Has<Grounded>,
|
|
)>,
|
|
rig_transform_q: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
|
|
mut jump_used: Local<bool>,
|
|
) {
|
|
let delta_time = time.delta_secs();
|
|
|
|
let mut direction = controls.keyboard_state.move_dir;
|
|
|
|
let mut jump_requested = controls.keyboard_state.jump;
|
|
|
|
if let Some(gamepad) = controls.gamepad_state {
|
|
direction += gamepad.move_dir;
|
|
|
|
jump_requested |= gamepad.jump;
|
|
}
|
|
|
|
direction = direction.normalize_or_zero();
|
|
|
|
for (movement_acceleration, factor, jump_impulse, mut linear_velocity, is_grounded) in
|
|
&mut controllers
|
|
{
|
|
let mut direction = direction.extend(0.0).xzy();
|
|
|
|
if let Some(ref rig_transform) = rig_transform_q {
|
|
direction =
|
|
(rig_transform.forward() * direction.z) + (rig_transform.right() * direction.x);
|
|
}
|
|
|
|
linear_velocity.x = -direction.x * movement_acceleration.0 * delta_time * factor.0;
|
|
linear_velocity.z = -direction.z * movement_acceleration.0 * delta_time * factor.0;
|
|
|
|
if is_grounded && jump_requested && !*jump_used {
|
|
linear_velocity.y = jump_impulse.0;
|
|
debug!("jump");
|
|
*jump_used = true;
|
|
}
|
|
|
|
if !controls.keyboard_state.jump
|
|
&& !controls
|
|
.gamepad_state
|
|
.map(|state| state.jump)
|
|
.unwrap_or_default()
|
|
{
|
|
*jump_used = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Applies [`ControllerGravity`] to character controllers.
|
|
fn apply_gravity(
|
|
time: Res<Time>,
|
|
mut controllers: Query<(&ControllerGravity, &mut LinearVelocity, Option<&Grounded>)>,
|
|
) {
|
|
let delta_time = time.delta_secs();
|
|
|
|
for (gravity, mut linear_velocity, grounded) in &mut controllers {
|
|
if grounded.is_none() {
|
|
linear_velocity.0 += gravity.0 * delta_time;
|
|
}
|
|
}
|
|
}
|