bevy-brackeys-platformer/src/player/controller.rs
2024-05-21 16:11:02 -04:00

293 lines
8.5 KiB
Rust

use bevy::prelude::*;
use bevy_xpbd_2d::{
math::{Scalar, Vector, PI},
prelude::*,
SubstepSchedule, SubstepSet,
};
use crate::GRAVITY_VECTOR;
#[derive(Component, Reflect)]
#[component(storage = "SparseSet")]
pub struct Grounded;
#[derive(Component, Reflect)]
pub struct JumpImpulse(Scalar);
impl Default for JumpImpulse {
fn default() -> Self {
JumpImpulse(7.)
}
}
#[derive(Component, Reflect)]
pub struct MaxJumpDuration(Scalar);
impl Default for MaxJumpDuration {
fn default() -> Self {
MaxJumpDuration(0.1)
}
}
#[derive(Component, Reflect)]
pub struct MovementAcceleration(Scalar);
impl Default for MovementAcceleration {
fn default() -> Self {
MovementAcceleration(75.)
}
}
#[derive(Component, Reflect)]
pub struct MovementDampingFactor(Scalar);
impl Default for MovementDampingFactor {
fn default() -> Self {
MovementDampingFactor(0.9)
}
}
#[derive(Component, Reflect)]
pub struct MaxSlopeAngle(Scalar);
impl Default for MaxSlopeAngle {
fn default() -> Self {
MaxSlopeAngle(PI * 0.45)
}
}
#[derive(Component, Default, Reflect)]
pub struct CharacterController;
#[derive(Component, Reflect)]
pub struct ControllerGravity(Vector);
impl Default for ControllerGravity {
fn default() -> Self {
ControllerGravity(GRAVITY_VECTOR)
}
}
#[derive(Bundle, Default)]
pub struct MovementBundle {
acceleration: MovementAcceleration,
damping: MovementDampingFactor,
jump_impulse: JumpImpulse,
max_slope_angle: MaxSlopeAngle,
direction: MovementDirection,
}
#[derive(Bundle)]
pub struct CharacterControllerBundle {
character_controller: CharacterController,
rigidbody: RigidBody,
collider: Collider,
ground_caster: ShapeCaster,
gravity: ControllerGravity,
movement: MovementBundle,
locked_axes: LockedAxes,
}
impl Default for CharacterControllerBundle {
fn default() -> Self {
let collider = Collider::rectangle(12., 24.);
let mut caster_shape = collider.clone();
caster_shape.set_scale(Vector::ONE * 0.5, 1);
Self {
locked_axes: LockedAxes::ROTATION_LOCKED,
character_controller: CharacterController,
rigidbody: RigidBody::Kinematic,
collider,
ground_caster: ShapeCaster::new(caster_shape, Vector::ZERO, 0.0, Direction2d::NEG_Y)
.with_max_time_of_impact(1.0),
gravity: ControllerGravity(GRAVITY_VECTOR * 5.),
movement: MovementBundle {
acceleration: MovementAcceleration(420.),
jump_impulse: JumpImpulse(46.),
..default()
},
}
}
}
#[derive(Component, Reflect)]
#[component(storage = "SparseSet")]
pub struct Jumping;
#[derive(Component, Default, Reflect)]
pub struct MovementDirection(pub Scalar);
pub struct CharacterControllerPlugin;
impl Plugin for CharacterControllerPlugin {
fn build(&self, app: &mut App) {
app.register_type::<Grounded>()
.register_type::<MovementAcceleration>()
.register_type::<MovementDampingFactor>()
.register_type::<JumpImpulse>()
.register_type::<MaxSlopeAngle>()
.register_type::<MovementDirection>()
.register_type::<CharacterController>()
.register_type::<ControllerGravity>()
.add_systems(
Update,
(
update_grounded,
apply_gravity,
handle_movement,
handle_jump,
apply_movement_damping,
)
.chain(),
)
.add_systems(
// Run collision handling in substep schedule
SubstepSchedule,
kinematic_controller_collisions.in_set(SubstepSet::SolveUserConstraints),
);
}
}
fn handle_movement(
time: Res<Time>,
mut query: Query<
(
&mut LinearVelocity,
&MovementAcceleration,
&MovementDirection,
),
With<CharacterController>,
>,
) {
let delta = time.delta_seconds();
for (mut velocity, MovementAcceleration(acceleration), MovementDirection(direction)) in
query.iter_mut()
{
velocity.x += direction * acceleration * delta;
}
}
fn handle_jump(
mut query: Query<
(&mut LinearVelocity, &JumpImpulse),
(With<CharacterController>, With<Jumping>, With<Grounded>),
>,
) {
for (mut velocity, JumpImpulse(impulse)) in query.iter_mut() {
velocity.y = *impulse;
}
}
fn apply_movement_damping(mut query: Query<(&MovementDampingFactor, &mut LinearVelocity)>) {
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 *= damping_factor.0;
}
}
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.rotate(-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 apply_gravity(time: Res<Time>, mut query: Query<(&mut LinearVelocity, &ControllerGravity)>) {
let delta = time.delta_seconds();
for (mut velocity, ControllerGravity(gravity)) in query.iter_mut() {
velocity.0 += *gravity * delta;
}
}
#[allow(clippy::type_complexity)]
fn kinematic_controller_collisions(
collisions: Res<Collisions>,
collider_parents: Query<&ColliderParent, Without<Sensor>>,
mut character_controllers: Query<
(
&RigidBody,
&mut Position,
&Rotation,
&mut LinearVelocity,
Option<&MaxSlopeAngle>,
),
With<CharacterController>,
>,
) {
// Iterate through collisions and move the kinematic body to resolve penetration
for contacts in collisions.iter() {
// If the collision didn't happen during this substep, skip the collision
if !contacts.during_current_substep {
continue;
}
// 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 (rb, mut position, rotation, mut linear_velocity, max_slope_angle) =
if let Ok(character) = character_controllers.get_mut(collider_parent1.get()) {
is_first = true;
character
} else if let Ok(character) = character_controllers.get_mut(collider_parent2.get()) {
is_first = false;
character
} else {
continue;
};
// This system only handles collision response for kinematic character controllers
if !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)
};
// Solve each penetrating contact in the manifold
for contact in manifold.contacts.iter().filter(|c| c.penetration > 0.0) {
position.0 += normal * contact.penetration;
}
// If the slope isn't too steep to walk on but the character
// is falling, reset vertical velocity.
if max_slope_angle.is_some_and(|angle| normal.angle_between(Vector::Y).abs() <= angle.0)
&& linear_velocity.y < 0.0
{
linear_velocity.y = linear_velocity.y.max(0.0);
}
}
}
}