use avian3d::{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(Vector2),
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,
Quaternion::default(),
Dir3::NEG_Y,
)
.with_max_distance(0.2),
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 up = keyboard_input.any_pressed([KeyCode::KeyW, KeyCode::ArrowUp]);
let down = keyboard_input.any_pressed([KeyCode::KeyS, KeyCode::ArrowDown]);
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 vertical = up as i8 - down as i8;
let direction = Vector2::new(horizontal as Scalar, vertical as Scalar).clamp_length_max(1.0);
if direction != Vector2::ZERO {
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), Some(y)) = (
gamepad.get(GamepadAxis::LeftStickX),
gamepad.get(GamepadAxis::LeftStickY),
) {
movement_writer.write(MovementAction::Move(
Vector2::new(x as Scalar, y as Scalar).clamp_length_max(1.0),
));
}
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_between(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.x * movement_acceleration.0 * delta_time;
linear_velocity.z -= direction.y * 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;
linear_velocity.z *= damping_factor.0;
}
}