use avian2d::{math::*, prelude::*};
use bevy::{ecs::query::Has, prelude::*};
use crate::UpdateSystems;
pub struct CharacterControllerPlugin;
impl Plugin for CharacterControllerPlugin {
fn build(&self, app: &mut App) {
app.register_type::<MovementAcceleration>();
app.register_type::<MovementDampingFactor>();
app.register_type::<JumpImpulse>();
app.register_type::<MaxSlopeAngle>();
app.add_message::<MovementEvent>().add_systems(
Update,
(update_grounded, movement, apply_movement_damping)
.chain()
.in_set(UpdateSystems::ApplyMovement),
);
app.insert_resource(Gravity(Vec2::NEG_Y * 1000.));
}
}
#[derive(Message)]
pub struct MovementEvent {
pub entity: Entity,
pub action: MovementAction,
}
pub enum MovementAction {
Move(Scalar),
Jump,
}
#[derive(Component)]
pub struct CharacterController;
#[derive(Component)]
#[component(storage = "SparseSet")]
pub struct Grounded;
#[derive(Component, Reflect)]
pub struct MovementAcceleration(Scalar);
#[derive(Component, Reflect)]
pub struct MovementDampingFactor(Scalar);
#[derive(Component, Reflect)]
pub struct JumpImpulse(Scalar);
#[derive(Component, Reflect)]
pub struct MaxSlopeAngle(Scalar);
#[derive(Bundle)]
pub struct CharacterControllerBundle {
character_controller: CharacterController,
body: RigidBody,
collider: Collider,
ground_caster: ShapeCaster,
locked_axes: LockedAxes,
movement: MovementBundle,
}
#[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) -> Self {
let mut caster_shape = collider.clone();
caster_shape.set_scale(Vector::ONE * 0.99, 10);
Self {
character_controller: CharacterController,
body: RigidBody::Dynamic,
collider,
ground_caster: ShapeCaster::new(caster_shape, Vector::ZERO, 0.0, Dir2::NEG_Y)
.with_max_distance(10.0),
locked_axes: LockedAxes::ROTATION_LOCKED,
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
}
}
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 {
let is_grounded = hits.iter().any(|hit| {
if let Some(angle) = max_slope_angle {
(rotation * -hit.normal2).angle_to(Vector::Y).abs() <= angle.0
} else {
true
}
});
if is_grounded {
commands.entity(entity).insert(Grounded);
} else {
commands.entity(entity).remove::<Grounded>();
}
}
}
fn movement(
time: Res<Time>,
mut movement_message_reader: MessageReader<MovementEvent>,
mut controllers: Query<(
&MovementAcceleration,
&JumpImpulse,
&mut LinearVelocity,
Has<Grounded>,
)>,
) {
let delta_time = time.delta_secs_f64().adjust_precision();
for event in movement_message_reader.read() {
if let Ok((movement_acceleration, jump_impulse, mut linear_velocity, is_grounded)) =
controllers.get_mut(event.entity)
{
match event.action {
MovementAction::Move(direction) => {
linear_velocity.x += direction * movement_acceleration.0 * delta_time;
}
MovementAction::Jump => {
if is_grounded {
linear_velocity.y = jump_impulse.0;
}
}
}
}
}
}
fn apply_movement_damping(mut query: Query<(&MovementDampingFactor, &mut LinearVelocity)>) {
for (damping_factor, mut linear_velocity) in &mut query {
linear_velocity.x *= damping_factor.0;
}
}