From 9b3765c41dbd0a2929a97487af8e47685530d470 Mon Sep 17 00:00:00 2001 From: extrawurst <776816+extrawurst@users.noreply.github.com> Date: Sat, 12 Apr 2025 17:16:41 +0200 Subject: [PATCH] Fly controller (#26) --- src/camera.rs | 37 ++--- src/control/collisions.rs | 2 +- src/control/controller.rs | 246 ------------------------------ src/control/controller_common.rs | 174 +++++++++++++++++++++ src/control/controller_flying.rs | 122 +++++++++++++++ src/control/controller_running.rs | 178 +++++++++++++++++++++ src/control/mod.rs | 56 ++++++- src/main.rs | 2 - src/player.rs | 56 +------ src/water.rs | 2 +- 10 files changed, 550 insertions(+), 325 deletions(-) delete mode 100644 src/control/controller.rs create mode 100644 src/control/controller_common.rs create mode 100644 src/control/controller_flying.rs create mode 100644 src/control/controller_running.rs diff --git a/src/camera.rs b/src/camera.rs index c24466d..e3cbbe5 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -2,10 +2,8 @@ use avian3d::prelude::*; use bevy::prelude::*; use crate::{ - GameState, - control::{Controls, controller::PlayerMovement}, - loading_assets::UIAssets, - physics_layers::GameLayer, + GameState, control::Controls, control::controller_common::PlayerMovement, + loading_assets::UIAssets, physics_layers::GameLayer, }; #[derive(Component, Reflect, Debug)] @@ -14,9 +12,10 @@ pub struct CameraTarget; #[derive(Component, Reflect, Debug)] pub struct CameraArmRotation; -#[derive(Component, Reflect, Debug)] +/// Requested camera rotation based on various input sources (keyboard, gamepad) +#[derive(Component, Reflect, Debug, Default, Deref, DerefMut)] #[reflect(Component)] -pub struct CameraRotation(pub f32); +pub struct CameraRotationInput(pub Vec2); #[derive(Resource, Reflect, Debug, Default)] #[reflect(Resource)] @@ -43,7 +42,7 @@ impl MainCamera { } pub fn plugin(app: &mut App) { - app.register_type::(); + app.register_type::(); app.register_type::(); app.register_type::(); @@ -66,7 +65,7 @@ fn startup(mut commands: Commands) { commands.spawn(( Camera3d::default(), MainCamera::new(Vec3::new(0., 2.5, -13.)), - CameraRotation(0.), + CameraRotationInput::default(), )); } @@ -118,7 +117,7 @@ fn update_ui( fn update( mut cam: Query< - (&MainCamera, &mut Transform, &CameraRotation), + (&MainCamera, &mut Transform, &CameraRotationInput), (Without, Without), >, target: Query<&GlobalTransform, (With, Without)>, @@ -134,15 +133,15 @@ fn update( return; }; - let Ok(rotation) = arm_rotation.get_single() else { + let Ok(arm_tf) = arm_rotation.get_single() else { return; }; - let Ok((camera, mut cam_transform, cam_rotation)) = cam.get_single_mut() else { + let Ok((camera, mut cam_transform, cam_rotation_input)) = cam.get_single_mut() else { return; }; - let direction = rotation.rotation * Quat::from_rotation_y(cam_rotation.0) * camera.dir; + let direction = arm_tf.rotation * Quat::from_rotation_y(cam_rotation_input.x) * camera.dir; let max_distance = camera.distance; @@ -162,7 +161,7 @@ fn update( fn rotate_view_keyboard( mut controls: ResMut, - mut cam: Query<&mut CameraRotation>, + mut cam: Query<&mut CameraRotationInput>, movement: Res, ) { let Ok(mut cam) = cam.get_single_mut() else { @@ -171,20 +170,21 @@ fn rotate_view_keyboard( if !controls.keyboard_state.view_mode { if movement.any_direction { - cam.0 = 0.; + cam.x = 0.; } return; } - cam.0 += controls.keyboard_state.look_dir.x * -0.001; + cam.x += controls.keyboard_state.look_dir.x * -0.001; + cam.y += controls.keyboard_state.look_dir.y * -0.001; controls.keyboard_state.look_dir = Vec2::ZERO; } fn rotate_view_gamepad( controls: Res, - mut cam: Query<&mut CameraRotation>, + mut cam: Query<&mut CameraRotationInput>, movement: Res, ) { let Ok(mut cam) = cam.get_single_mut() else { @@ -197,9 +197,10 @@ fn rotate_view_gamepad( if !gamepad.view_mode { if movement.any_direction { - cam.0 = 0.; + cam.x = 0.; } } else { - cam.0 += gamepad.look_dir.x * -0.001; + cam.x += gamepad.look_dir.x * -0.001; + cam.y += gamepad.look_dir.y * -0.001; } } diff --git a/src/control/collisions.rs b/src/control/collisions.rs index 33615c2..641cc45 100644 --- a/src/control/collisions.rs +++ b/src/control/collisions.rs @@ -1,7 +1,7 @@ use avian3d::{math::*, prelude::*}; use bevy::prelude::*; -use super::controller::{CharacterController, MaxSlopeAngle}; +use super::controller_common::{CharacterController, MaxSlopeAngle}; /// Kinematic bodies do not get pushed by collisions by default, /// so it needs to be done manually. diff --git a/src/control/controller.rs b/src/control/controller.rs deleted file mode 100644 index 40d47b7..0000000 --- a/src/control/controller.rs +++ /dev/null @@ -1,246 +0,0 @@ -use super::{ControllerSet, Controls, collisions::kinematic_controller_collisions}; -use crate::{GameState, player::PlayerBodyMesh}; -use avian3d::{math::*, prelude::*}; -use bevy::{ecs::query::Has, prelude::*}; - -pub struct CharacterControllerPlugin; - -impl Plugin for CharacterControllerPlugin { - fn build(&self, app: &mut App) { - app.init_resource::(); - - app.register_type::(); - app.register_type::(); - app.register_type::(); - - app.add_systems( - Update, - (set_movement_flag, update_grounded, apply_gravity, movement) - .chain() - .in_set(ControllerSet::ApplyControls) - .run_if(in_state(GameState::Playing)), - ); - app.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. - PostProcessCollisions, - kinematic_controller_collisions.run_if(in_state(GameState::Playing)), - ); - } -} - -/// 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; - -#[derive(Resource, Default)] -pub struct PlayerMovement { - pub any_direction: bool, -} - -/// The acceleration used for character movement. -#[derive(Component, Reflect)] -#[reflect(Component)] -pub struct MovementAcceleration(Scalar); - -#[derive(Component, Reflect)] -#[reflect(Component)] -pub struct MovementSpeedFactor(pub f32); - -/// The strength of a jump. -#[derive(Component, Reflect)] -#[reflect(Component)] -pub struct JumpImpulse(Scalar); - -/// The gravitational acceleration used for a character controller. -#[derive(Component, Reflect)] -#[reflect(Component)] -pub struct ControllerGravity(Vector); - -/// 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, Reflect)] -#[reflect(Component)] -pub struct MaxSlopeAngle(pub Scalar); - -/// A bundle that contains the components needed for a basic -/// kinematic character controller. -#[derive(Bundle)] -pub struct CharacterControllerBundle { - character_controller: CharacterController, - rigid_body: RigidBody, - collider: Collider, - ground_caster: ShapeCaster, - gravity: ControllerGravity, - movement: MovementBundle, -} - -/// A bundle that contains components for character movement. -#[derive(Bundle)] -pub struct MovementBundle { - acceleration: MovementAcceleration, - jump_impulse: JumpImpulse, - max_slope_angle: MaxSlopeAngle, - factor: MovementSpeedFactor, -} - -impl MovementBundle { - pub const fn new(acceleration: Scalar, jump_impulse: Scalar, max_slope_angle: Scalar) -> Self { - Self { - acceleration: MovementAcceleration(acceleration), - jump_impulse: JumpImpulse(jump_impulse), - max_slope_angle: MaxSlopeAngle(max_slope_angle), - factor: MovementSpeedFactor(1.), - } - } -} - -impl CharacterControllerBundle { - pub fn new(collider: Collider, gravity: Vector, movement: MovementBundle) -> Self { - // Create shape caster as a slightly smaller version of collider - let mut caster_shape = collider.clone(); - caster_shape.set_scale(Vector::ONE * 0.98, 10); - - Self { - character_controller: CharacterController, - rigid_body: RigidBody::Kinematic, - collider, - ground_caster: ShapeCaster::new( - caster_shape, - Vector::ZERO, - Quaternion::default(), - Dir3::NEG_Y, - ) - .with_max_distance(0.2), - gravity: ControllerGravity(gravity), - movement, - } - } -} - -/// Updates the [`Grounded`] status for character controllers. -fn update_grounded( - mut commands: Commands, - mut query: Query< - (Entity, &ShapeHits, &Rotation, Option<&MaxSlopeAngle>), - With, - >, -) { - 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 * -hit.normal2).angle_between(Vector::Y).abs() <= angle.0 - } else { - true - } - }); - - if is_grounded { - debug!("grounded"); - commands.entity(entity).insert(Grounded); - } else { - debug!("not grounded"); - commands.entity(entity).remove::(); - } - } -} - -/// Sets the movement flag, which is an indicator for the rig animation and the braking system. -fn set_movement_flag(mut player_movement: ResMut, controls: Res) { - let mut direction = controls.keyboard_state.move_dir; - let deadzone = 0.2; - - if let Some(gamepad) = controls.gamepad_state { - direction += gamepad.move_dir; - } - - if player_movement.any_direction { - if direction.length_squared() < deadzone { - player_movement.any_direction = false; - } - } else if direction.length_squared() > deadzone { - player_movement.any_direction = true; - } -} - -/// Responds to [`MovementAction`] events and moves character controllers accordingly. -fn movement( - time: Res