From f80e1a9d0b8288a8d6dce7df8efffffdefa4b3d3 Mon Sep 17 00:00:00 2001 From: GitGhillie Date: Fri, 21 Mar 2025 11:54:03 +0100 Subject: [PATCH] Kinematic character controller (#11) --- Cargo.lock | 250 +++++++++------------ Cargo.toml | 2 - src/camera.rs | 2 +- src/controller.rs | 539 +++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 9 +- src/player.rs | 95 ++------ src/tb_entities.rs | 10 +- 7 files changed, 672 insertions(+), 235 deletions(-) create mode 100644 src/controller.rs diff --git a/Cargo.lock b/Cargo.lock index 3aa5189..8daba38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,7 +94,7 @@ dependencies = [ "getrandom", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -428,37 +428,6 @@ dependencies = [ "syn", ] -[[package]] -name = "bevy-tnua" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a5783db12b6f927e7cc9976c15e23fd5b107319a9c1be8b92622fa10b94a62" -dependencies = [ - "bevy", - "bevy-tnua-physics-integration-layer", - "thiserror 1.0.69", -] - -[[package]] -name = "bevy-tnua-avian3d" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3348f84267ced2c4535bd3415b384df84d49b9d04824442aef7f3b6b3af37c" -dependencies = [ - "avian3d", - "bevy", - "bevy-tnua-physics-integration-layer", -] - -[[package]] -name = "bevy-tnua-physics-integration-layer" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbe99d25b7253cb99a00f69fe9accc875645b4ecf9910bb5f24459835ff177e" -dependencies = [ - "bevy", -] - [[package]] name = "bevy_a11y" version = "0.15.3" @@ -996,9 +965,9 @@ dependencies = [ [[package]] name = "bevy_materialize" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20040879c95d5f3210eafd7fc00d64e9e75b5cc7eddbedfc752bd9c73b8b12e2" +checksum = "9b7309299cf4caf0bf915582a2e3dd5fbd1b564c72611a5f464e6c85ca7f2d23" dependencies = [ "bevy", "serde", @@ -1589,9 +1558,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "675f87afced0413c9bb02843499dbbd3882a237645883f71a2b59644a6d2f753" +checksum = "b17679a8d69b6d7fd9cd9801a536cec9fa5e5970b69f9d4747f70b39b031f5e7" dependencies = [ "arrayref", "arrayvec", @@ -1645,9 +1614,9 @@ dependencies = [ [[package]] name = "bytemuck_derive" -version = "1.8.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" +checksum = "2ff22c2722516255d1823ce3cc4bc0b154dbc9364be5c905d6baa6eccbbc8774" dependencies = [ "proc-macro2", "quote", @@ -2424,9 +2393,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "font-types" @@ -2577,9 +2546,9 @@ dependencies = [ [[package]] name = "gilrs-core" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ed3920aa2e2a5b02fb67182e269b7c988ffbba86e1278bb9f9bbe1815e3ae1" +checksum = "0383b9f5df02975b56f25775330ba2f70ff181fe1091f698c8737868696eb856" dependencies = [ "core-foundation 0.10.0", "inotify", @@ -2593,7 +2562,7 @@ dependencies = [ "vec_map", "wasm-bindgen", "web-sys", - "windows 0.59.0", + "windows 0.60.0", ] [[package]] @@ -2802,8 +2771,6 @@ dependencies = [ "avian3d", "bevy", "bevy-inspector-egui", - "bevy-tnua", - "bevy-tnua-avian3d", "bevy_asset_loader", "bevy_sprite3d", "bevy_trenchbroom", @@ -2864,9 +2831,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -3039,9 +3006,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.170" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libloading" @@ -3724,9 +3691,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.3" +version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" [[package]] name = "orbclient" @@ -3956,11 +3923,11 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy", + "zerocopy 0.8.23", ] [[package]] @@ -3971,9 +3938,9 @@ checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" [[package]] name = "prettyplease" -version = "0.2.30" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1ccf34da56fc294e7d4ccf69a85992b7dfb826b7cf57bac6a70bba3494cc08a" +checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" dependencies = [ "proc-macro2", "syn", @@ -4074,9 +4041,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -4421,18 +4388,18 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -4668,9 +4635,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.99" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -5490,12 +5457,24 @@ dependencies = [ [[package]] name = "windows" -version = "0.59.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1" +checksum = "ddf874e74c7a99773e62b1c671427abf01a425e77c3d3fb9fb1e4883ea934529" dependencies = [ - "windows-core 0.59.0", - "windows-targets 0.53.0", + "windows-collections", + "windows-core 0.60.1", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5467f79cc1ba3f52ebb2ed41dbb459b8e7db636cc3429458d9a852e15bc24dec" +dependencies = [ + "windows-core 0.60.1", ] [[package]] @@ -5535,15 +5514,25 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.59.0" +version = "0.60.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce" +checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247" dependencies = [ "windows-implement 0.59.0", - "windows-interface 0.59.0", - "windows-result 0.3.1", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.2", "windows-strings 0.3.1", - "windows-targets 0.53.0", +] + +[[package]] +name = "windows-future" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a787db4595e7eb80239b74ce8babfb1363d8e343ab072f2ffe901400c03349f0" +dependencies = [ + "windows-core 0.60.1", + "windows-link", ] [[package]] @@ -5603,9 +5592,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.0" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", @@ -5614,9 +5603,19 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-numerics" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005dea54e2f6499f2cee279b8f703b3cf3b5734a2d8d21867c8f44003182eeed" +dependencies = [ + "windows-core 0.60.1", + "windows-link", +] [[package]] name = "windows-result" @@ -5638,9 +5637,9 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ "windows-link", ] @@ -5739,29 +5738,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" -dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -5780,12 +5763,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -5804,12 +5781,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -5828,24 +5799,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -5864,12 +5823,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -5888,12 +5841,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -5912,12 +5859,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -5936,12 +5877,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - [[package]] name = "winit" version = "0.30.9" @@ -5996,9 +5931,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" dependencies = [ "memchr", ] @@ -6084,8 +6019,16 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +dependencies = [ + "zerocopy-derive 0.8.23", ] [[package]] @@ -6098,3 +6041,14 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index a7eba27..53428a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,6 @@ bevy = "0.15.3" bevy_trenchbroom = { version = "0.6.5", features = ["auto_register", "avian"] } nil = "0.14.0" bevy-inspector-egui = "0.29.1" -bevy-tnua = "0.21.0" -bevy-tnua-avian3d = "0.2.0" bevy_asset_loader = "0.22.0" bevy_sprite3d = "4.0.0" rand = "0.8.5" diff --git a/src/camera.rs b/src/camera.rs index 16c450d..4c69e1e 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -1,7 +1,7 @@ use avian3d::prelude::*; use bevy::prelude::*; -use crate::{controls::Controls, physics_layers::GameLayer, player::PlayerMovement}; +use crate::{controller::PlayerMovement, controls::Controls, physics_layers::GameLayer}; #[derive(Component, Reflect, Debug)] pub struct CameraTarget; diff --git a/src/controller.rs b/src/controller.rs new file mode 100644 index 0000000..a0eb8c2 --- /dev/null +++ b/src/controller.rs @@ -0,0 +1,539 @@ +use avian3d::{math::*, prelude::*}; +use bevy::{ecs::query::Has, prelude::*}; + +use crate::player::PlayerRig; + +pub struct CharacterControllerPlugin; + +impl Plugin for CharacterControllerPlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + app.init_resource::(); + app.register_type::(); + app.register_type::(); + app.register_type::(); + app.register_type::(); + app.register_type::(); + + app.add_event::(); + app.add_systems( + Update, + ( + keyboard_input, + gamepad_input, + clear_movement_flag, + brake_on_release, + update_grounded, + apply_gravity, + movement, + apply_movement_damping, + ) + .chain(), + ); + 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, + ); + } +} + +/// An event sent for a movement input action. +#[derive(Event)] +pub enum MovementAction { + Move(Vector2), + Jump, +} + +/// 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, +} + +#[derive(Resource, Reflect)] +#[reflect(Resource)] +struct MovementSettings { + damping_normal: f32, + damping_brake: f32, + damping_brake_air: f32, +} + +// todo some duplicate with player.rs settings +impl Default for MovementSettings { + fn default() -> Self { + Self { + damping_normal: 1.0, + damping_brake: 30.0, + damping_brake_air: 1.0, + } + } +} + +/// The acceleration used for character movement. +#[derive(Component, Reflect)] +#[reflect(Component)] +pub struct MovementAcceleration(Scalar); + +/// The damping factor used for slowing down movement. +#[derive(Component, Reflect)] +#[reflect(Component)] +pub struct MovementDampingFactor(Scalar); + +/// 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(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, + 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, gravity: Vector) -> 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: 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 + } +} + +/// Sends [`MovementAction`] events based on keyboard input. +fn keyboard_input( + mut movement_event_writer: EventWriter, + keyboard_input: Res>, +) { + let up_binds = [KeyCode::KeyW, KeyCode::ArrowUp]; + let down_binds = [KeyCode::KeyS, KeyCode::ArrowDown]; + let left_binds = [KeyCode::KeyA, KeyCode::ArrowLeft]; + let right_binds = [KeyCode::KeyD, KeyCode::ArrowRight]; + + let up = keyboard_input.any_pressed(up_binds); + let down = keyboard_input.any_pressed(down_binds); + let left = keyboard_input.any_pressed(left_binds); + let right = keyboard_input.any_pressed(right_binds); + + 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_event_writer.send(MovementAction::Move(direction)); + } + + if keyboard_input.just_pressed(KeyCode::Space) { + movement_event_writer.send(MovementAction::Jump); + } +} + +/// Sends [`MovementAction`] events based on gamepad input. +fn gamepad_input( + mut movement_event_writer: EventWriter, + gamepads: Query<&Gamepad>, +) { + for gamepad in gamepads.iter() { + if let (Some(x), Some(y)) = ( + gamepad.get(GamepadAxis::LeftStickX), + gamepad.get(GamepadAxis::LeftStickY), + ) { + let deadzone = 0.01; + let dir = Vector2::new(x as Scalar, y as Scalar).clamp_length_max(1.0); + + if dir.length_squared() > deadzone { + movement_event_writer.send(MovementAction::Move(dir)); + } + } + + if gamepad.just_pressed(GamepadButton::South) { + movement_event_writer.send(MovementAction::Jump); + } + } +} + +/// Apply extra friction when no movement input is given +/// In the original you stop instantly in this case +fn brake_on_release( + player_movement: Res, + movement_settings: Res, + mut damping_q: Query<(Entity, &mut MovementDampingFactor)>, + grounded_q: Query<&Grounded>, +) { + for (entity, mut damping) in &mut damping_q { + let is_grounded = grounded_q.get(entity).is_ok(); + + if !player_movement.any_direction && is_grounded { + damping.0 = movement_settings.damping_brake; + } else if !player_movement.any_direction && !is_grounded { + damping.0 = movement_settings.damping_brake_air; + } else { + damping.0 = movement_settings.damping_normal; + } + } +} + +/// 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 { + commands.entity(entity).insert(Grounded); + } else { + commands.entity(entity).remove::(); + } + } +} + +fn clear_movement_flag( + mut player_movement: ResMut, + movement_event_reader: EventReader, +) { + if movement_event_reader.is_empty() { + player_movement.any_direction = false; + } +} + +/// Responds to [`MovementAction`] events and moves character controllers accordingly. +fn movement( + time: Res