avian start

This commit is contained in:
Mitchell Marino 2026-01-24 21:17:43 -06:00
parent 12eea03a37
commit 42651aa4be
5 changed files with 859 additions and 34 deletions

455
Cargo.lock generated
View File

@ -103,6 +103,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "alsa" name = "alsa"
version = "0.9.1" version = "0.9.1"
@ -308,6 +314,45 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "avian2d"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "060ae40aced85ed01296f556ed1909fcafe81a5e5059889bed126339ccd55b43"
dependencies = [
"approx",
"arrayvec",
"avian_derive",
"bevy",
"bevy_heavy",
"bevy_math",
"bevy_transform_interpolation",
"bitflags 2.10.0",
"derive_more",
"disqualified",
"glam_matrix_extras",
"itertools 0.13.0",
"nalgebra",
"parry2d",
"parry2d-f64",
"slab",
"smallvec",
"thiserror 2.0.17",
"thread_local",
]
[[package]]
name = "avian_derive"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b257f601a1535e0d4a7a7796f535e3a13de62fd422b16dff7c14d27f0d4048"
dependencies = [
"proc-macro-error2",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
@ -794,6 +839,17 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "bevy_heavy"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc496d1d43b890896cf561d8ce3dcf7b7b8e4c03c4e837a49a83a530abccbc5e"
dependencies = [
"bevy_math",
"bevy_reflect",
"glam_matrix_extras",
]
[[package]] [[package]]
name = "bevy_image" name = "bevy_image"
version = "0.18.0" version = "0.18.0"
@ -972,7 +1028,7 @@ dependencies = [
"arrayvec", "arrayvec",
"bevy_reflect", "bevy_reflect",
"derive_more", "derive_more",
"glam", "glam 0.30.10",
"itertools 0.14.0", "itertools 0.14.0",
"libm", "libm",
"rand", "rand",
@ -1146,7 +1202,7 @@ dependencies = [
"downcast-rs 2.0.2", "downcast-rs 2.0.2",
"erased-serde", "erased-serde",
"foldhash 0.2.0", "foldhash 0.2.0",
"glam", "glam 0.30.10",
"indexmap", "indexmap",
"inventory", "inventory",
"petgraph", "petgraph",
@ -1206,7 +1262,7 @@ dependencies = [
"downcast-rs 2.0.2", "downcast-rs 2.0.2",
"encase", "encase",
"fixedbitset", "fixedbitset",
"glam", "glam 0.30.10",
"image", "image",
"indexmap", "indexmap",
"js-sys", "js-sys",
@ -1436,6 +1492,15 @@ dependencies = [
"thiserror 2.0.17", "thiserror 2.0.17",
] ]
[[package]]
name = "bevy_transform_interpolation"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88545c8b0fa8f3502b9a439c71fa6b596ee9e808bfb16f27a51c8c6f7405a657"
dependencies = [
"bevy",
]
[[package]] [[package]]
name = "bevy_ui" name = "bevy_ui"
version = "0.18.0" version = "0.18.0"
@ -2041,6 +2106,25 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-queue" name = "crossbeam-queue"
version = "0.3.12" version = "0.3.12"
@ -2180,6 +2264,15 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "ena"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5"
dependencies = [
"log",
]
[[package]] [[package]]
name = "encase" name = "encase"
version = "0.12.0" version = "0.12.0"
@ -2514,12 +2607,103 @@ dependencies = [
"xml-rs", "xml-rs",
] ]
[[package]]
name = "glam"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "333928d5eb103c5d4050533cec0384302db6be8ef7d3cebd30ec6a35350353da"
[[package]]
name = "glam"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3abb554f8ee44336b72d522e0a7fe86a29e09f839a36022fa869a7dfe941a54b"
[[package]]
name = "glam"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4126c0479ccf7e8664c36a2d719f5f2c140fbb4f9090008098d2c291fa5b3f16"
[[package]]
name = "glam"
version = "0.17.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01732b97afd8508eee3333a541b9f7610f454bb818669e66e90f5f57c93a776"
[[package]]
name = "glam"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525a3e490ba77b8e326fb67d4b44b4bd2f920f44d4cc73ccec50adc68e3bee34"
[[package]]
name = "glam"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b8509e6791516e81c1a630d0bd7fbac36d2fa8712a9da8662e716b52d5051ca"
[[package]]
name = "glam"
version = "0.20.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f43e957e744be03f5801a55472f593d43fabdebf25a4585db250f04d86b1675f"
[[package]]
name = "glam"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "518faa5064866338b013ff9b2350dc318e14cc4fcd6cb8206d7e7c9886c98815"
[[package]]
name = "glam"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f597d56c1bd55a811a1be189459e8fad2bbc272616375602443bdfb37fa774"
[[package]]
name = "glam"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e4afd9ad95555081e109fe1d21f2a30c691b5f0919c67dfa690a2e1eb6bd51c"
[[package]]
name = "glam"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945"
[[package]]
name = "glam"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3"
[[package]]
name = "glam"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9"
[[package]]
name = "glam"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94"
[[package]]
name = "glam"
version = "0.29.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee"
[[package]] [[package]]
name = "glam" name = "glam"
version = "0.30.10" version = "0.30.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9"
dependencies = [ dependencies = [
"approx",
"bytemuck", "bytemuck",
"encase", "encase",
"libm", "libm",
@ -2527,6 +2711,16 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "glam_matrix_extras"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4664468a60479272b880a8bfc00ad2915229b93d2b2d585556fb33f9ba80e72"
dependencies = [
"bevy_reflect",
"glam 0.30.10",
]
[[package]] [[package]]
name = "glob" name = "glob"
version = "0.3.3" version = "0.3.3"
@ -2697,6 +2891,8 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [ dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.1.5", "foldhash 0.1.5",
] ]
@ -2736,7 +2932,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29a164ceff4500f2a72b1d21beaa8aa8ad83aec2b641844c659b190cb3ea2e0b" checksum = "29a164ceff4500f2a72b1d21beaa8aa8ad83aec2b641844c659b190cb3ea2e0b"
dependencies = [ dependencies = [
"constgebra", "constgebra",
"glam", "glam 0.30.10",
"tinyvec", "tinyvec",
] ]
@ -3024,6 +3220,16 @@ dependencies = [
"regex-automata", "regex-automata",
] ]
[[package]]
name = "matrixmultiply"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
dependencies = [
"autocfg",
"rawpointer",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.6" version = "2.7.6"
@ -3124,6 +3330,49 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "nalgebra"
version = "0.34.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4d5b3eff5cd580f93da45e64715e8c20a3996342f1e466599cf7a267a0c2f5f"
dependencies = [
"approx",
"glam 0.14.0",
"glam 0.15.2",
"glam 0.16.0",
"glam 0.17.3",
"glam 0.18.0",
"glam 0.19.0",
"glam 0.20.5",
"glam 0.21.3",
"glam 0.22.0",
"glam 0.23.0",
"glam 0.24.2",
"glam 0.25.0",
"glam 0.27.0",
"glam 0.28.0",
"glam 0.29.3",
"glam 0.30.10",
"matrixmultiply",
"nalgebra-macros",
"num-complex",
"num-rational",
"num-traits",
"simba",
"typenum",
]
[[package]]
name = "nalgebra-macros"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "973e7178a678cfd059ccec50887658d482ce16b0aa9da3888ddeab5cd5eb4889"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "ndk" name = "ndk"
version = "0.8.0" version = "0.8.0"
@ -3223,6 +3472,25 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "num-derive" name = "num-derive"
version = "0.4.2" version = "0.4.2"
@ -3234,6 +3502,26 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -3612,6 +3900,60 @@ dependencies = [
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "parry2d"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef681740349cec3ab9b5996b03b459b383b6998e1ffcb2804e8b57eb1e8491d9"
dependencies = [
"approx",
"arrayvec",
"bitflags 2.10.0",
"downcast-rs 2.0.2",
"either",
"ena",
"foldhash 0.2.0",
"hashbrown 0.16.1",
"log",
"nalgebra",
"num-derive",
"num-traits",
"ordered-float",
"rayon",
"simba",
"slab",
"smallvec",
"spade",
"thiserror 2.0.17",
]
[[package]]
name = "parry2d-f64"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "835d4ca2b68026b04a75ca6196da961d8c0895420eacc9cc2201f7ad54775780"
dependencies = [
"approx",
"arrayvec",
"bitflags 2.10.0",
"downcast-rs 2.0.2",
"either",
"ena",
"foldhash 0.2.0",
"hashbrown 0.16.1",
"log",
"nalgebra",
"num-derive",
"num-traits",
"ordered-float",
"rayon",
"simba",
"slab",
"smallvec",
"spade",
"thiserror 2.0.17",
]
[[package]] [[package]]
name = "paste" name = "paste"
version = "1.0.15" version = "1.0.15"
@ -3761,6 +4103,28 @@ dependencies = [
"toml_edit", "toml_edit",
] ]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.105" version = "1.0.105"
@ -3872,6 +4236,32 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "rawpointer"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "rayon"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]] [[package]]
name = "read-fonts" name = "read-fonts"
version = "0.35.0" version = "0.35.0"
@ -3961,6 +4351,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "robust"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839"
[[package]] [[package]]
name = "rodio" name = "rodio"
version = "0.20.1" version = "0.20.1"
@ -4053,6 +4449,15 @@ dependencies = [
"twox-hash", "twox-hash",
] ]
[[package]]
name = "safe_arch"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323"
dependencies = [
"bytemuck",
]
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@ -4163,6 +4568,19 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "simba"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c99284beb21666094ba2b75bbceda012e610f5479dfcc2d6e2426f53197ffd95"
dependencies = [
"approx",
"num-complex",
"num-traits",
"paste",
"wide",
]
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.8" version = "0.3.8"
@ -4244,6 +4662,18 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "spade"
version = "2.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb313e1c8afee5b5647e00ee0fe6855e3d529eb863a0fdae1d60006c4d1e9990"
dependencies = [
"hashbrown 0.15.5",
"num-traits",
"robust",
"smallvec",
]
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.10.0" version = "0.10.0"
@ -4414,6 +4844,7 @@ dependencies = [
name = "time_travel" name = "time_travel"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"avian2d",
"bevy", "bevy",
] ]
@ -4592,6 +5023,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]] [[package]]
name = "typewit" name = "typewit"
version = "1.14.2" version = "1.14.2"
@ -5037,6 +5474,16 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "wide"
version = "0.7.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03"
dependencies = [
"bytemuck",
"safe_arch",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View File

@ -4,4 +4,5 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
avian2d = "0.5.0"
bevy = "0.18.0" bevy = "0.18.0"

BIN
assets/player.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

357
src/avian.rs Normal file
View File

@ -0,0 +1,357 @@
use avian2d::{math::*, prelude::*};
use bevy::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),
movement,
apply_movement_damping,
)
.chain(),
)
.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.
PhysicsSchedule,
kinematic_controller_collisions.in_set(NarrowPhaseSystems::Last),
);
}
}
/// A [`Message`] written for a movement input action.
#[derive(Message)]
pub struct MovementAction(Vec2);
/// 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;
/// The acceleration used for character movement.
#[derive(Component)]
pub struct MovementAcceleration(Scalar);
/// The damping factor used for slowing down movement.
#[derive(Component)]
pub struct MovementDampingFactor(Scalar);
/// 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)]
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,
body: RigidBody,
collider: Collider,
ground_caster: ShapeCaster,
movement: MovementBundle,
}
/// A bundle that contains components for character movement.
#[derive(Bundle)]
pub struct MovementBundle {
acceleration: MovementAcceleration,
damping: MovementDampingFactor,
max_slope_angle: MaxSlopeAngle,
}
impl MovementBundle {
pub const fn new(acceleration: Scalar, damping: Scalar, max_slope_angle: Scalar) -> Self {
Self {
acceleration: MovementAcceleration(acceleration),
damping: MovementDampingFactor(damping),
max_slope_angle: MaxSlopeAngle(max_slope_angle),
}
}
}
impl Default for MovementBundle {
fn default() -> Self {
Self::new(30.0, 0.99, PI * 0.45)
}
}
impl CharacterControllerBundle {
pub fn new(collider: Collider) -> Self {
// Create shape caster as a slightly smaller version of collider
let mut caster_shape = collider.clone();
caster_shape.set_scale(Vector::ONE * 0.99, 10);
Self {
character_controller: CharacterController,
body: RigidBody::Kinematic,
collider,
ground_caster: ShapeCaster::new(caster_shape, Vector::ZERO, 0.0, Dir2::NEG_Y)
.with_max_distance(10.0),
movement: MovementBundle::default(),
}
}
pub fn with_movement(
mut self,
acceleration: Scalar,
damping: Scalar,
max_slope_angle: Scalar,
) -> Self {
self.movement = MovementBundle::new(acceleration, damping, max_slope_angle);
self
}
}
/// Sends [`MovementAction`] events based on keyboard input.
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 up = keyboard_input.any_pressed([KeyCode::KeyW, KeyCode::ArrowUp]);
let down = keyboard_input.any_pressed([KeyCode::KeyS, KeyCode::ArrowDown]);
let x = right as i8 - left as i8;
let y = up as i8 - down as i8;
let dir = Vec2::new(x as f32, y as f32);
if let Some(dir) = dir.try_normalize() {
movement_writer.write(MovementAction(dir));
}
}
/// Sends [`MovementAction`] events based on gamepad input.
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),
) {
let mut dir = Vec2::new(x, y);
let len = dir.length();
if len == 0. {
continue;
}
if len > 1. {
dir = dir.normalize();
}
movement_writer.write(MovementAction(dir));
}
}
}
/// Responds to [`MovementAction`] events and moves character controllers accordingly.
fn movement(
time: Res<Time>,
mut movement_reader: MessageReader<MovementAction>,
mut controllers: Query<(
&MovementAcceleration,
&MovementDampingFactor,
&mut LinearVelocity,
)>,
) {
// Precision is adjusted so that the example works with
// both the `f32` and `f64` features. Otherwise you don't need this.
let delta_time = time.delta_secs_f64().adjust_precision();
for (movement_acceleration, dampening, mut linear_velocity) in &mut controllers {
if movement_reader.is_empty() {
linear_velocity.x *= 1.0 / (1.0 + damping_factor.0 * delta_time);
}
for event in movement_reader.read() {
match event {
MovementAction::Move(direction) => {
linear_velocity.x += *direction * movement_acceleration.0 * delta_time;
}
MovementAction::Jump => {}
}
}
}
}
/// Slows down movement in the X direction.
fn apply_movement_damping(
time: Res<Time>,
mut query: Query<(&MovementDampingFactor, &mut LinearVelocity)>,
) {
// Precision is adjusted so that the example works with
// both the `f32` and `f64` features. Otherwise you don't need this.
let delta_time = time.delta_secs_f64().adjust_precision();
for (damping_factor, mut linear_velocity) in &mut query {
// We could use `LinearDamping`, but we don't want to dampen movement along the Y axis
linear_velocity.x *= 1.0 / (1.0 + damping_factor.0 * delta_time);
}
}
/// Kinematic bodies do not get pushed by collisions by default,
/// so it needs to be done manually.
///
/// This system handles collision response for kinematic character controllers
/// by pushing them along their contact normals by the current penetration depth,
/// and applying velocity corrections in order to snap to slopes, slide along walls,
/// and predict collisions using speculative contacts.
#[allow(clippy::type_complexity)]
fn kinematic_controller_collisions(
collisions: Collisions,
bodies: Query<&RigidBody>,
collider_rbs: Query<&ColliderOf, Without<Sensor>>,
mut character_controllers: Query<
(&mut Position, &mut LinearVelocity, Option<&MaxSlopeAngle>),
(With<RigidBody>, With<CharacterController>),
>,
time: Res<Time>,
) {
// Iterate through collisions and move the kinematic body to resolve penetration
for contacts in collisions.iter() {
// Get the rigid body entities of the colliders (colliders could be children)
let Ok([&ColliderOf { body: rb1 }, &ColliderOf { body: rb2 }]) =
collider_rbs.get_many([contacts.collider1, contacts.collider2])
else {
continue;
};
// Get the body of the character controller and whether it is the first
// or second entity in the collision.
let is_first: bool;
let character_rb: RigidBody;
let is_other_dynamic: bool;
let (mut position, mut linear_velocity, max_slope_angle) =
if let Ok(character) = character_controllers.get_mut(rb1) {
is_first = true;
character_rb = *bodies.get(rb1).unwrap();
is_other_dynamic = bodies.get(rb2).is_ok_and(|rb| rb.is_dynamic());
character
} else if let Ok(character) = character_controllers.get_mut(rb2) {
is_first = false;
character_rb = *bodies.get(rb2).unwrap();
is_other_dynamic = bodies.get(rb1).is_ok_and(|rb| rb.is_dynamic());
character
} else {
continue;
};
// This system only handles collision response for kinematic character controllers.
if !character_rb.is_kinematic() {
continue;
}
// Iterate through contact manifolds and their contacts.
// Each contact in a single manifold shares the same contact normal.
for manifold in contacts.manifolds.iter() {
let normal = if is_first {
-manifold.normal
} else {
manifold.normal
};
let mut deepest_penetration: Scalar = Scalar::MIN;
// Solve each penetrating contact in the manifold.
for contact in manifold.points.iter() {
if contact.penetration > 0.0 {
position.0 += normal * contact.penetration;
}
deepest_penetration = deepest_penetration.max(contact.penetration);
}
// For now, this system only handles velocity corrections for collisions against static geometry.
if is_other_dynamic {
continue;
}
// Determine if the slope is climbable or if it's too steep to walk on.
let slope_angle = normal.angle_to(Vector::Y);
let climbable = max_slope_angle.is_some_and(|angle| slope_angle.abs() <= angle.0);
if deepest_penetration > 0.0 {
// If the slope is climbable, snap the velocity so that the character
// up and down the surface smoothly.
if climbable {
// Points either left or right depending on which side the normal is leaning on.
// (This could be simplified for 2D, but this approach is dimension-agnostic)
let normal_direction_x =
normal.reject_from_normalized(Vector::Y).normalize_or_zero();
// The movement speed along the direction above.
let linear_velocity_x = linear_velocity.dot(normal_direction_x);
// Snap the Y speed based on the speed at which the character is moving
// up or down the slope, and how steep the slope is.
//
// A 2D visualization of the slope, the contact normal, and the velocity components:
//
//
// normal
// *
// │ * velocity_x
// │ * - - - - - -
// │ * | velocity_y
// │ * |
// *───────────────────*
let max_y_speed = -linear_velocity_x * slope_angle.tan();
linear_velocity.y = linear_velocity.y.max(max_y_speed);
} else {
// The character is intersecting an unclimbable object, like a wall.
// We want the character to slide along the surface, similarly to
// a collide-and-slide algorithm.
// Don't apply an impulse if the character is moving away from the surface.
if linear_velocity.dot(normal) > 0.0 {
continue;
}
// Slide along the surface, rejecting the velocity along the contact normal.
let impulse = linear_velocity.reject_from_normalized(normal);
linear_velocity.0 = impulse;
}
} else {
// The character is not yet intersecting the other object,
// but the narrow phase detected a speculative collision.
//
// We need to push back the part of the velocity
// that would cause penetration within the next frame.
let normal_speed = linear_velocity.dot(normal);
// Don't apply an impulse if the character is moving away from the surface.
if normal_speed > 0.0 {
continue;
}
// Compute the impulse to apply.
let impulse_magnitude =
normal_speed - (deepest_penetration / time.delta_secs_f64().adjust_precision());
let mut impulse = impulse_magnitude * normal;
// Apply the impulse differently depending on the slope angle.
if climbable {
// Avoid sliding down slopes.
linear_velocity.y -= impulse.y.min(0.0);
} else {
// Avoid climbing up walls.
impulse.y = impulse.y.max(0.0);
linear_velocity.0 -= impulse;
}
}
}
}
}

