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:
@@ -1,7 +1,7 @@
|
|||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::{controller::PlayerMovement, controls::Controls, physics_layers::GameLayer};
|
use crate::{control::Controls, control::controller::PlayerMovement, physics_layers::GameLayer};
|
||||||
|
|
||||||
#[derive(Component, Reflect, Debug)]
|
#[derive(Component, Reflect, Debug)]
|
||||||
pub struct CameraTarget;
|
pub struct CameraTarget;
|
||||||
|
|||||||
169
src/control/collisions.rs
Normal file
169
src/control/collisions.rs
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
use avian3d::{math::*, prelude::*};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use super::controller::{CharacterController, MaxSlopeAngle};
|
||||||
|
|
||||||
|
/// Kinematic bodies do not get pushed by collisions by default,
|
||||||
|
/// so it needs to be done manually.
|
||||||
|
///
|
||||||
|
/// This system handles collision response for kinematic character controllers
|
||||||
|
/// by pushing them along their contact normals by the current penetration depth,
|
||||||
|
/// and applying velocity corrections in order to snap to slopes, slide along walls,
|
||||||
|
/// and predict collisions using speculative contacts.
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
pub fn kinematic_controller_collisions(
|
||||||
|
collisions: Res<Collisions>,
|
||||||
|
bodies: Query<&RigidBody>,
|
||||||
|
collider_parents: Query<&ColliderParent, Without<Sensor>>,
|
||||||
|
mut character_controllers: Query<
|
||||||
|
(
|
||||||
|
&mut Position,
|
||||||
|
&Rotation,
|
||||||
|
&mut LinearVelocity,
|
||||||
|
Option<&MaxSlopeAngle>,
|
||||||
|
),
|
||||||
|
(With<RigidBody>, With<CharacterController>),
|
||||||
|
>,
|
||||||
|
time: Res<Time>,
|
||||||
|
) {
|
||||||
|
// Iterate through collisions and move the kinematic body to resolve penetration
|
||||||
|
for contacts in collisions.iter() {
|
||||||
|
// Get the rigid body entities of the colliders (colliders could be children)
|
||||||
|
let Ok([collider_parent1, collider_parent2]) =
|
||||||
|
collider_parents.get_many([contacts.entity1, contacts.entity2])
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the body of the character controller and whether it is the first
|
||||||
|
// or second entity in the collision.
|
||||||
|
let is_first: bool;
|
||||||
|
|
||||||
|
let character_rb: RigidBody;
|
||||||
|
let is_other_dynamic: bool;
|
||||||
|
|
||||||
|
let (mut position, rotation, mut linear_velocity, max_slope_angle) =
|
||||||
|
if let Ok(character) = character_controllers.get_mut(collider_parent1.get()) {
|
||||||
|
is_first = true;
|
||||||
|
character_rb = *bodies.get(collider_parent1.get()).unwrap();
|
||||||
|
is_other_dynamic = bodies
|
||||||
|
.get(collider_parent2.get())
|
||||||
|
.is_ok_and(|rb| rb.is_dynamic());
|
||||||
|
character
|
||||||
|
} else if let Ok(character) = character_controllers.get_mut(collider_parent2.get()) {
|
||||||
|
is_first = false;
|
||||||
|
character_rb = *bodies.get(collider_parent2.get()).unwrap();
|
||||||
|
is_other_dynamic = bodies
|
||||||
|
.get(collider_parent1.get())
|
||||||
|
.is_ok_and(|rb| rb.is_dynamic());
|
||||||
|
character
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// This system only handles collision response for kinematic character controllers.
|
||||||
|
if !character_rb.is_kinematic() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through contact manifolds and their contacts.
|
||||||
|
// Each contact in a single manifold shares the same contact normal.
|
||||||
|
for manifold in contacts.manifolds.iter() {
|
||||||
|
let normal = if is_first {
|
||||||
|
-manifold.global_normal1(rotation)
|
||||||
|
} else {
|
||||||
|
-manifold.global_normal2(rotation)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut deepest_penetration: Scalar = Scalar::MIN;
|
||||||
|
|
||||||
|
// Solve each penetrating contact in the manifold.
|
||||||
|
for contact in manifold.contacts.iter() {
|
||||||
|
if contact.penetration > 0.0 {
|
||||||
|
position.0 += normal * contact.penetration;
|
||||||
|
}
|
||||||
|
deepest_penetration = deepest_penetration.max(contact.penetration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, this system only handles velocity corrections for collisions against static geometry.
|
||||||
|
if is_other_dynamic {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if the slope is climbable or if it's too steep to walk on.
|
||||||
|
let slope_angle = normal.angle_between(Vector::Y);
|
||||||
|
let climbable = max_slope_angle.is_some_and(|angle| slope_angle.abs() <= angle.0);
|
||||||
|
|
||||||
|
if deepest_penetration > 0.0 {
|
||||||
|
// If the slope is climbable, snap the velocity so that the character
|
||||||
|
// up and down the surface smoothly.
|
||||||
|
if climbable {
|
||||||
|
// Points in the normal's direction in the XZ plane.
|
||||||
|
let normal_direction_xz =
|
||||||
|
normal.reject_from_normalized(Vector::Y).normalize_or_zero();
|
||||||
|
|
||||||
|
// The movement speed along the direction above.
|
||||||
|
let linear_velocity_xz = linear_velocity.dot(normal_direction_xz);
|
||||||
|
|
||||||
|
// Snap the Y speed based on the speed at which the character is moving
|
||||||
|
// up or down the slope, and how steep the slope is.
|
||||||
|
//
|
||||||
|
// A 2D visualization of the slope, the contact normal, and the velocity components:
|
||||||
|
//
|
||||||
|
// ╱
|
||||||
|
// normal ╱
|
||||||
|
// * ╱
|
||||||
|
// │ * ╱ velocity_x
|
||||||
|
// │ * - - - - - -
|
||||||
|
// │ * | velocity_y
|
||||||
|
// │ * |
|
||||||
|
// *───────────────────*
|
||||||
|
|
||||||
|
let max_y_speed = -linear_velocity_xz * slope_angle.tan();
|
||||||
|
linear_velocity.y = linear_velocity.y.max(max_y_speed);
|
||||||
|
} else {
|
||||||
|
// The character is intersecting an unclimbable object, like a wall.
|
||||||
|
// We want the character to slide along the surface, similarly to
|
||||||
|
// a collide-and-slide algorithm.
|
||||||
|
|
||||||
|
// Don't apply an impulse if the character is moving away from the surface.
|
||||||
|
if linear_velocity.dot(normal) > 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slide along the surface, rejecting the velocity along the contact normal.
|
||||||
|
let impulse = linear_velocity.reject_from_normalized(normal);
|
||||||
|
linear_velocity.0 = impulse;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The character is not yet intersecting the other object,
|
||||||
|
// but the narrow phase detected a speculative collision.
|
||||||
|
//
|
||||||
|
// We need to push back the part of the velocity
|
||||||
|
// that would cause penetration within the next frame.
|
||||||
|
|
||||||
|
let normal_speed = linear_velocity.dot(normal);
|
||||||
|
|
||||||
|
// Don't apply an impulse if the character is moving away from the surface.
|
||||||
|
if normal_speed > 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the impulse to apply.
|
||||||
|
let impulse_magnitude =
|
||||||
|
normal_speed - (deepest_penetration / time.delta_secs_f64().adjust_precision());
|
||||||
|
let mut impulse = impulse_magnitude * normal;
|
||||||
|
|
||||||
|
// Apply the impulse differently depending on the slope angle.
|
||||||
|
if climbable {
|
||||||
|
// Avoid sliding down slopes.
|
||||||
|
linear_velocity.y -= impulse.y.min(0.0);
|
||||||
|
} else {
|
||||||
|
// Avoid climbing up walls.
|
||||||
|
impulse.y = impulse.y.max(0.0);
|
||||||
|
linear_velocity.0 -= impulse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
315
src/control/controller.rs
Normal file
315
src/control/controller.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,29 +8,26 @@ use bevy::{
|
|||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Resource, Debug, Clone, Copy, Default, PartialEq)]
|
use super::{ControlState, ControllerSet, Controls};
|
||||||
pub struct ControlState {
|
|
||||||
pub move_dir: Vec2,
|
|
||||||
pub look_dir: Vec2,
|
|
||||||
pub jump: bool,
|
|
||||||
pub view_mode: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Resource, Debug, Default)]
|
|
||||||
pub struct Controls {
|
|
||||||
pub keyboard_state: ControlState,
|
|
||||||
pub gamepad_state: Option<ControlState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn plugin(app: &mut App) {
|
pub fn plugin(app: &mut App) {
|
||||||
app.init_resource::<Controls>();
|
app.init_resource::<Controls>();
|
||||||
|
|
||||||
app.add_systems(Update, (gamepad_controls, keyboard_controls, mouse_rotate));
|
app.add_systems(
|
||||||
app.add_systems(Update, mouse_click.run_if(on_event::<MouseButtonInput>));
|
Update,
|
||||||
app.add_systems(Update, gamepad_connections.run_if(on_event::<GamepadEvent>));
|
(
|
||||||
|
gamepad_controls,
|
||||||
|
keyboard_controls,
|
||||||
|
mouse_rotate,
|
||||||
|
mouse_click.run_if(on_event::<MouseButtonInput>),
|
||||||
|
gamepad_connections.run_if(on_event::<GamepadEvent>),
|
||||||
|
)
|
||||||
|
.in_set(ControllerSet::CollectInputs),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clamp_vec2(v: Vec2, min: f32) -> Vec2 {
|
/// Applies a square deadzone to a Vec2
|
||||||
|
fn deadzone_square(v: Vec2, min: f32) -> Vec2 {
|
||||||
Vec2::new(
|
Vec2::new(
|
||||||
if v.x.abs() < min { 0. } else { v.x },
|
if v.x.abs() < min { 0. } else { v.x },
|
||||||
if v.y.abs() < min { 0. } else { v.y },
|
if v.y.abs() < min { 0. } else { v.y },
|
||||||
@@ -49,13 +46,16 @@ fn gamepad_controls(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let deadzone_left_stick = 0.15;
|
||||||
|
let deadzone_right_stick = 0.15;
|
||||||
|
|
||||||
// info!("gamepad: {:?}", gamepad);
|
// info!("gamepad: {:?}", gamepad);
|
||||||
|
|
||||||
let rotate = gamepad
|
let rotate = gamepad
|
||||||
.get(GamepadButton::RightTrigger2)
|
.get(GamepadButton::RightTrigger2)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
//8BitDo Ultimate wireless Controller for PC
|
// 8BitDo Ultimate wireless Controller for PC
|
||||||
let look_dir = if gamepad.vendor_id() == Some(11720) && gamepad.product_id() == Some(12306) {
|
let look_dir = if gamepad.vendor_id() == Some(11720) && gamepad.product_id() == Some(12306) {
|
||||||
const EPSILON: f32 = 0.015;
|
const EPSILON: f32 = 0.015;
|
||||||
Vec2::new(
|
Vec2::new(
|
||||||
@@ -69,11 +69,11 @@ fn gamepad_controls(
|
|||||||
0.,
|
0.,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
clamp_vec2(gamepad.right_stick(), 0.01) * 40.
|
deadzone_square(gamepad.right_stick(), deadzone_right_stick) * 40.
|
||||||
};
|
};
|
||||||
|
|
||||||
let state = ControlState {
|
let state = ControlState {
|
||||||
move_dir: gamepad.left_stick(),
|
move_dir: deadzone_square(gamepad.left_stick(), deadzone_left_stick),
|
||||||
look_dir,
|
look_dir,
|
||||||
jump: gamepad.pressed(GamepadButton::South),
|
jump: gamepad.pressed(GamepadButton::South),
|
||||||
view_mode: gamepad.pressed(GamepadButton::LeftTrigger2),
|
view_mode: gamepad.pressed(GamepadButton::LeftTrigger2),
|
||||||
@@ -126,20 +126,19 @@ fn keyboard_controls(
|
|||||||
keyboard: Res<ButtonInput<KeyCode>>,
|
keyboard: Res<ButtonInput<KeyCode>>,
|
||||||
mut controls: ResMut<Controls>,
|
mut controls: ResMut<Controls>,
|
||||||
) {
|
) {
|
||||||
let mut direction = Vec2::ZERO;
|
let up_binds = [KeyCode::KeyW, KeyCode::ArrowUp];
|
||||||
|
let down_binds = [KeyCode::KeyS, KeyCode::ArrowDown];
|
||||||
|
let left_binds = [KeyCode::KeyA, KeyCode::ArrowLeft];
|
||||||
|
let right_binds = [KeyCode::KeyD, KeyCode::ArrowRight];
|
||||||
|
|
||||||
if keyboard.pressed(KeyCode::KeyW) {
|
let up = keyboard.any_pressed(up_binds);
|
||||||
direction = Vec2::Y;
|
let down = keyboard.any_pressed(down_binds);
|
||||||
}
|
let left = keyboard.any_pressed(left_binds);
|
||||||
if keyboard.pressed(KeyCode::KeyS) {
|
let right = keyboard.any_pressed(right_binds);
|
||||||
direction = -Vec2::Y;
|
|
||||||
}
|
let horizontal = right as i8 - left as i8;
|
||||||
if keyboard.pressed(KeyCode::KeyA) {
|
let vertical = up as i8 - down as i8;
|
||||||
direction += -Vec2::X;
|
let direction = Vec2::new(horizontal as f32, vertical as f32).clamp_length_max(1.0);
|
||||||
}
|
|
||||||
if keyboard.pressed(KeyCode::KeyD) {
|
|
||||||
direction += Vec2::X;
|
|
||||||
}
|
|
||||||
|
|
||||||
if keyboard.just_pressed(KeyCode::KeyB) {
|
if keyboard.just_pressed(KeyCode::KeyB) {
|
||||||
commands.trigger(BackpackAction::OpenClose);
|
commands.trigger(BackpackAction::OpenClose);
|
||||||
@@ -206,9 +205,6 @@ fn gamepad_connections(mut evr_gamepad: EventReader<GamepadEvent>) {
|
|||||||
info!("Lost connection with gamepad: {:?}", connection.gamepad);
|
info!("Lost connection with gamepad: {:?}", connection.gamepad);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
GamepadEvent::Button(gamepad_button_changed_event) => {
|
|
||||||
info!("Gamepad Button: {:?}", gamepad_button_changed_event);
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
33
src/control/mod.rs
Normal file
33
src/control/mod.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
mod collisions;
|
||||||
|
pub mod controller;
|
||||||
|
pub mod controls;
|
||||||
|
|
||||||
|
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
|
||||||
|
enum ControllerSet {
|
||||||
|
CollectInputs,
|
||||||
|
ApplyControls,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Debug, Clone, Copy, Default, PartialEq)]
|
||||||
|
pub struct ControlState {
|
||||||
|
pub move_dir: Vec2,
|
||||||
|
pub look_dir: Vec2,
|
||||||
|
pub jump: bool,
|
||||||
|
pub view_mode: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Debug, Default)]
|
||||||
|
pub struct Controls {
|
||||||
|
pub keyboard_state: ControlState,
|
||||||
|
pub gamepad_state: Option<ControlState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn plugin(app: &mut App) {
|
||||||
|
app.add_plugins(controls::plugin);
|
||||||
|
app.configure_sets(
|
||||||
|
Update,
|
||||||
|
(ControllerSet::CollectInputs, ControllerSet::ApplyControls).chain(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,539 +0,0 @@
|
|||||||
use avian3d::{math::*, prelude::*};
|
|
||||||
use bevy::{ecs::query::Has, prelude::*};
|
|
||||||
|
|
||||||
use crate::player::PlayerRig;
|
|
||||||
|
|
||||||
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_event::<MovementAction>();
|
|
||||||
app.add_systems(
|
|
||||||
Update,
|
|
||||||
(
|
|
||||||
keyboard_input,
|
|
||||||
gamepad_input,
|
|
||||||
clear_movement_flag,
|
|
||||||
brake_on_release,
|
|
||||||
update_grounded,
|
|
||||||
apply_gravity,
|
|
||||||
movement,
|
|
||||||
apply_movement_damping,
|
|
||||||
)
|
|
||||||
.chain(),
|
|
||||||
);
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An event sent for a movement input action.
|
|
||||||
#[derive(Event)]
|
|
||||||
pub enum MovementAction {
|
|
||||||
Move(Vector2),
|
|
||||||
Jump,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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(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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends [`MovementAction`] events based on keyboard input.
|
|
||||||
fn keyboard_input(
|
|
||||||
mut movement_event_writer: EventWriter<MovementAction>,
|
|
||||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
|
||||||
) {
|
|
||||||
let up_binds = [KeyCode::KeyW, KeyCode::ArrowUp];
|
|
||||||
let down_binds = [KeyCode::KeyS, KeyCode::ArrowDown];
|
|
||||||
let left_binds = [KeyCode::KeyA, KeyCode::ArrowLeft];
|
|
||||||
let right_binds = [KeyCode::KeyD, KeyCode::ArrowRight];
|
|
||||||
|
|
||||||
let up = keyboard_input.any_pressed(up_binds);
|
|
||||||
let down = keyboard_input.any_pressed(down_binds);
|
|
||||||
let left = keyboard_input.any_pressed(left_binds);
|
|
||||||
let right = keyboard_input.any_pressed(right_binds);
|
|
||||||
|
|
||||||
let horizontal = right as i8 - left as i8;
|
|
||||||
let vertical = up as i8 - down as i8;
|
|
||||||
let direction = Vector2::new(horizontal as Scalar, vertical as Scalar).clamp_length_max(1.0);
|
|
||||||
|
|
||||||
if direction != Vector2::ZERO {
|
|
||||||
movement_event_writer.send(MovementAction::Move(direction));
|
|
||||||
}
|
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::Space) {
|
|
||||||
movement_event_writer.send(MovementAction::Jump);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends [`MovementAction`] events based on gamepad input.
|
|
||||||
fn gamepad_input(
|
|
||||||
mut movement_event_writer: EventWriter<MovementAction>,
|
|
||||||
gamepads: Query<&Gamepad>,
|
|
||||||
) {
|
|
||||||
for gamepad in gamepads.iter() {
|
|
||||||
if let (Some(x), Some(y)) = (
|
|
||||||
gamepad.get(GamepadAxis::LeftStickX),
|
|
||||||
gamepad.get(GamepadAxis::LeftStickY),
|
|
||||||
) {
|
|
||||||
let deadzone = 0.01;
|
|
||||||
let dir = Vector2::new(x as Scalar, y as Scalar).clamp_length_max(1.0);
|
|
||||||
|
|
||||||
if dir.length_squared() > deadzone {
|
|
||||||
movement_event_writer.send(MovementAction::Move(dir));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if gamepad.just_pressed(GamepadButton::South) {
|
|
||||||
movement_event_writer.send(MovementAction::Jump);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_movement_flag(
|
|
||||||
mut player_movement: ResMut<PlayerMovement>,
|
|
||||||
movement_event_reader: EventReader<MovementAction>,
|
|
||||||
) {
|
|
||||||
if movement_event_reader.is_empty() {
|
|
||||||
player_movement.any_direction = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Responds to [`MovementAction`] events and moves character controllers accordingly.
|
|
||||||
fn movement(
|
|
||||||
time: Res<Time>,
|
|
||||||
mut movement_event_reader: EventReader<MovementAction>,
|
|
||||||
mut controllers: Query<(
|
|
||||||
&MovementAcceleration,
|
|
||||||
&JumpImpulse,
|
|
||||||
&mut LinearVelocity,
|
|
||||||
Has<Grounded>,
|
|
||||||
)>,
|
|
||||||
rig_transform_q: Option<Single<&GlobalTransform, With<PlayerRig>>>,
|
|
||||||
mut player_movement: ResMut<PlayerMovement>,
|
|
||||||
) {
|
|
||||||
let delta_time = time.delta_secs();
|
|
||||||
|
|
||||||
for event in movement_event_reader.read() {
|
|
||||||
for (movement_acceleration, jump_impulse, mut linear_velocity, is_grounded) in
|
|
||||||
&mut controllers
|
|
||||||
{
|
|
||||||
match event {
|
|
||||||
MovementAction::Move(direction) => {
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Update movement flag
|
|
||||||
let deadzone = 0.2;
|
|
||||||
|
|
||||||
// todo this is probably not necessary
|
|
||||||
if player_movement.any_direction && direction.length_squared() < deadzone {
|
|
||||||
player_movement.any_direction = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !player_movement.any_direction && direction.length_squared() > deadzone {
|
|
||||||
player_movement.any_direction = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MovementAction::Jump => {
|
|
||||||
if is_grounded {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Kinematic bodies do not get pushed by collisions by default,
|
|
||||||
/// so it needs to be done manually.
|
|
||||||
///
|
|
||||||
/// This system handles collision response for kinematic character controllers
|
|
||||||
/// by pushing them along their contact normals by the current penetration depth,
|
|
||||||
/// and applying velocity corrections in order to snap to slopes, slide along walls,
|
|
||||||
/// and predict collisions using speculative contacts.
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
fn kinematic_controller_collisions(
|
|
||||||
collisions: Res<Collisions>,
|
|
||||||
bodies: Query<&RigidBody>,
|
|
||||||
collider_parents: Query<&ColliderParent, Without<Sensor>>,
|
|
||||||
mut character_controllers: Query<
|
|
||||||
(
|
|
||||||
&mut Position,
|
|
||||||
&Rotation,
|
|
||||||
&mut LinearVelocity,
|
|
||||||
Option<&MaxSlopeAngle>,
|
|
||||||
),
|
|
||||||
(With<RigidBody>, With<CharacterController>),
|
|
||||||
>,
|
|
||||||
time: Res<Time>,
|
|
||||||
) {
|
|
||||||
// Iterate through collisions and move the kinematic body to resolve penetration
|
|
||||||
for contacts in collisions.iter() {
|
|
||||||
// Get the rigid body entities of the colliders (colliders could be children)
|
|
||||||
let Ok([collider_parent1, collider_parent2]) =
|
|
||||||
collider_parents.get_many([contacts.entity1, contacts.entity2])
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the body of the character controller and whether it is the first
|
|
||||||
// or second entity in the collision.
|
|
||||||
let is_first: bool;
|
|
||||||
|
|
||||||
let character_rb: RigidBody;
|
|
||||||
let is_other_dynamic: bool;
|
|
||||||
|
|
||||||
let (mut position, rotation, mut linear_velocity, max_slope_angle) =
|
|
||||||
if let Ok(character) = character_controllers.get_mut(collider_parent1.get()) {
|
|
||||||
is_first = true;
|
|
||||||
character_rb = *bodies.get(collider_parent1.get()).unwrap();
|
|
||||||
is_other_dynamic = bodies
|
|
||||||
.get(collider_parent2.get())
|
|
||||||
.is_ok_and(|rb| rb.is_dynamic());
|
|
||||||
character
|
|
||||||
} else if let Ok(character) = character_controllers.get_mut(collider_parent2.get()) {
|
|
||||||
is_first = false;
|
|
||||||
character_rb = *bodies.get(collider_parent2.get()).unwrap();
|
|
||||||
is_other_dynamic = bodies
|
|
||||||
.get(collider_parent1.get())
|
|
||||||
.is_ok_and(|rb| rb.is_dynamic());
|
|
||||||
character
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// This system only handles collision response for kinematic character controllers.
|
|
||||||
if !character_rb.is_kinematic() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate through contact manifolds and their contacts.
|
|
||||||
// Each contact in a single manifold shares the same contact normal.
|
|
||||||
for manifold in contacts.manifolds.iter() {
|
|
||||||
let normal = if is_first {
|
|
||||||
-manifold.global_normal1(rotation)
|
|
||||||
} else {
|
|
||||||
-manifold.global_normal2(rotation)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut deepest_penetration: Scalar = Scalar::MIN;
|
|
||||||
|
|
||||||
// Solve each penetrating contact in the manifold.
|
|
||||||
for contact in manifold.contacts.iter() {
|
|
||||||
if contact.penetration > 0.0 {
|
|
||||||
position.0 += normal * contact.penetration;
|
|
||||||
}
|
|
||||||
deepest_penetration = deepest_penetration.max(contact.penetration);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now, this system only handles velocity corrections for collisions against static geometry.
|
|
||||||
if is_other_dynamic {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if the slope is climbable or if it's too steep to walk on.
|
|
||||||
let slope_angle = normal.angle_between(Vector::Y);
|
|
||||||
let climbable = max_slope_angle.is_some_and(|angle| slope_angle.abs() <= angle.0);
|
|
||||||
|
|
||||||
if deepest_penetration > 0.0 {
|
|
||||||
// If the slope is climbable, snap the velocity so that the character
|
|
||||||
// up and down the surface smoothly.
|
|
||||||
if climbable {
|
|
||||||
// Points in the normal's direction in the XZ plane.
|
|
||||||
let normal_direction_xz =
|
|
||||||
normal.reject_from_normalized(Vector::Y).normalize_or_zero();
|
|
||||||
|
|
||||||
// The movement speed along the direction above.
|
|
||||||
let linear_velocity_xz = linear_velocity.dot(normal_direction_xz);
|
|
||||||
|
|
||||||
// Snap the Y speed based on the speed at which the character is moving
|
|
||||||
// up or down the slope, and how steep the slope is.
|
|
||||||
//
|
|
||||||
// A 2D visualization of the slope, the contact normal, and the velocity components:
|
|
||||||
//
|
|
||||||
// ╱
|
|
||||||
// normal ╱
|
|
||||||
// * ╱
|
|
||||||
// │ * ╱ velocity_x
|
|
||||||
// │ * - - - - - -
|
|
||||||
// │ * | velocity_y
|
|
||||||
// │ * |
|
|
||||||
// *───────────────────*
|
|
||||||
|
|
||||||
let max_y_speed = -linear_velocity_xz * slope_angle.tan();
|
|
||||||
linear_velocity.y = linear_velocity.y.max(max_y_speed);
|
|
||||||
} else {
|
|
||||||
// The character is intersecting an unclimbable object, like a wall.
|
|
||||||
// We want the character to slide along the surface, similarly to
|
|
||||||
// a collide-and-slide algorithm.
|
|
||||||
|
|
||||||
// Don't apply an impulse if the character is moving away from the surface.
|
|
||||||
if linear_velocity.dot(normal) > 0.0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slide along the surface, rejecting the velocity along the contact normal.
|
|
||||||
let impulse = linear_velocity.reject_from_normalized(normal);
|
|
||||||
linear_velocity.0 = impulse;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// The character is not yet intersecting the other object,
|
|
||||||
// but the narrow phase detected a speculative collision.
|
|
||||||
//
|
|
||||||
// We need to push back the part of the velocity
|
|
||||||
// that would cause penetration within the next frame.
|
|
||||||
|
|
||||||
let normal_speed = linear_velocity.dot(normal);
|
|
||||||
|
|
||||||
// Don't apply an impulse if the character is moving away from the surface.
|
|
||||||
if normal_speed > 0.0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the impulse to apply.
|
|
||||||
let impulse_magnitude =
|
|
||||||
normal_speed - (deepest_penetration / time.delta_secs_f64().adjust_precision());
|
|
||||||
let mut impulse = impulse_magnitude * normal;
|
|
||||||
|
|
||||||
// Apply the impulse differently depending on the slope angle.
|
|
||||||
if climbable {
|
|
||||||
// Avoid sliding down slopes.
|
|
||||||
linear_velocity.y -= impulse.y.min(0.0);
|
|
||||||
} else {
|
|
||||||
// Avoid climbing up walls.
|
|
||||||
impulse.y = impulse.y.max(0.0);
|
|
||||||
linear_velocity.0 -= impulse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,7 @@ mod backpack;
|
|||||||
mod billboards;
|
mod billboards;
|
||||||
mod camera;
|
mod camera;
|
||||||
mod cash;
|
mod cash;
|
||||||
mod controller;
|
mod control;
|
||||||
mod controls;
|
|
||||||
mod cutscene;
|
mod cutscene;
|
||||||
mod gates;
|
mod gates;
|
||||||
mod heads_ui;
|
mod heads_ui;
|
||||||
@@ -28,7 +27,7 @@ use bevy::prelude::*;
|
|||||||
use bevy::render::view::ColorGrading;
|
use bevy::render::view::ColorGrading;
|
||||||
use bevy::scene::SceneInstanceReady;
|
use bevy::scene::SceneInstanceReady;
|
||||||
use bevy_trenchbroom::prelude::*;
|
use bevy_trenchbroom::prelude::*;
|
||||||
use controller::CharacterControllerPlugin;
|
use control::controller::CharacterControllerPlugin;
|
||||||
use physics_layers::GameLayer;
|
use physics_layers::GameLayer;
|
||||||
|
|
||||||
#[derive(Resource, Reflect, Debug)]
|
#[derive(Resource, Reflect, Debug)]
|
||||||
@@ -92,7 +91,7 @@ fn main() {
|
|||||||
app.add_plugins(keys::plugin);
|
app.add_plugins(keys::plugin);
|
||||||
app.add_plugins(squish_animation::plugin);
|
app.add_plugins(squish_animation::plugin);
|
||||||
app.add_plugins(cutscene::plugin);
|
app.add_plugins(cutscene::plugin);
|
||||||
app.add_plugins(controls::plugin);
|
app.add_plugins(control::plugin);
|
||||||
app.add_plugins(sounds::plugin);
|
app.add_plugins(sounds::plugin);
|
||||||
app.add_plugins(camera::plugin);
|
app.add_plugins(camera::plugin);
|
||||||
app.add_plugins(backpack::plugin);
|
app.add_plugins(backpack::plugin);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use crate::{
|
|||||||
alien::{ALIEN_ASSET_PATH, Animations},
|
alien::{ALIEN_ASSET_PATH, Animations},
|
||||||
camera::{CameraArmRotation, CameraTarget},
|
camera::{CameraArmRotation, CameraTarget},
|
||||||
cash::{Cash, CashCollectEvent},
|
cash::{Cash, CashCollectEvent},
|
||||||
controller::{CharacterControllerBundle, PlayerMovement},
|
control::Controls,
|
||||||
controls::Controls,
|
control::controller::{CharacterControllerBundle, PlayerMovement},
|
||||||
heads_ui::HeadChanged,
|
heads_ui::HeadChanged,
|
||||||
physics_layers::GameLayer,
|
physics_layers::GameLayer,
|
||||||
tb_entities::SpawnPoint,
|
tb_entities::SpawnPoint,
|
||||||
|
|||||||
Reference in New Issue
Block a user