avian start
This commit is contained in:
+357
@@ -0,0 +1,357 @@
|
||||
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,
|
||||
apply_movement_damping,
|
||||
)
|
||||
.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)]
|
||||
pub struct MovementAction(Vec2);
|
||||
|
||||
/// 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;
|
||||
|
||||
/// The acceleration used for character movement.
|
||||
#[derive(Component)]
|
||||
pub struct MovementAcceleration(Scalar);
|
||||
|
||||
/// The damping factor used for slowing down movement.
|
||||
#[derive(Component)]
|
||||
pub struct MovementDampingFactor(Scalar);
|
||||
|
||||
/// 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)]
|
||||
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,
|
||||
body: RigidBody,
|
||||
collider: Collider,
|
||||
ground_caster: ShapeCaster,
|
||||
movement: MovementBundle,
|
||||
}
|
||||
|
||||
/// A bundle that contains components for character movement.
|
||||
#[derive(Bundle)]
|
||||
pub struct MovementBundle {
|
||||
acceleration: MovementAcceleration,
|
||||
damping: MovementDampingFactor,
|
||||
max_slope_angle: MaxSlopeAngle,
|
||||
}
|
||||
|
||||
impl MovementBundle {
|
||||
pub const fn new(acceleration: Scalar, damping: Scalar, max_slope_angle: Scalar) -> Self {
|
||||
Self {
|
||||
acceleration: MovementAcceleration(acceleration),
|
||||
damping: MovementDampingFactor(damping),
|
||||
max_slope_angle: MaxSlopeAngle(max_slope_angle),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MovementBundle {
|
||||
fn default() -> Self {
|
||||
Self::new(30.0, 0.99, PI * 0.45)
|
||||
}
|
||||
}
|
||||
|
||||
impl CharacterControllerBundle {
|
||||
pub fn new(collider: Collider) -> Self {
|
||||
// Create shape caster as a slightly smaller version of collider
|
||||
let mut caster_shape = collider.clone();
|
||||
caster_shape.set_scale(Vector::ONE * 0.99, 10);
|
||||
|
||||
Self {
|
||||
character_controller: CharacterController,
|
||||
body: RigidBody::Kinematic,
|
||||
collider,
|
||||
ground_caster: ShapeCaster::new(caster_shape, Vector::ZERO, 0.0, Dir2::NEG_Y)
|
||||
.with_max_distance(10.0),
|
||||
movement: MovementBundle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_movement(
|
||||
mut self,
|
||||
acceleration: Scalar,
|
||||
damping: Scalar,
|
||||
max_slope_angle: Scalar,
|
||||
) -> Self {
|
||||
self.movement = MovementBundle::new(acceleration, damping, max_slope_angle);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<(
|
||||
&MovementAcceleration,
|
||||
&MovementDampingFactor,
|
||||
&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 (movement_acceleration, dampening, mut linear_velocity) in &mut controllers {
|
||||
if movement_reader.is_empty() {
|
||||
linear_velocity.x *= 1.0 / (1.0 + damping_factor.0 * delta_time);
|
||||
}
|
||||
for event in movement_reader.read() {
|
||||
match event {
|
||||
MovementAction::Move(direction) => {
|
||||
linear_velocity.x += *direction * movement_acceleration.0 * delta_time;
|
||||
}
|
||||
MovementAction::Jump => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Slows down movement in the X direction.
|
||||
fn apply_movement_damping(
|
||||
time: Res<Time>,
|
||||
mut query: Query<(&MovementDampingFactor, &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 (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 / (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: Collisions,
|
||||
bodies: Query<&RigidBody>,
|
||||
collider_rbs: Query<&ColliderOf, Without<Sensor>>,
|
||||
mut character_controllers: Query<
|
||||
(&mut Position, &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([&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, max_slope_angle) =
|
||||
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;
|
||||
}
|
||||
|
||||
// Determine if the slope is climbable or if it's too steep to walk on.
|
||||
let slope_angle = normal.angle_to(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 either left or right depending on which side the normal is leaning on.
|
||||
// (This could be simplified for 2D, but this approach is dimension-agnostic)
|
||||
let normal_direction_x =
|
||||
normal.reject_from_normalized(Vector::Y).normalize_or_zero();
|
||||
|
||||
// The movement speed along the direction above.
|
||||
let linear_velocity_x = linear_velocity.dot(normal_direction_x);
|
||||
|
||||
// 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_x * 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user