View File

@ -1,21 +1,35 @@
use avian2d::{
PhysicsPlugins,
math::{Scalar, Vector},
prelude::{Collider, Gravity, RigidBody},
};
use bevy::{camera::ScalingMode, color::palettes::css::GREEN, prelude::*}; use bevy::{camera::ScalingMode, color::palettes::css::GREEN, prelude::*};
use crate::avian::{CharacterControllerBundle, CharacterControllerPlugin};
pub mod avian;
fn main() { fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins) .add_plugins((
DefaultPlugins,
// Add physics plugins and specify a units-per-meter scaling factor, 1 meter = 20 pixels.
// The unit allows the engine to tune its parameters for the scale of the world, improving stability.
PhysicsPlugins::default().with_length_unit(20.0),
CharacterControllerPlugin,
))
.add_systems(Startup, setup) .add_systems(Startup, setup)
.add_systems(Update, sprite_movement) .add_systems(Update, debug_border)
.add_systems(Update, draw_gizmo) .insert_resource(Gravity(Vector::ZERO))
.run(); .run();
} }
#[derive(Component)] fn setup(
enum Direction { mut commands: Commands,
Left, mut materials: ResMut<Assets<ColorMaterial>>,
Right, mut meshes: ResMut<Assets<Mesh>>,
} asset_server: Res<AssetServer>,
) {
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
let mut projection = OrthographicProjection::default_2d(); let mut projection = OrthographicProjection::default_2d();
projection.scaling_mode = ScalingMode::AutoMin { projection.scaling_mode = ScalingMode::AutoMin {
min_width: 1920., min_width: 1920.,
@ -23,30 +37,36 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
}; };
commands.spawn((Camera2d, Projection::Orthographic(projection))); commands.spawn((Camera2d, Projection::Orthographic(projection)));
// player
commands.spawn(( commands.spawn((
Sprite::from_image(asset_server.load("icon.png")), Mesh2d(meshes.add(Capsule2d::new(12.5, 20.0))),
MeshMaterial2d(materials.add(Color::srgb(0.2, 0.7, 0.9))),
Transform::from_xyz(0.0, -100.0, 0.0),
CharacterControllerBundle::new(Collider::capsule(12.5, 20.0)).with_movement(
1250.0,
10.0,
(30.0 as Scalar).to_radians(),
),
));
// A cube to move around
commands.spawn((
Sprite {
color: Color::srgb(0.0, 0.4, 0.7),
custom_size: Some(Vec2::new(30.0, 30.0)),
..default()
},
Transform::from_xyz(50.0, -100.0, 0.0),
RigidBody::Dynamic,
Collider::rectangle(30.0, 30.0),
));
commands.spawn((
Sprite::from_image(asset_server.load("player.png")),
Transform::from_xyz(0., 0., 0.), Transform::from_xyz(0., 0., 0.),
Direction::Right,
)); ));
} }
/// The sprite is animated by changing its translation depending on the time that has passed since fn debug_border(mut gizmos: Gizmos) {
/// the last frame.
fn sprite_movement(time: Res<Time>, mut sprite_position: Query<(&mut Direction, &mut Transform)>) {
for (mut logo, mut transform) in &mut sprite_position {
match *logo {
Direction::Right => transform.translation.x += 150. * time.delta_secs(),
Direction::Left => transform.translation.x -= 150. * time.delta_secs(),
}
if transform.translation.x > 200. {
*logo = Direction::Left;
} else if transform.translation.x < -200. {
*logo = Direction::Right;
}
}
}
fn draw_gizmo(mut gizmos: Gizmos) {
gizmos.rect_2d(Isometry2d::IDENTITY, Vec2::new(1920., 1080.), GREEN); gizmos.rect_2d(Isometry2d::IDENTITY, Vec2::new(1920., 1080.), GREEN);
} }