Refactor controls (#14)

* Move controls

* Use system sets

* Use control states for controller

* Unite controls

* move collisions part

* Right deadzone as well

* Remove comment
This commit is contained in:
GitGhillie
2025-03-22 20:32:09 +01:00
committed by GitHub
parent e21efb9bdb
commit f6b640d06c
8 changed files with 555 additions and 582 deletions

315
src/control/controller.rs Normal file
View File

@@ -0,0 +1,315 @@
use avian3d::{math::*, prelude::*};
use bevy::{ecs::query::Has, prelude::*};
use crate::player::PlayerRig;
use super::{ControllerSet, Controls, collisions::kinematic_controller_collisions};
pub struct CharacterControllerPlugin;
impl Plugin for CharacterControllerPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<PlayerMovement>();
app.init_resource::<MovementSettings>();
app.register_type::<MovementSettings>();
app.register_type::<MovementDampingFactor>();
app.register_type::<JumpImpulse>();
app.register_type::<ControllerGravity>();
app.register_type::<MovementAcceleration>();
app.add_systems(
Update,
(
set_movement_flag,
brake_on_release,
update_grounded,
apply_gravity,
movement,
apply_movement_damping,
)
.chain()
.in_set(ControllerSet::ApplyControls),
);
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,
);
}
}
/// 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,
}
#[derive(Resource, Reflect)]
#[reflect(Resource)]
struct MovementSettings {
damping_normal: f32,
damping_brake: f32,
damping_brake_air: f32,
}
// todo some duplicate with player.rs settings
impl Default for MovementSettings {
fn default() -> Self {
Self {
damping_normal: 1.0,
damping_brake: 30.0,
damping_brake_air: 1.0,
}
}
}
/// The acceleration used for character movement.
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct MovementAcceleration(Scalar);
/// The damping factor used for slowing down movement.
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct MovementDampingFactor(Scalar);
/// 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,
damping: MovementDampingFactor,
jump_impulse: JumpImpulse,
max_slope_angle: MaxSlopeAngle,
}
impl MovementBundle {
pub const fn new(
acceleration: Scalar,
damping: Scalar,
jump_impulse: Scalar,
max_slope_angle: Scalar,
) -> Self {
Self {
acceleration: MovementAcceleration(acceleration),
damping: MovementDampingFactor(damping),
jump_impulse: JumpImpulse(jump_impulse),
max_slope_angle: MaxSlopeAngle(max_slope_angle),
}
}
}
impl Default for MovementBundle {
fn default() -> Self {
Self::new(30.0, 0.9, 7.0, PI * 0.45)
}
}
impl CharacterControllerBundle {
pub fn new(collider: Collider, gravity: Vector) -> 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: MovementBundle::default(),
}
}
pub fn with_movement(
mut self,
acceleration: Scalar,
damping: Scalar,
jump_impulse: Scalar,
max_slope_angle: Scalar,
) -> Self {
self.movement = MovementBundle::new(acceleration, damping, jump_impulse, max_slope_angle);
self
}
}
/// Apply extra friction when no movement input is given
/// In the original you stop instantly in this case
fn brake_on_release(
player_movement: Res<PlayerMovement>,
movement_settings: Res<MovementSettings>,
mut damping_q: Query<(Entity, &mut MovementDampingFactor)>,
grounded_q: Query<&Grounded>,
) {
for (entity, mut damping) in &mut damping_q {
let is_grounded = grounded_q.get(entity).is_ok();
if !player_movement.any_direction && is_grounded {
damping.0 = movement_settings.damping_brake;
} else if !player_movement.any_direction && !is_grounded {
damping.0 = movement_settings.damping_brake_air;
} else {
damping.0 = movement_settings.damping_normal;
}
}
}
/// 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 {
commands.entity(entity).insert(Grounded);
} else {
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,
&JumpImpulse,
&mut LinearVelocity,
Has<Grounded>,
)>,
rig_transform_q: Option<Single<&GlobalTransform, With<PlayerRig>>>,
) {
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;
direction = direction.normalize_or_zero();
jump_requested |= gamepad.jump;
}
for (movement_acceleration, 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;
linear_velocity.z -= direction.z * movement_acceleration.0 * delta_time;
if is_grounded && jump_requested {
linear_velocity.y = jump_impulse.0;
}
}
}
/// Applies [`ControllerGravity`] to character controllers.
fn apply_gravity(
time: Res<Time>,
mut controllers: Query<(&ControllerGravity, &mut LinearVelocity)>,
) {
let delta_time = time.delta_secs();
for (gravity, mut linear_velocity) in &mut controllers {
linear_velocity.0 += gravity.0 * delta_time;
}
}
/// Slows down movement in the XZ plane.
fn apply_movement_damping(
time: Res<Time>,
mut query: Query<(&MovementDampingFactor, &mut LinearVelocity)>,
) {
let delta_time = time.delta_secs();
for (damping_factor, mut linear_velocity) in &mut query {
// We could use `LinearDamping`, but we don't want to dampen movement along the Y axis
linear_velocity.x *= 1.0 - damping_factor.0 * delta_time;
linear_velocity.z *= 1.0 - damping_factor.0 * delta_time;
}
}