From 5c88521a9ebad08e1bb6cc7bd9517cb38b12972f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ferr=C3=A9?= Date: Sat, 9 Mar 2024 11:56:41 +0100 Subject: [PATCH 1/7] Apply minor changes in descriptions --- CHANGELOG.md | 10 +++++----- README.md | 6 +++--- crates/modor/Cargo.toml | 2 +- crates/modor_internal/Cargo.toml | 2 +- crates/modor_math/Cargo.toml | 2 +- crates/modor_math/README.md | 2 +- crates/modor_math/src/lib.rs | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fbe3c18..9d6d4f1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,10 @@ ### Added - Node definition -- Physics module -- Graphics module for 2D rendering -- Text module -- Input module with support of keyboard, mouse, touch and gamepads -- Resources module to easily load and access resources like textures, font, sounds, ... +- Physics for 2D objects +- Graphics for 2D rendering +- Text rendering +- Input support for keyboard, mouse, touch and gamepads +- Resources handling to easily load and access resources like textures, font, sounds, ... - Asynchronous job utilities - Support for Windows, Linux, macOS, Android and WebAssembly diff --git a/README.md b/README.md index 2afd5f54..8de9feaa 100644 --- a/README.md +++ b/README.md @@ -61,11 +61,11 @@ For example: `cargo run --example rendering_2d --release` ## Behind the scene -Here are the main libaries used behind the modules of modor: +Here are the main libraries used to implement Modor: -- Graphics module is backed by [winit](https://github.com/rust-windowing/winit) +- Graphics crate is backed by [winit](https://github.com/rust-windowing/winit) and [wgpu](https://wgpu.rs/). -- Physics module is backed by [rapier](https://rapier.rs/). +- Physics crate is backed by [rapier](https://rapier.rs/). ## License diff --git a/crates/modor/Cargo.toml b/crates/modor/Cargo.toml index 2eba6f4a..b9c05445 100644 --- a/crates/modor/Cargo.toml +++ b/crates/modor/Cargo.toml @@ -2,7 +2,7 @@ name = "modor" description = "Core library of Modor game engine" readme = "../../README.md" -keywords = ["game", "engine", "modular", "object", "framework"] +keywords = ["game", "engine", "modular", "node", "object"] categories = ["game-engines"] exclude = [".github", "README.md"] version.workspace = true diff --git a/crates/modor_internal/Cargo.toml b/crates/modor_internal/Cargo.toml index 9f4a3a57..6f0a9103 100644 --- a/crates/modor_internal/Cargo.toml +++ b/crates/modor_internal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "modor_internal" -description = "Internal module of Modor game engine for common utils" +description = "Internal utilities for Modor game engine" readme = "./README.md" keywords = ["modor", "internal"] categories = ["game-engines"] diff --git a/crates/modor_math/Cargo.toml b/crates/modor_math/Cargo.toml index 9f175c7c..607d4e6b 100644 --- a/crates/modor_math/Cargo.toml +++ b/crates/modor_math/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "modor_math" -description = "Math module of Modor game engine" +description = "Math crate of Modor game engine" readme = "./README.md" keywords = ["modor", "math", "vector"] categories = ["game-engines"] diff --git a/crates/modor_math/README.md b/crates/modor_math/README.md index 0a5eef80..5bce2afa 100644 --- a/crates/modor_math/README.md +++ b/crates/modor_math/README.md @@ -1,3 +1,3 @@ # modor_math -Math module of [Modor](https://github.com/modor-engine/modor). +Math crate of [Modor](https://github.com/modor-engine/modor). diff --git a/crates/modor_math/src/lib.rs b/crates/modor_math/src/lib.rs index 675ad2b1..7caf51df 100644 --- a/crates/modor_math/src/lib.rs +++ b/crates/modor_math/src/lib.rs @@ -1,4 +1,4 @@ -//! Math module of Modor. +//! Math crate of Modor. mod matrices_4d; mod quaternion; From ffae557eea342dbc509c214757046b385111a74f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ferr=C3=A9?= Date: Mon, 1 Apr 2024 12:31:38 +0200 Subject: [PATCH 2/7] Add `modor_physics` crate + make root node update deterministic + add `NoVisit` derive macro + give access to a `Context` from `App` + returns mutable reference from `App::root` and `Context::root` methods instead of `RefMut` --- .github/workflows/ci.yml | 4 - .../scripts/prepare_compilation_tests.sh | 6 - Cargo.toml | 2 + PUBLISHED-CRATES | 1 + crates/modor/src/app.rs | 90 ++-- crates/modor/src/lib.rs | 21 +- crates/modor/src/node.rs | 4 + crates/modor_derive/src/lib.rs | 9 +- crates/modor_derive/src/visit.rs | 16 +- crates/modor_internal/Cargo.toml | 1 + crates/modor_internal/src/index.rs | 56 +++ crates/modor_internal/src/lib.rs | 1 + .../modor_internal/tests/integration/index.rs | 25 ++ .../modor_internal/tests/integration/main.rs | 1 + crates/modor_physics/Cargo.toml | 27 ++ crates/modor_physics/README.md | 3 + crates/modor_physics/src/body.rs | 424 ++++++++++++++++++ crates/modor_physics/src/body_register.rs | 203 +++++++++ crates/modor_physics/src/collision_group.rs | 213 +++++++++ crates/modor_physics/src/delta.rs | 16 + crates/modor_physics/src/lib.rs | 46 ++ .../tests/integration/body/collisions.rs | 233 ++++++++++ .../tests/integration/body/drop.rs | 29 ++ .../tests/integration/body/dynamics.rs | 152 +++++++ .../tests/integration/body/mod.rs | 3 + .../tests/integration/body_register.rs | 41 ++ .../tests/integration/collision_group.rs | 68 +++ .../modor_physics/tests/integration/main.rs | 5 + examples/Cargo.toml | 1 + examples/examples/nodes.rs | 2 +- examples/examples/physics.rs | 69 +++ 31 files changed, 1721 insertions(+), 51 deletions(-) delete mode 100644 .github/workflows/scripts/prepare_compilation_tests.sh create mode 100644 crates/modor_internal/src/index.rs create mode 100644 crates/modor_internal/tests/integration/index.rs create mode 100644 crates/modor_internal/tests/integration/main.rs create mode 100644 crates/modor_physics/Cargo.toml create mode 100644 crates/modor_physics/README.md create mode 100644 crates/modor_physics/src/body.rs create mode 100644 crates/modor_physics/src/body_register.rs create mode 100644 crates/modor_physics/src/collision_group.rs create mode 100644 crates/modor_physics/src/delta.rs create mode 100644 crates/modor_physics/src/lib.rs create mode 100644 crates/modor_physics/tests/integration/body/collisions.rs create mode 100644 crates/modor_physics/tests/integration/body/drop.rs create mode 100644 crates/modor_physics/tests/integration/body/dynamics.rs create mode 100644 crates/modor_physics/tests/integration/body/mod.rs create mode 100644 crates/modor_physics/tests/integration/body_register.rs create mode 100644 crates/modor_physics/tests/integration/collision_group.rs create mode 100644 crates/modor_physics/tests/integration/main.rs create mode 100644 examples/examples/physics.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bb6c926..46f75212 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,8 +113,6 @@ jobs: - name: Install wasm-pack run: cargo install wasm-pack --debug if: matrix.target == 'wasm32-unknown-unknown' - - name: Prepare compilation tests - run: bash -x .github/workflows/scripts/prepare_compilation_tests.sh - name: Test WASM run: for crate_path in crates/*; do wasm-pack test --node "$crate_path"; done if: matrix.target == 'wasm32-unknown-unknown' @@ -149,8 +147,6 @@ jobs: run: bash -x .github/workflows/scripts/install_graphic_dependencies_linux.sh - name: Install grcov run: cargo install grcov --debug - - name: Prepare compilation tests - run: bash -x .github/workflows/scripts/prepare_compilation_tests.sh - name: Run unit tests run: xvfb-run --server-args="-screen 0 1920x1080x24" cargo test --lib --tests env: diff --git a/.github/workflows/scripts/prepare_compilation_tests.sh b/.github/workflows/scripts/prepare_compilation_tests.sh deleted file mode 100644 index 8000ce84..00000000 --- a/.github/workflows/scripts/prepare_compilation_tests.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -set -xeu - -# Used to avoid failures of compilation tests because of multiple compiled versions of the same library -rm -rf target/debug/*/*modor* -rm -rf target/debug/.*/*modor* diff --git a/Cargo.toml b/Cargo.toml index 7f748dda..ff8d0098 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ pico-args = "0.5" pretty_env_logger = "0.5" proc-macro-crate = "3.0" proc-macro2 = "1.0" +rapier2d = "0.18" quote = "1.0" syn = { version = "2.0", features = ["full"] } wasm-bindgen-test = "0.3" @@ -34,6 +35,7 @@ modor = { version = "0.1.0", path = "crates/modor" } modor_derive = { version = "0.1.0", path = "crates/modor_derive" } modor_internal = { version = "0.1.0", path = "crates/modor_internal" } modor_math = { version = "0.1.0", path = "crates/modor_math" } +modor_physics = { version = "0.1.0", path = "crates/modor_physics" } [workspace.lints.rust] anonymous_parameters = "warn" diff --git a/PUBLISHED-CRATES b/PUBLISHED-CRATES index 420cd954..273346ba 100644 --- a/PUBLISHED-CRATES +++ b/PUBLISHED-CRATES @@ -2,3 +2,4 @@ modor_derive modor_internal modor modor_math +modor_physics diff --git a/crates/modor/src/app.rs b/crates/modor/src/app.rs index 365e84ec..accbfe68 100644 --- a/crates/modor/src/app.rs +++ b/crates/modor/src/app.rs @@ -3,8 +3,6 @@ use fxhash::FxHashMap; use log::{debug, Level}; use std::any; use std::any::{Any, TypeId}; -use std::cell::{RefCell, RefMut}; -use std::rc::Rc; /// The entrypoint of the engine. /// @@ -13,7 +11,8 @@ use std::rc::Rc; /// See [`modor`](crate). #[derive(Debug)] pub struct App { - roots: FxHashMap, + root_indexes: FxHashMap, + roots: Vec, // ensures deterministic update order } impl App { @@ -33,7 +32,8 @@ impl App { platform::init_logging(log_level); debug!("initialize app..."); let mut app = Self { - roots: FxHashMap::default(), + root_indexes: FxHashMap::default(), + roots: vec![], }; app.root::(); debug!("app initialized"); @@ -44,64 +44,79 @@ impl App { /// /// [`Node::update`] method is called for each registered root node. /// - /// Note that update order is predictable inside a root node, but it is not between root nodes. + /// Root nodes are updated in the order in which they are created. #[allow(clippy::needless_collect)] pub fn update(&mut self) { debug!("run update app..."); - let roots = self.roots.values().cloned().collect::>(); - let mut ctx = Context { app: self }; - for root in roots { - (root.update_fn)(root.value, &mut ctx); + for type_id in self.root_indexes.keys().copied().collect::>() { + let root = self.root_data(type_id); + let mut value = root + .value + .take() + .expect("internal error: root node already borrowed"); + let update_fn = root.update_fn; + update_fn(&mut *value, &mut self.ctx()); + self.root_data(type_id).value = Some(value); } debug!("app updated"); } + /// Returns an update context. + /// + /// This method is generally used for testing purpose. + pub fn ctx(&mut self) -> Context<'_> { + Context { app: self } + } + /// Returns a mutable reference to a root node. /// /// The root node is created using [`RootNode::on_create`] if it doesn't exist. - pub fn root(&mut self) -> RefMut<'_, T> + pub fn root(&mut self) -> &mut T where T: RootNode, { let type_id = TypeId::of::(); - let root = if self.roots.contains_key(&type_id) { + let root = if self.root_indexes.contains_key(&type_id) { self.retrieve_root::(type_id) } else { self.create_root::(type_id) }; - RefMut::map(root, Self::downcast_root) + root.downcast_mut::() + .expect("internal error: misconfigured root node") } - fn create_root(&mut self, type_id: TypeId) -> RefMut<'_, dyn Any> + fn create_root(&mut self, type_id: TypeId) -> &mut dyn Any where T: RootNode, { - let mut ctx = Context { app: self }; debug!("create root node `{}`...", any::type_name::()); - let root = RootNodeData::new(T::on_create(&mut ctx)); + let root = RootNodeData::new(T::on_create(&mut self.ctx())); debug!("root node `{}` created", any::type_name::()); - self.roots.entry(type_id).or_insert(root).value.borrow_mut() + let index = self.roots.len(); + self.root_indexes.insert(type_id, index); + self.roots.push(root); + &mut **self.roots[index] + .value + .as_mut() + .expect("internal error: root node already borrowed") } - fn retrieve_root(&mut self, type_id: TypeId) -> RefMut<'_, dyn Any> { - self.roots - .get_mut(&type_id) - .expect("internal error: missing root node") + fn retrieve_root(&mut self, type_id: TypeId) -> &mut dyn Any { + &mut **self + .root_data(type_id) .value - .try_borrow_mut() - .unwrap_or_else(|_| panic!("root node `{}` already borrowed", any::type_name::())) + .as_mut() + .unwrap_or_else(|| panic!("root node `{}` already borrowed", any::type_name::())) } - fn downcast_root(value: &mut dyn Any) -> &mut T - where - T: Any, - { - value - .downcast_mut::() - .expect("internal error: misconfigured root node") + fn root_data(&mut self, type_id: TypeId) -> &mut RootNodeData { + let index = self.root_indexes[&type_id]; + &mut self.roots[index] } } +// If `App` was directly accessible during update, it would be possible to run `App::update`. +// As this is not wanted, `App` is wrapped in `Context` to limit the allowed operations. /// The context accessible during node update. #[derive(Debug)] pub struct Context<'a> { @@ -112,7 +127,11 @@ impl Context<'_> { /// Returns a mutable reference to a root node. /// /// The root node is created using [`RootNode::on_create`] if it doesn't exist. - pub fn root(&mut self) -> RefMut<'_, T> + /// + /// # Panics + /// + /// This will panic if root node `T` is currently updated. + pub fn root(&mut self) -> &mut T where T: RootNode, { @@ -120,10 +139,10 @@ impl Context<'_> { } } -#[derive(Clone, Debug)] +#[derive(Debug)] struct RootNodeData { - value: Rc>, - update_fn: fn(Rc>, &mut Context<'_>), + value: Option>, + update_fn: fn(&mut dyn Any, &mut Context<'_>), } impl RootNodeData { @@ -132,18 +151,17 @@ impl RootNodeData { T: RootNode, { Self { - value: Rc::new(RefCell::new(value)), + value: Some(Box::new(value)), update_fn: Self::update_root::, } } - fn update_root(value: Rc>, ctx: &mut Context<'_>) + fn update_root(value: &mut dyn Any, ctx: &mut Context<'_>) where T: RootNode, { Node::update( value - .borrow_mut() .downcast_mut::() .expect("internal error: misconfigured root node"), ctx, diff --git a/crates/modor/src/lib.rs b/crates/modor/src/lib.rs index 9a74a792..e40c81d3 100644 --- a/crates/modor/src/lib.rs +++ b/crates/modor/src/lib.rs @@ -138,7 +138,7 @@ pub use modor_derive::test; /// /// ```rust /// # use modor::*; -/// +/// # /// #[derive(Default, RootNode, Node, Visit)] /// struct Root { /// #[modor(skip)] @@ -154,7 +154,24 @@ pub use modor_derive::RootNode; /// See [`RootNode`](macro@crate::RootNode). pub use modor_derive::Node; -/// Implements [`Visit`]. +/// Implements [`Visit`] in case there is no inner node. +/// +/// This macro can be used instead of [`Visit`](macro@crate::Visit) when there is no inner node. +/// This avoids unnecessary usage of `#[modor(skip)]` in case all fields should be skipped. +/// +/// # Examples +/// +/// ```rust +/// # use modor::*; +/// # +/// #[derive(Default, RootNode, Node, NoVisit)] +/// struct Root { +/// value: u32, +/// } +/// ``` +pub use modor_derive::NoVisit; + +/// Implements [`Visit`] so that inner node are visited. /// /// `#[modor(skip)]` can be added on a field to skip its automatic update, for example because: /// - the field type doesn't implement [`Node`]. diff --git a/crates/modor/src/node.rs b/crates/modor/src/node.rs index 5bf7a030..6f9738c5 100644 --- a/crates/modor/src/node.rs +++ b/crates/modor/src/node.rs @@ -11,7 +11,9 @@ use std::ops::DerefMut; /// /// See [`modor`](crate). pub trait RootNode: 'static + Node { + /// Creates the root node. /// + /// Note that this method shouldn't be called manually to create the node. fn on_create(ctx: &mut Context<'_>) -> Self; } @@ -36,6 +38,8 @@ pub trait Node: Visit { /// Runs node update. /// + /// This method should be automatically called by [`Visit::visit`]. + /// /// By default, the following methods are executed in order: /// - [`Node::on_enter`] /// - [`Visit::visit`] diff --git a/crates/modor_derive/src/lib.rs b/crates/modor_derive/src/lib.rs index 0d5dc0dc..d1915a8d 100644 --- a/crates/modor_derive/src/lib.rs +++ b/crates/modor_derive/src/lib.rs @@ -46,7 +46,14 @@ pub fn node_derive(item: TokenStream) -> TokenStream { #[proc_macro_derive(Visit, attributes(modor))] pub fn visit_derive(item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as DeriveInput); - visit::impl_block(&input) + visit::impl_block_with_visit(&input) .unwrap_or_else(syn::Error::into_compile_error) .into() } + +#[allow(missing_docs)] // doc available in `modor` crate +#[proc_macro_derive(NoVisit)] +pub fn no_visit_derive(item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as DeriveInput); + visit::impl_block_without_visit(&input).into() +} diff --git a/crates/modor_derive/src/visit.rs b/crates/modor_derive/src/visit.rs index feb6c39a..cafbc6cc 100644 --- a/crates/modor_derive/src/visit.rs +++ b/crates/modor_derive/src/visit.rs @@ -6,7 +6,20 @@ use quote::{quote, quote_spanned, ToTokens}; use syn::spanned::Spanned; use syn::{DeriveInput, Index, Type}; -pub(crate) fn impl_block(input: &DeriveInput) -> syn::Result { +pub(crate) fn impl_block_without_visit(input: &DeriveInput) -> TokenStream { + let crate_ident = utils::crate_ident(); + let ident = &input.ident; + let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl(); + quote! { + #[automatically_derived] + impl #impl_generics ::#crate_ident::Visit for #ident #type_generics #where_clause { + #[inline] + fn visit(&mut self, ctx: &mut ::#crate_ident::Context<'_>) {} + } + } +} + +pub(crate) fn impl_block_with_visit(input: &DeriveInput) -> syn::Result { let crate_ident = utils::crate_ident(); let ident = &input.ident; let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl(); @@ -17,6 +30,7 @@ pub(crate) fn impl_block(input: &DeriveInput) -> syn::Result { Ok(quote! { #[automatically_derived] impl #impl_generics ::#crate_ident::Visit for #ident #type_generics #where_clause { + #[inline] fn visit(&mut self, ctx: &mut ::#crate_ident::Context<'_>) { #visit_body } diff --git a/crates/modor_internal/Cargo.toml b/crates/modor_internal/Cargo.toml index 6f0a9103..7c07eedc 100644 --- a/crates/modor_internal/Cargo.toml +++ b/crates/modor_internal/Cargo.toml @@ -14,6 +14,7 @@ rust-version.workspace = true [dependencies] approx.workspace = true +log.workspace = true [lints] workspace = true diff --git a/crates/modor_internal/src/index.rs b/crates/modor_internal/src/index.rs new file mode 100644 index 00000000..5b5c5a44 --- /dev/null +++ b/crates/modor_internal/src/index.rs @@ -0,0 +1,56 @@ +use log::error; +use std::mem; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; + +const ERROR: &str = "cannot access index pool"; + +#[derive(Debug, Default)] +pub struct IndexPool { + deleted_indexes: Mutex>, + available_indexes: Mutex>, + next_index: AtomicUsize, +} + +impl IndexPool { + pub fn generate(self: &Arc) -> Index { + Index { + index: if let Some(index) = self.available_indexes.lock().expect(ERROR).pop() { + index + } else { + self.next_index.fetch_add(1, Ordering::Relaxed) + }, + pool: self.clone(), + } + } + + pub fn take_deleted_indexes(&self) -> Vec { + let indexes = mem::take(&mut *self.deleted_indexes.lock().expect(ERROR)); + self.available_indexes + .lock() + .expect(ERROR) + .extend_from_slice(&indexes); + indexes + } +} + +#[derive(Debug)] +pub struct Index { + index: usize, + pool: Arc, +} + +impl Index { + pub fn value(&self) -> usize { + self.index + } +} + +impl Drop for Index { + fn drop(&mut self) { + match self.pool.deleted_indexes.lock() { + Ok(mut indexes) => indexes.push(self.index), + Err(err) => error!("error: {err}"), + } + } +} diff --git a/crates/modor_internal/src/lib.rs b/crates/modor_internal/src/lib.rs index 64c9cd56..26d418c8 100644 --- a/crates/modor_internal/src/lib.rs +++ b/crates/modor_internal/src/lib.rs @@ -1,5 +1,6 @@ #![allow(missing_docs)] +pub mod index; pub mod testing; pub use approx; diff --git a/crates/modor_internal/tests/integration/index.rs b/crates/modor_internal/tests/integration/index.rs new file mode 100644 index 00000000..27a86a59 --- /dev/null +++ b/crates/modor_internal/tests/integration/index.rs @@ -0,0 +1,25 @@ +use modor_internal::index::IndexPool; +use std::sync::Arc; + +#[test] +fn create_index() { + let pool = Arc::new(IndexPool::default()); + let index1 = pool.generate(); + let index2 = pool.generate(); + assert_eq!(index1.value(), 0); + assert_eq!(index2.value(), 1); +} + +#[test] +fn delete_index() { + let pool = Arc::new(IndexPool::default()); + assert!(pool.take_deleted_indexes().is_empty()); + let _index1 = pool.generate(); + let index2 = pool.generate(); + drop(index2); + let index3 = pool.generate(); + assert_eq!(index3.value(), 2); + assert_eq!(pool.take_deleted_indexes(), [1]); + let index4 = pool.generate(); + assert_eq!(index4.value(), 1); +} diff --git a/crates/modor_internal/tests/integration/main.rs b/crates/modor_internal/tests/integration/main.rs new file mode 100644 index 00000000..33edc959 --- /dev/null +++ b/crates/modor_internal/tests/integration/main.rs @@ -0,0 +1 @@ +pub mod index; diff --git a/crates/modor_physics/Cargo.toml b/crates/modor_physics/Cargo.toml new file mode 100644 index 00000000..354e0608 --- /dev/null +++ b/crates/modor_physics/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "modor_physics" +description = "Physics crate of Modor game engine" +readme = "./README.md" +keywords = ["modor", "physics", "collision", "game", "engine"] +categories = ["game-engines"] +exclude = [".github", "README.md"] +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +approx.workspace = true +fxhash.workspace = true +rapier2d.workspace = true +modor.workspace = true +modor_math.workspace = true +modor_internal.workspace = true + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen-test.workspace = true + +[lints] +workspace = true diff --git a/crates/modor_physics/README.md b/crates/modor_physics/README.md new file mode 100644 index 00000000..52c0615e --- /dev/null +++ b/crates/modor_physics/README.md @@ -0,0 +1,3 @@ +# modor_physics + +Physics crate of [Modor](https://github.com/modor-engine/modor). diff --git a/crates/modor_physics/src/body.rs b/crates/modor_physics/src/body.rs new file mode 100644 index 00000000..682e7283 --- /dev/null +++ b/crates/modor_physics/src/body.rs @@ -0,0 +1,424 @@ +use crate::body_register::Body2DRegister; +use crate::CollisionGroup; +use approx::AbsDiffEq; +use modor::{Context, NoVisit, Node}; +use modor_internal::index::Index; +use modor_math::Vec2; +use rapier2d::dynamics::{MassProperties, RigidBody, RigidBodyType}; +use rapier2d::geometry::{ActiveCollisionTypes, Collider, ColliderBuilder, SharedShape}; +use rapier2d::math::Rotation; +use rapier2d::na::{Point2, Vector2}; +use rapier2d::pipeline::ActiveHooks; +use rapier2d::prelude::{ContactManifold, InteractionGroups, RigidBodyBuilder}; +use std::sync::Arc; + +/// A physical 2D body. +/// +/// # Examples +/// +/// ```rust +/// # use std::f32::consts::FRAC_PI_2; +/// # use modor::*; +/// # use modor_math::*; +/// # use modor_physics::*; +/// # +/// #[derive(Default, RootNode, Node, NoVisit)] +/// struct CharacterDirection(Vec2); +/// +/// #[derive(Visit)] +/// struct Character { +/// body: Body2D, +/// } +/// +/// impl Node for Character { +/// fn on_enter(&mut self, ctx: &mut Context<'_>) { +/// self.body.velocity = ctx.root::().0 * 0.5; +/// } +/// +/// fn on_exit(&mut self, ctx: &mut Context<'_>) { +/// for collision in self.body.collisions() { +/// println!("Ball is colliding with body {}", collision.other_index); +/// } +/// } +/// } +/// +/// impl Character { +/// fn new(ctx: &mut Context<'_>, position: Vec2, group: &CollisionGroup) -> Self { +/// let mut body = Body2D::new(ctx, position, Vec2::ONE * 0.2); +/// body.rotation = FRAC_PI_2; +/// body.collision_group = Some(group.clone()); +/// body.shape = Shape2D::Circle; +/// Self { body } +/// } +/// } +/// ``` +#[derive(Debug, NoVisit)] +pub struct Body2D { + /// Position of the body in world units. + pub position: Vec2, + /// Size of the body in world units. + pub size: Vec2, + /// Rotation of the body in radians. + /// + /// Default value is `0.0`. + pub rotation: f32, + /// Linear velocity of the body in world units per second. + /// + /// Default value is `Vec2::ZERO`. + pub velocity: Vec2, + /// Angular velocity of the body in radians per second. + /// + /// Has no effect if [`angular_inertia`](#structfield.angular_inertia) is `0.0`. + /// + /// Default value is `0.0`. + pub angular_velocity: f32, + /// Force applied on the body. + /// + /// Has no effect if [`mass`](#structfield.mass) is `0.0`. + /// + /// The acceleration of the body corresponds to the force of the body divided by its mass. + /// + /// Default value is [`Vec2::ZERO`]. + pub force: Vec2, + /// Torque applied on the body. + /// + /// Has no effect if [`angular_inertia`](#structfield.angular_inertia) is `0.0`. + /// + /// Default value is `0.0`. + pub torque: f32, + /// Mass of the body. + /// + /// A mass of zero is considered as infinite. In this case, force will not have any effect + /// (even in case of collisions). + /// + /// Default value is `0.0`. + pub mass: f32, + /// Angular inertia of the body. + /// + /// An angular inertia of zero is considered as infinite. In this case, torque will not have + /// any effect (even in case of collisions). + /// + /// Default value is `0.0`. + pub angular_inertia: f32, + /// Linear damping of the body. + /// + /// This coefficient is used to automatically slow down the translation of the body. + /// + /// Default value is `0.0`. + pub damping: f32, + /// Angular damping of the body. + /// + /// This coefficient is used to automatically slow down the rotation of the body. + /// + /// Default value is `0.0`. + pub angular_damping: f32, + /// Dominance of the body. + /// + /// In case of collision between two bodies, if both bodies have a different dominance + /// group, then collision forces will only be applied on the body with the smallest dominance. + /// + /// Has no effect if [`collision_group`](#structfield.collision_group) is `None`. + /// + /// Default value is `0`. + pub dominance: i8, + /// Whether Continuous Collision Detection is enabled for the body. + /// + /// This option is used to detect a collision even if the body moves too fast. + /// CCD is performed using motion-clamping, which means each fast-moving body with CCD enabled + /// will be stopped at the moment of their first contact. Both angular and translational motions + /// are taken into account. + /// + /// Note that CCD require additional computation, so it is recommended to enable it only for + /// bodies that are expected to move fast. + /// + /// Has no effect if [`collision_group`](#structfield.collision_group) is [`None`]. + /// + /// Default value is `false`. + pub is_ccd_enabled: bool, + /// Collision group of the collider. + /// + /// Note that the collisions may not be updated when only the [`size`](#structfield.size) is + /// changed. However, it is ensured the collision is detected when updating + /// the [`position`](#structfield.position) or the [`rotation`](#structfield.rotation). + /// + /// Default value is `None` (no collision detection is performed). + pub collision_group: Option, + /// The shape of the body used to detect collisions. + /// + /// Default value is [`Shape2D::Rectangle`]. + pub shape: Shape2D, + collisions: Vec, + index: Arc, + old_position: Vec2, + old_size: Vec2, + old_rotation: f32, + old_velocity: Vec2, + old_angular_velocity: f32, + old_force: Vec2, + old_torque: f32, + old_mass: f32, + old_angular_inertia: f32, + old_shape: Shape2D, +} + +impl Node for Body2D { + fn on_enter(&mut self, ctx: &mut Context<'_>) { + let interaction_groups = self + .collision_group + .as_ref() + .map_or_else(InteractionGroups::none, |g| g.interaction_groups(ctx)); + let pipeline = ctx.root::(); + let rigid_body = pipeline.rigid_body_mut(&self.index); + self.update_from_rigid_body(rigid_body); + self.update_rigid_body(rigid_body); + let collider = pipeline.collider_mut(&self.index); + self.update_collider(collider, interaction_groups); + pipeline.set_size(&self.index, self.size); + self.collisions = pipeline.collisions(&self.index); + self.reset_old(); + } +} + +impl Body2D { + /// Creates and register a new body. + pub fn new(ctx: &mut Context<'_>, position: Vec2, size: Vec2) -> Self { + let active_hooks = ActiveHooks::FILTER_CONTACT_PAIRS | ActiveHooks::MODIFY_SOLVER_CONTACTS; + let resource = ctx.root::().register_body( + RigidBodyBuilder::new(RigidBodyType::Dynamic) + .can_sleep(false) + .translation(Vector2::new(position.x, position.y)) + .build(), + ColliderBuilder::new(SharedShape::cuboid(size.x / 2., size.y / 2.)) + .enabled(false) + .active_collision_types(ActiveCollisionTypes::all()) + .active_hooks(active_hooks) + .mass(0.) + .build(), + size, + ); + Self { + position, + size, + rotation: 0., + velocity: Vec2::ZERO, + angular_velocity: 0., + force: Vec2::ZERO, + torque: 0., + mass: 0., + angular_inertia: 0., + damping: 0., + angular_damping: 0., + dominance: 0, + is_ccd_enabled: false, + collision_group: None, + shape: Shape2D::Rectangle, + collisions: vec![], + index: Arc::new(resource), + old_position: position, + old_size: size, + old_rotation: 0., + old_velocity: Vec2::ZERO, + old_angular_velocity: 0., + old_force: Vec2::ZERO, + old_torque: 0., + old_mass: 0., + old_angular_inertia: 0., + old_shape: Shape2D::Rectangle, + } + } + + /// Returns the unique body index. + pub fn index(&self) -> Body2DIndex { + Body2DIndex(self.index.clone()) + } + + /// Returns the detected collisions. + pub fn collisions(&self) -> &[Collision2D] { + &self.collisions + } + + /// Returns whether the body collides with a body inside `group`. + pub fn is_colliding_with(&self, group: &CollisionGroup) -> bool { + self.collisions + .iter() + .any(|c| c.other_group_index == group.index()) + } + + #[allow(clippy::float_cmp)] + fn update_from_rigid_body(&mut self, rigid_body: &RigidBody) { + if self.position == self.old_position { + self.position.x = rigid_body.translation().x; + self.position.y = rigid_body.translation().y; + } + if self.rotation == self.old_rotation { + self.rotation = rigid_body.rotation().angle(); + } + if self.velocity == self.old_velocity { + self.velocity.x = rigid_body.linvel().x; + self.velocity.y = rigid_body.linvel().y; + } + if self.angular_velocity == self.old_angular_velocity { + self.angular_velocity = rigid_body.angvel(); + } + if self.force == self.old_force { + self.force.x = rigid_body.user_force().x; + self.force.y = rigid_body.user_force().y; + } + if self.torque == self.old_torque { + self.torque = rigid_body.user_torque(); + } + } + + #[allow(clippy::float_cmp)] + fn update_rigid_body(&mut self, rigid_body: &mut RigidBody) { + rigid_body.set_translation(Vector2::new(self.position.x, self.position.y), true); + rigid_body.set_rotation(Rotation::new(self.rotation), true); + rigid_body.set_linvel(Vector2::new(self.velocity.x, self.velocity.y), true); + rigid_body.set_angvel(self.angular_velocity, true); + rigid_body.reset_forces(true); + rigid_body.add_force(Vector2::new(self.force.x, self.force.y), true); + rigid_body.reset_torques(true); + rigid_body.add_torque(self.torque, true); + if self.mass != self.old_mass || self.angular_inertia != self.old_angular_inertia { + let mass = MassProperties::new(Point2::new(0., 0.), self.mass, self.angular_inertia); + rigid_body.set_additional_mass_properties(mass, true); + } + rigid_body.set_linear_damping(self.damping); + rigid_body.set_angular_damping(self.angular_damping); + rigid_body.set_dominance_group(self.dominance); + rigid_body.enable_ccd(self.is_ccd_enabled); + } + + fn update_collider(&mut self, collider: &mut Collider, interaction_groups: InteractionGroups) { + if self.size != self.old_size || self.shape != self.old_shape { + collider.set_shape(match self.shape { + Shape2D::Rectangle => SharedShape::cuboid(self.size.x / 2., self.size.y / 2.), + Shape2D::Circle => SharedShape::ball(self.size.x.min(self.size.y) / 2.), + }); + } + collider.user_data = self + .collision_group + .as_ref() + .map_or_else(|| 0, CollisionGroup::index) as u128; + collider.set_enabled(self.collision_group.is_some()); + collider.set_collision_groups(interaction_groups); + collider.set_mass(0.); + } + + fn reset_old(&mut self) { + self.old_position = self.position; + self.old_size = self.size; + self.old_rotation = self.rotation; + self.old_velocity = self.velocity; + self.old_angular_velocity = self.angular_velocity; + self.old_force = self.force; + self.old_torque = self.torque; + self.old_mass = self.mass; + self.old_angular_inertia = self.angular_inertia; + self.old_shape = self.shape; + } +} + +/// The unique index of a [`Body2D`]. +#[derive(Debug)] +pub struct Body2DIndex(Arc); + +impl Body2DIndex { + /// Returns the index as a [`usize`]. + /// + /// Note that in case the [`Body2D`] and all associated [`Body2DIndex`]s are dropped, this index + /// can be reused for a new body. + pub fn as_usize(&self) -> usize { + self.0.value() + } +} + +/// The shape of a [`Body2D`]. +/// +/// # Examples +/// +/// See [`Body2D`]. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Shape2D { + /// Rectangle shape. + #[default] + Rectangle, + /// Circle shape. + /// + /// The diameter of the circle is the smallest size component of [`Body2D`]. + Circle, +} + +/// A detected collision. +/// +/// # Examples +/// +/// See [`Body2D`]. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub struct Collision2D { + /// Index of the collided body. + pub other_index: usize, + /// Index of the collision group corresponding to the collided body. + pub other_group_index: usize, + /// Penetration of the body into the collided one in world units. + /// + /// Penetration vector starts at other body edge and ends at current body deepest point. + pub penetration: Vec2, + /// Position of the collision in world units. + /// + /// This position corresponds to the deepest point of the current body inside the other body. + /// If more than two points have the same depth, then the collision position is the average + /// of these points. + pub position: Vec2, +} + +impl Collision2D { + pub(crate) fn new( + is_collider2: bool, + other_index: usize, + other_group_index: usize, + collider: &Collider, + manifold: &ContactManifold, + ) -> Self { + let max_distance = manifold.points.iter().map(|p| -p.dist).fold(0., f32::max); + Self { + other_index, + other_group_index, + penetration: Self::penetration(is_collider2, manifold, max_distance), + position: Self::position(is_collider2, collider, manifold, max_distance), + } + } + + fn penetration(is_collider2: bool, manifold: &ContactManifold, max_distance: f32) -> Vec2 { + Vec2::new(manifold.data.normal.x, manifold.data.normal.y) + * max_distance + * if is_collider2 { -1. } else { 1. } + } + + #[allow(clippy::cast_precision_loss)] + fn position( + is_collider2: bool, + collider: &Collider, + manifold: &ContactManifold, + max_distance: f32, + ) -> Vec2 { + manifold + .points + .iter() + .filter(|d| d.dist.abs_diff_eq(&-max_distance, f32::EPSILON)) + .map(|p| if is_collider2 { p.local_p2 } else { p.local_p1 }) + .map(|p| Self::local_to_global_position(p, collider)) + .sum::() + / manifold + .points + .iter() + .filter(|d| d.dist.abs_diff_eq(&-max_distance, 100. * f32::EPSILON)) + .count() as f32 + } + + fn local_to_global_position(local_positions: Point2, collider: &Collider) -> Vec2 { + Vec2::new(local_positions.x, local_positions.y).with_rotation(collider.rotation().angle()) + + Vec2::new(collider.translation().x, collider.translation().y) + } +} diff --git a/crates/modor_physics/src/body_register.rs b/crates/modor_physics/src/body_register.rs new file mode 100644 index 00000000..265b0469 --- /dev/null +++ b/crates/modor_physics/src/body_register.rs @@ -0,0 +1,203 @@ +use crate::collision_group::CollisionGroupRegister; +use crate::{Body2DIndex, Collision2D, Delta}; +use fxhash::FxHashMap; +use modor::{Context, NoVisit, Node, RootNode}; +use modor_internal::index::{Index, IndexPool}; +use modor_math::Vec2; +use rapier2d::dynamics::{ + CCDSolver, ImpulseJointSet, IntegrationParameters, IslandManager, MultibodyJointSet, + RigidBodyHandle, RigidBodySet, +}; +use rapier2d::geometry::{BroadPhase, Collider, ColliderHandle, ColliderSet, NarrowPhase}; +use rapier2d::na::Vector2; +use rapier2d::pipeline::PhysicsPipeline; +use rapier2d::prelude::RigidBody; +use std::sync::Arc; + +/// The type responsible to register and update [`Body2D`](crate::Body2D)s. +#[derive(Default, RootNode, NoVisit)] +pub struct Body2DRegister { + rigid_bodies: RigidBodySet, + colliders: ColliderSet, + island_manager: IslandManager, + impulse_joints: ImpulseJointSet, + multibody_joints: MultibodyJointSet, + integration_parameters: IntegrationParameters, + #[allow(clippy::struct_field_names)] + physics_pipeline: PhysicsPipeline, + broad_phase: BroadPhase, + narrow_phase: NarrowPhase, + ccd_solver: CCDSolver, + body_ids: Arc, + body_details: Vec>, + collider_body_ids: FxHashMap, +} + +impl Node for Body2DRegister { + fn on_enter(&mut self, ctx: &mut Context<'_>) { + for index in self.body_ids.take_deleted_indexes() { + self.delete_body(index); + } + self.integration_parameters.dt = ctx.root::().duration.as_secs_f32(); + self.physics_pipeline.step( + &Vector2::zeros(), + &self.integration_parameters, + &mut self.island_manager, + &mut self.broad_phase, + &mut self.narrow_phase, + &mut self.rigid_bodies, + &mut self.colliders, + &mut self.impulse_joints, + &mut self.multibody_joints, + &mut self.ccd_solver, + None, + ctx.root::(), + &(), + ); + self.reset_collisions(); + self.register_collisions(); + } +} + +impl Body2DRegister { + /// Returns the transform properties of a [`Body2D`](crate::Body2D) with a given `index`. + pub fn transform(&self, index: &Body2DIndex) -> Transform2D { + let details = self.body_details[index.as_usize()] + .as_ref() + .expect("internal error: missing body"); + let rigid_body = &self.rigid_bodies[details.rigid_body]; + Transform2D { + position: Vec2::new(rigid_body.translation().x, rigid_body.translation().y), + size: details.size, + rotation: rigid_body.rotation().angle(), + } + } + + pub(crate) fn rigid_body_mut(&mut self, index: &Index) -> &mut RigidBody { + let details = self.body_details[index.value()] + .as_mut() + .expect("internal error: missing body"); + &mut self.rigid_bodies[details.rigid_body] + } + + pub(crate) fn collider_mut(&mut self, index: &Index) -> &mut Collider { + let details = self.body_details[index.value()] + .as_mut() + .expect("internal error: missing body"); + &mut self.colliders[details.collider] + } + + pub(crate) fn set_size(&mut self, index: &Index, size: Vec2) { + self.body_details[index.value()] + .as_mut() + .expect("internal error: missing body") + .size = size; + } + + pub(crate) fn collisions(&mut self, index: &Index) -> Vec { + self.body_details[index.value()] + .as_mut() + .expect("internal error: missing body") + .collisions + .clone() + } + + pub(crate) fn register_body( + &mut self, + rigid_body: RigidBody, + collider: Collider, + size: Vec2, + ) -> Index { + let index = self.body_ids.generate(); + for _ in self.body_details.len()..=index.value() { + self.body_details.push(None); + } + let rigid_body = self.rigid_bodies.insert(rigid_body); + let collider = + self.colliders + .insert_with_parent(collider, rigid_body, &mut self.rigid_bodies); + self.body_details[index.value()] = Some(BodyDetails { + rigid_body, + collider, + collisions: vec![], + size, + }); + self.collider_body_ids.insert(collider, index.value()); + index + } + + fn delete_body(&mut self, index: usize) { + let details = self.body_details[index] + .as_mut() + .expect("internal error: missing body"); + self.rigid_bodies.remove( + details.rigid_body, + &mut self.island_manager, + &mut self.colliders, + &mut self.impulse_joints, + &mut self.multibody_joints, + true, + ); + self.body_details[index] = None; + } + + fn reset_collisions(&mut self) { + for body in self.body_details.iter_mut().flatten() { + body.collisions.clear(); + } + } + + fn register_collisions(&mut self) { + for pair in self.narrow_phase.contact_pairs() { + let body1_index = self.collider_body_ids[&pair.collider1]; + let body2_index = self.collider_body_ids[&pair.collider2]; + let collider1 = &self.colliders[pair.collider1]; + let collider2 = &self.colliders[pair.collider2]; + let group1_index = collider1.user_data as usize; + let group2_index = collider2.user_data as usize; + for manifold in &pair.manifolds { + if manifold.points.iter().all(|p| p.dist > 0.) { + continue; + } + let body1 = self.body_details[body1_index] + .as_mut() + .expect("internal error: missing body"); + body1.collisions.push(Collision2D::new( + false, + body2_index, + group2_index, + collider1, + manifold, + )); + let body2 = self.body_details[body2_index] + .as_mut() + .expect("internal error: missing body"); + body2.collisions.push(Collision2D::new( + true, + body1_index, + group1_index, + collider2, + manifold, + )); + } + } + } +} + +/// The transform properties of a 2D object. +#[derive(Debug, Clone, Copy)] +pub struct Transform2D { + /// Position of the object in world units. + pub position: Vec2, + /// Size of the object in world units. + pub size: Vec2, + /// Rotation of the object in radians. + pub rotation: f32, +} + +struct BodyDetails { + rigid_body: RigidBodyHandle, + collider: ColliderHandle, + collisions: Vec, + size: Vec2, +} diff --git a/crates/modor_physics/src/collision_group.rs b/crates/modor_physics/src/collision_group.rs new file mode 100644 index 00000000..a2de5475 --- /dev/null +++ b/crates/modor_physics/src/collision_group.rs @@ -0,0 +1,213 @@ +use fxhash::FxHashMap; +use modor::{Context, NoVisit, Node, RootNode}; +use modor_internal::index::{Index, IndexPool}; +use rapier2d::geometry::SolverFlags; +use rapier2d::pipeline::{ContactModificationContext, PairFilterContext}; +use rapier2d::prelude::{Group, InteractionGroups}; +use std::sync::Arc; + +#[derive(Debug, Default, RootNode, NoVisit)] +pub(crate) struct CollisionGroupRegister { + ids: Arc, + collision_types: FxHashMap<(usize, usize), CollisionType>, + interaction_groups: Vec, +} + +impl Node for CollisionGroupRegister { + fn on_enter(&mut self, _ctx: &mut Context<'_>) { + for index in self.ids.take_deleted_indexes() { + self.interaction_groups[index] = InteractionGroups::none(); + self.collision_types + .retain(|&(index1, index2), _| index == index1 || index == index2); + } + for group in &mut self.interaction_groups { + group.filter = Group::empty(); + } + for &(index1, index2) in self.collision_types.keys() { + Self::add_filter(&mut self.interaction_groups, index1, index2); + } + } +} + +impl rapier2d::pipeline::PhysicsHooks for CollisionGroupRegister { + fn filter_contact_pair(&self, context: &PairFilterContext<'_>) -> Option { + let group1_index = context.colliders[context.collider1].user_data as usize; + let group2_index = context.colliders[context.collider2].user_data as usize; + match self.collision_types.get(&(group1_index, group2_index))? { + CollisionType::Sensor => Some(SolverFlags::empty()), + CollisionType::Impulse(_) => Some(SolverFlags::COMPUTE_IMPULSES), + } + } + + fn modify_solver_contacts(&self, context: &mut ContactModificationContext<'_>) { + let group1_index = context.colliders[context.collider1].user_data as usize; + let group2_index = context.colliders[context.collider2].user_data as usize; + if let Some(CollisionType::Impulse(impulse)) = + self.collision_types.get(&(group1_index, group2_index)) + { + for contact in context.solver_contacts.iter_mut() { + contact.restitution = impulse.restitution; + contact.friction = impulse.friction; + } + } + } +} + +impl CollisionGroupRegister { + fn register(&mut self) -> Index { + let index = self.ids.generate(); + for index in self.interaction_groups.len()..=index.value() { + self.interaction_groups.push(InteractionGroups::new( + Group::from(1 << (index % 32)), + Group::empty(), + )); + } + index + } + + fn add_interaction(&mut self, index1: &Index, index2: &Index, type_: CollisionType) { + self.collision_types + .insert((index1.value(), index2.value()), type_); + self.collision_types + .insert((index2.value(), index1.value()), type_); + Self::add_filter(&mut self.interaction_groups, index1.value(), index2.value()); + } + + fn add_filter(groups: &mut [InteractionGroups], index1: usize, index2: usize) { + groups[index1].filter |= Group::from(1 << (index2 % 32)); + groups[index2].filter |= Group::from(1 << (index1 % 32)); + } +} + +/// The reference to a collision group that can interact with other collision groups. +/// +/// # Examples +/// +/// ```rust +/// # use modor::*; +/// # use modor_math::*; +/// # use modor_physics::*; +/// # +/// #[derive(Node, NoVisit)] +/// struct CollisionGroups { +/// wall: CollisionGroup, +/// ball: CollisionGroup, +/// paddle: CollisionGroup, +/// } +/// +/// impl RootNode for CollisionGroups { +/// fn on_create(ctx: &mut Context<'_>) -> Self { +/// let wall = CollisionGroup::new(ctx); +/// let ball = CollisionGroup::new(ctx); +/// ball.add_interaction(ctx, &wall, CollisionType::Impulse(Impulse::new(1., 0.))); +/// let paddle = CollisionGroup::new(ctx); +/// paddle.add_interaction(ctx, &wall, CollisionType::Impulse(Impulse::new(0., 0.))); +/// paddle.add_interaction(ctx, &ball, CollisionType::Sensor); +/// Self { +/// wall, +/// ball, +/// paddle, +/// } +/// } +/// } +/// +/// fn create_wall_body(ctx: &mut Context<'_>, position: Vec2, size: Vec2) -> Body2D { +/// let mut body = Body2D::new(ctx, position, size); +/// let groups = ctx.root::(); +/// body.collision_group = Some(groups.wall.clone()); +/// body +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct CollisionGroup { + index: Arc, +} + +impl CollisionGroup { + /// Creates and register a new collision group. + pub fn new(ctx: &mut Context<'_>) -> Self { + Self { + index: Arc::new(ctx.root::().register()), + } + } + + /// Returns the unique index of the collision group. + /// + /// Note that index of a dropped group can be reused for a new group. + pub fn index(&self) -> usize { + self.index.value() + } + + /// Register an interaction of a given `type_` between the group and an `other` group. + /// + /// In case it already exists an interaction between these two groups, the collision type is + /// overwritten. + pub fn add_interaction(&self, ctx: &mut Context<'_>, other: &Self, type_: CollisionType) { + ctx.root::() + .add_interaction(&self.index, &other.index, type_); + } + + pub(crate) fn interaction_groups(&self, ctx: &mut Context<'_>) -> InteractionGroups { + ctx.root::().interaction_groups[self.index.value()] + } +} + +/// The collision behavior that should happen between two objects. +/// +/// # Examples +/// +/// See [`CollisionGroup`]. +#[derive(Clone, Copy, PartialEq, Debug)] +#[non_exhaustive] +pub enum CollisionType { + /// Collision should happen but it doesn't produce forces. + Sensor, + /// Collision should happen and it produces forces. + /// + /// Note that there is no effect if the body is not dynamic, or if its mass and angular inertia + /// are equal to zero. + Impulse(Impulse), +} + +/// Properties of a collision of type [`CollisionType::Impulse`]. +/// +/// # Examples +/// +/// See [`CollisionGroup`]. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Impulse { + /// Restitution coefficient of the collision. + /// + /// A coefficient of `0.0` means that the bodies do not bounce off each other at all.
+ /// A coefficient of `1.0` means that the exit velocity magnitude is the same as the initial + /// velocity along the contact normal. + /// + /// Default value is `0.0`. + pub restitution: f32, + /// Friction coefficient of the collision. + /// + /// A coefficient of `0.0` means there is no friction (i.e. objects slide completely over each + /// other). + /// + /// Default value is `0.5`. + pub friction: f32, +} + +impl Default for Impulse { + fn default() -> Self { + Self { + restitution: 0., + friction: 0.5, + } + } +} + +impl Impulse { + /// Creates a new impulse configuration. + pub fn new(restitution: f32, friction: f32) -> Self { + Self { + restitution, + friction, + } + } +} diff --git a/crates/modor_physics/src/delta.rs b/crates/modor_physics/src/delta.rs new file mode 100644 index 00000000..4a7047e1 --- /dev/null +++ b/crates/modor_physics/src/delta.rs @@ -0,0 +1,16 @@ +use modor::{NoVisit, Node, RootNode}; +use std::time::Duration; + +/// The duration of the latest update. +/// +/// The physics create does not update automatically this object.
+/// Instead, the delta time can be manually set to simulate time, or be automatically updated +/// by another crate (e.g. by the graphics crate). +#[non_exhaustive] +#[derive(Default, Debug, RootNode, Node, NoVisit)] +pub struct Delta { + /// Duration of the last update. + /// + /// Default value is [`Duration::ZERO`]. + pub duration: Duration, +} diff --git a/crates/modor_physics/src/lib.rs b/crates/modor_physics/src/lib.rs new file mode 100644 index 00000000..9b6b7406 --- /dev/null +++ b/crates/modor_physics/src/lib.rs @@ -0,0 +1,46 @@ +//! Physics crate of Modor. +//! +//! # Getting started +//! +//! You need to include these dependencies in your `Cargo.toml` file: +//! ```toml +//! modor_physics = "0.1" +//! ``` +//! +//! You can start to use this crate: +//! +//! ```rust +//! # use modor::*; +//! # use modor::log::*; +//! # use modor_math::*; +//! # use modor_physics::*; +//! # +//! let mut app = App::new::(Level::Info); +//! // ... +//! +//! #[derive(Node, Visit)] +//! struct Root { +//! body: Body2D, +//! } +//! +//! impl RootNode for Root { +//! fn on_create(ctx: &mut Context<'_>) -> Self { +//! Self { +//! body: Body2D::new(ctx, Vec2::ZERO, Vec2::ONE), +//! } +//! } +//! } +//! ``` + +mod body; +mod body_register; +mod collision_group; +mod delta; + +pub use body::*; +pub use body_register::*; +pub use collision_group::*; +pub use delta::*; + +pub use modor; +pub use modor_math; diff --git a/crates/modor_physics/tests/integration/body/collisions.rs b/crates/modor_physics/tests/integration/body/collisions.rs new file mode 100644 index 00000000..0faac063 --- /dev/null +++ b/crates/modor_physics/tests/integration/body/collisions.rs @@ -0,0 +1,233 @@ +use modor::log::Level; +use modor::{App, Context, Node, RootNode, Visit}; +use modor_internal::assert_approx_eq; +use modor_math::Vec2; +use modor_physics::{Body2D, CollisionGroup, CollisionType, Delta, Impulse, Shape2D}; +use std::time::Duration; + +#[modor::test] +fn colliding_bodies_without_collision_group() { + let mut app = App::new::(Level::Info); + app.update(); + app.update(); + assert!(body1(&mut app).collisions().is_empty()); + assert!(body2(&mut app).collisions().is_empty()); +} + +#[modor::test] +fn colliding_bodies_with_no_interaction() { + let mut app = App::new::(Level::Info); + body1(&mut app).collision_group = Some(CollisionGroup::new(&mut app.ctx())); + body2(&mut app).collision_group = Some(CollisionGroup::new(&mut app.ctx())); + app.update(); + app.update(); + assert!(body1(&mut app).collisions().is_empty()); + assert!(body2(&mut app).collisions().is_empty()); +} + +#[modor::test] +fn colliding_bodies_with_sensor() { + let mut app = App::new::(Level::Info); + configure_colliding_groups(&mut app, CollisionType::Sensor); + app.update(); + app.update(); + let group1 = body1(&mut app).collision_group.clone().unwrap(); + let group2 = body2(&mut app).collision_group.clone().unwrap(); + let body = body1(&mut app); + assert_approx_eq!(body.position, Vec2::ZERO); + assert_eq!(body.collisions().len(), 1); + assert_approx_eq!(body.collisions()[0].position, Vec2::X * 0.5); + assert_approx_eq!(body.collisions()[0].penetration, Vec2::X * 0.75); + assert_eq!(body.collisions()[0].other_index, 1); + assert_eq!(body.collisions()[0].other_group_index, group2.index()); + let body = body2(&mut app); + assert_approx_eq!(body.position, Vec2::X); + assert_eq!(body.collisions().len(), 1); + assert_approx_eq!(body.collisions()[0].position, Vec2::X * -0.25); + assert_approx_eq!(body.collisions()[0].penetration, Vec2::X * -0.75); + assert_eq!(body.collisions()[0].other_index, 0); + assert_eq!(body.collisions()[0].other_group_index, group1.index()); +} + +#[modor::test] +fn colliding_bodies_with_impulse() { + let mut app = App::new::(Level::Info); + configure_colliding_groups(&mut app, CollisionType::Impulse(Impulse::default())); + body2(&mut app).mass = 1.; + app.update(); + app.update(); + let group1 = body1(&mut app).collision_group.clone().unwrap(); + let group2 = body2(&mut app).collision_group.clone().unwrap(); + let body = body1(&mut app); + assert_approx_eq!(body.position, Vec2::ZERO); + assert_eq!(body.collisions().len(), 1); + assert_approx_eq!(body.collisions()[0].position, Vec2::X * 0.5); + assert_approx_eq!(body.collisions()[0].penetration, Vec2::X * 0.053_567); + assert_eq!(body.collisions()[0].other_index, 1); + assert_eq!(body.collisions()[0].other_group_index, group2.index()); + let body = body2(&mut app); + assert!(body.position.x > 1.1); + assert_eq!(body.collisions().len(), 1); + assert_approx_eq!(body.collisions()[0].position, Vec2::X * 0.446_432); + assert_approx_eq!(body.collisions()[0].penetration, Vec2::X * -0.053_567); + assert_eq!(body.collisions()[0].other_index, 0); + assert_eq!(body.collisions()[0].other_group_index, group1.index()); +} + +#[modor::test(cases( + zero = "0., Vec2::new(0.25, 0.249_921)", + one = "1., Vec2::new(0.228_143, 0.249_921)" +))] +fn set_friction(friction: f32, expected_position: Vec2) { + let mut app = App::new::(Level::Info); + configure_colliding_groups(&mut app, CollisionType::Impulse(Impulse::new(0., friction))); + configure_ground(&mut app); + configure_rolling_ball(&mut app); + app.update(); + app.update(); + assert_approx_eq!(body1(&mut app).position, Vec2::ZERO); + assert_approx_eq!(body2(&mut app).position, expected_position); +} + +#[modor::test(cases( + zero = "0., Vec2::new(0., 0.222_098)", + one = "1., Vec2::new(0., 0.341_609)" +))] +fn set_restitution(restitution: f32, expected_position: Vec2) { + let mut app = App::new::(Level::Info); + app.root::().duration = Duration::from_secs_f32(0.1); + let impulse = Impulse::new(restitution, 0.5); + configure_colliding_groups(&mut app, CollisionType::Impulse(impulse)); + configure_ground(&mut app); + configure_falling_ball(&mut app); + for _ in 0..11 { + app.update(); + } + assert_approx_eq!(body1(&mut app).position, Vec2::ZERO); + assert_approx_eq!(body2(&mut app).position, expected_position); +} + +#[modor::test(cases( + less = "-1, Vec2::new(0., 0.341_609)", + equal = "0, Vec2::new(0., 0.341_609)", + greater = "1, Vec2::new(0., -0.0249_998)" +))] +fn set_dominance(dominance: i8, expected_position: Vec2) { + let mut app = App::new::(Level::Info); + app.root::().duration = Duration::from_secs_f32(0.1); + configure_colliding_groups(&mut app, CollisionType::Impulse(Impulse::new(1., 0.5))); + configure_ground(&mut app); + configure_falling_ball(&mut app); + body2(&mut app).dominance = dominance; + for _ in 0..11 { + app.update(); + } + assert_approx_eq!(body1(&mut app).position, Vec2::ZERO); + assert_approx_eq!(body2(&mut app).position, expected_position); +} + +#[modor::test(cases( + enabled = "true, Vec2::new(0., 0.255)", + disabled = "false, Vec2::new(0., -4.)" +))] +fn set_ccd(is_enabled: bool, expected_position: Vec2) { + let mut app = App::new::(Level::Info); + configure_colliding_groups(&mut app, CollisionType::Impulse(Impulse::new(1., 0.5))); + configure_ground(&mut app); + configure_falling_ball(&mut app); + body2(&mut app).is_ccd_enabled = is_enabled; + app.update(); + app.update(); + assert_approx_eq!(body1(&mut app).position, Vec2::ZERO); + assert_approx_eq!(body2(&mut app).position, expected_position); +} + +#[modor::test(cases( + diagonal_rectangle = "Vec2::new(0.9, 0.9), Vec2::ONE, Shape2D::Rectangle, 1", + horizontal_rectangle = "Vec2::X * 0.9, Vec2::ONE, Shape2D::Rectangle, 1", + vectical_rectangle = "Vec2::Y * 0.9, Vec2::ONE, Shape2D::Rectangle, 1", + diagonal_circle = "Vec2::new(0.9, 0.9), Vec2::ONE, Shape2D::Circle, 0", + horizontal_circle = "Vec2::X * 0.9, Vec2::ONE, Shape2D::Circle, 1", + vectical_circle = "Vec2::Y * 0.9, Vec2::ONE, Shape2D::Circle, 1", + horizontal_circle_lower_height = "Vec2::X * 0.9, Vec2::new(1., 0.79), Shape2D::Circle, 0", + vectical_circle_lower_height = "Vec2::Y * 0.9, Vec2::new(1., 0.79), Shape2D::Circle, 0", +))] +fn set_shape(position: Vec2, size: Vec2, shape: Shape2D, collision_count: usize) { + let mut app = App::new::(Level::Info); + configure_colliding_groups(&mut app, CollisionType::Sensor); + body2(&mut app).position = position; + body2(&mut app).size = size; + body2(&mut app).shape = shape; + app.update(); + app.update(); + assert_eq!(body1(&mut app).collisions().len(), collision_count); + assert_eq!(body2(&mut app).collisions().len(), collision_count); +} + +#[modor::test] +fn update_size() { + let mut app = App::new::(Level::Info); + configure_colliding_groups(&mut app, CollisionType::Sensor); + app.update(); + app.update(); + assert_eq!(body1(&mut app).collisions().len(), 1); + assert_eq!(body2(&mut app).collisions().len(), 1); + body2(&mut app).size.x = 0.5; + app.update(); + app.update(); + assert_eq!(body1(&mut app).collisions().len(), 0); + assert_eq!(body2(&mut app).collisions().len(), 0); +} + +fn body1(app: &mut App) -> &mut Body2D { + &mut app.root::().body1 +} + +fn body2(app: &mut App) -> &mut Body2D { + &mut app.root::().body2 +} + +fn configure_colliding_groups(app: &mut App, collision_type: CollisionType) { + let ground_group = CollisionGroup::new(&mut app.ctx()); + let ball_group = CollisionGroup::new(&mut app.ctx()); + ball_group.add_interaction(&mut app.ctx(), &ground_group, collision_type); + body1(app).collision_group = Some(ground_group); + body2(app).collision_group = Some(ball_group); +} + +fn configure_ground(app: &mut App) { + body1(app).position = Vec2::ZERO; + body1(app).size = Vec2::new(1., 0.01); +} + +fn configure_falling_ball(app: &mut App) { + body2(app).position = Vec2::Y * 1.; + body2(app).size = Vec2::ONE * 0.5; + body2(app).mass = 10.; + body2(app).force = -20. * Vec2::Y; + body2(app).shape = Shape2D::Circle; +} + +fn configure_rolling_ball(app: &mut App) { + body2(app).position = Vec2::Y * 0.251; + body2(app).size = Vec2::ONE * 0.5; + body2(app).mass = 10.; + body2(app).force = Vec2::new(1., -0.1); + body2(app).shape = Shape2D::Circle; +} + +#[derive(Node, Visit)] +struct Root { + body1: Body2D, + body2: Body2D, +} + +impl RootNode for Root { + fn on_create(ctx: &mut Context<'_>) -> Self { + ctx.root::().duration = Duration::from_secs(2); + Self { + body1: Body2D::new(ctx, Vec2::ZERO, Vec2::ONE), + body2: Body2D::new(ctx, Vec2::X, Vec2::new(2.5, 0.5)), + } + } +} diff --git a/crates/modor_physics/tests/integration/body/drop.rs b/crates/modor_physics/tests/integration/body/drop.rs new file mode 100644 index 00000000..6381893d --- /dev/null +++ b/crates/modor_physics/tests/integration/body/drop.rs @@ -0,0 +1,29 @@ +use modor::log::Level; +use modor::{App, Node, RootNode, Visit}; +use modor_math::Vec2; +use modor_physics::Body2D; + +#[modor::test] +fn drop_body() { + let mut app = App::new::(Level::Info); + push_body(&mut app); + push_body(&mut app); + app.update(); + app.root::().bodies.remove(0); + push_body(&mut app); + app.update(); + push_body(&mut app); + assert_eq!(app.root::().bodies[0].index().as_usize(), 1); + assert_eq!(app.root::().bodies[1].index().as_usize(), 2); + assert_eq!(app.root::().bodies[2].index().as_usize(), 0); +} + +fn push_body(app: &mut App) { + let body = Body2D::new(&mut app.ctx(), Vec2::ZERO, Vec2::ONE); + app.root::().bodies.push(body); +} + +#[derive(Default, RootNode, Node, Visit)] +struct Root { + bodies: Vec, +} diff --git a/crates/modor_physics/tests/integration/body/dynamics.rs b/crates/modor_physics/tests/integration/body/dynamics.rs new file mode 100644 index 00000000..4af0532a --- /dev/null +++ b/crates/modor_physics/tests/integration/body/dynamics.rs @@ -0,0 +1,152 @@ +use modor::log::Level; +use modor::{App, Context, Node, RootNode, Visit}; +use modor_internal::assert_approx_eq; +use modor_math::Vec2; +use modor_physics::{Body2D, Delta}; +use std::f32::consts::{FRAC_PI_2, FRAC_PI_4, FRAC_PI_8, PI}; +use std::time::Duration; + +#[modor::test] +fn update_velocity() { + let mut app = App::new::(Level::Info); + app.update(); + assert_approx_eq!(body(&mut app).position, Vec2::ZERO); + body(&mut app).velocity = Vec2::new(2., 1.); + app.update(); + app.update(); + assert_approx_eq!(body(&mut app).position, Vec2::new(4., 2.)); + app.update(); + assert_approx_eq!(body(&mut app).position, Vec2::new(8., 4.)); +} + +#[modor::test] +fn update_angular_velocity() { + let mut app = App::new::(Level::Info); + app.update(); + assert_approx_eq!(body(&mut app).rotation, 0.); + body(&mut app).angular_inertia = 1.; + body(&mut app).angular_velocity = FRAC_PI_4; + app.update(); + app.update(); + assert_approx_eq!(body(&mut app).rotation, FRAC_PI_2); + app.update(); + assert_approx_eq!(body(&mut app).rotation, -PI); + app.update(); + assert_approx_eq!(body(&mut app).rotation, -FRAC_PI_2); + app.update(); + assert_approx_eq!(body(&mut app).rotation, 0.); +} + +#[modor::test] +fn update_damping() { + let mut app = App::new::(Level::Info); + body(&mut app).velocity = Vec2::new(2., 1.); + app.update(); + app.update(); + assert_approx_eq!(body(&mut app).position, Vec2::new(4., 2.)); + body(&mut app).damping = 0.5; + app.update(); + app.update(); + assert_approx_eq!(body(&mut app).position, Vec2::new(11.2, 5.6)); +} + +#[modor::test] +fn update_angular_damping() { + let mut app = App::new::(Level::Info); + body(&mut app).angular_inertia = 1.; + body(&mut app).angular_velocity = FRAC_PI_4; + app.update(); + app.update(); + assert_approx_eq!(body(&mut app).rotation, FRAC_PI_2); + body(&mut app).angular_damping = 0.5; + app.update(); + app.update(); + assert_approx_eq!(body(&mut app).rotation, -0.6 * PI); +} + +#[modor::test(cases( + equal_to_one = "1., Vec2::new(5., 2.5), Vec2::new(18., 9.)", + equal_to_two = "2., Vec2::new(2.5, 1.25), Vec2::new(9., 4.5)" +))] +fn update_force_and_mass(mass: f32, expected_position1: Vec2, expected_position2: Vec2) { + let mut app = App::new::(Level::Info); + body(&mut app).mass = mass; + app.update(); + app.update(); + assert_approx_eq!(body(&mut app).position, Vec2::ZERO); + body(&mut app).force = Vec2::new(2., 1.); + app.update(); + app.update(); + assert_approx_eq!(body(&mut app).position, expected_position1); + app.update(); + assert_approx_eq!(body(&mut app).position, expected_position2); +} + +#[modor::test(cases( + equal_to_one = "1., 0.98174775, -2.7488935", + equal_to_two = "2., 0.98174775 / 2., 1.767146" +))] +fn update_torque_and_angular_inertia( + angular_inertia: f32, + expected_rotation1: f32, + expected_rotation2: f32, +) { + let mut app = App::new::(Level::Info); + body(&mut app).angular_inertia = angular_inertia; + app.update(); + app.update(); + assert_approx_eq!(body(&mut app).rotation, 0.); + body(&mut app).torque = FRAC_PI_8; + app.update(); + app.update(); + assert_approx_eq!(body(&mut app).rotation, expected_rotation1); + app.update(); + assert_approx_eq!(body(&mut app).rotation, expected_rotation2); +} + +#[modor::test] +fn update_position() { + let mut app = App::new::(Level::Info); + body(&mut app).velocity = Vec2::new(2., 1.); + app.update(); + app.update(); + assert_approx_eq!(body(&mut app).position, Vec2::new(4., 2.)); + body(&mut app).position = Vec2::ZERO; + app.update(); + assert_approx_eq!(body(&mut app).position, Vec2::ZERO); + app.update(); + assert_approx_eq!(body(&mut app).position, Vec2::new(4., 2.)); +} + +#[modor::test] +fn update_rotation() { + let mut app = App::new::(Level::Info); + body(&mut app).angular_inertia = 1.; + body(&mut app).angular_velocity = FRAC_PI_4; + app.update(); + app.update(); + assert_approx_eq!(body(&mut app).rotation, FRAC_PI_2); + body(&mut app).rotation = 0.; + app.update(); + assert_approx_eq!(body(&mut app).rotation, 0.); + app.update(); + assert_approx_eq!(body(&mut app).rotation, FRAC_PI_2); +} + +fn body(app: &mut App) -> &mut Body2D { + &mut app.root::().body +} + +#[derive(Node, Visit)] +struct Root { + body: Body2D, +} + +impl RootNode for Root { + fn on_create(ctx: &mut Context<'_>) -> Self { + ctx.root::().duration = Duration::from_secs(2); + Self { + body: Body2D::new(ctx, Vec2::ZERO, Vec2::ONE), + } + } +} diff --git a/crates/modor_physics/tests/integration/body/mod.rs b/crates/modor_physics/tests/integration/body/mod.rs new file mode 100644 index 00000000..cb7dd605 --- /dev/null +++ b/crates/modor_physics/tests/integration/body/mod.rs @@ -0,0 +1,3 @@ +pub mod collisions; +pub mod drop; +pub mod dynamics; diff --git a/crates/modor_physics/tests/integration/body_register.rs b/crates/modor_physics/tests/integration/body_register.rs new file mode 100644 index 00000000..cb713e9c --- /dev/null +++ b/crates/modor_physics/tests/integration/body_register.rs @@ -0,0 +1,41 @@ +use modor::log::Level; +use modor::{App, Context, Node, RootNode, Visit}; +use modor_internal::assert_approx_eq; +use modor_math::Vec2; +use modor_physics::{Body2D, Body2DRegister}; +use std::f32::consts::FRAC_PI_2; + +#[modor::test] +fn retrieve_transform() { + let mut app = App::new::(Level::Info); + let body_index = body(&mut app).index(); + let transform = app.root::().transform(&body_index); + assert_approx_eq!(transform.position, Vec2::ZERO); + assert_approx_eq!(transform.size, Vec2::ONE); + assert_approx_eq!(transform.rotation, 0.); + body(&mut app).position = Vec2::new(1., 2.); + body(&mut app).size = Vec2::new(3., 4.); + body(&mut app).rotation = FRAC_PI_2; + app.update(); + let transform = app.root::().transform(&body_index); + assert_approx_eq!(transform.position, Vec2::new(1., 2.)); + assert_approx_eq!(transform.size, Vec2::new(3., 4.)); + assert_approx_eq!(transform.rotation, FRAC_PI_2); +} + +fn body(app: &mut App) -> &mut Body2D { + &mut app.root::().body +} + +#[derive(Node, Visit)] +struct Root { + body: Body2D, +} + +impl RootNode for Root { + fn on_create(ctx: &mut Context<'_>) -> Self { + Self { + body: Body2D::new(ctx, Vec2::ZERO, Vec2::ONE), + } + } +} diff --git a/crates/modor_physics/tests/integration/collision_group.rs b/crates/modor_physics/tests/integration/collision_group.rs new file mode 100644 index 00000000..2cf6f44d --- /dev/null +++ b/crates/modor_physics/tests/integration/collision_group.rs @@ -0,0 +1,68 @@ +use modor::log::Level; +use modor::{App, Context, Node, RootNode, Visit}; +use modor_math::Vec2; +use modor_physics::{Body2D, CollisionGroup, CollisionType}; + +#[modor::test] +fn recreate_group() { + let mut app = App::new::(Level::Info); + let group1 = CollisionGroup::new(&mut app.ctx()); + let group2 = CollisionGroup::new(&mut app.ctx()); + drop(group2); + let group3 = CollisionGroup::new(&mut app.ctx()); + assert_eq!(group1.index(), 0); + assert_eq!(group3.index(), 2); + app.update(); + let group4 = CollisionGroup::new(&mut app.ctx()); + assert_eq!(group4.index(), 1); +} + +#[modor::test] +fn drop_group_and_associated_interactions() { + let mut app = App::new::(Level::Info); + let group1 = CollisionGroup::new(&mut app.ctx()); + let group2 = CollisionGroup::new(&mut app.ctx()); + let group2_index = group2.index(); + group1.add_interaction(&mut app.ctx(), &group2, CollisionType::Sensor); + body1(&mut app).collision_group = Some(group1); + body2(&mut app).collision_group = Some(group2); + app.update(); + app.update(); + assert_eq!(body1(&mut app).collisions().len(), 1); + assert_eq!(body2(&mut app).collisions().len(), 1); + body2(&mut app).collision_group = None; + app.update(); + app.update(); + assert_eq!(body1(&mut app).collisions().len(), 0); + assert_eq!(body2(&mut app).collisions().len(), 0); + let group2 = CollisionGroup::new(&mut app.ctx()); + assert_eq!(group2.index(), group2_index); + body2(&mut app).collision_group = Some(group2); + app.update(); + app.update(); + assert_eq!(body1(&mut app).collisions().len(), 0); + assert_eq!(body2(&mut app).collisions().len(), 0); +} + +fn body1(app: &mut App) -> &mut Body2D { + &mut app.root::().body1 +} + +fn body2(app: &mut App) -> &mut Body2D { + &mut app.root::().body2 +} + +#[derive(Node, Visit)] +struct Root { + body1: Body2D, + body2: Body2D, +} + +impl RootNode for Root { + fn on_create(ctx: &mut Context<'_>) -> Self { + Self { + body1: Body2D::new(ctx, Vec2::ZERO, Vec2::ONE), + body2: Body2D::new(ctx, Vec2::ZERO, Vec2::ONE), + } + } +} diff --git a/crates/modor_physics/tests/integration/main.rs b/crates/modor_physics/tests/integration/main.rs new file mode 100644 index 00000000..3476d4a3 --- /dev/null +++ b/crates/modor_physics/tests/integration/main.rs @@ -0,0 +1,5 @@ +#![allow(clippy::unwrap_used)] + +pub mod body; +pub mod body_register; +pub mod collision_group; diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 53c655d6..3bd912e5 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -11,6 +11,7 @@ rust-version.workspace = true [dependencies] modor.workspace = true +modor_physics.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] instant.workspace = true diff --git a/examples/examples/nodes.rs b/examples/examples/nodes.rs index a5b9786a..5add8d74 100644 --- a/examples/examples/nodes.rs +++ b/examples/examples/nodes.rs @@ -1,8 +1,8 @@ #![allow(missing_docs)] +use instant::Instant; use modor::log::{info, Level}; use modor::{App, Context, Node, RootNode, Visit}; -use std::time::Instant; fn main() { let start = Instant::now(); diff --git a/examples/examples/physics.rs b/examples/examples/physics.rs new file mode 100644 index 00000000..45aa7630 --- /dev/null +++ b/examples/examples/physics.rs @@ -0,0 +1,69 @@ +#![allow(missing_docs)] + +use instant::Instant; +use modor::log::{info, Level}; +use modor::{App, Context, NoVisit, Node, RootNode, Visit}; +use modor_physics::modor_math::Vec2; +use modor_physics::{Body2D, CollisionGroup, CollisionType, Delta, Impulse}; +use std::time::Duration; + +fn main() { + let start = Instant::now(); + let mut app = App::new::(Level::Info); + info!("Build time: {:?}", start.elapsed()); + let start = Instant::now(); + let update_count = 100; + for _ in 0..update_count { + app.update(); + } + info!("Update time: {:?}", start.elapsed() / update_count); +} + +#[derive(Node, Visit)] +struct Root { + bodies: Vec, +} + +impl RootNode for Root { + fn on_create(ctx: &mut Context<'_>) -> Self { + ctx.root::().duration = Duration::from_secs(1); + Self { + bodies: (0..10_000).map(|i| BodyWrapper::new(ctx, i)).collect(), + } + } +} + +#[derive(Node, Visit)] +struct BodyWrapper(Body2D); + +impl BodyWrapper { + #[allow(clippy::cast_precision_loss)] + fn new(ctx: &mut Context<'_>, index: usize) -> Self { + let mut body = Body2D::new(ctx, Vec2::ZERO, Vec2::ONE); + body.position = Vec2::new(index as f32 * 0.5, index as f32 * 0.5) * 0.5; + body.size = Vec2::ONE * 0.1; + body.velocity = Vec2::new(1., 2.); + body.mass = 1.; + body.collision_group = Some(if index % 2 == 0 { + ctx.root::().group1.clone() + } else { + ctx.root::().group2.clone() + }); + Self(body) + } +} + +#[derive(Node, NoVisit)] +struct CollisionGroups { + group1: CollisionGroup, + group2: CollisionGroup, +} + +impl RootNode for CollisionGroups { + fn on_create(ctx: &mut Context<'_>) -> Self { + let group1 = CollisionGroup::new(ctx); + let group2 = CollisionGroup::new(ctx); + group1.add_interaction(ctx, &group2, CollisionType::Impulse(Impulse::default())); + Self { group1, group2 } + } +} From a6da18ecebb49d23503cd25ad66f03665da00cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ferr=C3=A9?= Date: Mon, 1 Apr 2024 12:40:56 +0200 Subject: [PATCH 3/7] Fix tests --- crates/modor_physics/tests/integration/collision_group.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/modor_physics/tests/integration/collision_group.rs b/crates/modor_physics/tests/integration/collision_group.rs index 2cf6f44d..0ab13974 100644 --- a/crates/modor_physics/tests/integration/collision_group.rs +++ b/crates/modor_physics/tests/integration/collision_group.rs @@ -1,7 +1,8 @@ +use std::time::Duration; use modor::log::Level; use modor::{App, Context, Node, RootNode, Visit}; use modor_math::Vec2; -use modor_physics::{Body2D, CollisionGroup, CollisionType}; +use modor_physics::{Body2D, CollisionGroup, CollisionType, Delta}; #[modor::test] fn recreate_group() { @@ -60,6 +61,7 @@ struct Root { impl RootNode for Root { fn on_create(ctx: &mut Context<'_>) -> Self { + ctx.root::().duration = Duration::from_secs(2); Self { body1: Body2D::new(ctx, Vec2::ZERO, Vec2::ONE), body2: Body2D::new(ctx, Vec2::ZERO, Vec2::ONE), From 0439de7c27429c1c66c1c08fa01a55f76f75d70e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ferr=C3=A9?= Date: Mon, 1 Apr 2024 12:41:20 +0200 Subject: [PATCH 4/7] Reformat file --- crates/modor_physics/tests/integration/collision_group.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/modor_physics/tests/integration/collision_group.rs b/crates/modor_physics/tests/integration/collision_group.rs index 0ab13974..0a8cd0ba 100644 --- a/crates/modor_physics/tests/integration/collision_group.rs +++ b/crates/modor_physics/tests/integration/collision_group.rs @@ -1,8 +1,8 @@ -use std::time::Duration; use modor::log::Level; use modor::{App, Context, Node, RootNode, Visit}; use modor_math::Vec2; use modor_physics::{Body2D, CollisionGroup, CollisionType, Delta}; +use std::time::Duration; #[modor::test] fn recreate_group() { From d90277591474ec8b00aa599fff94120c0ab70fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ferr=C3=A9?= Date: Mon, 1 Apr 2024 12:50:00 +0200 Subject: [PATCH 5/7] Fix root node update order --- crates/modor/src/app.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/modor/src/app.rs b/crates/modor/src/app.rs index accbfe68..dd3a2474 100644 --- a/crates/modor/src/app.rs +++ b/crates/modor/src/app.rs @@ -48,15 +48,15 @@ impl App { #[allow(clippy::needless_collect)] pub fn update(&mut self) { debug!("run update app..."); - for type_id in self.root_indexes.keys().copied().collect::>() { - let root = self.root_data(type_id); + for root_index in 0..self.roots.len() { + let root = &mut self.roots[root_index]; let mut value = root .value .take() .expect("internal error: root node already borrowed"); let update_fn = root.update_fn; update_fn(&mut *value, &mut self.ctx()); - self.root_data(type_id).value = Some(value); + self.roots[root_index].value = Some(value); } debug!("app updated"); } @@ -102,17 +102,11 @@ impl App { } fn retrieve_root(&mut self, type_id: TypeId) -> &mut dyn Any { - &mut **self - .root_data(type_id) + &mut **self.roots[self.root_indexes[&type_id]] .value .as_mut() .unwrap_or_else(|| panic!("root node `{}` already borrowed", any::type_name::())) } - - fn root_data(&mut self, type_id: TypeId) -> &mut RootNodeData { - let index = self.root_indexes[&type_id]; - &mut self.roots[index] - } } // If `App` was directly accessible during update, it would be possible to run `App::update`. From ac4d2dfe23dabf8780387615bdf7fe30d43c54b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ferr=C3=A9?= Date: Mon, 1 Apr 2024 12:50:40 +0200 Subject: [PATCH 6/7] Remove unnecessary annotation --- crates/modor/src/app.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/modor/src/app.rs b/crates/modor/src/app.rs index dd3a2474..33a4ed01 100644 --- a/crates/modor/src/app.rs +++ b/crates/modor/src/app.rs @@ -45,7 +45,6 @@ impl App { /// [`Node::update`] method is called for each registered root node. /// /// Root nodes are updated in the order in which they are created. - #[allow(clippy::needless_collect)] pub fn update(&mut self) { debug!("run update app..."); for root_index in 0..self.roots.len() { From cd961c8b425701b4c314044b9f7e74ffe33d8148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ferr=C3=A9?= Date: Mon, 1 Apr 2024 12:57:08 +0200 Subject: [PATCH 7/7] Add missing tests --- crates/modor_internal/src/index.rs | 2 +- crates/modor_physics/tests/integration/body/collisions.rs | 8 ++++++++ crates/modor_physics/tests/integration/collision_group.rs | 4 +--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/modor_internal/src/index.rs b/crates/modor_internal/src/index.rs index 5b5c5a44..50101ff7 100644 --- a/crates/modor_internal/src/index.rs +++ b/crates/modor_internal/src/index.rs @@ -50,7 +50,7 @@ impl Drop for Index { fn drop(&mut self) { match self.pool.deleted_indexes.lock() { Ok(mut indexes) => indexes.push(self.index), - Err(err) => error!("error: {err}"), + Err(err) => error!("error: {err}"), // no-coverage (difficult to test poisoning) } } } diff --git a/crates/modor_physics/tests/integration/body/collisions.rs b/crates/modor_physics/tests/integration/body/collisions.rs index 0faac063..4e365a01 100644 --- a/crates/modor_physics/tests/integration/body/collisions.rs +++ b/crates/modor_physics/tests/integration/body/collisions.rs @@ -40,6 +40,8 @@ fn colliding_bodies_with_sensor() { assert_approx_eq!(body.collisions()[0].penetration, Vec2::X * 0.75); assert_eq!(body.collisions()[0].other_index, 1); assert_eq!(body.collisions()[0].other_group_index, group2.index()); + assert!(body.is_colliding_with(&group2)); + assert!(!body.is_colliding_with(&group1)); let body = body2(&mut app); assert_approx_eq!(body.position, Vec2::X); assert_eq!(body.collisions().len(), 1); @@ -47,6 +49,8 @@ fn colliding_bodies_with_sensor() { assert_approx_eq!(body.collisions()[0].penetration, Vec2::X * -0.75); assert_eq!(body.collisions()[0].other_index, 0); assert_eq!(body.collisions()[0].other_group_index, group1.index()); + assert!(body.is_colliding_with(&group1)); + assert!(!body.is_colliding_with(&group2)); } #[modor::test] @@ -65,6 +69,8 @@ fn colliding_bodies_with_impulse() { assert_approx_eq!(body.collisions()[0].penetration, Vec2::X * 0.053_567); assert_eq!(body.collisions()[0].other_index, 1); assert_eq!(body.collisions()[0].other_group_index, group2.index()); + assert!(body.is_colliding_with(&group2)); + assert!(!body.is_colliding_with(&group1)); let body = body2(&mut app); assert!(body.position.x > 1.1); assert_eq!(body.collisions().len(), 1); @@ -72,6 +78,8 @@ fn colliding_bodies_with_impulse() { assert_approx_eq!(body.collisions()[0].penetration, Vec2::X * -0.053_567); assert_eq!(body.collisions()[0].other_index, 0); assert_eq!(body.collisions()[0].other_group_index, group1.index()); + assert!(body.is_colliding_with(&group1)); + assert!(!body.is_colliding_with(&group2)); } #[modor::test(cases( diff --git a/crates/modor_physics/tests/integration/collision_group.rs b/crates/modor_physics/tests/integration/collision_group.rs index 0a8cd0ba..2cf6f44d 100644 --- a/crates/modor_physics/tests/integration/collision_group.rs +++ b/crates/modor_physics/tests/integration/collision_group.rs @@ -1,8 +1,7 @@ use modor::log::Level; use modor::{App, Context, Node, RootNode, Visit}; use modor_math::Vec2; -use modor_physics::{Body2D, CollisionGroup, CollisionType, Delta}; -use std::time::Duration; +use modor_physics::{Body2D, CollisionGroup, CollisionType}; #[modor::test] fn recreate_group() { @@ -61,7 +60,6 @@ struct Root { impl RootNode for Root { fn on_create(ctx: &mut Context<'_>) -> Self { - ctx.root::().duration = Duration::from_secs(2); Self { body1: Body2D::new(ctx, Vec2::ZERO, Vec2::ONE), body2: Body2D::new(ctx, Vec2::ZERO, Vec2::ONE),