250 lines
9.1 KiB
Rust
250 lines
9.1 KiB
Rust
use avian2d::{math::*, prelude::*};
|
|
use bevy::prelude::*;
|
|
|
|
pub struct CharacterControllerPlugin;
|
|
|
|
impl Plugin for CharacterControllerPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.add_message::<MovementAction>()
|
|
.add_systems(Update, ((keyboard_input, gamepad_input), movement).chain())
|
|
.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.
|
|
PhysicsSchedule,
|
|
kinematic_controller_collisions.in_set(NarrowPhaseSystems::Last),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A [`Message`] written for a movement input action.
|
|
#[derive(Message, Default, Copy, Clone)]
|
|
pub struct MovementAction(Vec2);
|
|
|
|
/// A marker component indicating that an entity is using a character controller.
|
|
#[derive(Component, Default, Copy, Clone)]
|
|
pub struct CharacterController;
|
|
|
|
/// The max speed for a CharacterController.
|
|
#[derive(Component, Default, Copy, Clone)]
|
|
pub struct MaxSpeed(Scalar);
|
|
|
|
/// The max acceleration per second for a CharacterController.
|
|
#[derive(Component, Default, Copy, Clone)]
|
|
pub struct MaxAcceleration(Scalar);
|
|
|
|
/// A bundle that contains the components needed for a basic
|
|
/// kinematic character controller.
|
|
#[derive(Bundle)]
|
|
pub struct CharacterControllerBundle {
|
|
character_controller: CharacterController,
|
|
body: RigidBody,
|
|
collider: Collider,
|
|
speed: MaxSpeed,
|
|
acceleration: MaxAcceleration,
|
|
}
|
|
|
|
impl CharacterControllerBundle {
|
|
pub fn new(collider: Collider) -> Self {
|
|
Self {
|
|
character_controller: CharacterController,
|
|
body: RigidBody::Kinematic,
|
|
collider,
|
|
speed: MaxSpeed(100.),
|
|
acceleration: MaxAcceleration(100.),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sends [`MovementAction`] events based on keyboard input.
|
|
fn keyboard_input(
|
|
mut movement_writer: MessageWriter<MovementAction>,
|
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
|
) {
|
|
let left = keyboard_input.any_pressed([KeyCode::KeyA, KeyCode::ArrowLeft]);
|
|
let right = keyboard_input.any_pressed([KeyCode::KeyD, KeyCode::ArrowRight]);
|
|
let up = keyboard_input.any_pressed([KeyCode::KeyW, KeyCode::ArrowUp]);
|
|
let down = keyboard_input.any_pressed([KeyCode::KeyS, KeyCode::ArrowDown]);
|
|
|
|
let x = right as i8 - left as i8;
|
|
let y = up as i8 - down as i8;
|
|
let dir = Vec2::new(x as f32, y as f32);
|
|
|
|
if let Some(dir) = dir.try_normalize() {
|
|
movement_writer.write(MovementAction(dir));
|
|
}
|
|
}
|
|
|
|
/// Sends [`MovementAction`] events based on gamepad input.
|
|
fn gamepad_input(mut movement_writer: MessageWriter<MovementAction>, gamepads: Query<&Gamepad>) {
|
|
for gamepad in gamepads.iter() {
|
|
if let (Some(x), Some(y)) = (
|
|
gamepad.get(GamepadAxis::LeftStickX),
|
|
gamepad.get(GamepadAxis::LeftStickY),
|
|
) {
|
|
let mut dir = Vec2::new(x, y);
|
|
let len = dir.length();
|
|
if len == 0. {
|
|
continue;
|
|
}
|
|
if len > 1. {
|
|
dir = dir.normalize();
|
|
}
|
|
|
|
movement_writer.write(MovementAction(dir));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Responds to [`MovementAction`] events and moves character controllers accordingly.
|
|
fn movement(
|
|
time: Res<Time>,
|
|
mut movement_reader: MessageReader<MovementAction>,
|
|
mut controllers: Query<(&MaxSpeed, &MaxAcceleration, &mut LinearVelocity)>,
|
|
) {
|
|
// Precision is adjusted so that the example works with
|
|
// both the `f32` and `f64` features. Otherwise you don't need this.
|
|
let delta_time = time.delta_secs_f64().adjust_precision();
|
|
|
|
for (max_speed, max_acceleration, mut linear_velocity) in &mut controllers {
|
|
while movement_reader.len() > 1 {
|
|
movement_reader.read();
|
|
}
|
|
|
|
let target = movement_reader
|
|
.read()
|
|
.next()
|
|
.map(|ma| ma.0)
|
|
.unwrap_or_default()
|
|
* max_speed.0;
|
|
|
|
let mut delta = target - **linear_velocity;
|
|
let delta_len = delta.length();
|
|
let max_acceleration = max_acceleration.0 * delta_time;
|
|
if delta_len > max_acceleration {
|
|
delta = delta.normalize() * max_acceleration;
|
|
}
|
|
**linear_velocity += delta;
|
|
}
|
|
}
|
|
|
|
/// 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: Collisions,
|
|
bodies: Query<&RigidBody>,
|
|
collider_rbs: Query<&ColliderOf, Without<Sensor>>,
|
|
mut character_controllers: Query<
|
|
(&mut Position, &mut LinearVelocity),
|
|
(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([&ColliderOf { body: rb1 }, &ColliderOf { body: rb2 }]) =
|
|
collider_rbs.get_many([contacts.collider1, contacts.collider2])
|
|
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, mut linear_velocity) =
|
|
if let Ok(character) = character_controllers.get_mut(rb1) {
|
|
is_first = true;
|
|
character_rb = *bodies.get(rb1).unwrap();
|
|
is_other_dynamic = bodies.get(rb2).is_ok_and(|rb| rb.is_dynamic());
|
|
character
|
|
} else if let Ok(character) = character_controllers.get_mut(rb2) {
|
|
is_first = false;
|
|
character_rb = *bodies.get(rb2).unwrap();
|
|
is_other_dynamic = bodies.get(rb1).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.normal
|
|
} else {
|
|
manifold.normal
|
|
};
|
|
|
|
let mut deepest_penetration: Scalar = Scalar::MIN;
|
|
|
|
// Solve each penetrating contact in the manifold.
|
|
for contact in manifold.points.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;
|
|
}
|
|
|
|
if deepest_penetration > 0.0 {
|
|
// 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.
|
|
// Avoid climbing up walls.
|
|
impulse.y = impulse.y.max(0.0);
|
|
linear_velocity.0 -= impulse;
|
|
}
|
|
}
|
|
}
|
|
}
|