diff --git a/Cargo.lock b/Cargo.lock index beff71c..df852bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -372,7 +372,7 @@ dependencies = [ [[package]] name = "bevy_fps_controller" -version = "0.1.6-dev" +version = "0.1.8-dev" dependencies = [ "bevy", "bevy_rapier3d", @@ -980,9 +980,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "clang-sys" -version = "1.4.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" +checksum = "77ed9a53e5d4d9c573ae844bfac6872b159cb1d1585a83b29e7a64b7eef7332a" dependencies = [ "glob", "libc", @@ -2203,15 +2203,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nom8" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" -dependencies = [ - "memchr", -] - [[package]] name = "notify" version = "5.1.0" @@ -2293,18 +2284,18 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d829733185c1ca374f17e52b762f24f535ec625d2cc1f070e34c8a9068f341b" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be1598bf1c313dcdd12092e3f1920f463462525a21b7b4e11b4168353d0123e" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2421,9 +2412,9 @@ dependencies = [ [[package]] name = "parry3d" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2852a4cf8f65177e6f3755a725fdc0a433cca4fa6a97a07ae8c8abd7a4b52ea" +checksum = "008d029b6e85462d4af7c9fae8728b02bf8ab328c4bf6aa93c1e8fa1e1297723" dependencies = [ "approx", "arrayvec", @@ -2503,9 +2494,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66618389e4ec1c7afe67d51a9bf34ff9236480f8d51e7489b7d5ab0303c13f34" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", "toml_edit", @@ -2549,9 +2540,9 @@ checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" [[package]] name = "rapier3d" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c3a4adb69feadac44d74be058aaaec4502f9c193ec20c0e629b07387ee61a2" +checksum = "62a8a0bd9d3135f7b4eb45d0796540e7bab47b6b7c974f90567ccc5a0454f42b" dependencies = [ "approx", "arrayvec", @@ -2807,9 +2798,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg", ] @@ -2923,9 +2914,9 @@ checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2" [[package]] name = "syn" -version = "1.0.107" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -3010,19 +3001,19 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" [[package]] name = "toml_edit" -version = "0.18.1" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b" +checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825" dependencies = [ "indexmap", - "nom8", "toml_datetime", + "winnow", ] [[package]] @@ -3357,9 +3348,9 @@ dependencies = [ [[package]] name = "wide" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feff0a412894d67223777b6cc8d68c0dab06d52d95e9890d5f2d47f10dd9366c" +checksum = "b689b6c49d6549434bf944e6b0f39238cf63693cb7a147e9d887507fffa3b223" dependencies = [ "bytemuck", "safe_arch", @@ -3607,6 +3598,15 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "winnow" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf09497b8f8b5ac5d3bb4d05c0a99be20f26fd3d5f2db7b0716e946d5103658" +dependencies = [ + "memchr", +] + [[package]] name = "x11-dl" version = "2.21.0" diff --git a/Cargo.toml b/Cargo.toml index 2b5ac42..bbc3df1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_fps_controller" -version = "0.1.7-dev" +version = "0.1.8-dev" edition = "2021" authors = ["bevy_fps_controller"] repository = "https://github.com/qhdwight/bevy_fps_controller" diff --git a/README.md b/README.md index 0fe8e86..c4e288a 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,16 @@ Inspired from Source engine movement, this plugin implements movement suitable f ### Features -* Air strafing -* Bunny hopping (hold down jump) -* Sprinting +* Air strafing and bunny hopping (hold down jump key) +* Support for sloped ground +* Crouching (prevents falling off ledges), sprinting * Noclip mode * Configurable settings -* SOON: crouching, walking ### Examples See [main.rs](./examples/minimal.rs) + ```bash cargo run --example minimal ``` @@ -34,13 +34,22 @@ fn main() { ... .add_plugin(RapierPhysicsPlugin::::default()) .add_plugin(FpsControllerPlugin) + .add_startup_system(setup) ... } -fn setup(...) { +fn setup(mut commands: Commands, ...) { ... commands.spawn(( Collider::capsule(Vec3::Y * 0.5, Vec3::Y * 1.5, 0.5), + Friction { + coefficient: 0.0, + combine_rule: CoefficientCombineRule::Min, + }, + Restitution { + coefficient: 0.0, + combine_rule: CoefficientCombineRule::Min, + }, ActiveEvents::COLLISION_EVENTS, Velocity::zero(), RigidBody::Dynamic, @@ -68,6 +77,6 @@ fn setup(...) { ### Demo -Used by my other project: https://github.com/qhdwight/voxel-game-rs +https://user-images.githubusercontent.com/20666629/221995601-2ec352fe-a8b0-4f8c-9a81-beaf898b2b41.mp4 -https://user-images.githubusercontent.com/20666629/157115719-719a1e7b-a308-4239-919f-8daa9f2ef6e3.mp4 +Used by my other project: https://github.com/qhdwight/voxel-game-rs diff --git a/assets/fira_mono.ttf b/assets/fira_mono.ttf new file mode 100644 index 0000000..1e95ced Binary files /dev/null and b/assets/fira_mono.ttf differ diff --git a/assets/playground.blend b/assets/playground.blend new file mode 100644 index 0000000..7e8c503 Binary files /dev/null and b/assets/playground.blend differ diff --git a/assets/playground.glb b/assets/playground.glb new file mode 100644 index 0000000..50908b4 Binary files /dev/null and b/assets/playground.glb differ diff --git a/assets/texture.png b/assets/texture.png new file mode 100644 index 0000000..323df84 Binary files /dev/null and b/assets/texture.png differ diff --git a/examples/minimal.rs b/examples/minimal.rs index f1678f8..15f98d3 100644 --- a/examples/minimal.rs +++ b/examples/minimal.rs @@ -1,6 +1,9 @@ use std::f32::consts::TAU; use bevy::{ + gltf::{GltfMesh, GltfNode}, + gltf::Gltf, + math::Vec3Swizzles, prelude::*, window::CursorGrabMode, }; @@ -8,11 +11,13 @@ use bevy_rapier3d::prelude::*; use bevy_fps_controller::controller::*; +const SPAWN_POINT: Vec3 = Vec3::new(0.0, 1.0, 0.0); + fn main() { App::new() .insert_resource(AmbientLight { color: Color::WHITE, - brightness: 0.25, + brightness: 0.5, }) .insert_resource(ClearColor(Color::hex("D4F5F5").unwrap())) .insert_resource(RapierConfiguration::default()) @@ -28,21 +33,23 @@ fn main() { .add_plugin(FpsControllerPlugin) .add_startup_system(setup) .add_system(manage_cursor) + .add_system(scene_colliders) + .add_system(display_text) + .add_system(respawn) .run(); } fn setup( mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, + assets: Res, ) { commands.spawn(DirectionalLightBundle { directional_light: DirectionalLight { - illuminance: 2000.0, + illuminance: 6000.0, shadows_enabled: true, ..default() }, - transform: Transform::from_xyz(-38.0, 40.0, 34.0), + transform: Transform::from_xyz(4.0, 7.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y), ..default() }); @@ -69,101 +76,144 @@ fn setup( AdditionalMassProperties::Mass(1.0), GravityScale(0.0), Ccd { enabled: true }, // Prevent clipping when going fast - TransformBundle::from_transform(Transform::from_xyz(0.0, 3.0, 0.0)), + TransformBundle::from_transform(Transform::from_translation(SPAWN_POINT)), LogicalPlayer(0), FpsControllerInput { pitch: -TAU / 12.0, yaw: TAU * 5.0 / 8.0, ..default() }, - FpsController { ..default() } - )); - commands.spawn(( - Camera3dBundle::default(), - RenderPlayer(0), + FpsController { + air_acceleration: 80.0, + ..default() + } )); - - // Floor commands.spawn(( - PbrBundle { - mesh: meshes.add(Mesh::from(shape::Box { - min_x: -20.0, - max_x: 20.0, - min_y: -0.25, - max_y: 0.25, - min_z: -20.0, - max_z: 20.0, - })), - material: materials.add(StandardMaterial { - base_color: Color::hex("8C9A9E").unwrap(), + Camera3dBundle { + projection: Projection::Perspective(PerspectiveProjection { + fov: TAU / 5.0, ..default() }), - transform: Transform::from_xyz(0.0, -0.25, 0.0), ..default() }, - Collider::cuboid(20.0, 0.25, 20.0), - RigidBody::Fixed, + RenderPlayer(0), )); - let material = materials.add(StandardMaterial { - base_color: Color::hex("747578").unwrap(), - ..default() + commands.insert_resource(MainScene { + handle: assets.load("playground.glb"), + is_loaded: false, }); - commands.spawn(( - PbrBundle { - mesh: meshes.add(Mesh::from(shape::Box { - min_x: -1.0, - max_x: 1.0, - min_y: -0.5, - max_y: 0.5, - min_z: -1.0, - max_z: 1.0, - })), - material: material.clone(), - transform: Transform::from_xyz(4.0, 0.5, 4.0), - ..default() + + commands.spawn(TextBundle::from_section( + "", + TextStyle { + font: assets.load("fira_mono.ttf"), + font_size: 24.0, + color: Color::BLACK, }, - Collider::cuboid(1.0, 1.0, 1.0), - RigidBody::Fixed, - )); - commands.spawn(( - PbrBundle { - mesh: meshes.add(Mesh::from(shape::Box { - min_x: -1.0, - max_x: 1.0, - min_y: -1.0, - max_y: 1.0, - min_z: -1.0, - max_z: 1.0, - })), - material: material.clone(), - transform: Transform::from_xyz(2.0, 1.0, 4.0), + ).with_style(Style { + position_type: PositionType::Absolute, + position: UiRect { + top: Val::Px(5.0), + left: Val::Px(5.0), ..default() }, - Collider::cuboid(1.0, 2.0, 1.0), - RigidBody::Fixed, - )); + ..default() + })); } -pub fn manage_cursor( +fn respawn( + mut query: Query<(&mut Transform, &mut Velocity)>, +) { + for (mut transform, mut velocity) in &mut query { + if transform.translation.y > -50.0 { + continue; + } + + velocity.linvel = Vec3::ZERO; + transform.translation = SPAWN_POINT; + } +} + +#[derive(Resource)] +struct MainScene { + handle: Handle, + is_loaded: bool, +} + +fn scene_colliders( + mut commands: Commands, + mut main_scene: ResMut, + gltf_assets: Res>, + gltf_mesh_assets: Res>, + gltf_node_assets: Res>, + mesh_assets: Res>, +) { + if main_scene.is_loaded { + return; + } + + let gltf = gltf_assets.get(&main_scene.handle); + + if let Some(gltf) = gltf { + let scene = gltf.scenes.first().unwrap().clone(); + commands.spawn(SceneBundle { + scene, + ..default() + }); + for node in &gltf.nodes { + let node = gltf_node_assets.get(&node).unwrap(); + if let Some(gltf_mesh) = node.mesh.clone() { + let gltf_mesh = gltf_mesh_assets.get(&gltf_mesh).unwrap(); + for mesh_primitive in &gltf_mesh.primitives { + let mesh = mesh_assets.get(&mesh_primitive.mesh).unwrap(); + commands.spawn(( + Collider::from_bevy_mesh(mesh, &ComputedColliderShape::TriMesh).unwrap(), + RigidBody::Fixed, + TransformBundle::from_transform(node.transform), + )); + } + } + } + main_scene.is_loaded = true; + } +} + +fn manage_cursor( mut windows: ResMut, btn: Res>, key: Res>, - mut controllers: Query<&mut FpsController>, + mut query: Query<&mut FpsController>, ) { let window = windows.get_primary_mut().unwrap(); if btn.just_pressed(MouseButton::Left) { window.set_cursor_grab_mode(CursorGrabMode::Locked); window.set_cursor_visibility(false); - for mut controller in &mut controllers { + for mut controller in &mut query { controller.enable_input = true; } } if key.just_pressed(KeyCode::Escape) { window.set_cursor_grab_mode(CursorGrabMode::None); window.set_cursor_visibility(true); - for mut controller in &mut controllers { + for mut controller in &mut query { controller.enable_input = false; } } } + +fn display_text( + mut controller_query: Query<(&Transform, &Velocity)>, + mut text_query: Query<&mut Text>, +) { + for (transform, velocity) in &mut controller_query { + for mut text in &mut text_query { + text.sections[0].value = format!( + "vel: {:.2}, {:.2}, {:.2}\npos: {:.2}, {:.2}, {:.2}\nspd: {:.2}", + velocity.linvel.x, velocity.linvel.y, velocity.linvel.z, + transform.translation.x, transform.translation.y, transform.translation.z, + velocity.linvel.xz().length() + ); + } + } +} diff --git a/src/controller.rs b/src/controller.rs index 69b9f90..9454d15 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -66,9 +66,9 @@ pub struct FpsController { pub max_air_speed: f32, pub acceleration: f32, pub friction: f32, - /// If the dot product of the normal of the surface and the upward vector, - /// which is a value from [-1, 1], is greater than this value, friction will be applied - pub friction_normal_cutoff: f32, + /// If the dot product (alignment) of the normal of the surface and the upward vector, + /// which is a value from [-1, 1], is greater than this value, ground movement is applied + pub traction_normal_cutoff: f32, pub friction_speed_cutoff: f32, pub jump_speed: f32, pub fly_speed: f32, @@ -82,11 +82,11 @@ pub struct FpsController { pub fly_friction: f32, pub pitch: f32, pub yaw: f32, - pub velocity: Vec3, pub ground_tick: u8, pub stop_speed: f32, pub sensitivity: f32, pub enable_input: bool, + pub step_offset: f32, pub key_forward: KeyCode, pub key_back: KeyCode, pub key_left: KeyCode, @@ -122,15 +122,15 @@ impl Default for FpsController { crouch_height: 1.25, acceleration: 10.0, friction: 10.0, - friction_normal_cutoff: 0.7, + traction_normal_cutoff: 0.7, friction_speed_cutoff: 0.1, fly_friction: 0.5, pitch: 0.0, yaw: 0.0, - velocity: Vec3::ZERO, ground_tick: 0, stop_speed: 1.0, jump_speed: 8.5, + step_offset: 0.0, enable_input: true, key_forward: KeyCode::W, key_back: KeyCode::S, @@ -174,9 +174,11 @@ pub fn fps_controller_input( } mouse_delta *= controller.sensitivity; - input.pitch = (input.pitch - mouse_delta.y) - .clamp(-FRAC_PI_2 + ANGLE_EPSILON, FRAC_PI_2 - ANGLE_EPSILON); - input.yaw = input.yaw - mouse_delta.x; + input.pitch = (input.pitch - mouse_delta.y).clamp(-FRAC_PI_2 + ANGLE_EPSILON, FRAC_PI_2 - ANGLE_EPSILON); + input.yaw -= mouse_delta.x; + if input.yaw.abs() > PI { + input.yaw = input.yaw.rem_euclid(TAU); + } } input.movement = Vec3::new( @@ -212,7 +214,7 @@ pub fn fps_controller_move( ) { let dt = time.delta_seconds(); - for (entity, input, mut controller, mut collider, transform, mut velocity) in query.iter_mut() { + for (entity, input, mut controller, mut collider, mut transform, mut velocity) in query.iter_mut() { if input.fly { controller.move_mode = match controller.move_mode { MoveMode::Noclip => MoveMode::Ground, @@ -220,23 +222,13 @@ pub fn fps_controller_move( } } - let orientation = if controller.move_mode == MoveMode::Noclip { - look_quat(input.pitch, input.yaw) - } else { - Quat::from_axis_angle(Vec3::Y, input.yaw) - }; - let right = orientation * Vec3::X; - let forward = orientation * -Vec3::Z; - let position = transform.translation; - let rotation = transform.rotation; - match controller.move_mode { MoveMode::Noclip => { if input.movement == Vec3::ZERO { let friction = controller.fly_friction.clamp(0.0, 1.0); - controller.velocity *= 1.0 - friction; - if controller.velocity.length_squared() < 1e-6 { - controller.velocity = Vec3::ZERO; + velocity.linvel *= 1.0 - friction; + if velocity.linvel.length_squared() < f32::EPSILON { + velocity.linvel = Vec3::ZERO; } } else { let fly_speed = if input.sprint { @@ -244,49 +236,40 @@ pub fn fps_controller_move( } else { controller.fly_speed }; - controller.velocity = input.movement.normalize() * fly_speed; + let mut move_to_world = Mat3::from_euler(EulerRot::YXZ, input.yaw, input.pitch, 0.0); + move_to_world.z_axis *= -1.0; // Forward is -Z + move_to_world.y_axis = Vec3::Y; // Vertical movement aligned with world up + velocity.linvel = move_to_world * input.movement * fly_speed; } - velocity.linvel = controller.velocity.x * right - + controller.velocity.y * Vec3::Y - + controller.velocity.z * forward; } - MoveMode::Ground => { if let Some(capsule) = collider.as_capsule() { - let capsule = capsule.raw; - let mut start_velocity = controller.velocity; - let mut end_velocity = start_velocity; - let lateral_speed = start_velocity.xz().length(); - // Capsule cast downwards to find ground - // Better than single raycast as it handles when you are near the edge of a surface - let mut ground_hit = None; + // Better than a ray cast as it handles when you are near the edge of a surface + let capsule = capsule.raw; let cast_capsule = Collider::capsule( - capsule.segment.a.into(), - capsule.segment.b.into(), - capsule.radius * 0.9375, + capsule.segment.a.into(), capsule.segment.b.into(), + capsule.radius * 0.9, ); // Avoid self collisions - let cast_groups = QueryFilter::default().exclude_rigid_body(entity); - if let Some((_handle, hit)) = physics_context.cast_shape( - position, - rotation, + let filter = QueryFilter::default().exclude_rigid_body(entity); + let ground_cast = physics_context.cast_shape( + transform.translation, transform.rotation, -Vec3::Y, &cast_capsule, 0.125, - cast_groups, - ) { - ground_hit = Some(hit); - } + filter, + ); - let mut wish_direction = input.movement.z * controller.forward_speed * forward - + input.movement.x * controller.side_speed * right; + let speeds = Vec3::new(controller.side_speed, 0.0, controller.forward_speed); + let mut move_to_world = Mat3::from_axis_angle(Vec3::Y, input.yaw); + move_to_world.z_axis *= -1.0; // Forward is -Z + let mut wish_direction = move_to_world * (input.movement * speeds); let mut wish_speed = wish_direction.length(); - if wish_speed > 1e-6 { + if wish_speed > f32::EPSILON { // Avoid division by zero wish_direction /= wish_speed; // Effectively normalize, avoid length computation twice } - let max_speed = if input.crouch { controller.crouched_speed } else if input.sprint { @@ -294,75 +277,74 @@ pub fn fps_controller_move( } else { controller.walk_speed }; - wish_speed = f32::min(wish_speed, max_speed); - let apply_friction = ground_hit.map(|hit| { - // "dot" will be [-1, 1] and tells us how aligned we are with the surface normal - // A value close to 1 means we are on flat ground - let dot = Vec3::dot(hit.normal1, Vec3::Y); - dot > controller.friction_normal_cutoff - }).unwrap_or(false); + if let Some((_, toi)) = ground_cast { + let has_traction = Vec3::dot(toi.normal1, Vec3::Y) > controller.traction_normal_cutoff; - if apply_friction { // Only apply friction after at least one tick, allows b-hopping without losing speed - if controller.ground_tick >= 1 { + if controller.ground_tick >= 1 && has_traction { + let lateral_speed = velocity.linvel.xz().length(); if lateral_speed > controller.friction_speed_cutoff { - friction( - lateral_speed, - controller.friction, - controller.stop_speed, - dt, - &mut end_velocity, - ); + let control = f32::max(lateral_speed, controller.stop_speed); + let drop = control * controller.friction * dt; + let new_speed = f32::max((lateral_speed - drop) / lateral_speed, 0.0); + velocity.linvel.x *= new_speed; + velocity.linvel.z *= new_speed; } else { - end_velocity.x = 0.0; - end_velocity.z = 0.0; + velocity.linvel = Vec3::ZERO; + } + if controller.ground_tick == 1 { + velocity.linvel.y = -toi.toi; } - end_velocity.y = 0.0; } - accelerate( + + let mut add = acceleration( wish_direction, wish_speed, controller.acceleration, + velocity.linvel, dt, - &mut end_velocity, ); - if input.jump { - // Simulate one update ahead, since this is an instant velocity change - start_velocity.y = controller.jump_speed; - end_velocity.y = start_velocity.y - controller.gravity * dt; + if !has_traction { + add.y -= controller.gravity * dt; + } + velocity.linvel += add; + + if has_traction { + let linvel = velocity.linvel; + velocity.linvel -= Vec3::dot(linvel, toi.normal1) * toi.normal1; + + if input.jump { + velocity.linvel.y = controller.jump_speed; + } } + // Increment ground tick but cap at max value controller.ground_tick = controller.ground_tick.saturating_add(1); } else { controller.ground_tick = 0; wish_speed = f32::min(wish_speed, controller.air_speed_cap); - accelerate( + + let mut add = acceleration( wish_direction, wish_speed, controller.air_acceleration, + velocity.linvel, dt, - &mut end_velocity, ); - end_velocity.y -= controller.gravity * dt; - let air_speed = end_velocity.xz().length(); + add.y = -controller.gravity * dt; + velocity.linvel += add; + + let air_speed = velocity.linvel.xz().length(); if air_speed > controller.max_air_speed { let ratio = controller.max_air_speed / air_speed; - end_velocity.x *= ratio; - end_velocity.z *= ratio; + velocity.linvel.x *= ratio; + velocity.linvel.z *= ratio; } } - // TODO: try to add this in - // At this point our collider may be intersecting with the ground - // Fix up our collider by offsetting it to be flush with the ground - // if end_vel.y < -1e6 { - // if let Some(ground_hit) = ground_hit { - // let normal = Vec3::from(*ground_hit.normal2); - // next_translation += normal * ground_hit.toi; - // } - // } + /* Crouching */ let crouch_height = controller.crouch_height; let upright_height = controller.upright_height; @@ -382,55 +364,82 @@ pub fn fps_controller_move( ); } - let mut motion = (start_velocity + end_velocity) * 0.5; - - // Prevent falling off of ledges - // TODO: instead of setting to zero subtract out the part that would make us fall - if input.crouch && ground_hit.is_some() && physics_context.cast_shape( - position + Vec3::new(motion.x, 0.0, motion.z) * dt, - rotation, - -Vec3::Y, - &cast_capsule, - 0.125, - cast_groups, - ).is_none() { - motion.x = 0.0; - motion.z = 0.0; - end_velocity.x = 0.0; - end_velocity.z = 0.0; + // Step offset + if controller.step_offset > f32::EPSILON && controller.ground_tick >= 1 { + let cast_offset = velocity.linvel.normalize_or_zero() * controller.radius * 1.0625; + let cast = physics_context.cast_ray_and_get_normal( + transform.translation + cast_offset + Vec3::Y * controller.step_offset * 1.0625, + -Vec3::Y, + controller.step_offset * 0.9375, + false, + filter, + ); + if let Some((_, hit)) = cast { + transform.translation.y += controller.step_offset * 1.0625 - hit.toi; + transform.translation += cast_offset; + } } - controller.velocity = end_velocity; - velocity.linvel = motion; + // Prevent falling off ledges + if controller.ground_tick >= 1 && input.crouch { + for _ in 0..2 { + // Find the component of our velocity that is overhanging and subtract it off + let overhang = overhang_component(entity, transform.as_ref(), physics_context.as_ref(), velocity.linvel, dt); + if let Some(overhang) = overhang { + velocity.linvel -= overhang; + } + } + // If we are still overhanging consider unsolvable and freeze + if overhang_component(entity, transform.as_ref(), physics_context.as_ref(), velocity.linvel, dt).is_some() { + velocity.linvel = Vec3::ZERO; + } + } } } } } } -fn look_quat(pitch: f32, yaw: f32) -> Quat { - Quat::from_euler(EulerRot::ZYX, 0.0, yaw, pitch) -} - -fn friction(lateral_speed: f32, friction: f32, stop_speed: f32, dt: f32, velocity: &mut Vec3) { - let control = f32::max(lateral_speed, stop_speed); - let drop = control * friction * dt; - let new_speed = f32::max((lateral_speed - drop) / lateral_speed, 0.0); - velocity.x *= new_speed; - velocity.z *= new_speed; +fn overhang_component(entity: Entity, transform: &Transform, physics_context: &RapierContext, velocity: Vec3, dt: f32) -> Option { + // Cast a segment (zero radius on capsule) from our next position back towards us + // If there is a ledge in front of us we will hit the edge of it + // We can use the normal of the hit to subtract off the component that is overhanging + let cast_capsule = Collider::capsule(Vec3::Y * 0.125, -Vec3::Y * 0.125, 0.0); + let filter = QueryFilter::default().exclude_rigid_body(entity); + let future_position = transform.translation + velocity * dt; + let cast = physics_context.cast_shape( + future_position, transform.rotation, + -velocity, + &cast_capsule, + 0.5, + filter, + ); + if let Some((_, toi)) = cast { + let cast = physics_context.cast_ray( + future_position + Vec3::Y * 0.125, -Vec3::Y, + 0.375, + false, + filter, + ); + // Make sure that this is actually a ledge, e.g. there is no ground in front of us + if cast.is_none() { + let normal = -toi.normal1; + let alignment = Vec3::dot(velocity, normal); + return Some(alignment * normal); + } + } + None } -fn accelerate(wish_dir: Vec3, wish_speed: f32, accel: f32, dt: f32, velocity: &mut Vec3) { - let velocity_projection = Vec3::dot(*velocity, wish_dir); +fn acceleration(wish_direction: Vec3, wish_speed: f32, acceleration: f32, velocity: Vec3, dt: f32) -> Vec3 { + let velocity_projection = Vec3::dot(velocity, wish_direction); let add_speed = wish_speed - velocity_projection; if add_speed <= 0.0 { - return; + return Vec3::ZERO; } - let accel_speed = f32::min(accel * wish_speed * dt, add_speed); - let wish_direction = wish_dir * accel_speed; - velocity.x += wish_direction.x; - velocity.z += wish_direction.z; + let acceleration_speed = f32::min(acceleration * wish_speed * dt, add_speed); + wish_direction * acceleration_speed } fn get_pressed(key_input: &Res>, key: KeyCode) -> f32 { @@ -468,9 +477,8 @@ pub fn fps_controller_render( } // TODO: let this be more configurable let camera_height = capsule.segment().b().y + capsule.radius() * 0.75; - render_transform.translation = - logical_transform.translation + Vec3::Y * camera_height; - render_transform.rotation = look_quat(controller.pitch, controller.yaw); + render_transform.translation = logical_transform.translation + Vec3::Y * camera_height; + render_transform.rotation = Quat::from_euler(EulerRot::YXZ, controller.yaw, controller.pitch, 0.0); } } }