use avian2d::{math::*, prelude::*};
use bevy::{ecs::query::Has, 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,
update_grounded,
movement,
apply_movement_damping,
)
.chain(),
);
}
}
#[derive(Message)]
pub enum MovementAction {
Move(Scalar),
Jump,
}
#[derive(Component)]
pub struct CharacterController;
#[derive(Component)]
#[component(storage = "SparseSet")]
pub struct Grounded;
#[derive(Component)]
pub struct MovementAcceleration(Scalar);
#[derive(Component)]
pub struct MovementDampingFactor(Scalar);
#[derive(Component)]
pub struct JumpImpulse(Scalar);
#[derive(Component)]
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 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 horizontal = right as i8 - left as i8;
let direction = horizontal as Scalar;
if direction != 0.0 {
movement_writer.write(MovementAction::Move(direction));
}
if keyboard_input.just_pressed(KeyCode::Space) {
movement_writer.write(MovementAction::Jump);
}
}
fn gamepad_input(mut movement_writer: MessageWriter<MovementAction>, gamepads: Query<&Gamepad>) {
for gamepad in gamepads.iter() {
if let Some(x) = gamepad.get(GamepadAxis::LeftStickX) {
movement_writer.write(MovementAction::Move(x as Scalar));
}
if gamepad.just_pressed(GamepadButton::South) {
movement_writer.write(MovementAction::Jump);
}
}
}
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_reader: MessageReader<MovementAction>,
mut controllers: Query<(
&MovementAcceleration,
&JumpImpulse,
&mut LinearVelocity,
Has<Grounded>,
)>,
) {
let delta_time = time.delta_secs_f64().adjust_precision();
for event in movement_reader.read() {
for (movement_acceleration, jump_impulse, mut linear_velocity, is_grounded) in
&mut controllers
{
match event {
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(
time: Res<Time>,
mut query: Query<(&MovementDampingFactor, &mut LinearVelocity)>,
) {
let delta_time = time.delta_secs_f64().adjust_precision();
for (damping_factor, mut linear_velocity) in &mut query {
linear_velocity.x *= 1.0 / (1.0 + damping_factor.0 * delta_time);
}
}