From 0b4973f9d8af07bd54113ff56471032737478161 Mon Sep 17 00:00:00 2001 From: Jasper Meggitt Date: Sat, 24 Aug 2019 17:03:09 -0400 Subject: [PATCH 1/7] Support nphysics2d and nphysics3d --- Cargo.toml | 27 +++-- src/bodies.rs | 122 ++++++++-------------- src/colliders.rs | 96 +++++++++-------- src/events.rs | 3 +- src/lib.rs | 105 ++++++++++--------- src/parameters.rs | 39 ++++--- src/positon.rs | 80 ++++++++++++++ src/systems/physics_stepper.rs | 14 ++- src/systems/sync_bodies_from_physics.rs | 11 +- src/systems/sync_bodies_to_physics.rs | 22 ++-- src/systems/sync_colliders_to_physics.rs | 24 ++--- src/systems/sync_parameters_to_physics.rs | 12 +-- 12 files changed, 304 insertions(+), 251 deletions(-) create mode 100644 src/positon.rs diff --git a/Cargo.toml b/Cargo.toml index 854e5fd..4901e4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,19 +14,21 @@ description = "nphysics integration for the Specs entity component system" keywords = ["specs", "nphysics", "nphysics3d"] [features] -default = [] - +physics3d = ["ncollide3d", "nphysics3d"] +physics2d = ["ncollide2d", "nphysics2d"] amethyst = ["amethyst_core"] [dependencies] -log = "0.4.6" -specs = "0.15.0" -specs-hierarchy = { git = "https://github.com/cedric-h/specs-hierarchy.git", branch = "update-specs" } -shrev = "1.1.1" -nalgebra = "0.18.0" -ncollide3d = "0.19" -nphysics3d = "0.11.1" -amethyst_core = { git = "https://github.com/amethyst/amethyst", optional = true } +log = "^0.4.6" +specs = "^0.14" +specs-hierarchy = "^0.3" +shrev = "^1.0" +nalgebra = "^0.18" +ncollide2d = { version = "^0.19", optional = true } +ncollide3d = { version = "^0.19", optional = true } +nphysics2d = { version = "^0.11", optional = true } +nphysics3d = { version = "^0.11", optional = true } +amethyst_core = { version = "^0.7", optional = true } objekt = "0.1.2" [dev-dependencies] @@ -36,20 +38,25 @@ approx = "0.3.2" [[example]] name = "basic" path = "examples/basic.rs" +required-features = ["physics3d"] [[example]] name = "hierarchy" path = "examples/hierarchy.rs" +required-features = ["physics3d"] [[example]] name = "positions" path = "examples/positions.rs" +required-features = ["physics3d"] [[example]] name = "collision" path = "examples/collision.rs" +required-features = ["physics3d"] [[example]] name = "events" path = "examples/events.rs" +required-features = ["physics3d"] diff --git a/src/bodies.rs b/src/bodies.rs index 58c33c5..95db879 100644 --- a/src/bodies.rs +++ b/src/bodies.rs @@ -1,72 +1,19 @@ use specs::{Component, DenseVecStorage, FlaggedStorage}; +use nalgebra::RealField; -use crate::{ - nalgebra::{Isometry3, Matrix3, Point3, RealField}, - nphysics::{ - algebra::{Force3, ForceType, Velocity3}, - object::{Body, BodyHandle, BodyPart, BodyStatus, RigidBody, RigidBodyDesc}, - }, -}; +#[cfg(feature = "physics3d")] +use nalgebra::{Point3 as Point, Matrix3}; -pub mod util { - use specs::{Component, DenseVecStorage, FlaggedStorage}; +#[cfg(feature = "physics2d")] +use nalgebra::{Point2 as Point}; - use crate::{ - bodies::Position, - nalgebra::{Isometry3, RealField}, - }; +#[cfg(feature = "physics3d")] +use nphysics::{algebra::{Force3 as Force, Velocity3 as Velocity, ForceType,}, + object::{Body, BodyHandle, BodyPart, BodyStatus, RigidBody, RigidBodyDesc}}; - pub struct SimplePosition(pub Isometry3); - - impl Position for SimplePosition { - fn isometry(&self) -> &Isometry3 { - &self.0 - } - - fn isometry_mut(&mut self) -> &mut Isometry3 { - &mut self.0 - } - - fn set_isometry(&mut self, isometry: &Isometry3) -> &mut Self { - self.0.rotation = isometry.rotation; - self.0.translation = isometry.translation; - self - } - } - - impl Component for SimplePosition { - type Storage = FlaggedStorage>; - } -} - -/// An implementation of the `Position` trait is required for the -/// synchronisation of the position of Specs and nphysics objects. -/// -/// Initially, it is used to position bodies in the nphysics `World`. Then after -/// progressing the `World` it is used to synchronise the updated positions back -/// towards Specs. -pub trait Position: - Component>> + Send + Sync -{ - fn isometry(&self) -> &Isometry3; - fn isometry_mut(&mut self) -> &mut Isometry3; - fn set_isometry(&mut self, isometry: &Isometry3) -> &mut Self; -} - -#[cfg(feature = "amethyst")] -impl Position for amethyst_core::Transform { - fn isometry(&self) -> &Isometry3 { - self.isometry() - } - - fn isometry_mut(&mut self) -> &mut Isometry3 { - self.isometry_mut() - } - - fn set_isometry(&mut self, isometry: &Isometry3) -> &mut Self { - self.set_isometry(*isometry) - } -} +#[cfg(feature = "physics2d")] +use nphysics::{algebra::{Force2 as Force, Velocity2 as Velocity, ForceType,}, + object::{Body, BodyHandle, BodyPart, BodyStatus, RigidBody, RigidBodyDesc}}; /// The `PhysicsBody` `Component` represents a `PhysicsWorld` `RigidBody` in /// Specs and contains all the data required for the synchronisation between @@ -76,11 +23,14 @@ pub struct PhysicsBody { pub(crate) handle: Option, pub gravity_enabled: bool, pub body_status: BodyStatus, - pub velocity: Velocity3, + pub velocity: Velocity, + #[cfg(feature = "physics3d")] pub angular_inertia: Matrix3, + #[cfg(feature = "physics2d")] + pub angular_inertia: N, pub mass: N, - pub local_center_of_mass: Point3, - external_forces: Force3, + pub local_center_of_mass: Point, + external_forces: Force, } impl Component for PhysicsBody { @@ -88,11 +38,11 @@ impl Component for PhysicsBody { } impl PhysicsBody { - pub fn check_external_force(&self) -> &Force3 { + pub fn check_external_force(&self) -> &Force { &self.external_forces } - pub fn apply_external_force(&mut self, force: &Force3) -> &mut Self { + pub fn apply_external_force(&mut self, force: &Force) -> &mut Self { self.external_forces += *force; self } @@ -133,9 +83,9 @@ impl PhysicsBody { self } - fn drain_external_force(&mut self) -> Force3 { + fn drain_external_force(&mut self) -> Force { let value = self.external_forces; - self.external_forces = Force3::::zero(); + self.external_forces = Force::::zero(); value } } @@ -146,7 +96,7 @@ impl PhysicsBody { /// /// # Example /// -/// ```rust +/// ```rust,ignore /// use specs_physics::{ /// nalgebra::{Matrix3, Point3}, /// nphysics::{algebra::Velocity3, object::BodyStatus}, @@ -164,10 +114,13 @@ impl PhysicsBody { pub struct PhysicsBodyBuilder { gravity_enabled: bool, body_status: BodyStatus, - velocity: Velocity3, + velocity: Velocity, + #[cfg(feature = "physics3d")] angular_inertia: Matrix3, + #[cfg(feature = "physics2d")] + angular_inertia: N, mass: N, - local_center_of_mass: Point3, + local_center_of_mass: Point, } impl From for PhysicsBodyBuilder { @@ -177,10 +130,13 @@ impl From for PhysicsBodyBuilder { Self { gravity_enabled: false, body_status, - velocity: Velocity3::zero(), + velocity: Velocity::zero(), + #[cfg(feature = "physics3d")] angular_inertia: Matrix3::zeros(), + #[cfg(feature = "physics2d")] + angular_inertia: N::zero(), mass: N::from_f32(1.2).unwrap(), - local_center_of_mass: Point3::origin(), + local_center_of_mass: Point::origin(), } } } @@ -193,17 +149,25 @@ impl PhysicsBodyBuilder { } // Sets the `velocity` value of the `PhysicsBodyBuilder`. - pub fn velocity(mut self, velocity: Velocity3) -> Self { + pub fn velocity(mut self, velocity: Velocity) -> Self { self.velocity = velocity; self } /// Sets the `angular_inertia` value of the `PhysicsBodyBuilder`. + #[cfg(feature = "physics3d")] pub fn angular_inertia(mut self, angular_inertia: Matrix3) -> Self { self.angular_inertia = angular_inertia; self } + /// Sets the `angular_inertia` value of the `PhysicsBodyBuilder`. + #[cfg(feature = "physics2d")] + pub fn angular_inertia(mut self, angular_inertia: N) -> Self { + self.angular_inertia = angular_inertia; + self + } + /// Sets the `mass` value of the `PhysicsBodyBuilder`. pub fn mass(mut self, mass: N) -> Self { self.mass = mass; @@ -211,7 +175,7 @@ impl PhysicsBodyBuilder { } /// Sets the `local_center_of_mass` value of the `PhysicsBodyBuilder`. - pub fn local_center_of_mass(mut self, local_center_of_mass: Point3) -> Self { + pub fn local_center_of_mass(mut self, local_center_of_mass: Point) -> Self { self.local_center_of_mass = local_center_of_mass; self } @@ -227,7 +191,7 @@ impl PhysicsBodyBuilder { angular_inertia: self.angular_inertia, mass: self.mass, local_center_of_mass: self.local_center_of_mass, - external_forces: Force3::zero(), + external_forces: Force::zero(), } } } diff --git a/src/colliders.rs b/src/colliders.rs index b13b808..fdd497c 100644 --- a/src/colliders.rs +++ b/src/colliders.rs @@ -2,31 +2,25 @@ use std::{f32::consts::PI, fmt, ops::Deref}; use specs::{Component, DenseVecStorage, FlaggedStorage}; -use crate::{ - nalgebra::{DMatrix, Isometry3, Point2, Point3, RealField, Unit, Vector3}, - ncollide::{ - shape::{ - Ball, - Capsule, - Compound, - ConvexHull, - Cuboid, - HeightField, - Plane, - Polyline, - Segment, - ShapeHandle, - TriMesh, - Triangle, - }, - world::CollisionGroups, - }, - nphysics::{ - material::{BasicMaterial, MaterialHandle}, - object::ColliderHandle, - }, +use nalgebra::{Point2, Point3, RealField, Unit}; +use ncollide::{ + shape::{Ball, Capsule, Compound, Cuboid, HeightField, Plane, Polyline, Segment, ShapeHandle}, + world::CollisionGroups, +}; +use nphysics::{ + material::{BasicMaterial, MaterialHandle}, + object::ColliderHandle, }; +#[cfg(feature = "physics3d")] +use ncollide::shape::{ConvexHull, TriMesh, Triangle}; + +#[cfg(feature = "physics3d")] +use nalgebra::{DMatrix, Isometry3 as Isometry, Point3 as Point, Vector3 as Vector}; + +#[cfg(feature = "physics2d")] +use nalgebra::{DVector, Isometry2 as Isometry, Point2 as Point, Vector2 as Vector}; + pub type MeshData = (Vec>, Vec>, Option>>); pub trait IntoMesh: objekt::Clone + Send + Sync { @@ -62,36 +56,41 @@ pub enum Shape { radius: N, }, Compound { - parts: Vec<(Isometry3, Shape)>, + parts: Vec<(Isometry, Shape)>, }, + #[cfg(feature = "physics3d")] ConvexHull { - points: Vec>, + points: Vec>, }, Cuboid { - half_extents: Vector3, + half_extents: Vector, }, HeightField { + #[cfg(feature = "physics3d")] heights: DMatrix, - scale: Vector3, + #[cfg(feature = "physics2d")] + heights: DVector, + scale: Vector, }, Plane { - normal: Unit>, + normal: Unit>, }, Polyline { - points: Vec>, + points: Vec>, indices: Option>>, }, Segment { - a: Point3, - b: Point3, + a: Point, + b: Point, }, + #[cfg(feature = "physics3d")] TriMesh { handle: Box>, }, Triangle { - a: Point3, - b: Point3, - c: Point3, + a: Point, + b: Point, + c: Point, }, } @@ -109,6 +108,7 @@ impl Shape { Shape::Compound { parts } => ShapeHandle::new(Compound::new( parts.iter().map(|part| (part.0, part.1.handle())).collect(), )), + #[cfg(feature = "physics3d")] Shape::ConvexHull { points } => ShapeHandle::new( ConvexHull::try_from_points(&points) .expect("Failed to generate Convex Hull from points."), @@ -122,11 +122,15 @@ impl Shape { ShapeHandle::new(Polyline::new(points.clone(), indices.clone())) } Shape::Segment { a, b } => ShapeHandle::new(Segment::new(*a, *b)), + #[cfg(feature = "physics3d")] Shape::TriMesh { handle } => { let data = handle.points(); ShapeHandle::new(TriMesh::new(data.0, data.1, data.2)) } + #[cfg(feature = "physics3d")] Shape::Triangle { a, b, c } => ShapeHandle::new(Triangle::new(*a, *b, *c)), + #[cfg(feature = "physics2d")] + Shape::Triangle { a, b, c } => ShapeHandle::new(Polyline::new(vec![*a, *b, *c], None)), } } } @@ -141,13 +145,15 @@ pub struct PhysicsCollider { pub(crate) handle: Option, /// The shape of this collider. pub shape: Shape, - /// The position/rotation offset of the collider from the entity it is attached to. - pub offset_from_parent: Isometry3, + /// The position/rotation offset of the collider from the entity it is + /// attached to. + pub offset_from_parent: Isometry, pub density: N, /// The physics material of which this collider is composed. /// Defines properties like bounciness and others. pub material: MaterialHandle, - /// Margin between the detection zone of what is "near" the collider and the actual collider. + /// Margin between the detection zone of what is "near" the collider and the + /// actual collider. pub margin: N, /// Collision groups this collider is part of. /// Defines with which other colliders this collider can interact. @@ -156,8 +162,8 @@ pub struct PhysicsCollider { pub linear_prediction: N, /// Prediction amount of the angular momentum. pub angular_prediction: N, - /// Whether this collider is a sensor and only emits events without interacting (true) or - /// if it is a regular collider (false). + /// Whether this collider is a sensor and only emits events without + /// interacting (true) or if it is a regular collider (false). pub sensor: bool, } @@ -206,17 +212,17 @@ impl PhysicsCollider { /// /// # Example /// -/// ```rust +/// ```rust,ignore /// use specs_physics::{ /// colliders::Shape, -/// nalgebra::{Isometry3, Vector3}, +/// nalgebra::{Isometry, Vector3}, /// ncollide::world::CollisionGroups, /// nphysics::material::{BasicMaterial, MaterialHandle}, /// PhysicsColliderBuilder, /// }; /// /// let physics_collider = PhysicsColliderBuilder::from(Shape::Cuboid{ half_extents: Vector3::new(10.0, 10.0, 1.0) }) -/// .offset_from_parent(Isometry3::identity()) +/// .offset_from_parent(Isometry::identity()) /// .density(1.2) /// .material(MaterialHandle::new(BasicMaterial::default())) /// .margin(0.02) @@ -228,7 +234,7 @@ impl PhysicsCollider { /// ``` pub struct PhysicsColliderBuilder { shape: Shape, - offset_from_parent: Isometry3, + offset_from_parent: Isometry, density: N, material: MaterialHandle, margin: N, @@ -244,7 +250,7 @@ impl From> for PhysicsColliderBuilder { fn from(shape: Shape) -> Self { Self { shape, - offset_from_parent: Isometry3::identity(), + offset_from_parent: Isometry::identity(), density: N::from_f32(1.3).unwrap(), material: MaterialHandle::new(BasicMaterial::default()), margin: N::from_f32(0.2).unwrap(), // default was: 0.01 @@ -258,7 +264,7 @@ impl From> for PhysicsColliderBuilder { impl PhysicsColliderBuilder { /// Sets the `offset_from_parent` value of the `PhysicsColliderBuilder`. - pub fn offset_from_parent(mut self, offset_from_parent: Isometry3) -> Self { + pub fn offset_from_parent(mut self, offset_from_parent: Isometry) -> Self { self.offset_from_parent = offset_from_parent; self } diff --git a/src/events.rs b/src/events.rs index c32cfb3..b8fc87d 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,6 +1,7 @@ use specs::Entity; -use crate::{ncollide::query::Proximity, shrev::EventChannel}; +use ncollide::query::Proximity; +use shrev::EventChannel; /// The `ContactType` is set accordingly to whether a contact began or ended. #[derive(Debug)] diff --git a/src/lib.rs b/src/lib.rs index ceaccdc..c473af9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,9 +8,9 @@ //! specs-physics = "0.3.0" //! ``` //! -//! **specs-physics** defines a set of [Specs][] `System`s and `Component`s to -//! handle the creation, modification and removal of [nphysics][] objects -//! ([RigidBody][], [Collider][]) and the synchronisation of object positions +//! **specs-physics** defines a set of [Specs] `System`s and `Component`s to +//! handle the creation, modification and removal of [nphysics] objects +//! ([RigidBody], [Collider]) and the synchronisation of object positions //! and global gravity between both worlds. //! //! ### Generic types @@ -22,7 +22,7 @@ //! //! #### `N: RealField` //! -//! [nphysics][] is built upon [nalgebra][] and uses various types and +//! [nphysics] is built upon [nalgebra] and uses various types and //! structures from this crate. **specs-physics** builds up on this even further //! and utilises the same structures, which all work with any type that //! implements `nalgebra::RealField`. `nalgebra::RealField` is by default @@ -34,14 +34,14 @@ //! a type parameter which implements the `specs_physics::bodies::Position` //! *trait*, requiring also a `Component` implementation with a //! `FlaggedStorage`. This `Position` `Component` is used to initially place a -//! [RigidBody][] in the [nphysics][] world and later used to synchronise the -//! updated translation and rotation of these bodies back into the [Specs][] +//! [RigidBody] in the [nphysics] world and later used to synchronise the +//! updated translation and rotation of these bodies back into the [Specs] //! world. //! //! Example for a `Position` `Component`, simply using the "Isometry" type (aka //! combined translation and rotation structure) directly: //! -//! ```rust +//! ```rust,ignore //! use specs::{Component, DenseVecStorage, FlaggedStorage}; //! use specs_physics::{bodies::Position, nalgebra::Isometry3}; //! @@ -59,15 +59,10 @@ //! fn isometry_mut(&mut self) -> &mut Isometry3 { //! &mut self.0 //! } -//! -//! fn set_isometry(&mut self, isometry: &Isometry3) -> &mut Pos { -//! self.0 = *isometry; -//! self -//! } //! } //! ``` //! -//! If you're using [Amethyst][], you can enable the "amethyst" feature for this +//! If you're using [Amethyst], you can enable the "amethyst" feature for this //! crate which provides a `Position` impl for `Transform`. //! //! ```toml @@ -79,13 +74,13 @@ //! //! ##### PhysicsBody //! -//! The `specs_physics::PhysicsBody` `Component` is used to define [RigidBody][] -//! from the comforts of your [Specs][] world. Changes to the `PhysicsBody` will -//! automatically be synchronised with [nphysics][]. +//! The `specs_physics::PhysicsBody` `Component` is used to define [RigidBody] +//! from the comforts of your [Specs] world. Changes to the `PhysicsBody` will +//! automatically be synchronised with [nphysics]. //! //! Example: //! -//! ```rust +//! ```rust,ignore //! use specs_physics::{ //! nalgebra::{Matrix3, Point3}, //! nphysics::{algebra::Velocity3, object::BodyStatus}, @@ -105,12 +100,12 @@ //! //! `specs_physics::PhysicsCollider`s are the counterpart to `PhysicsBody`s. //! They can exist on their own or as a part of a `PhysicsBody` -//! `PhysicsCollider`s are used to define and create [Collider][]'s in -//! [nphysics][]. +//! `PhysicsCollider`s are used to define and create [Collider]'s in +//! [nphysics]. //! //! Example: //! -//! ```rust +//! ```rust,ignore //! use specs_physics::{ //! colliders::Shape, //! nalgebra::{Isometry3, Vector3}, @@ -132,8 +127,8 @@ //! .build(); //! ``` //! -//! To assign multiple [Collider][]'s the the same body, [Entity hierarchy][] -//! can be used. This utilises [specs-hierarchy][]. +//! To assign multiple [Collider]'s the the same body, [Entity hierarchy] +//! can be used. This utilises [specs-hierarchy]. //! //! ### Systems //! @@ -141,31 +136,31 @@ //! `Dispatcher` in order: //! //! 1. `specs_physics::systems::SyncBodiesToPhysicsSystem` - handles the -//! creation, modification and removal of [RigidBody][]'s based on the +//! creation, modification and removal of [RigidBody]'s based on the //! `PhysicsBody` `Component` and an implementation of the `Position` //! *trait*. //! //! 2. `specs_physics::systems::SyncCollidersToPhysicsSystem` - handles -//! the creation, modification and removal of [Collider][]'s based on the +//! the creation, modification and removal of [Collider]'s based on the //! `PhysicsCollider` `Component`. This `System` depends on -//! `SyncBodiesToPhysicsSystem` as [Collider][] can depend on [RigidBody][]. +//! `SyncBodiesToPhysicsSystem` as [Collider] can depend on [RigidBody]. //! //! 3. `specs_physics::systems::SyncParametersToPhysicsSystem` - handles the -//! modification of the [nphysics][] `World`s parameters. +//! modification of the [nphysics] `World`s parameters. //! //! 4. `specs_physics::systems::PhysicsStepperSystem` - handles the progression -//! of the [nphysics][] `World` and causes objects to actually move and +//! of the [nphysics] `World` and causes objects to actually move and //! change their position. This `System` is the backbone for collision //! detection. //! //! 5. `specs_physics::systems::SyncBodiesFromPhysicsSystem` - -//! handles the synchronisation of [RigidBody][] positions and dynamics back -//! into the [Specs][] `Component`s. This `System` also utilises the +//! handles the synchronisation of [RigidBody] positions and dynamics back +//! into the [Specs] `Component`s. This `System` also utilises the //! `Position` *trait* implementation. //! //! An example `Dispatcher` with all required `System`s: //! -//! ```rust +//! ```rust,no_run //! use specs::DispatcherBuilder; //! use specs_physics::{ //! systems::{ @@ -211,11 +206,11 @@ //! .build(); //! ``` //! -//! If you're using [Amethyst][] Transforms directly, you'd pass the generic +//! If you're using [Amethyst] Transforms directly, you'd pass the generic //! arguments like so: //! -//! ``` -//! use amethyst_core::Transform; +//! ```rust,ignore +//! use amethyst::core::{Float, Transform}; //! use specs_physics::systems::SyncBodiesToPhysicsSystem; //! SyncBodiesToPhysicsSystem::::default(); //! ``` @@ -240,8 +235,14 @@ extern crate log; pub use nalgebra; -pub use ncollide3d as ncollide; -pub use nphysics3d as nphysics; +#[cfg(feature = "physics3d")] +pub extern crate ncollide3d as ncollide; +#[cfg(feature = "physics3d")] +pub extern crate nphysics3d as nphysics; +#[cfg(feature = "physics2d")] +pub extern crate ncollide2d as ncollide; +#[cfg(feature = "physics2d")] +pub extern crate nphysics2d as nphysics; pub use shrev; use std::collections::HashMap; @@ -258,20 +259,21 @@ use specs::{ use specs_hierarchy::Parent; pub use self::{ - bodies::{util::SimplePosition, PhysicsBody, PhysicsBodyBuilder}, + bodies::{PhysicsBody, PhysicsBodyBuilder}, + positon::{Position, SimplePosition}, colliders::{PhysicsCollider, PhysicsColliderBuilder}, }; +use nphysics::{ + counters::Counters, + material::MaterialsCoefficientsTable, + object::{BodyHandle, ColliderHandle}, + solver::IntegrationParameters, + world::World, +}; + use self::{ - bodies::Position, - nalgebra::{RealField, Vector3}, - nphysics::{ - counters::Counters, - material::MaterialsCoefficientsTable, - object::{BodyHandle, ColliderHandle}, - solver::IntegrationParameters, - world::World, - }, + nalgebra::RealField, systems::{ PhysicsStepperSystem, SyncBodiesFromPhysicsSystem, @@ -281,11 +283,18 @@ use self::{ }, }; +#[cfg(feature = "physics3d")] +use nalgebra::Vector3 as Vector; + +#[cfg(feature = "physics2d")] +use nalgebra::Vector2 as Vector; + pub mod bodies; pub mod colliders; pub mod events; pub mod parameters; pub mod systems; +pub mod positon; /// Resource holding the internal fields where physics computation occurs. /// Some inspection methods are exposed to allow debugging. @@ -317,7 +326,7 @@ impl Physics { /// Reports the internal value for the gravity. /// See also `Gravity` for setting this value. - pub fn gravity(&self) -> &Vector3 { + pub fn gravity(&self) -> &Vector { self.world.gravity() } @@ -378,8 +387,8 @@ impl Parent for PhysicsParent { /// required physics related `System`s. /// /// # Examples -/// ``` -/// use specs_physics::bodies::util::SimplePosition; +/// ```rust +/// use specs_physics::SimplePosition; /// let dispatcher = specs_physics::physics_dispatcher::>(); /// ``` pub fn physics_dispatcher<'a, 'b, N, P>() -> Dispatcher<'a, 'b> diff --git a/src/parameters.rs b/src/parameters.rs index df8c58a..062bfe3 100644 --- a/src/parameters.rs +++ b/src/parameters.rs @@ -4,10 +4,15 @@ use std::ops::{Deref, DerefMut}; -use crate::{ - nalgebra::{self as na, RealField, Scalar, Vector3}, - nphysics::solver::IntegrationParameters, -}; +use nalgebra::{convert, RealField, Scalar}; +use nphysics::solver::IntegrationParameters; + + +#[cfg(feature = "physics3d")] +use nalgebra::Vector3 as Vector; + +#[cfg(feature = "physics2d")] +use nalgebra::Vector2 as Vector; /// The `TimeStep` is used to set the timestep of the nphysics integration, see /// `nphysics::world::World::set_timestep(..)`. @@ -37,17 +42,17 @@ impl DerefMut for TimeStep { impl Default for TimeStep { fn default() -> Self { - Self(na::convert(1.0 / 60.0)) + Self(convert(1.0 / 60.0)) } } -/// `Gravity` is a newtype for `Vector3`. It represents a constant +/// `Gravity` is a newtype for `Vector`. It represents a constant /// acceleration affecting all physical objects in the scene. #[derive(Debug, PartialEq)] -pub struct Gravity(pub Vector3); +pub struct Gravity(pub Vector); impl Deref for Gravity { - type Target = Vector3; + type Target = Vector; fn deref(&self) -> &Self::Target { &self.0 @@ -62,7 +67,7 @@ impl DerefMut for Gravity { impl Default for Gravity { fn default() -> Self { - Self(Vector3::::zeros()) + Self(Vector::::zeros()) } } @@ -189,14 +194,14 @@ impl PartialEq> for PhysicsIntegrationPar impl Default for PhysicsIntegrationParameters { fn default() -> Self { PhysicsIntegrationParameters { - error_reduction_parameter: na::convert(0.2), - warmstart_coefficient: na::convert(1.0), - restitution_velocity_threshold: na::convert(1.0), - allowed_linear_error: na::convert(0.001), - allowed_angular_error: na::convert(0.001), - max_linear_correction: na::convert(100.0), - max_angular_correction: na::convert(0.2), - max_stabilization_multiplier: na::convert(0.2), + error_reduction_parameter: convert(0.2), + warmstart_coefficient: convert(1.0), + restitution_velocity_threshold: convert(1.0), + allowed_linear_error: convert(0.001), + allowed_angular_error: convert(0.001), + max_linear_correction: convert(100.0), + max_angular_correction: convert(0.2), + max_stabilization_multiplier: convert(0.2), max_velocity_iterations: 8, max_position_iterations: 3, } diff --git a/src/positon.rs b/src/positon.rs new file mode 100644 index 0000000..0d200b3 --- /dev/null +++ b/src/positon.rs @@ -0,0 +1,80 @@ +use specs::{Component, DenseVecStorage, FlaggedStorage}; +use std::ops::{Deref, DerefMut}; +use nalgebra::RealField; + +#[cfg(feature = "physics3d")] +use nalgebra::{Point3 as Point, Isometry3 as Isometry}; + +#[cfg(feature = "physics2d")] +use nalgebra::{Point2 as Point, Isometry2 as Isometry}; + + +/// An implementation of the `Position` trait is required for the +/// synchronisation of the position of Specs and nphysics objects. +/// +/// Initially, it is used to position bodies in the nphysics `World`. Then after +/// progressing the `World` it is used to synchronise the updated positions back +/// towards Specs. +pub trait Position: +Component>> + Send + Sync +{ + fn isometry(&self) -> &Isometry; + fn isometry_mut(&mut self) -> &mut Isometry; + + /// Helper function to extract the location of this `Position`. Using `Position::isometry()` is + /// preferable, but can be harder to work with. The translation of this `Position` can be set + /// using `Position::isometry_mut()`. + fn translation(&self) -> Point { + self.isometry().translation.vector.into() + } + + /// Helper function to extract the rotation of this `Position`. Using `Position::isometry()` is + /// preferable, but can be harder to work with. The rotation of this `Position` can be set + /// using `Position::isometry_mut()`. This is only available when the `physics2d` feature is + /// enabled. + #[cfg(feature = "physics2d")] + fn angle(&self) -> N { + self.isometry().rotation.angle() + } +} + +#[cfg(feature = "amethyst")] +impl Position for amethyst_core::Transform { + fn isometry(&self) -> &Isometry { + self.isometry() + } + + fn isometry_mut(&mut self) -> &mut Isometry { + self.isometry_mut() + } +} + +pub struct SimplePosition(pub Isometry); + +impl Position for SimplePosition { + fn isometry(&self) -> &Isometry { + &self.0 + } + + fn isometry_mut(&mut self) -> &mut Isometry { + &mut self.0 + } +} + +impl Component for SimplePosition { + type Storage = FlaggedStorage>; +} + +impl Deref for SimplePosition { + type Target = Isometry; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SimplePosition { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/src/systems/physics_stepper.rs b/src/systems/physics_stepper.rs index e63e868..9bd0721 100644 --- a/src/systems/physics_stepper.rs +++ b/src/systems/physics_stepper.rs @@ -2,14 +2,12 @@ use std::marker::PhantomData; use specs::{world::Index, Entities, Entity, Read, System, SystemData, World, Write, WriteExpect}; -use crate::{ - events::{ContactEvent, ContactEvents, ContactType, ProximityEvent, ProximityEvents}, - nalgebra::RealField, - ncollide::{events::ContactEvent as NContactEvent, world::CollisionObjectHandle}, - nphysics::world::ColliderWorld, - parameters::TimeStep, - Physics, -}; +use crate::events::{ContactEvent, ContactEvents, ContactType, ProximityEvent, ProximityEvents}; +use nalgebra::RealField; +use ncollide::{events::ContactEvent as NContactEvent, world::CollisionObjectHandle}; +use nphysics::world::ColliderWorld; +use crate::parameters::TimeStep; +use crate::Physics; /// The `PhysicsStepperSystem` progresses the nphysics `World`. pub struct PhysicsStepperSystem { diff --git a/src/systems/sync_bodies_from_physics.rs b/src/systems/sync_bodies_from_physics.rs index c88ffe0..70edcc5 100644 --- a/src/systems/sync_bodies_from_physics.rs +++ b/src/systems/sync_bodies_from_physics.rs @@ -2,11 +2,10 @@ use std::marker::PhantomData; use specs::{Join, ReadExpect, System, SystemData, World, WriteStorage}; -use crate::{ - bodies::{PhysicsBody, Position}, - nalgebra::RealField, - Physics, -}; +use crate::bodies::PhysicsBody; +use crate::positon::Position; +use crate::Physics; +use nalgebra::RealField; /// The `SyncBodiesFromPhysicsSystem` synchronised the updated position of /// the `RigidBody`s in the nphysics `World` with their Specs counterparts. This @@ -35,7 +34,7 @@ where // if a RigidBody exists in the nphysics World we fetch it and update the // Position component accordingly if let Some(rigid_body) = physics.world.rigid_body(physics_body.handle.unwrap()) { - position.set_isometry(rigid_body.position()); + *position.isometry_mut() = *rigid_body.position(); physics_body.update_from_physics_world(rigid_body); } } diff --git a/src/systems/sync_bodies_to_physics.rs b/src/systems/sync_bodies_to_physics.rs index befc8b0..8a0adb6 100644 --- a/src/systems/sync_bodies_to_physics.rs +++ b/src/systems/sync_bodies_to_physics.rs @@ -14,11 +14,10 @@ use specs::{ WriteStorage, }; -use crate::{ - bodies::{PhysicsBody, Position}, - nalgebra::RealField, - Physics, -}; +use nalgebra::RealField; +use crate::bodies::PhysicsBody; +use crate::positon::Position; +use crate::Physics; use super::iterate_component_events; @@ -206,16 +205,11 @@ where } } -#[cfg(test)] +#[cfg(all(test, feature = "physics3d"))] mod tests { - use crate::{ - nalgebra::Isometry3, - nphysics::object::BodyStatus, - systems::SyncBodiesToPhysicsSystem, - Physics, - PhysicsBodyBuilder, - SimplePosition, - }; + use nalgebra::Isometry3; + use nphysics::object::BodyStatus; + use crate::{systems::SyncBodiesToPhysicsSystem, Physics, PhysicsBodyBuilder, SimplePosition}; use specs::prelude::*; diff --git a/src/systems/sync_colliders_to_physics.rs b/src/systems/sync_colliders_to_physics.rs index cab49e0..37e6903 100644 --- a/src/systems/sync_colliders_to_physics.rs +++ b/src/systems/sync_colliders_to_physics.rs @@ -13,14 +13,11 @@ use specs::{ WriteStorage, }; -use crate::{ - bodies::Position, - colliders::PhysicsCollider, - nalgebra::RealField, - nphysics::object::{BodyPartHandle, ColliderDesc}, - Physics, - PhysicsParent, -}; +use crate::positon::Position; +use crate::colliders::PhysicsCollider; +use crate::{Physics, PhysicsParent}; +use nalgebra::RealField; +use nphysics::object::{BodyPartHandle, ColliderDesc}; use super::iterate_component_events; @@ -258,17 +255,14 @@ where } } -#[cfg(test)] +#[cfg(all(test, feature = "physics3d"))] mod tests { use specs::prelude::*; + use nalgebra::Isometry3; use crate::{ - colliders::Shape, - nalgebra::Isometry3, - systems::SyncCollidersToPhysicsSystem, - Physics, - PhysicsColliderBuilder, - SimplePosition, + colliders::Shape, systems::SyncCollidersToPhysicsSystem, Physics, + PhysicsColliderBuilder, SimplePosition, }; #[test] diff --git a/src/systems/sync_parameters_to_physics.rs b/src/systems/sync_parameters_to_physics.rs index cfa862a..2ce5859 100644 --- a/src/systems/sync_parameters_to_physics.rs +++ b/src/systems/sync_parameters_to_physics.rs @@ -2,8 +2,8 @@ use std::marker::PhantomData; use specs::{Read, System, SystemData, World, WriteExpect}; +use nalgebra::RealField; use crate::{ - nalgebra::RealField, parameters::{Gravity, PhysicsIntegrationParameters, PhysicsProfilingEnabled}, Physics, }; @@ -77,17 +77,13 @@ where } } -#[cfg(test)] +#[cfg(all(test, feature = "physics3d"))] mod tests { use approx::assert_ulps_eq; use specs::prelude::*; - use crate::{ - nalgebra::Vector3, - parameters::Gravity, - systems::SyncParametersToPhysicsSystem, - Physics, - }; + use nalgebra::Vector3; + use crate::{parameters::Gravity, systems::SyncParametersToPhysicsSystem, Physics}; #[test] fn update_gravity() { From c99776348d1f9d98845b48431e729d512b8838b6 Mon Sep 17 00:00:00 2001 From: Kel Date: Fri, 4 Oct 2019 01:43:23 -0400 Subject: [PATCH 2/7] 0.4.0 rewrite for new nphysics API's --- .gitattributes | 1 + .gitignore | 1 + Cargo.toml | 70 +-- examples/README.md | 42 +- examples/amethyst.rs | 0 examples/basic.rs | 175 +++++- examples/batch.rs | 85 +++ examples/collision.rs | 74 --- examples/events.rs | 73 --- examples/hierarchy.rs | 65 -- examples/positions.rs | 56 -- rustfmt.toml | 2 - src/bodies.rs | 197 ------- src/builder.rs | 61 ++ src/colliders.rs | 330 ----------- src/events.rs | 43 -- src/lib.rs | 684 +++++++++------------- src/parameters.rs | 209 ------- src/position.rs | 79 +++ src/positon.rs | 80 --- src/stepper.rs | 499 ++++++++++++++++ src/systems/batch.rs | 54 ++ src/systems/mod.rs | 185 ++++-- src/systems/physics_stepper.rs | 144 ----- src/systems/pose.rs | 29 + src/systems/stepper.rs | 62 ++ src/systems/sync_bodies_from_physics.rs | 63 -- src/systems/sync_bodies_to_physics.rs | 243 -------- src/systems/sync_colliders_to_physics.rs | 296 ---------- src/systems/sync_parameters_to_physics.rs | 108 ---- src/world/body_set.rs | 236 ++++++++ src/world/collider_set.rs | 206 +++++++ src/world/mod.rs | 33 ++ 33 files changed, 1952 insertions(+), 2533 deletions(-) create mode 100644 .gitattributes create mode 100644 examples/amethyst.rs create mode 100644 examples/batch.rs delete mode 100644 examples/collision.rs delete mode 100644 examples/events.rs delete mode 100644 examples/hierarchy.rs delete mode 100644 examples/positions.rs delete mode 100644 src/bodies.rs create mode 100644 src/builder.rs delete mode 100644 src/colliders.rs delete mode 100644 src/events.rs delete mode 100644 src/parameters.rs create mode 100644 src/position.rs delete mode 100644 src/positon.rs create mode 100644 src/stepper.rs create mode 100644 src/systems/batch.rs delete mode 100644 src/systems/physics_stepper.rs create mode 100644 src/systems/pose.rs create mode 100644 src/systems/stepper.rs delete mode 100644 src/systems/sync_bodies_from_physics.rs delete mode 100644 src/systems/sync_bodies_to_physics.rs delete mode 100644 src/systems/sync_colliders_to_physics.rs delete mode 100644 src/systems/sync_parameters_to_physics.rs create mode 100644 src/world/body_set.rs create mode 100644 src/world/collider_set.rs create mode 100644 src/world/mod.rs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1a4112a --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.obj binary \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5d801c9..17595cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea +*.iml Cargo.lock target diff --git a/Cargo.toml b/Cargo.toml index 4901e4b..2909431 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "specs-physics" -version = "0.3.0" +version = "0.4.0" authors = ["Benjamin Amling ", "Kel ", "jojolepro "] repository = "https://github.com/amethyst/specs-physics.git" homepage = "https://github.com/amethyst/specs-physics.git" @@ -9,54 +9,38 @@ edition = "2018" license = "MIT" readme = "README.md" documentation = "https://docs.rs/specs-physics" -description = "nphysics integration for the Specs entity component system" +description = "Integration with nphysics for the SPECS Parallel ECS." keywords = ["specs", "nphysics", "nphysics3d"] [features] -physics3d = ["ncollide3d", "nphysics3d"] -physics2d = ["ncollide2d", "nphysics2d"] -amethyst = ["amethyst_core"] +default = ["dim3", "nightly", "amethyst"] +dim3 = ["ncollide3d", "nphysics3d"] +dim2 = ["ncollide2d", "nphysics2d"] +nightly = ["specs/nightly"] [dependencies] -log = "^0.4.6" -specs = "^0.14" -specs-hierarchy = "^0.3" -shrev = "^1.0" -nalgebra = "^0.18" -ncollide2d = { version = "^0.19", optional = true } -ncollide3d = { version = "^0.19", optional = true } -nphysics2d = { version = "^0.11", optional = true } -nphysics3d = { version = "^0.11", optional = true } -amethyst_core = { version = "^0.7", optional = true } -objekt = "0.1.2" +log = "0.4" +objekt = "0.1" +shrinkwraprs = "0.2" + +specs = "0.15" +specs-hierarchy = "0.5" +nalgebra = "0.19" + +ncollide2d = { version = "0.21", optional = true } +ncollide3d = { version = "0.21", optional = true } + +nphysics2d = { version = "0.13", optional = true } +nphysics3d = { version = "0.13", optional = true } + +[dependencies.amethyst] +git = "https://github.com/amethyst/amethyst" +optional = true +default-features = false +features = ["vulkan"] [dev-dependencies] -simple_logger = "1.2.0" -approx = "0.3.2" - -[[example]] -name = "basic" -path = "examples/basic.rs" -required-features = ["physics3d"] - -[[example]] -name = "hierarchy" -path = "examples/hierarchy.rs" -required-features = ["physics3d"] - -[[example]] -name = "positions" -path = "examples/positions.rs" -required-features = ["physics3d"] - -[[example]] -name = "collision" -path = "examples/collision.rs" -required-features = ["physics3d"] - -[[example]] -name = "events" -path = "examples/events.rs" -required-features = ["physics3d"] +simple_logger = "1.3" +approx = "0.3" diff --git a/examples/README.md b/examples/README.md index d9f57e4..3ef4cd6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,14 +1,44 @@ -# Examples +# Specs-Physics Examples -You can execute the examples with the following *cargo* command: +Read the source code of these files to get a feeling +of how to perform common operations through the interface of specs-physics. + +It's important to also read through the [nphysics guide][] +so you know what's happening in nphysics as well. +Wherever there is a BodySet or a ColliderSet, instead think about how you're using +[`BodyComponent`][] and [`ColliderComponent`][] Storages + +[nphysics guide]: https://nphysics.org/ +[`BodyComponent`]: https:// +[`ColliderComponent`]: https:// + +## Basic Usage (`basic.rs`) + +Explains basic general use of specs-physics, based on the nphysics [balls3.rs][] example +which you can view in the nphysics testbed [here][balls3 testbed]. + + +To run this example, execute the following command from the specs-physics directory: ```bash -cargo run --example +cargo run --example basic ``` -e.g. +[balls3.rs]: https://github.com/rustsim/nphysics/blob/master/examples3d/balls3.rs +[balls3 testbed]: https://www.nphysics.org/demo_all_examples3/ + +## Batch Dispatching (`batch.rs`) + +Demonstrates usage of the fixed batch dispatcher, +which executes your physics code that depends on a fixed timestep, in that timestep. +Read the documentation of [`PhysicsBatchSystem`][] and the [`stepper`][] module +to better understand how this works. +To run this example, execute the following command from the specs-physics directory: ```bash -cargo run --example hierarchy -``` \ No newline at end of file +cargo run --example batch +``` + +[`PhysicsBatchSystem`]: https:// +[`stepper`]: https:// \ No newline at end of file diff --git a/examples/amethyst.rs b/examples/amethyst.rs new file mode 100644 index 0000000..e69de29 diff --git a/examples/basic.rs b/examples/basic.rs index d1d6137..75c2c9c 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,44 +1,159 @@ -extern crate log; -extern crate simple_logger; - -use specs::{Builder, World, WorldExt}; +use specs::{ + Builder, DispatcherBuilder, Join, ReadExpect, ReadStorage, System, World, WorldExt, + WriteStorage, +}; use specs_physics::{ - colliders::Shape, - nalgebra::{Isometry3, Vector3}, - nphysics::object::BodyStatus, - physics_dispatcher, - PhysicsBodyBuilder, - PhysicsColliderBuilder, - SimplePosition, + ncollide::shape::{Ball, Cuboid, ShapeHandle}, + nphysics::{ + math::Vector, + object::{ColliderDesc, Ground, RigidBody, RigidBodyDesc}, + }, + systems::PhysicsBundle, + world::MechanicalWorldRes, + BodyComponent, EntityBuilderExt, SimplePosition, }; +/* + * Our program will be split up in three parts. + * 1. Specs setup, where we initialize our World and create our Dispatcher + * with all of our systems on it. + * 2. Adding the Ground body for the static colliders of our world. + * 3. Adding a cube of rigidbody balls like in the nphysics 3d ball example + * 4. Calling our dispatcher. + */ fn main() { - // initialise the logger for system logs - simple_logger::init().unwrap(); - - // initialise the Specs world; this will contain our Resources and Entities + /* + * 1: Specs Setup + */ + // Initialise the Specs world + // This will contain our Resources and Entities let mut world = World::new(); - // create the dispatcher containing all relevant Systems; alternatively to using - // the convenience function you can add all required Systems by hand - let mut dispatcher = physics_dispatcher::>(); + // Create the dispatcher with our systems on it + let mut dispatcher_builder = + DispatcherBuilder::new().with(MyPhysicsSystem, "my_physics_system", &[]); + + // Attach the specs-physics systems to the dispatcher, + // with our pre-physics-stepping systems as a dependency + PhysicsBundle::>::new(Vector::y() * -9.81) + .with_deps(&["my_physics_system"]) + .register(&mut world, &mut dispatcher_builder); + + // Build our dispatcher for use in the application loop + let mut dispatcher = dispatcher_builder + .with( + MyRenderingSystem, + "my_rendering_system", + // This is the name of the system you want to depend on + // if you want your system to run after physics executes. + &["physics_pose_system"], + ) + .build(); + + // Sets up all resources for our dispatcher. dispatcher.setup(&mut world); - // create an Entity containing the required Components + /* + * 2: Add the ground body + */ + let ground_thickness = 0.2; + + // Create collider description for ground + let ground_shape = ShapeHandle::new(Cuboid::new(Vector::new(3.0, ground_thickness, 3.0))); + let ground_collider = + ColliderDesc::new(ground_shape).translation(Vector::y() * -ground_thickness); + + // Create ground body in our world and attach our ground collider world .create_entity() - .with(SimplePosition::(Isometry3::::translation( - 1.0, 1.0, 1.0, - ))) - .with(PhysicsBodyBuilder::::from(BodyStatus::Dynamic).build()) - .with( - PhysicsColliderBuilder::::from(Shape::Cuboid { - half_extents: Vector3::new(1.0, 1.0, 1.0), - }) - .build(), - ) + .with(SimplePosition::::default()) + .with_body::(Ground::new()) + .with_collider::(&ground_collider) .build(); - // execute the dispatcher + /* + * 3: Add dynamic bodies + */ + + // Some values used to build the balls + let ball_amount: usize = 8; + let ball_radius = 0.1; + let ball_shift = (ball_radius + ColliderDesc::::default_margin()) * 2.0 + 0.002; + let ball_center = ( + ball_shift * (ball_amount as f32) / 2.0, + ball_shift / 2.0, + ball_shift * (ball_amount as f32) / 2.0, + ); + let ball_height = 3.0; + + // Create collider description we can reuse per ball + let ball_collider = ColliderDesc::new(ShapeHandle::new(Ball::new(ball_radius))).density(1.0); + + // Create ball rigidbodies and add them to our world + for i in 0..ball_amount { + for j in 0..ball_amount { + for k in 0..ball_amount { + world + .create_entity() + // If our Position type is on the same entity as a Body, + // don't worry about setting it to any value other than default/zero + // it will be overwritten by the Pose system anyways. + .with(SimplePosition::::default()) + .with_body::( + RigidBodyDesc::new() + // Offset each body to build a cube of balls + .translation(Vector::new( + i as f32 * ball_shift - ball_center.0, + j as f32 * ball_shift + ball_center.1 + ball_height, + k as f32 * ball_shift - ball_center.2, + )) + .build(), + ) + .with_collider::(&ball_collider) + .build(); + } + } + } + + /* + * 4: Execution + * Call this in your application loop. + */ dispatcher.dispatch(&world); } + +/// An example physics system, performing joins on our bodies +struct MyPhysicsSystem; +impl<'s> System<'s> for MyPhysicsSystem { + type SystemData = WriteStorage<'s, BodyComponent>; + + fn run(&mut self, mut bodies: Self::SystemData) { + for (body,) in (&mut bodies,).join() { + if let Some(_rigid_body) = body.downcast_mut::>() { + // Operate on our bodies. + } + } + } +} + +/// An example rendering system, +/// using the synchronized positions from nphysics simulation. +struct MyRenderingSystem; +impl<'s> System<'s> for MyRenderingSystem { + type SystemData = ( + ReadStorage<'s, SimplePosition>, + ReadExpect<'s, MechanicalWorldRes>, + ); + + fn run(&mut self, (positions, mechanical_world): Self::SystemData) { + for pos in (&positions,).join() { + println!( + "Body Position (X: {:?}, Y: {:?}, Z: {:?}) @ {:?}s", + pos.0.translation.vector.x, + pos.0.translation.vector.y, + pos.0.translation.vector.z, + mechanical_world.integration_parameters.t, + ); + } + } +} diff --git a/examples/batch.rs b/examples/batch.rs new file mode 100644 index 0000000..f1fcdac --- /dev/null +++ b/examples/batch.rs @@ -0,0 +1,85 @@ +use specs::{ + Builder, DispatcherBuilder, Join, ReadExpect, ReadStorage, System, World, WorldExt, + WriteStorage, +}; +use specs_physics::{ + ncollide::shape::{Ball, ShapeHandle}, + nphysics::object::{ColliderDesc, RigidBody, RigidBodyDesc}, + systems::{PhysicsBatchSystem, PhysicsBundle}, + world::MechanicalWorldRes, + BodyComponent, EntityBuilderExt, SimplePosition, +}; + +fn main() { + // Just as usual we initialize our World + let mut world = World::new(); + + // And create a dispatcher with systems our stepper depends on + let mut physics_builder = + DispatcherBuilder::new().with(MyPhysicsSystem, "my_physics_system", &[]); + + // However, when constructing our PhysicsBundle, + // we must be sure to construct our bundle with a value for its stepper + // Refer to the PhysicsBundle documentation to see its various constructor + // options + PhysicsBundle::>::stepper(60) + .with_deps(&["my_physics_system"]) + .register(&mut world, &mut physics_builder); + + // And then create a second DispatcherBuilder, which will be our "per-frame" + // dispatcher + let mut dispatcher = DispatcherBuilder::new() + // We add all of our fixed-step dispatching as a batch, + // to be executed N times a frame to keep stepping in time + .with_batch::>(physics_builder, "physics_bundle", &[]) + // And at the end of a frame, we render + .with(MyRenderingSystem, "rendering_system", &["physics_bundle"]) + .build(); + dispatcher.setup(&mut world); + + // Everything else is just as normal. + let collider = ColliderDesc::new(ShapeHandle::new(Ball::new(1.0))); + world + .create_entity() + .with(SimplePosition::::default()) + .with_body::(RigidBodyDesc::new().build()) + .with_collider::(&collider) + .build(); + + // This executes our top level frame dispatcher, + // which may call our fixed step dispatcher multiple times per frame. + dispatcher.dispatch(&world); +} + +struct MyPhysicsSystem; +impl<'s> System<'s> for MyPhysicsSystem { + type SystemData = WriteStorage<'s, BodyComponent>; + + fn run(&mut self, mut bodies: Self::SystemData) { + for (body,) in (&mut bodies,).join() { + if let Some(_rigid_body) = body.downcast_mut::>() { + // Operate on our bodies. + } + } + } +} + +struct MyRenderingSystem; +impl<'s> System<'s> for MyRenderingSystem { + type SystemData = ( + ReadStorage<'s, SimplePosition>, + ReadExpect<'s, MechanicalWorldRes>, + ); + + fn run(&mut self, (positions, mechanical_world): Self::SystemData) { + for pos in (&positions,).join() { + println!( + "Body Position (X: {:?}, Y: {:?}, Z: {:?}) @ {:?}s", + pos.0.translation.vector.x, + pos.0.translation.vector.y, + pos.0.translation.vector.z, + mechanical_world.integration_parameters.t, + ); + } + } +} diff --git a/examples/collision.rs b/examples/collision.rs deleted file mode 100644 index 52c6576..0000000 --- a/examples/collision.rs +++ /dev/null @@ -1,74 +0,0 @@ -#[macro_use] -extern crate log; -extern crate simple_logger; - -use specs::{Builder, World, WorldExt}; -use specs_physics::{ - colliders::Shape, - nalgebra::{Isometry3, Vector3}, - nphysics::{algebra::Velocity3, object::BodyStatus}, - physics_dispatcher, - PhysicsBodyBuilder, - PhysicsColliderBuilder, - SimplePosition, -}; - -fn main() { - // initialise the logger for system logs - simple_logger::init().unwrap(); - - // initialise the Specs world; this will contain our Resources and Entities - let mut world = World::new(); - - // create the dispatcher containing all relevant Systems; alternatively to using - // the convenience function you can add all required Systems by hand - let mut dispatcher = physics_dispatcher::>(); - dispatcher.setup(&mut world); - - // create an Entity with a dynamic PhysicsBody component and a velocity - let entity = world - .create_entity() - .with(SimplePosition::(Isometry3::::translation( - 1.0, 1.0, 1.0, - ))) - .with( - PhysicsBodyBuilder::::from(BodyStatus::Dynamic) - .velocity(Velocity3::linear(1.0, 0.0, 0.0)) - .build(), - ) - .with( - PhysicsColliderBuilder::::from(Shape::Cuboid { - half_extents: Vector3::new(1.9, 2.0, 1.0), - }) - .margin(0.1) - .build(), - ) - .build(); - - // create an Entity with a static PhysicsBody component right next to the first - // one - world - .create_entity() - .with(SimplePosition::(Isometry3::::translation( - 5.0, 1.0, 1.0, - ))) - .with(PhysicsBodyBuilder::::from(BodyStatus::Static).build()) - .with( - PhysicsColliderBuilder::::from(Shape::Cuboid { - half_extents: Vector3::new(1.9, 2.0, 1.0), - }) - .margin(0.1) - .build(), - ) - .build(); - - // execute the dispatcher - dispatcher.dispatch(&world); - - // fetch the translation component for the Entity with the dynamic body; the - // position should still be approx the same - let pos_storage = world.read_storage::>(); - let pos = pos_storage.get(entity).unwrap(); - - info!("updated position: {}", pos.0.translation); -} diff --git a/examples/events.rs b/examples/events.rs deleted file mode 100644 index b24a927..0000000 --- a/examples/events.rs +++ /dev/null @@ -1,73 +0,0 @@ -#[macro_use] -extern crate log; -extern crate simple_logger; - -use specs::{Builder, World, WorldExt}; -use specs_physics::{ - colliders::Shape, - events::ContactEvents, - nalgebra::{Isometry3, Vector3}, - nphysics::{algebra::Velocity3, object::BodyStatus}, - physics_dispatcher, - PhysicsBodyBuilder, - PhysicsColliderBuilder, - SimplePosition, -}; - -fn main() { - // initialise the logger for system logs - simple_logger::init().unwrap(); - - // initialise the Specs world; this will contain our Resources and Entities - let mut world = World::new(); - - // create the dispatcher containing all relevant Systems; alternatively to using - // the convenience function you can add all required Systems by hand - let mut dispatcher = physics_dispatcher::>(); - dispatcher.setup(&mut world); - let mut contact_event_reader = world.fetch_mut::().register_reader(); - - // create an Entity with a dynamic PhysicsBody component and a velocity - world - .create_entity() - .with(SimplePosition::(Isometry3::::translation( - 1.0, 1.0, 1.0, - ))) - .with( - PhysicsBodyBuilder::::from(BodyStatus::Dynamic) - .velocity(Velocity3::linear(1.0, 0.0, 0.0)) - .build(), - ) - .with( - PhysicsColliderBuilder::::from(Shape::Cuboid { - half_extents: Vector3::new(2.0, 2.0, 1.0), - }) - .build(), - ) - .build(); - - // create an Entity with a static PhysicsBody component right now to the first - // one - world - .create_entity() - .with(SimplePosition::(Isometry3::::translation( - 3.0, 1.0, 1.0, - ))) - .with(PhysicsBodyBuilder::::from(BodyStatus::Static).build()) - .with( - PhysicsColliderBuilder::::from(Shape::Cuboid { - half_extents: Vector3::new(2.0, 2.0, 1.0), - }) - .build(), - ) - .build(); - - // execute the dispatcher - dispatcher.dispatch(&world); - - // check the ContactEvents channel for events - let contact_events = world.read_resource::(); - for contact_event in contact_events.read(&mut contact_event_reader) { - info!("Read ContactEvent from channel: {:?}", contact_event); - } -} diff --git a/examples/hierarchy.rs b/examples/hierarchy.rs deleted file mode 100644 index cd18857..0000000 --- a/examples/hierarchy.rs +++ /dev/null @@ -1,65 +0,0 @@ -extern crate log; -extern crate simple_logger; - -use specs::{world::World, Builder, WorldExt}; -use specs_physics::{ - colliders::Shape, - nalgebra::{Isometry3, Vector3}, - nphysics::object::BodyStatus, - physics_dispatcher, - PhysicsBodyBuilder, - PhysicsColliderBuilder, - PhysicsParent, - SimplePosition, -}; - -fn main() { - // initialise the logger for system logs - simple_logger::init().unwrap(); - - // initialise the Specs world; this will contain our Resources and Entities - let mut world = World::new(); - - // create the dispatcher containing all relevant Systems; alternatively to using - // the convenience function you can add all required Systems by hand - let mut dispatcher = physics_dispatcher::>(); - dispatcher.setup(&mut world); - - // create an Entity containing the required Components; this Entity will be the - // parent - let parent = world - .create_entity() - .with(SimplePosition::(Isometry3::::translation( - 1.0, 1.0, 1.0, - ))) - .with(PhysicsBodyBuilder::::from(BodyStatus::Dynamic).build()) - .with( - PhysicsColliderBuilder::::from(Shape::Cuboid { - half_extents: Vector3::new(1.0, 1.0, 1.0), - }) - .build(), - ) - .build(); - - // create the child Entity; if this Entity has its own PhysicsBody it'll more or - // less be its own object in the nphysics World, however if it's just a - // PhysicsCollider the parent/child hierarchy will actually take effect and the - // collider will be attached to the parent - let _child = world - .create_entity() - .with(SimplePosition::(Isometry3::::translation( - 1.0, 1.0, 1.0, - ))) - .with( - PhysicsColliderBuilder::::from(Shape::Cuboid { - half_extents: Vector3::new(1.0, 1.0, 1.0), - }) - .sensor(true) - .build(), - ) - .with(PhysicsParent { entity: parent }) - .build(); - - // execute the dispatcher - dispatcher.dispatch(&world); -} diff --git a/examples/positions.rs b/examples/positions.rs deleted file mode 100644 index cd031c7..0000000 --- a/examples/positions.rs +++ /dev/null @@ -1,56 +0,0 @@ -#[macro_use] -extern crate log; -extern crate simple_logger; - -use specs::{Builder, World, WorldExt}; -use specs_physics::{ - colliders::Shape, - nalgebra::{Isometry3, Vector3}, - nphysics::{algebra::Velocity3, object::BodyStatus}, - physics_dispatcher, - PhysicsBodyBuilder, - PhysicsColliderBuilder, - SimplePosition, -}; - -fn main() { - // initialise the logger for system logs - simple_logger::init().unwrap(); - - // initialise the Specs world; this will contain our Resources and Entities - let mut world = World::new(); - - // create the dispatcher containing all relevant Systems; alternatively to using - // the convenience function you can add all required Systems by hand - let mut dispatcher = physics_dispatcher::>(); - dispatcher.setup(&mut world); - - // create an Entity containing the required Components; for this examples sake - // we'll give the PhysicsBody a velocity - let entity = world - .create_entity() - .with(SimplePosition::(Isometry3::::translation( - 1.0, 1.0, 1.0, - ))) - .with( - PhysicsBodyBuilder::::from(BodyStatus::Dynamic) - .velocity(Velocity3::linear(1.0, 0.0, 0.0)) - .build(), - ) - .with( - PhysicsColliderBuilder::::from(Shape::Cuboid { - half_extents: Vector3::new(1.0, 1.0, 1.0), - }) - .build(), - ) - .build(); - - // execute the dispatcher - dispatcher.dispatch(&world); - - // fetch the SimpleTranslation component for the created Entity - let pos_storage = world.read_storage::>(); - let pos = pos_storage.get(entity).unwrap(); - - info!("updated position: {}", pos.0.translation); -} diff --git a/rustfmt.toml b/rustfmt.toml index 1e6fc46..f45c28e 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,6 +1,4 @@ -imports_layout = "HorizontalVertical" condense_wildcard_suffixes = true -#brace_style = "PreferSameLine" merge_imports = true reorder_impl_items = true use_field_init_shorthand = true diff --git a/src/bodies.rs b/src/bodies.rs deleted file mode 100644 index 95db879..0000000 --- a/src/bodies.rs +++ /dev/null @@ -1,197 +0,0 @@ -use specs::{Component, DenseVecStorage, FlaggedStorage}; -use nalgebra::RealField; - -#[cfg(feature = "physics3d")] -use nalgebra::{Point3 as Point, Matrix3}; - -#[cfg(feature = "physics2d")] -use nalgebra::{Point2 as Point}; - -#[cfg(feature = "physics3d")] -use nphysics::{algebra::{Force3 as Force, Velocity3 as Velocity, ForceType,}, - object::{Body, BodyHandle, BodyPart, BodyStatus, RigidBody, RigidBodyDesc}}; - -#[cfg(feature = "physics2d")] -use nphysics::{algebra::{Force2 as Force, Velocity2 as Velocity, ForceType,}, - object::{Body, BodyHandle, BodyPart, BodyStatus, RigidBody, RigidBodyDesc}}; - -/// The `PhysicsBody` `Component` represents a `PhysicsWorld` `RigidBody` in -/// Specs and contains all the data required for the synchronisation between -/// both worlds. -#[derive(Clone, Copy, Debug)] -pub struct PhysicsBody { - pub(crate) handle: Option, - pub gravity_enabled: bool, - pub body_status: BodyStatus, - pub velocity: Velocity, - #[cfg(feature = "physics3d")] - pub angular_inertia: Matrix3, - #[cfg(feature = "physics2d")] - pub angular_inertia: N, - pub mass: N, - pub local_center_of_mass: Point, - external_forces: Force, -} - -impl Component for PhysicsBody { - type Storage = FlaggedStorage>; -} - -impl PhysicsBody { - pub fn check_external_force(&self) -> &Force { - &self.external_forces - } - - pub fn apply_external_force(&mut self, force: &Force) -> &mut Self { - self.external_forces += *force; - self - } - - /// For creating new rigid body from this component's values - pub(crate) fn to_rigid_body_desc(&self) -> RigidBodyDesc { - RigidBodyDesc::new() - .gravity_enabled(self.gravity_enabled) - .status(self.body_status) - .velocity(self.velocity) - .angular_inertia(self.angular_inertia) - .mass(self.mass) - .local_center_of_mass(self.local_center_of_mass) - } - - /// Note: applies forces by draining external force property - pub(crate) fn apply_to_physics_world(&mut self, rigid_body: &mut RigidBody) -> &mut Self { - rigid_body.enable_gravity(self.gravity_enabled); - rigid_body.set_status(self.body_status); - rigid_body.set_velocity(self.velocity); - rigid_body.set_angular_inertia(self.angular_inertia); - rigid_body.set_mass(self.mass); - rigid_body.set_local_center_of_mass(self.local_center_of_mass); - rigid_body.apply_force(0, &self.drain_external_force(), ForceType::Force, true); - self - } - - pub(crate) fn update_from_physics_world(&mut self, rigid_body: &RigidBody) -> &mut Self { - // These two probably won't be modified but hey - self.gravity_enabled = rigid_body.gravity_enabled(); - self.body_status = rigid_body.status(); - - self.velocity = *rigid_body.velocity(); - - let local_inertia = rigid_body.local_inertia(); - self.angular_inertia = local_inertia.angular; - self.mass = local_inertia.linear; - self - } - - fn drain_external_force(&mut self) -> Force { - let value = self.external_forces; - self.external_forces = Force::::zero(); - value - } -} - -/// The `PhysicsBodyBuilder` implements the builder pattern for `PhysicsBody`s -/// and is the recommended way of instantiating and customising new -/// `PhysicsBody` instances. -/// -/// # Example -/// -/// ```rust,ignore -/// use specs_physics::{ -/// nalgebra::{Matrix3, Point3}, -/// nphysics::{algebra::Velocity3, object::BodyStatus}, -/// PhysicsBodyBuilder, -/// }; -/// -/// let physics_body = PhysicsBodyBuilder::from(BodyStatus::Dynamic) -/// .gravity_enabled(true) -/// .velocity(Velocity3::linear(1.0, 1.0, 1.0)) -/// .angular_inertia(Matrix3::from_diagonal_element(3.0)) -/// .mass(1.3) -/// .local_center_of_mass(Point3::new(0.0, 0.0, 0.0)) -/// .build(); -/// ``` -pub struct PhysicsBodyBuilder { - gravity_enabled: bool, - body_status: BodyStatus, - velocity: Velocity, - #[cfg(feature = "physics3d")] - angular_inertia: Matrix3, - #[cfg(feature = "physics2d")] - angular_inertia: N, - mass: N, - local_center_of_mass: Point, -} - -impl From for PhysicsBodyBuilder { - /// Creates a new `PhysicsBodyBuilder` from the given `BodyStatus`. This - /// also populates the `PhysicsBody` with sane defaults. - fn from(body_status: BodyStatus) -> Self { - Self { - gravity_enabled: false, - body_status, - velocity: Velocity::zero(), - #[cfg(feature = "physics3d")] - angular_inertia: Matrix3::zeros(), - #[cfg(feature = "physics2d")] - angular_inertia: N::zero(), - mass: N::from_f32(1.2).unwrap(), - local_center_of_mass: Point::origin(), - } - } -} - -impl PhysicsBodyBuilder { - /// Sets the `gravity_enabled` value of the `PhysicsBodyBuilder`. - pub fn gravity_enabled(mut self, gravity_enabled: bool) -> Self { - self.gravity_enabled = gravity_enabled; - self - } - - // Sets the `velocity` value of the `PhysicsBodyBuilder`. - pub fn velocity(mut self, velocity: Velocity) -> Self { - self.velocity = velocity; - self - } - - /// Sets the `angular_inertia` value of the `PhysicsBodyBuilder`. - #[cfg(feature = "physics3d")] - pub fn angular_inertia(mut self, angular_inertia: Matrix3) -> Self { - self.angular_inertia = angular_inertia; - self - } - - /// Sets the `angular_inertia` value of the `PhysicsBodyBuilder`. - #[cfg(feature = "physics2d")] - pub fn angular_inertia(mut self, angular_inertia: N) -> Self { - self.angular_inertia = angular_inertia; - self - } - - /// Sets the `mass` value of the `PhysicsBodyBuilder`. - pub fn mass(mut self, mass: N) -> Self { - self.mass = mass; - self - } - - /// Sets the `local_center_of_mass` value of the `PhysicsBodyBuilder`. - pub fn local_center_of_mass(mut self, local_center_of_mass: Point) -> Self { - self.local_center_of_mass = local_center_of_mass; - self - } - - /// Builds the `PhysicsBody` from the values set in the `PhysicsBodyBuilder` - /// instance. - pub fn build(self) -> PhysicsBody { - PhysicsBody { - handle: None, - gravity_enabled: self.gravity_enabled, - body_status: self.body_status, - velocity: self.velocity, - angular_inertia: self.angular_inertia, - mass: self.mass, - local_center_of_mass: self.local_center_of_mass, - external_forces: Force::zero(), - } - } -} diff --git a/src/builder.rs b/src/builder.rs new file mode 100644 index 0000000..1846ca5 --- /dev/null +++ b/src/builder.rs @@ -0,0 +1,61 @@ +use crate::{ + nalgebra::RealField, + nphysics::object::{Body, BodyPartHandle, ColliderDesc}, + world::{BodyComponent, ColliderComponent}, +}; + +use specs::{world::Builder, EntityBuilder, WorldExt}; + +/// Provides methods on `EntityBuilder` that make building +/// physics bodies easier. +/// +/// # Usage +/// +/// ``` +/// use specs::Builder; +/// use specs_physics::{ +/// ncollide::shape::{Ball, ShapeHandle}, +/// nphysics::{math::Vector, object::{ColliderDesc, RigidBodyDesc}}, +/// EntityBuilderExt, SimplePosition, +/// }; +/// +/// # use specs::WorldExt; +/// # let mut world = specs::World::new(); +/// # world.register::>(); +/// # world.register::>(); +/// # world.register::>(); +/// let body = RigidBodyDesc::new().translation(Vector::x() * 2.0).build(); +/// let shape = ShapeHandle::new(Ball::new(1.6)); +/// let collider_desc = ColliderDesc::new(shape); +/// +/// let entity = world +/// .create_entity() +/// .with(SimplePosition::::default()) +/// .with_body::(body) +/// .with_collider::(&collider_desc) +/// .build(); +/// ``` +pub trait EntityBuilderExt { + /// Attaches `body` to this entity. + fn with_body>(self, body: B) -> Self; + fn with_collider(self, collider: &ColliderDesc) -> Self; +} + +impl EntityBuilderExt for EntityBuilder<'_> { + fn with_body>(self, body: B) -> Self { + self.with(BodyComponent::new(body)) + } + + fn with_collider(self, collider: &ColliderDesc) -> Self { + { + let mut storage = self.world.write_storage::>(); + storage + .insert( + self.entity, + ColliderComponent(collider.build(BodyPartHandle(self.entity, 0))), + ) + .unwrap(); + } + self + } +} diff --git a/src/colliders.rs b/src/colliders.rs deleted file mode 100644 index fdd497c..0000000 --- a/src/colliders.rs +++ /dev/null @@ -1,330 +0,0 @@ -use std::{f32::consts::PI, fmt, ops::Deref}; - -use specs::{Component, DenseVecStorage, FlaggedStorage}; - -use nalgebra::{Point2, Point3, RealField, Unit}; -use ncollide::{ - shape::{Ball, Capsule, Compound, Cuboid, HeightField, Plane, Polyline, Segment, ShapeHandle}, - world::CollisionGroups, -}; -use nphysics::{ - material::{BasicMaterial, MaterialHandle}, - object::ColliderHandle, -}; - -#[cfg(feature = "physics3d")] -use ncollide::shape::{ConvexHull, TriMesh, Triangle}; - -#[cfg(feature = "physics3d")] -use nalgebra::{DMatrix, Isometry3 as Isometry, Point3 as Point, Vector3 as Vector}; - -#[cfg(feature = "physics2d")] -use nalgebra::{DVector, Isometry2 as Isometry, Point2 as Point, Vector2 as Vector}; - -pub type MeshData = (Vec>, Vec>, Option>>); - -pub trait IntoMesh: objekt::Clone + Send + Sync { - type N: RealField; - - fn points(&self) -> MeshData; -} - -impl<'clone, N: RealField> IntoMesh for Box<(dyn IntoMesh + 'clone)> { - type N = N; - - fn points(&self) -> MeshData { - self.deref().points() - } -} - -impl<'clone, N: RealField> Clone for Box + 'clone> { - fn clone(&self) -> Self { - objekt::clone_box(&*self) - } -} - -/// `Shape` serves as an abstraction over nphysics `ShapeHandle`s and makes it -/// easier to configure and define said `ShapeHandle`s for the user without -/// having to know the underlying nphysics API. -#[derive(Clone)] -pub enum Shape { - Ball { - radius: N, - }, - Capsule { - half_height: N, - radius: N, - }, - Compound { - parts: Vec<(Isometry, Shape)>, - }, - #[cfg(feature = "physics3d")] - ConvexHull { - points: Vec>, - }, - Cuboid { - half_extents: Vector, - }, - HeightField { - #[cfg(feature = "physics3d")] - heights: DMatrix, - #[cfg(feature = "physics2d")] - heights: DVector, - scale: Vector, - }, - Plane { - normal: Unit>, - }, - Polyline { - points: Vec>, - indices: Option>>, - }, - Segment { - a: Point, - b: Point, - }, - #[cfg(feature = "physics3d")] - TriMesh { - handle: Box>, - }, - Triangle { - a: Point, - b: Point, - c: Point, - }, -} - -impl Shape { - /// Converts a `Shape` and its values into its corresponding `ShapeHandle` - /// type. The `ShapeHandle` is used to define a `Collider` in the - /// `PhysicsWorld`. - pub fn handle(&self) -> ShapeHandle { - match self { - Shape::Ball { radius } => ShapeHandle::new(Ball::::new(*radius)), - Shape::Capsule { - half_height, - radius, - } => ShapeHandle::new(Capsule::new(*half_height, *radius)), - Shape::Compound { parts } => ShapeHandle::new(Compound::new( - parts.iter().map(|part| (part.0, part.1.handle())).collect(), - )), - #[cfg(feature = "physics3d")] - Shape::ConvexHull { points } => ShapeHandle::new( - ConvexHull::try_from_points(&points) - .expect("Failed to generate Convex Hull from points."), - ), - Shape::Cuboid { half_extents } => ShapeHandle::new(Cuboid::new(*half_extents)), - Shape::HeightField { heights, scale } => { - ShapeHandle::new(HeightField::new(heights.clone(), *scale)) - } - Shape::Plane { normal } => ShapeHandle::new(Plane::new(*normal)), - Shape::Polyline { points, indices } => { - ShapeHandle::new(Polyline::new(points.clone(), indices.clone())) - } - Shape::Segment { a, b } => ShapeHandle::new(Segment::new(*a, *b)), - #[cfg(feature = "physics3d")] - Shape::TriMesh { handle } => { - let data = handle.points(); - ShapeHandle::new(TriMesh::new(data.0, data.1, data.2)) - } - #[cfg(feature = "physics3d")] - Shape::Triangle { a, b, c } => ShapeHandle::new(Triangle::new(*a, *b, *c)), - #[cfg(feature = "physics2d")] - Shape::Triangle { a, b, c } => ShapeHandle::new(Polyline::new(vec![*a, *b, *c], None)), - } - } -} - -/// The `PhysicsCollider` `Component` represents a `Collider` in the physics -/// world. A physics `Collider` is automatically created when this `Component` -/// is added to an `Entity`. Value changes are automatically synchronised with -/// the physic worlds `Collider`. -#[derive(Clone)] -pub struct PhysicsCollider { - /// The handle to the collider in the physics world. - pub(crate) handle: Option, - /// The shape of this collider. - pub shape: Shape, - /// The position/rotation offset of the collider from the entity it is - /// attached to. - pub offset_from_parent: Isometry, - pub density: N, - /// The physics material of which this collider is composed. - /// Defines properties like bounciness and others. - pub material: MaterialHandle, - /// Margin between the detection zone of what is "near" the collider and the - /// actual collider. - pub margin: N, - /// Collision groups this collider is part of. - /// Defines with which other colliders this collider can interact. - pub collision_groups: CollisionGroups, - /// Prediction amount of the linear momentum. - pub linear_prediction: N, - /// Prediction amount of the angular momentum. - pub angular_prediction: N, - /// Whether this collider is a sensor and only emits events without - /// interacting (true) or if it is a regular collider (false). - pub sensor: bool, -} - -impl Component for PhysicsCollider { - type Storage = FlaggedStorage>; -} - -impl fmt::Debug for PhysicsCollider { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "PhysicsCollider {{ \ - handle: {:?}, \ - offset_from_parent: {:?}, \ - density: {}, \ - margin: {}, \ - collision_group: {:?}, \ - linear_prediction: {}, \ - angular_prediction: {}, \ - sensor: {} \ - }}", - self.handle, - self.offset_from_parent, - self.density, - self.margin, - self.collision_groups, - self.linear_prediction, - self.angular_prediction, - self.sensor, - )?; - Ok(()) - } -} - -impl PhysicsCollider { - /// Returns the `ShapeHandle` for `shape`, taking the `margin` into - /// consideration. - pub(crate) fn shape_handle(&self) -> ShapeHandle { - self.shape.handle() - } -} - -/// The `PhysicsColliderBuilder` implements the builder pattern for -/// `PhysicsCollider`s and is the recommended way of instantiating and -/// customising new `PhysicsCollider` instances. -/// -/// # Example -/// -/// ```rust,ignore -/// use specs_physics::{ -/// colliders::Shape, -/// nalgebra::{Isometry, Vector3}, -/// ncollide::world::CollisionGroups, -/// nphysics::material::{BasicMaterial, MaterialHandle}, -/// PhysicsColliderBuilder, -/// }; -/// -/// let physics_collider = PhysicsColliderBuilder::from(Shape::Cuboid{ half_extents: Vector3::new(10.0, 10.0, 1.0) }) -/// .offset_from_parent(Isometry::identity()) -/// .density(1.2) -/// .material(MaterialHandle::new(BasicMaterial::default())) -/// .margin(0.02) -/// .collision_groups(CollisionGroups::default()) -/// .linear_prediction(0.001) -/// .angular_prediction(0.0) -/// .sensor(true) -/// .build(); -/// ``` -pub struct PhysicsColliderBuilder { - shape: Shape, - offset_from_parent: Isometry, - density: N, - material: MaterialHandle, - margin: N, - collision_groups: CollisionGroups, - linear_prediction: N, - angular_prediction: N, - sensor: bool, -} - -impl From> for PhysicsColliderBuilder { - /// Creates a new `PhysicsColliderBuilder` from the given `Shape`. This - // also populates the `PhysicsCollider` with sane defaults. - fn from(shape: Shape) -> Self { - Self { - shape, - offset_from_parent: Isometry::identity(), - density: N::from_f32(1.3).unwrap(), - material: MaterialHandle::new(BasicMaterial::default()), - margin: N::from_f32(0.2).unwrap(), // default was: 0.01 - collision_groups: CollisionGroups::default(), - linear_prediction: N::from_f32(0.002).unwrap(), - angular_prediction: N::from_f32(PI / 180.0 * 5.0).unwrap(), - sensor: false, - } - } -} - -impl PhysicsColliderBuilder { - /// Sets the `offset_from_parent` value of the `PhysicsColliderBuilder`. - pub fn offset_from_parent(mut self, offset_from_parent: Isometry) -> Self { - self.offset_from_parent = offset_from_parent; - self - } - - /// Sets the `density` value of the `PhysicsColliderBuilder`. - pub fn density(mut self, density: N) -> Self { - self.density = density; - self - } - - /// Sets the `material` value of the `PhysicsColliderBuilder`. - pub fn material(mut self, material: MaterialHandle) -> Self { - self.material = material; - self - } - - /// Sets the `margin` value of the `PhysicsColliderBuilder`. - pub fn margin(mut self, margin: N) -> Self { - self.margin = margin; - self - } - - /// Sets the `collision_groups` value of the `PhysicsColliderBuilder`. - pub fn collision_groups(mut self, collision_groups: CollisionGroups) -> Self { - self.collision_groups = collision_groups; - self - } - - /// Sets the `linear_prediction` value of the `PhysicsColliderBuilder`. - pub fn linear_prediction(mut self, linear_prediction: N) -> Self { - self.linear_prediction = linear_prediction; - self - } - - /// Sets the `angular_prediction` value of the `PhysicsColliderBuilder`. - pub fn angular_prediction(mut self, angular_prediction: N) -> Self { - self.angular_prediction = angular_prediction; - self - } - - /// Sets the `sensor` value of the `PhysicsColliderBuilder`. - pub fn sensor(mut self, sensor: bool) -> Self { - self.sensor = sensor; - self - } - - /// Builds the `PhysicsCollider` from the values set in the - /// `PhysicsColliderBuilder` instance. - pub fn build(self) -> PhysicsCollider { - PhysicsCollider { - handle: None, - shape: self.shape, - offset_from_parent: self.offset_from_parent, - density: self.density, - material: self.material, - margin: self.margin, - collision_groups: self.collision_groups, - linear_prediction: self.linear_prediction, - angular_prediction: self.angular_prediction, - sensor: self.sensor, - } - } -} diff --git a/src/events.rs b/src/events.rs deleted file mode 100644 index b8fc87d..0000000 --- a/src/events.rs +++ /dev/null @@ -1,43 +0,0 @@ -use specs::Entity; - -use ncollide::query::Proximity; -use shrev::EventChannel; - -/// The `ContactType` is set accordingly to whether a contact began or ended. -#[derive(Debug)] -pub enum ContactType { - /// Event occurring when two collision objects start being in contact. - Started, - /// Event occurring when two collision objects stop being in contact. - Stopped, -} - -/// The `ContactEvent` type contains information about the objects that -/// collided. -#[derive(Debug)] -pub struct ContactEvent { - pub collider1: Entity, - pub collider2: Entity, - - pub contact_type: ContactType, -} - -/// `ContactEvents` is a custom `EventChannel` type used to expose -/// `ContactEvent`s. -pub type ContactEvents = EventChannel; - -/// The `ProximityEvent` type contains information about the objects that -/// triggered a proximity "collision". These kind of events contain at least one -/// *sensor* `PhysicsCollider`. -#[derive(Debug)] -pub struct ProximityEvent { - pub collider1: Entity, - pub collider2: Entity, - - pub prev_status: Proximity, - pub new_status: Proximity, -} - -/// `ProximityEvent` is a custom `EventChannel` type used to expose -/// `ProximityEvent`s. -pub type ProximityEvents = EventChannel; diff --git a/src/lib.rs b/src/lib.rs index c473af9..fd2cc9a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,459 +1,299 @@ -//! ## Usage -//! -//! To use **specs-physics**, add the following dependency to your projects -//! *Cargo.toml*: -//! -//! ```toml -//! [dependencies] -//! specs-physics = "0.3.0" -//! ``` -//! -//! **specs-physics** defines a set of [Specs] `System`s and `Component`s to -//! handle the creation, modification and removal of [nphysics] objects -//! ([RigidBody], [Collider]) and the synchronisation of object positions -//! and global gravity between both worlds. -//! -//! ### Generic types -//! -//! All `System`s and `Component`s provided by this crate require between one -//! and two type parameters to function properly. These were explicitly -//! introduced to keep this integration as generic as possible and allow -//! compatibility with as many external crates and game engines as possible. -//! -//! #### `N: RealField` -//! -//! [nphysics] is built upon [nalgebra] and uses various types and -//! structures from this crate. **specs-physics** builds up on this even further -//! and utilises the same structures, which all work with any type that -//! implements `nalgebra::RealField`. `nalgebra::RealField` is by default -//! implemented for various standard types, such as `f32` and`f64`. `nalgebra` -//! is re-exported under `specs_physics::nalgebra`. -//! -//! #### `P: Position` -//! -//! a type parameter which implements the `specs_physics::bodies::Position` -//! *trait*, requiring also a `Component` implementation with a -//! `FlaggedStorage`. This `Position` `Component` is used to initially place a -//! [RigidBody] in the [nphysics] world and later used to synchronise the -//! updated translation and rotation of these bodies back into the [Specs] -//! world. -//! -//! Example for a `Position` `Component`, simply using the "Isometry" type (aka -//! combined translation and rotation structure) directly: -//! -//! ```rust,ignore -//! use specs::{Component, DenseVecStorage, FlaggedStorage}; -//! use specs_physics::{bodies::Position, nalgebra::Isometry3}; -//! -//! struct Pos(pub Isometry3); -//! -//! impl Component for Pos { -//! type Storage = FlaggedStorage>; -//! } -//! -//! impl Position for Pos { -//! fn isometry(&self) -> &Isometry3 { -//! &self.0 -//! } -//! -//! fn isometry_mut(&mut self) -> &mut Isometry3 { -//! &mut self.0 -//! } -//! } -//! ``` -//! -//! If you're using [Amethyst], you can enable the "amethyst" feature for this -//! crate which provides a `Position` impl for `Transform`. -//! -//! ```toml -//! [dependencies] -//! specs-physics = { version = "0.3", features = ["amethyst"] } -//! ``` -//! -//! ### Components -//! -//! ##### PhysicsBody -//! -//! The `specs_physics::PhysicsBody` `Component` is used to define [RigidBody] -//! from the comforts of your [Specs] world. Changes to the `PhysicsBody` will -//! automatically be synchronised with [nphysics]. -//! -//! Example: -//! -//! ```rust,ignore -//! use specs_physics::{ -//! nalgebra::{Matrix3, Point3}, -//! nphysics::{algebra::Velocity3, object::BodyStatus}, -//! PhysicsBodyBuilder, -//! }; -//! -//! let physics_body = PhysicsBodyBuilder::from(BodyStatus::Dynamic) -//! .gravity_enabled(true) -//! .velocity(Velocity3::linear(1.0, 1.0, 1.0)) -//! .angular_inertia(Matrix3::from_diagonal_element(3.0)) -//! .mass(1.3) -//! .local_center_of_mass(Point3::new(0.0, 0.0, 0.0)) -//! .build(); -//! ``` -//! -//! ##### PhysicsCollider -//! -//! `specs_physics::PhysicsCollider`s are the counterpart to `PhysicsBody`s. -//! They can exist on their own or as a part of a `PhysicsBody` -//! `PhysicsCollider`s are used to define and create [Collider]'s in -//! [nphysics]. -//! -//! Example: -//! -//! ```rust,ignore -//! use specs_physics::{ -//! colliders::Shape, -//! nalgebra::{Isometry3, Vector3}, -//! ncollide::world::CollisionGroups, -//! nphysics::material::{BasicMaterial, MaterialHandle}, -//! PhysicsColliderBuilder, -//! }; -//! -//! let physics_collider = PhysicsColliderBuilder::from( -//! Shape::Cuboid{ half_extents: Vector3::new(10.0, 10.0, 1.0) }) -//! .offset_from_parent(Isometry3::identity()) -//! .density(1.2) -//! .material(MaterialHandle::new(BasicMaterial::default())) -//! .margin(0.02) -//! .collision_groups(CollisionGroups::default()) -//! .linear_prediction(0.001) -//! .angular_prediction(0.0) -//! .sensor(true) -//! .build(); -//! ``` -//! -//! To assign multiple [Collider]'s the the same body, [Entity hierarchy] -//! can be used. This utilises [specs-hierarchy]. -//! -//! ### Systems -//! -//! The following `System`s currently exist and should be added to your -//! `Dispatcher` in order: -//! -//! 1. `specs_physics::systems::SyncBodiesToPhysicsSystem` - handles the -//! creation, modification and removal of [RigidBody]'s based on the -//! `PhysicsBody` `Component` and an implementation of the `Position` -//! *trait*. -//! -//! 2. `specs_physics::systems::SyncCollidersToPhysicsSystem` - handles -//! the creation, modification and removal of [Collider]'s based on the -//! `PhysicsCollider` `Component`. This `System` depends on -//! `SyncBodiesToPhysicsSystem` as [Collider] can depend on [RigidBody]. -//! -//! 3. `specs_physics::systems::SyncParametersToPhysicsSystem` - handles the -//! modification of the [nphysics] `World`s parameters. -//! -//! 4. `specs_physics::systems::PhysicsStepperSystem` - handles the progression -//! of the [nphysics] `World` and causes objects to actually move and -//! change their position. This `System` is the backbone for collision -//! detection. -//! -//! 5. `specs_physics::systems::SyncBodiesFromPhysicsSystem` - -//! handles the synchronisation of [RigidBody] positions and dynamics back -//! into the [Specs] `Component`s. This `System` also utilises the -//! `Position` *trait* implementation. -//! -//! An example `Dispatcher` with all required `System`s: -//! -//! ```rust,no_run -//! use specs::DispatcherBuilder; -//! use specs_physics::{ -//! systems::{ -//! PhysicsStepperSystem, -//! SyncBodiesFromPhysicsSystem, -//! SyncBodiesToPhysicsSystem, -//! SyncCollidersToPhysicsSystem, -//! SyncParametersToPhysicsSystem, -//! }, -//! SimplePosition, -//! }; -//! -//! let dispatcher = DispatcherBuilder::new() -//! .with( -//! SyncBodiesToPhysicsSystem::>::default(), -//! "sync_bodies_to_physics_system", -//! &[], -//! ) -//! .with( -//! SyncCollidersToPhysicsSystem::>::default(), -//! "sync_colliders_to_physics_system", -//! &["sync_bodies_to_physics_system"], -//! ) -//! .with( -//! SyncParametersToPhysicsSystem::::default(), -//! "sync_gravity_to_physics_system", -//! &[], -//! ) -//! .with( -//! PhysicsStepperSystem::::default(), -//! "physics_stepper_system", -//! &[ -//! "sync_bodies_to_physics_system", -//! "sync_colliders_to_physics_system", -//! "sync_gravity_to_physics_system", -//! ], -//! ) -//! .with( -//! SyncBodiesFromPhysicsSystem::>::default(), -//! "sync_bodies_from_physics_system", -//! &["physics_stepper_system"], -//! ) -//! .build(); -//! ``` -//! -//! If you're using [Amethyst] Transforms directly, you'd pass the generic -//! arguments like so: -//! -//! ```rust,ignore -//! use amethyst::core::{Float, Transform}; -//! use specs_physics::systems::SyncBodiesToPhysicsSystem; -//! SyncBodiesToPhysicsSystem::::default(); -//! ``` -//! -//! Alternatively to building your own `Dispatcher`, you can always fall back on -//! the convenience function `specs_physics::physics_dispatcher()`, which -//! returns a configured *default* `Dispatcher` for you or -//! `specs_physics::register_physics_systems()` which takes a -//! `DispatcherBuilder` as an argument and registers the required `System`s for -//! you. -//! -//! [Specs]: https://slide-rs.github.io/specs/ -//! [nphysics]: https://www.nphysics.org/ -//! [nalgebra]: https://nalgebra.org/ -//! [RigidBody]: https://www.nphysics.org/rigid_body_simulations_with_contacts/#rigid-bodies -//! [Collider]: https://www.nphysics.org/rigid_body_simulations_with_contacts/#colliders -//! [Amethyst]: https://amethyst.rs/ -//! [Entity hierarchy]: https://github.com/bamling/specs-physics/blob/master/examples/hierarchy.rs -//! [specs-hierarchy]: https://github.com/rustgd/specs-hierarchy +/*! +# specs-physics -#[macro_use] -extern crate log; +**For when you want some [nphysics] in your [SPECS Parallel ECS][Specs]!** +***Somewhat*** **better than sliced bread!** -pub use nalgebra; -#[cfg(feature = "physics3d")] -pub extern crate ncollide3d as ncollide; -#[cfg(feature = "physics3d")] -pub extern crate nphysics3d as nphysics; -#[cfg(feature = "physics2d")] -pub extern crate ncollide2d as ncollide; -#[cfg(feature = "physics2d")] -pub extern crate nphysics2d as nphysics; -pub use shrev; - -use std::collections::HashMap; - -use specs::{ - world::Index, - Component, - DenseVecStorage, - Dispatcher, - DispatcherBuilder, - Entity, - FlaggedStorage, -}; -use specs_hierarchy::Parent; +Remember those "default" types in the [nphysics tutorial]? +**specs-physics** provides [ECS Component]-based implementations for nphysics data sets, +as well as faculties for synchronizing pose data to your position type of choice, +and stepping functionality for stepping your simulations to a good beat. -pub use self::{ - bodies::{PhysicsBody, PhysicsBodyBuilder}, - positon::{Position, SimplePosition}, - colliders::{PhysicsCollider, PhysicsColliderBuilder}, -}; +[Specs]: https://slide-rs.github.io/specs/ +[nphysics]: https://www.nphysics.org/ +[nphysics tutorial]: https://www.nphysics.org/rigid_body_simulations_with_contacts/#basic-setup +[ECS Component]: https://amethyst.github.io/specs/01_intro.html#whats-an-ecs -use nphysics::{ - counters::Counters, - material::MaterialsCoefficientsTable, - object::{BodyHandle, ColliderHandle}, - solver::IntegrationParameters, - world::World, -}; +## Usage -use self::{ - nalgebra::RealField, - systems::{ - PhysicsStepperSystem, - SyncBodiesFromPhysicsSystem, - SyncBodiesToPhysicsSystem, - SyncCollidersToPhysicsSystem, - SyncParametersToPhysicsSystem, - }, -}; +To use **specs-physics** with 3D nphysics, +add the following dependency to your project's *[Cargo.toml]*: -#[cfg(feature = "physics3d")] -use nalgebra::Vector3 as Vector; +```toml +[dependencies] +specs-physics = { version = "0.4.0", features = ["dim3"] } +``` -#[cfg(feature = "physics2d")] -use nalgebra::Vector2 as Vector; +For 2D nphysics, replace `dim3` with `dim2`. +You **must** enable one of these two features, and you can *only* enable one of them! -pub mod bodies; -pub mod colliders; -pub mod events; -pub mod parameters; -pub mod systems; -pub mod positon; - -/// Resource holding the internal fields where physics computation occurs. -/// Some inspection methods are exposed to allow debugging. -pub struct Physics { - /// Core structure where physics computation and synchronization occurs. - /// Also contains ColliderWorld. - pub(crate) world: World, - - /// Hashmap of Entities to internal Physics bodies. - /// Necessary for reacting to removed Components. - pub(crate) body_handles: HashMap, - /// Hashmap of Entities to internal Collider handles. - /// Necessary for reacting to removed Components. - pub(crate) collider_handles: HashMap, -} +Also available is an `amethyst` feature, for [Amethyst], +which adds synchronization support for Amethyst's [`Transform`] type +as well as a [`SystemBundle`] trait impl for [`PhysicsBundle`]. +Usage is explained further below. -// Some non-mutating methods for diagnostics and testing -impl Physics { - /// Creates a new instance of the physics structure. - pub fn new() -> Self { - Self::default() - } +[Cargo.toml]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html +[Amethyst]: https://amethyst.rs/ +[`Transform`]: https://docs.amethyst.rs/stable/amethyst_core/transform/components/struct.Transform.html +[`SystemBundle`]: https://docs.amethyst.rs/stable/amethyst_core/bundle/trait.SystemBundle.html +[`PhysicsBundle`]: struct.PhysicsBundle.html - /// Reports the internal value for the timestep. - /// See also `TimeStep` for setting this value. - pub fn timestep(&self) -> N { - self.world.timestep() - } +### Dispatching - /// Reports the internal value for the gravity. - /// See also `Gravity` for setting this value. - pub fn gravity(&self) -> &Vector { - self.world.gravity() - } - /// Reports the internal value for prediction distance in collision - /// detection. This cannot change and will normally be `0.002m` - pub fn prediction(&self) -> N { - self.world.prediction() - } - /// Retrieves the performance statistics for the last simulated timestep. - /// Profiling is disabled by default. - /// See also `PhysicsProfilingEnabled` for enabling performance counters. - pub fn performance_counters(&self) -> &Counters { - self.world.performance_counters() - } +### Generic types - /// Retrieves the internal parameters for integration. - /// See also `PhysicsIntegrationParameters` for setting these parameters. - pub fn integration_parameters(&self) -> &IntegrationParameters { - self.world.integration_parameters() - } +All `System`s and `Component`s provided by this crate require between one +and two type parameters to function properly. These were explicitly +introduced to keep this integration as generic as possible and allow +compatibility with as many external crates and game engines as possible. - /// Retrieves the internal lookup table for friction and restitution - /// constants. Exposing this for modification is TODO. - pub fn materials_coefficients_table(&self) -> &MaterialsCoefficientsTable { - self.world.materials_coefficients_table() - } -} +#### `N: RealField` -impl Default for Physics { - fn default() -> Self { - Self { - world: World::new(), - body_handles: HashMap::new(), - collider_handles: HashMap::new(), - } - } -} +[nphysics] is built upon [nalgebra] and uses various types and +structures from this crate. **specs-physics** builds up on this even further +and utilises the same structures, which all work with any type that +implements `nalgebra::RealField`. `nalgebra::RealField` is by default +implemented for various standard types, such as `f32` and`f64`. `nalgebra` +is re-exported under `specs_physics::nalgebra`. -/// The `PhysicsParent` `Component` is used to represent a parent/child -/// relationship between physics based `Entity`s. -#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)] -pub struct PhysicsParent { - pub entity: Entity, -} +#### `P: Position` + +a type parameter which implements the `specs_physics::bodies::Position` +*trait*, requiring also a `Component` implementation with a +`FlaggedStorage`. This `Position` `Component` is used to initially place a +[RigidBody] in the [nphysics] world and later used to synchronise the +updated translation and rotation of these bodies back into the [Specs] +world. -impl Component for PhysicsParent { +Example for a `Position` `Component`, simply using the "Isometry" type (aka +combined translation and rotation structure) directly: + +```rust,ignore +use specs::{Component, DenseVecStorage, FlaggedStorage}; +use specs_physics::{bodies::Position, nalgebra::Isometry3}; + +struct Pos(pub Isometry3); + +impl Component for Pos { type Storage = FlaggedStorage>; } -impl Parent for PhysicsParent { - fn parent_entity(&self) -> Entity { - self.entity +impl Position for Pos { + fn isometry(&self) -> &Isometry3 { + &self.0 } -} -/// Convenience function for configuring and building a `Dispatcher` with all -/// required physics related `System`s. -/// -/// # Examples -/// ```rust -/// use specs_physics::SimplePosition; -/// let dispatcher = specs_physics::physics_dispatcher::>(); -/// ``` -pub fn physics_dispatcher<'a, 'b, N, P>() -> Dispatcher<'a, 'b> -where - N: RealField, - P: Position, -{ - let mut dispatcher_builder = DispatcherBuilder::new(); - register_physics_systems::(&mut dispatcher_builder); - - dispatcher_builder.build() + fn isometry_mut(&mut self) -> &mut Isometry3 { + &mut self.0 + } } +``` + +If you're using [Amethyst], you can enable the "amethyst" feature for this +crate which provides a `Position` impl for `Transform`. + +```toml +[dependencies] +specs-physics = { version = "0.3", features = ["amethyst"] } +``` + +### Components + +##### PhysicsBody + +The `specs_physics::PhysicsBody` `Component` is used to define [RigidBody] +from the comforts of your [Specs] world. Changes to the `PhysicsBody` will +automatically be synchronised with [nphysics]. + +Example: + +```rust,ignore +use specs_physics::{ + nalgebra::{Matrix3, Point3}, + nphysics::{algebra::Velocity3, object::BodyStatus}, + PhysicsBodyBuilder, +}; + +let physics_body = PhysicsBodyBuilder::from(BodyStatus::Dynamic) + .gravity_enabled(true) + .velocity(Velocity3::linear(1.0, 1.0, 1.0)) + .angular_inertia(Matrix3::from_diagonal_element(3.0)) + .mass(1.3) + .local_center_of_mass(Point3::new(0.0, 0.0, 0.0)) + .build(); +``` + +##### PhysicsCollider + +`specs_physics::PhysicsCollider`s are the counterpart to `PhysicsBody`s. +They can exist on their own or as a part of a `PhysicsBody` +`PhysicsCollider`s are used to define and create [Collider]'s in +[nphysics]. -/// Convenience function for registering all required physics related `System`s -/// to the given `DispatcherBuilder`. This also serves as a blueprint on how -///// to properly set up the `System`s and have them depend on each other. -pub fn register_physics_systems(dispatcher_builder: &mut DispatcherBuilder) -where - N: RealField, - P: Position, -{ - // add SyncBodiesToPhysicsSystem first since we have to start with bodies; - // colliders can exist without a body but in most cases have a body parent - dispatcher_builder.add( - SyncBodiesToPhysicsSystem::::default(), +Example: + +```rust,ignore +use specs_physics::{ + colliders::Shape, + nalgebra::{Isometry3, Vector3}, + ncollide::world::CollisionGroups, + nphysics::material::{BasicMaterial, MaterialHandle}, + PhysicsColliderBuilder, +}; + +let physics_collider = PhysicsColliderBuilder::from( + Shape::Cuboid{ half_extents: Vector3::new(10.0, 10.0, 1.0) }) + .offset_from_parent(Isometry3::identity()) + .density(1.2) + .material(MaterialHandle::new(BasicMaterial::default())) + .margin(0.02) + .collision_groups(CollisionGroups::default()) + .linear_prediction(0.001) + .angular_prediction(0.0) + .sensor(true) + .build(); +``` + +To assign multiple [Collider]'s the the same body, [Entity hierarchy] +can be used. This utilises [specs-hierarchy]. + +### Systems + +The following `System`s currently exist and should be added to your +`Dispatcher` in order: + +1. `specs_physics::systems::SyncBodiesToPhysicsSystem` - handles the +creation, modification and removal of [RigidBody]'s based on the +`PhysicsBody` `Component` and an implementation of the `Position` +*trait*. + +2. `specs_physics::systems::SyncCollidersToPhysicsSystem` - handles +the creation, modification and removal of [Collider]'s based on the +`PhysicsCollider` `Component`. This `System` depends on +`SyncBodiesToPhysicsSystem` as [Collider] can depend on [RigidBody]. + +3. `specs_physics::systems::SyncParametersToPhysicsSystem` - handles the +modification of the [nphysics] `World`s parameters. + +4. `specs_physics::systems::PhysicsStepperSystem` - handles the progression +of the [nphysics] `World` and causes objects to actually move and +change their position. This `System` is the backbone for collision +detection. + +5. `specs_physics::systems::SyncBodiesFromPhysicsSystem` - +handles the synchronisation of [RigidBody] positions and dynamics back +into the [Specs] `Component`s. This `System` also utilises the +`Position` *trait* implementation. + +An example `Dispatcher` with all required `System`s: + +```rust,no_run +use specs::DispatcherBuilder; +use specs_physics::{ + systems::{ + PhysicsStepperSystem, + PhysicsPoseSystem, + SyncBodiesToPhysicsSystem, + SyncCollidersToPhysicsSystem, + SyncParametersToPhysicsSystem, + }, + SimplePosition, +}; + +let dispatcher = DispatcherBuilder::new() + .with( + SyncBodiesToPhysicsSystem::>::default(), "sync_bodies_to_physics_system", &[], - ); - - // add SyncCollidersToPhysicsSystem next with SyncBodiesToPhysicsSystem as its - // dependency - dispatcher_builder.add( - SyncCollidersToPhysicsSystem::::default(), + ) + .with( + SyncCollidersToPhysicsSystem::>::default(), "sync_colliders_to_physics_system", &["sync_bodies_to_physics_system"], - ); - - // add SyncParametersToPhysicsSystem; this System can be added at any point in - // time as it merely synchronizes the simulation parameters of the world, - // thus it has no other dependencies. - dispatcher_builder.add( - SyncParametersToPhysicsSystem::::default(), - "sync_parameters_to_physics_system", + ) + .with( + SyncParametersToPhysicsSystem::::default(), + "sync_gravity_to_physics_system", &[], - ); - - // add PhysicsStepperSystem after all other Systems that write data to the - // nphysics World and has to depend on them; this System is used to progress the - // nphysics World for all existing objects - dispatcher_builder.add( - PhysicsStepperSystem::::default(), + ) + .with( + PhysicsStepperSystem::::default(), "physics_stepper_system", &[ "sync_bodies_to_physics_system", "sync_colliders_to_physics_system", - "sync_parameters_to_physics_system", + "sync_gravity_to_physics_system", ], - ); - - // add SyncBodiesFromPhysicsSystem last as it handles the - // synchronisation between nphysics World bodies and the Position - // components; this depends on the PhysicsStepperSystem - dispatcher_builder.add( - SyncBodiesFromPhysicsSystem::::default(), + ) + .with( + PhysicsPoseSystem::>::default(), "sync_bodies_from_physics_system", &["physics_stepper_system"], - ); -} + ) + .build(); +``` + +If you're using [Amethyst] Transforms directly, you'd pass the generic +arguments like so: + +```rust,ignore +use amethyst::core::{Float, Transform}; +use specs_physics::systems::SyncBodiesToPhysicsSystem; +SyncBodiesToPhysicsSystem::::default(); +``` + +Alternatively to building your own `Dispatcher`, you can always fall back on +the convenience function `specs_physics::physics_dispatcher()`, which +returns a configured *default* `Dispatcher` for you or +`specs_physics::register_physics_systems()` which takes a +`DispatcherBuilder` as an argument and registers the required `System`s for +you. + +[nalgebra]: https://nalgebra.org/ +[RigidBody]: https://www.nphysics.org/rigid_body_simulations_with_contacts/#rigid-bodies +[Collider]: https://www.nphysics.org/rigid_body_simulations_with_contacts/#colliders +[Amethyst]: https://amethyst.rs/ +[Entity hierarchy]: https://github.com/bamling/specs-physics/blob/master/examples/hierarchy.rs +[specs-hierarchy]: https://github.com/rustgd/specs-hierarchy +*/ + +#[cfg(any( + not(any(feature = "dim2", feature = "dim3")), + all(feature = "dim2", feature = "dim3") +))] +compile_error!( + r#"Either the feature "dim3" or the feature "dim2" must be enabled. +You cannot enable both, and you cannot enable neither."# +); + +#[macro_use] +extern crate log; + +#[macro_use] +extern crate shrinkwraprs; + +pub use nalgebra; + +#[cfg(feature = "dim3")] +pub use ncollide3d as ncollide; +#[cfg(feature = "dim3")] +pub use nphysics3d as nphysics; + +#[cfg(feature = "dim2")] +pub use ncollide2d as ncollide; +#[cfg(feature = "dim2")] +pub use nphysics2d as nphysics; + +mod builder; +mod position; +pub mod stepper; +pub mod systems; +pub mod world; + +pub use self::{ + builder::EntityBuilderExt, + position::{Position, SimplePosition}, + world::{BodyComponent, ColliderComponent}, +}; diff --git a/src/parameters.rs b/src/parameters.rs deleted file mode 100644 index 062bfe3..0000000 --- a/src/parameters.rs +++ /dev/null @@ -1,209 +0,0 @@ -//! # Parameters module -//! Resources for modifying the various simulation parameters of the -//! nphysics World. - -use std::ops::{Deref, DerefMut}; - -use nalgebra::{convert, RealField, Scalar}; -use nphysics::solver::IntegrationParameters; - - -#[cfg(feature = "physics3d")] -use nalgebra::Vector3 as Vector; - -#[cfg(feature = "physics2d")] -use nalgebra::Vector2 as Vector; - -/// The `TimeStep` is used to set the timestep of the nphysics integration, see -/// `nphysics::world::World::set_timestep(..)`. -/// -/// Warning: Do **NOT** change this value every frame, doing so will destabilize -/// the simulation. The stepping system itself should be called in a "fixed" -/// update which maintains a running delta. See [this blog post][gaffer] -/// by Glenn Fiedler to learn more about timesteps. -/// -/// [gaffer]: https://gafferongames.com/game-physics/fix-your-timestep/%22 -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct TimeStep(pub N); - -impl Deref for TimeStep { - type Target = N; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for TimeStep { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Default for TimeStep { - fn default() -> Self { - Self(convert(1.0 / 60.0)) - } -} - -/// `Gravity` is a newtype for `Vector`. It represents a constant -/// acceleration affecting all physical objects in the scene. -#[derive(Debug, PartialEq)] -pub struct Gravity(pub Vector); - -impl Deref for Gravity { - type Target = Vector; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Gravity { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Default for Gravity { - fn default() -> Self { - Self(Vector::::zeros()) - } -} - -/// Enables reporting of `nphysics::counters`, -/// which can be read via `Physics::performance_counters` -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub struct PhysicsProfilingEnabled(pub bool); - -impl Deref for PhysicsProfilingEnabled { - type Target = bool; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for PhysicsProfilingEnabled { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Default for PhysicsProfilingEnabled { - fn default() -> Self { - Self(false) - } -} - -/// Essentially identical to the nphysics IntegrationParameters struct except -/// without the t and dt fields. Manages the details of physics integration. -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct PhysicsIntegrationParameters { - /// The `[0,1]` proportion of the positional error to be corrected at each - /// time step. - /// - /// default: `0.2` - pub error_reduction_parameter: N, - - /// Each cached impulse are multiplied by this `[0, 1]` coefficient when - /// they are re-used to initialize the solver. - /// - /// default: `1.0` - pub warmstart_coefficient: N, - - /// Contacts at points where the involved bodies have a relative velocity - /// smaller than this threshold won't be affected by the restitution force. - /// - /// default: `1.0` - pub restitution_velocity_threshold: N, - - /// Ammount of penetration the engine won't attempt to correct. - /// - /// default: `0.001m` - pub allowed_linear_error: N, - - /// Ammount of angular drift of joint limits the engine won't attempt to - /// correct. - /// - /// default: `0.001rad` - pub allowed_angular_error: N, - - /// Maximum linear correction during one step of the non-linear position - /// solver. - /// - /// default: `100.0` - pub max_linear_correction: N, - - /// Maximum angular correction during one step of the non-linear position - /// solver. - /// - /// default: `0.2` - pub max_angular_correction: N, - - /// Maximum nonlinera SOR-prox scaling parameter when the constraint - /// correction direction is close to the kernel of the involved multibody's - /// jacobian. - /// - /// default: `0.2` - pub max_stabilization_multiplier: N, - - /// Maximum number of iterations performed by the velocity constraints - /// solver. - /// - /// default: `8` - pub max_velocity_iterations: usize, - - /// Maximum number of iterations performed by the position-based constraints - /// solver. - /// - /// default: `3` - pub max_position_iterations: usize, -} - -impl PhysicsIntegrationParameters { - pub(crate) fn apply(&self, to: &mut IntegrationParameters) { - to.erp = self.error_reduction_parameter; - to.warmstart_coeff = self.warmstart_coefficient; - to.restitution_velocity_threshold = self.restitution_velocity_threshold; - to.allowed_linear_error = self.allowed_linear_error; - to.allowed_angular_error = self.allowed_angular_error; - to.max_linear_correction = self.max_linear_correction; - to.max_angular_correction = self.max_angular_correction; - to.max_stabilization_multiplier = self.max_stabilization_multiplier; - to.max_velocity_iterations = self.max_velocity_iterations; - to.max_position_iterations = self.max_position_iterations; - } -} - -impl PartialEq> for PhysicsIntegrationParameters { - fn eq(&self, other: &IntegrationParameters) -> bool { - self.error_reduction_parameter == other.erp - && self.warmstart_coefficient == other.warmstart_coeff - && self.restitution_velocity_threshold == other.restitution_velocity_threshold - && self.allowed_linear_error == other.allowed_linear_error - && self.allowed_angular_error == other.allowed_angular_error - && self.max_linear_correction == other.max_linear_correction - && self.max_angular_correction == other.max_angular_correction - && self.max_stabilization_multiplier == other.max_stabilization_multiplier - && self.max_velocity_iterations == other.max_velocity_iterations - && self.max_position_iterations == other.max_position_iterations - } -} - -impl Default for PhysicsIntegrationParameters { - fn default() -> Self { - PhysicsIntegrationParameters { - error_reduction_parameter: convert(0.2), - warmstart_coefficient: convert(1.0), - restitution_velocity_threshold: convert(1.0), - allowed_linear_error: convert(0.001), - allowed_angular_error: convert(0.001), - max_linear_correction: convert(100.0), - max_angular_correction: convert(0.2), - max_stabilization_multiplier: convert(0.2), - max_velocity_iterations: 8, - max_position_iterations: 3, - } - } -} diff --git a/src/position.rs b/src/position.rs new file mode 100644 index 0000000..ed636e3 --- /dev/null +++ b/src/position.rs @@ -0,0 +1,79 @@ +use crate::{ + nalgebra::RealField, + nphysics::math::{Isometry, Point}, +}; + +use specs::{Component, DenseVecStorage, FlaggedStorage}; + +/// An implementation of the `Position` trait is required for the +/// synchronisation of the position of Specs and nphysics objects. +/// +/// Initially, it is used to position bodies in the nphysics `World`. Then after +/// progressing the `World` it is used to synchronise the updated positions back +/// towards Specs. +pub trait Pose: Component + Send + Sync { + fn sync(&mut self, pose: &Isometry); +} + +#[cfg(all(feature = "amethyst", feature = "dim3"))] +impl Pose for amethyst::core::Transform { + fn sync(&mut self, pose: &Isometry) { + *self.isometry_mut() = pose; + } +} + +#[cfg(all(feature = "amethyst", feature = "dim2"))] +impl Pose for amethyst::core::Transform { + fn sync(&mut self, pose: &Isometry) { + let euler = self.rotation().euler_angles(); + self.set_rotation_euler(euler.0, euler.1, pose.rotation.angle()); + self.set_translation_x(pose.translation.x); + self.set_translation_y(pose.translation.y); + } +} + +// TODO: 64 bit implementation for amethyst + +#[derive(Copy, Clone, Debug, PartialEq, Hash, Shrinkwrap)] +#[shrinkwrap(mutable)] +pub struct SimplePosition(pub Isometry); + +impl SimplePosition { + /// Helper function to extract the location of this `Position`. Using + /// `Position::isometry()` is preferable, but can be harder to work + /// with. The translation of this `Position` can be set using `Position: + /// :isometry_mut()`. + fn translation(&self) -> Point { + self.0.translation.vector.into() + } + + /// Helper function to extract the rotation of this `Position`. Using + /// `Position::isometry()` is preferable, but can be harder to work + /// with. The rotation of this `Position` can be set using `Position:: + /// isometry_mut()`. This is only available when the `physics2d` feature is + /// enabled. + #[cfg(feature = "dim2")] + fn angle(&self) -> N { + self.0.rotation.angle() + } +} + +impl Position for SimplePosition { + fn isometry(&self) -> &Isometry { + &self.0 + } + + fn isometry_mut(&mut self) -> &mut Isometry { + &mut self.0 + } +} + +impl Component for SimplePosition { + type Storage = FlaggedStorage>; +} + +impl Default for SimplePosition { + fn default() -> Self { + Self(Isometry::identity()) + } +} diff --git a/src/positon.rs b/src/positon.rs deleted file mode 100644 index 0d200b3..0000000 --- a/src/positon.rs +++ /dev/null @@ -1,80 +0,0 @@ -use specs::{Component, DenseVecStorage, FlaggedStorage}; -use std::ops::{Deref, DerefMut}; -use nalgebra::RealField; - -#[cfg(feature = "physics3d")] -use nalgebra::{Point3 as Point, Isometry3 as Isometry}; - -#[cfg(feature = "physics2d")] -use nalgebra::{Point2 as Point, Isometry2 as Isometry}; - - -/// An implementation of the `Position` trait is required for the -/// synchronisation of the position of Specs and nphysics objects. -/// -/// Initially, it is used to position bodies in the nphysics `World`. Then after -/// progressing the `World` it is used to synchronise the updated positions back -/// towards Specs. -pub trait Position: -Component>> + Send + Sync -{ - fn isometry(&self) -> &Isometry; - fn isometry_mut(&mut self) -> &mut Isometry; - - /// Helper function to extract the location of this `Position`. Using `Position::isometry()` is - /// preferable, but can be harder to work with. The translation of this `Position` can be set - /// using `Position::isometry_mut()`. - fn translation(&self) -> Point { - self.isometry().translation.vector.into() - } - - /// Helper function to extract the rotation of this `Position`. Using `Position::isometry()` is - /// preferable, but can be harder to work with. The rotation of this `Position` can be set - /// using `Position::isometry_mut()`. This is only available when the `physics2d` feature is - /// enabled. - #[cfg(feature = "physics2d")] - fn angle(&self) -> N { - self.isometry().rotation.angle() - } -} - -#[cfg(feature = "amethyst")] -impl Position for amethyst_core::Transform { - fn isometry(&self) -> &Isometry { - self.isometry() - } - - fn isometry_mut(&mut self) -> &mut Isometry { - self.isometry_mut() - } -} - -pub struct SimplePosition(pub Isometry); - -impl Position for SimplePosition { - fn isometry(&self) -> &Isometry { - &self.0 - } - - fn isometry_mut(&mut self) -> &mut Isometry { - &mut self.0 - } -} - -impl Component for SimplePosition { - type Storage = FlaggedStorage>; -} - -impl Deref for SimplePosition { - type Target = Isometry; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for SimplePosition { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} diff --git a/src/stepper.rs b/src/stepper.rs new file mode 100644 index 0000000..13a7e89 --- /dev/null +++ b/src/stepper.rs @@ -0,0 +1,499 @@ +/*! +# Stepper Data Module + +This module contains types exclusively used for the specs-physics +fixed stepping implementation. If you choose to use some other way to +perform fixed stepping, such as using Amethyst's fixed dispatcher instead, +you can simply ignore this module. + + +*/ +use std::{ + fmt, + time::{Duration, Instant}, +}; + +#[derive(Copy, Clone, Debug)] +pub enum TimeError { + WrongStepType, + OutOfSemiStepBounds, +} + +impl fmt::Display for TimeError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "{}", + match self { + TimeError::WrongStepType => + r#"Operation performed on wrong step type. +Did you use a fixed timestep when you intended to use a semi-fixed timstep?"#, + TimeError::OutOfSemiStepBounds => "Step index not in bounds of step list.", + } + ) + } +} + +pub struct StepperRes { + /// When set to Some, the Batch system will only execute that many steps in + /// one dispatch/frame. Useful for preventing death spirals in the stepper + /// when your application does not require synchronization. In such cases + /// where synchronization of physics state is performed, you should + /// instead watch the value of `BatchTimeRes::accumulator()` and + /// invalidate local state after crossing a certain threshold. + pub max_steps_per_frame: Option, + + /// When set to Some, the Batch system will postpone remaining steps to the + /// next dispatch/frame after stepping for this time or longer. Useful for + /// preventing death spirals in the stepper when your application does + /// not require synchronization. In such cases where synchronization of + /// physics state is performed, you should instead watch the value of + /// `BatchTimeRes::accumulator()` and invalidate local state after + /// crossing a certain threshold. + pub frame_time_limit: Option, + + // Tracks how far "behind" physics time we are + accumulator: Duration, + + // Timestep interval state data + time_step: TimeStep, + + // Whether the MechanicalWorld timestep should be updated + time_step_dirty: bool, + + // Number of steps since start + // Safety: Given a liberal timestep of 120hz, this would take 4.8b years to saturate. + // A u32 on the other hand would take 414 days to saturate. War Boys voice MEDIOCRE! + global_steps: u64, + + // Steps in current iteration + // Safety: Let's pray that nobody has a frame that takes 414 days to render. WITNESS ME! + frame_steps: u32, + + frame_start: Option, +} + +impl StepperRes { + pub fn new_fixed(interval: u32) -> Self { + Self::new_fixed_exact(Duration::from_secs(1) / interval, None, None) + } + + pub fn new_fixed_exact( + step_delta: Duration, + max_steps_per_frame: Option, + frame_time_limit: Option, + ) -> Self { + Self { + max_steps_per_frame, + frame_time_limit, + accumulator: Duration::default(), + time_step: TimeStep::Fixed(step_delta), + time_step_dirty: true, + global_steps: 0, + frame_steps: 0, + frame_start: None, + } + } + + pub fn set_fixed_time_step(&mut self, interval: u32) { + self.time_step = TimeStep::Fixed(Duration::from_secs(1) / interval); + self.time_step_dirty = true; + } + + /// Creates a new resource instance utilizing a semi-fixed timestep method. + pub fn new_semi_fixed( + steps: &[u32], + minimum_time_running_slow: Option, + minimum_time_running_fast: Option, + max_steps_per_frame: Option, + frame_time_limit: Option, + ) -> Self { + Self::new_semi_fixed_exact( + steps.iter().map(|x| Duration::from_secs(1) / *x).collect(), + minimum_time_running_slow, + minimum_time_running_fast, + max_steps_per_frame, + frame_time_limit, + ) + } + + pub fn new_semi_fixed_exact( + steps: Vec, + minimum_time_running_slow: Option, + minimum_time_running_fast: Option, + max_steps_per_frame: Option, + frame_time_limit: Option, + ) -> Self { + assert!(steps.len() > 0, "No steps provided for semi-fixed timer."); + Self { + max_steps_per_frame, + frame_time_limit, + accumulator: Duration::default(), + time_step: TimeStep::SemiFixed(SemiFixedStep { + minimum_time_running_slow, + minimum_time_running_fast, + steps, + active_step: 0, + first_slow_step_in_last_series: None, + last_slow_step: None, + }), + time_step_dirty: true, + global_steps: 0, + frame_steps: 0, + frame_start: None, + } + } + + /// When utilizing the semi-fixed timestep method, attempts to switch to the + /// step at `index` in the list of steps, optionally changing the state of + /// the qualifier for switching steps. + /// + /// The following are the options for `qualifier_mod`: + /// - `None` - Do nothing. If there were timestamps for when slow frames + /// were occuring they are left alone. + /// - `SemiFixedQualifierState::NoPostponement` - Remove any timestamps for + /// slow frames, setting the qualifier state as though no frames have + /// recently been postponed. This is what is done internally when the + /// `minimum_time_running_fast` duration is hit, if it is set, and the + /// `index` is subsequently brought to zero. + /// - `SemiFixedQualifierState::StartPostponementNow(u64)` - Update + /// timestamps for slow frames to begin new slow period right now, stamped + /// with the u64 frame number. This is what is done internally when the + /// `minimum_time_running_slow` duration is hit, if it is set. + pub fn semi_fixed_switch_to_step( + &mut self, + index: usize, + qualifier_mod: Option, + ) -> Result { + match &mut self.time_step { + TimeStep::Fixed(_) => Err(TimeError::WrongStepType), + TimeStep::SemiFixed(step_data) => { + if index < step_data.steps.len() { + step_data.active_step = index; + self.time_step_dirty = true; + match qualifier_mod { + Some(SemiFixedQualifierState::NoPostponement) => { + step_data.last_slow_step = None; + step_data.first_slow_step_in_last_series = None; + } + Some(SemiFixedQualifierState::StartPostponementNow(frame)) => { + let tuple = Some((frame, Instant::now())); + step_data.last_slow_step = tuple.clone(); + step_data.first_slow_step_in_last_series = tuple; + } + _ => {} + } + Ok(step_data.steps[index]) + } else { + Err(TimeError::OutOfSemiStepBounds) + } + } + } + } + + pub fn accumulator(&self) -> Duration { + self.accumulator + } + + pub fn time_step(&self) -> Duration { + match &self.time_step { + TimeStep::Fixed(duration) => *duration, + TimeStep::SemiFixed(data) => data.steps[data.active_step], + } + } + + pub fn frame_steps(&self) -> u32 { + self.frame_steps + } + + pub fn global_steps(&self) -> u64 { + self.global_steps + } +} + +// That's right, I'm clever. +impl Iterator for StepperRes { + type Item = Step; + + fn next(&mut self) -> Option { + self.time_step_dirty = false; + + // Initialize frame. + if self.frame_start.is_none() || self.frame_steps == 0 { + self.frame_steps = 0; + self.accumulator += match self.frame_start { + Some(instant) => instant.elapsed(), + None => self.time_step(), + }; + self.frame_start = Some(Instant::now()); + } + + let current_frame_delta = self.time_step(); + if let Some(slow_frame_error) = SlowFrameError::check_for( + self.frame_steps, + self.frame_start + .expect( + "Frame start stamp should've been initialized at the beginning of this frame.", + ) + .elapsed(), + self.max_steps_per_frame, + self.frame_time_limit, + ) { + // We're running slow and the user's requested + // we postpone steps to the next frame. + if let TimeStep::SemiFixed(step_data) = &mut self.time_step { + if step_data.degrade_from_being_slow(self.global_steps, slow_frame_error) { + self.time_step_dirty = true; + } + } else { + warn!("Physics stepping is falling behind. {}", slow_frame_error); + } + + // Signal end of stepping due to postponement. + self.frame_steps = 0; + None + } else if self.accumulator >= current_frame_delta { + // Drain the accumulator by the length of our timestep. + self.frame_steps += 1; + self.global_steps += 1; + self.accumulator -= current_frame_delta; + + Some(Step { + delta: current_frame_delta, + step_dirty: self.time_step_dirty, + accumulator_remaining: self.accumulator, + global_step_number: self.global_steps, + frame_step_number: self.frame_steps, + }) + } else { + if let TimeStep::SemiFixed(step_data) = &mut self.time_step { + if step_data.upgrade_from_being_fast(self.global_steps) { + self.time_step_dirty = true; + } + } + + // Signal end of stepping due to exhaustion of accumulator. + None + } + } +} + +impl Default for StepperRes { + fn default() -> Self { + StepperRes::new_fixed(60) + } +} + +/// Affect to be applied to state of rate change qualifier via +/// `BatchTimeRes::semi_fixed_switch_to_step`. +#[derive(Debug, Clone, Copy)] +pub enum SemiFixedQualifierState { + /// Remove any timestamps for slow frames, setting the qualifier state as + /// though no frames have recently been postponed. This is what is done + /// internally when the `minimum_time_running_fast` duration is hit, if it + /// is set, and the `index` is subsequently brought to zero. + NoPostponement, + /// Update timestamps for slow frames to begin new slow period right now, + /// stamped with the u64 frame number. This is what is done internally when + /// the `minimum_time_running_slow` duration is hit, if it is set. + StartPostponementNow(u64), +} + +/// Data associated with a single step performed by `::next`. +#[derive(Debug, Default, Clone)] +pub struct Step { + delta: Duration, + step_dirty: bool, + accumulator_remaining: Duration, + global_step_number: u64, + frame_step_number: u32, +} + +impl Step { + /// Delta time of the current physics step. + /// If you're using a fixed step, this will always be the same. + pub fn delta(&self) -> Duration { + self.delta + } + + /// Time left in the delta accumulator for the stepper. + pub fn accumulator_remaining(&self) -> Duration { + self.accumulator_remaining + } + + /// Whether the delta of the step has been modified. + pub fn step_dirty(&self) -> bool { + self.step_dirty + } + + /// How many physics steps have occured globally + pub fn global_step_number(&self) -> u64 { + self.global_step_number + } + + /// How many physics steps have occured in this frame. + pub fn frame_step_number(&self) -> u32 { + self.frame_step_number + } + + pub(crate) fn update(&mut self, other: Self) { + self.delta = other.delta; + self.step_dirty = other.step_dirty; + self.accumulator_remaining = other.accumulator_remaining; + self.global_step_number = other.global_step_number; + self.frame_step_number = other.frame_step_number; + } +} + +enum TimeStep { + Fixed(Duration), + SemiFixed(SemiFixedStep), +} + +struct SemiFixedStep { + minimum_time_running_slow: Option, + minimum_time_running_fast: Option, + steps: Vec, + active_step: usize, + last_slow_step: Option<(u64, Instant)>, + first_slow_step_in_last_series: Option<(u64, Instant)>, +} + +impl SemiFixedStep { + fn degrade_from_being_slow( + &mut self, + step_number: u64, + slow_frame_error: SlowFrameError, + ) -> bool { + let tuple = Some((step_number, Instant::now())); + + if self.first_slow_step_in_last_series.is_none() { + warn!( + "Physics stepping is starting to fall behind. {}", + slow_frame_error + ); + + self.first_slow_step_in_last_series = tuple.clone(); + self.last_slow_step = tuple; + + false + } else if self.last_slow_step.is_some() + && self.minimum_time_running_slow.map_or(false, |minimum| { + self.last_slow_step.unwrap().1.elapsed() > minimum + }) + { + if self.active_step >= self.steps.len() - 1 { + error!( + r#"Physics stepping has fallen beyond the threshold +for setting the step to a lower level. However, +there are no lower level step rates to fall back to. {}"#, + slow_frame_error + ); + + self.last_slow_step = tuple; + + false + } else { + warn!( + r#"Physics stepping has fallen beyond the threshold +and is lowering the step rate to the next level. {}"#, + slow_frame_error + ); + + self.first_slow_step_in_last_series = tuple.clone(); + self.last_slow_step = tuple; + self.active_step += 1; + + true + } + } else { + // We've fallen behind, but not yet triggered a threshold to change step rate. + warn!("Physics stepping has fallen behind. {}", slow_frame_error); + + self.last_slow_step = tuple; + + false + } + } + + fn upgrade_from_being_fast(&mut self, step_number: u64) -> bool { + if self.minimum_time_running_fast.is_some() + && self.active_step > 0 + && (self.last_slow_step.is_none() + || self.last_slow_step.unwrap().1.elapsed() + > self.minimum_time_running_fast.unwrap()) + { + self.active_step -= 1; + + if self.active_step == 0 { + info!("Physics stepping has resumed base stepping rate."); + + self.last_slow_step = None; + self.first_slow_step_in_last_series = None; + } else { + info!( + "Physics stepping has upgraded to the stepping rate index {}.", + self.active_step + ); + + let tuple = Some((step_number, Instant::now())); + self.first_slow_step_in_last_series = tuple.clone(); + self.last_slow_step = tuple; + } + + true + } else { + false + } + } +} + +struct SlowFrameError(bool, Option); + +impl SlowFrameError { + fn check_for( + frame_steps: u32, + frame_duration: Duration, + max_steps_per_frame: Option, + frame_time_limit: Option, + ) -> Option { + // Check if we've ran past the frame step limit + let max_steps_failure = max_steps_per_frame.map_or(false, |max| frame_steps >= max); + + // Check if we've ran past the frame time limit, and calculate how much by + let frame_time_limit_failure = frame_time_limit.and_then(|limit| { + if frame_duration > limit { + Some(frame_duration - limit) + } else { + None + } + }); + + if max_steps_failure || frame_time_limit_failure.is_some() { + Some(SlowFrameError(max_steps_failure, frame_time_limit_failure)) + } else { + None + } + } +} + +impl fmt::Display for SlowFrameError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SlowFrameError(true, Some(duration)) => write!( + f, + r#"The stepper has hit the frame step limit +and exceeded the frame time limit by {:#?}."#, + duration + ), + SlowFrameError(true, None) => write!(f, "The stepper has hit the frame step limit."), + SlowFrameError(false, Some(duration)) => write!( + f, + "The stepper has exceeded the frame time limit by {:#?}.", + duration + ), + SlowFrameError(false, None) => write!(f, "Haha, just kidding."), + } + } +} diff --git a/src/systems/batch.rs b/src/systems/batch.rs new file mode 100644 index 0000000..2ba00db --- /dev/null +++ b/src/systems/batch.rs @@ -0,0 +1,54 @@ +use crate::{ + nalgebra::RealField, + stepper::{Step, StepperRes}, +}; + +use specs::{ + AccessorCow, BatchAccessor, BatchController, BatchUncheckedWorld, Dispatcher, RunningTime, + System, World, WriteExpect, +}; + +use std::{marker::PhantomData, ops::DerefMut}; + +pub struct PhysicsBatchSystem<'a, 'b, N: RealField> { + accessor: BatchAccessor, + dispatcher: Dispatcher<'a, 'b>, + marker: PhantomData, +} + +impl<'a, 'b, N: RealField> BatchController<'a, 'b> for PhysicsBatchSystem<'a, 'b, N> { + type BatchSystemData = (WriteExpect<'a, StepperRes>, WriteExpect<'a, Step>); + + unsafe fn create(accessor: BatchAccessor, dispatcher: Dispatcher<'a, 'b>) -> Self { + PhysicsBatchSystem { + accessor, + dispatcher, + marker: PhantomData, + } + } +} + +impl<'a, 'b, 's, N: RealField> System<'s> for PhysicsBatchSystem<'a, 'b, N> { + type SystemData = BatchUncheckedWorld<'s>; + + fn run(&mut self, data: Self::SystemData) { + for step in data.0.fetch_mut::().deref_mut() { + data.0.fetch_mut::().update(step); + self.dispatcher.dispatch(data.0); + } + } + + fn running_time(&self) -> RunningTime { + RunningTime::VeryLong + } + + fn accessor<'c>(&'c self) -> AccessorCow<'s, 'c, Self> { + AccessorCow::Ref(&self.accessor) + } + + fn setup(&mut self, world: &mut World) { + self.dispatcher.setup(world); + } +} + +unsafe impl Send for PhysicsBatchSystem<'_, '_, N> {} diff --git a/src/systems/mod.rs b/src/systems/mod.rs index e051ab0..a238928 100644 --- a/src/systems/mod.rs +++ b/src/systems/mod.rs @@ -1,56 +1,143 @@ -use std::ops::Deref; - -use specs::{ - storage::{ComponentEvent, MaskedStorage}, - BitSet, - Component, - ReaderId, - Storage, - Tracked, +use crate::{ + nalgebra::{convert as na_convert, RealField}, + nphysics::math::Vector, + position::Position, + stepper::{Step, StepperRes}, + world::{ForceGeneratorSetRes, GeometricalWorldRes, JointConstraintSetRes, MechanicalWorldRes}, }; +use specs::{DispatcherBuilder, World}; +use std::marker::PhantomData; -pub use self::{ - physics_stepper::PhysicsStepperSystem, - sync_bodies_from_physics::SyncBodiesFromPhysicsSystem, - sync_bodies_to_physics::SyncBodiesToPhysicsSystem, - sync_colliders_to_physics::SyncCollidersToPhysicsSystem, - sync_parameters_to_physics::SyncParametersToPhysicsSystem, -}; +mod batch; +mod pose; +mod stepper; -mod physics_stepper; -mod sync_bodies_from_physics; -mod sync_bodies_to_physics; -mod sync_colliders_to_physics; -mod sync_parameters_to_physics; - -/// Iterated over the `ComponentEvent::Inserted`s of a given, tracked `Storage` -/// and returns the results in a `BitSet`. -pub(crate) fn iterate_component_events( - tracked_storage: &Storage, - reader_id: &mut ReaderId, -) -> (BitSet, BitSet, BitSet) -where - T: Component, - T::Storage: Tracked, - D: Deref>, -{ - let (mut inserted, mut modified, mut removed) = (BitSet::new(), BitSet::new(), BitSet::new()); - for component_event in tracked_storage.channel().read(reader_id) { - match component_event { - ComponentEvent::Inserted(id) => { - debug!("Got Inserted event with id: {}", id); - inserted.add(*id); - } - ComponentEvent::Modified(id) => { - debug!("Got Modified event with id: {}", id); - modified.add(*id); - } - ComponentEvent::Removed(id) => { - debug!("Got Removed event with id: {}", id); - removed.add(*id); - } +pub use self::{batch::PhysicsBatchSystem, pose::PhysicsPoseSystem, stepper::PhysicsStepperSystem}; + +pub struct PhysicsBundle> { + mechanical_world: MechanicalWorldRes, + geometrical_world: GeometricalWorldRes, + stepper_res: Option, + // Exercising superfluous allocations at init-time + // is better than figuring out the lifetimes + // for the slice version of this at programming-time. + stepper_deps: Vec>, + marker: PhantomData

, +} + +impl> PhysicsBundle { + pub fn new(gravity: Vector) -> Self { + Self::from_deps(gravity, &[]) + } + + pub fn stepper(interval: u32) -> Self { + Self::default().with_stepper(interval) + } + + pub fn from_deps(gravity: Vector, dep: &[&str]) -> Self { + Self::from_parts( + MechanicalWorldRes::::new(gravity), + GeometricalWorldRes::::new(), + None, + dep, + ) + } + + pub fn from_parts( + mechanical_world: MechanicalWorldRes, + geometrical_world: GeometricalWorldRes, + stepper_res: Option, + dep: &[&str], + ) -> Self { + Self { + mechanical_world, + geometrical_world, + stepper_res, + stepper_deps: dep.iter().map(|s| Box::from(*s)).collect(), + marker: PhantomData, + } + } + + /// Add dependencies which will be attached to the stepper, + /// to be executed before physics stepping. + pub fn with_deps(mut self, dep: &[&str]) -> Self { + self.stepper_deps = [ + self.stepper_deps + .iter() + .map(|s| s.as_ref()) + .collect::>() + .as_slice(), + dep, + ] + .concat() + .iter() + .map(|s| Box::from(*s)) + .collect(); + self + } + + pub fn with_stepper(mut self, interval: u32) -> Self { + self.stepper_res = Some(StepperRes::new_fixed(interval)); + self + } + + pub fn with_stepper_instance(mut self, stepper: StepperRes) -> Self { + self.stepper_res = Some(stepper); + self + } + + pub fn register(self, world: &mut World, builder: &mut DispatcherBuilder) { + world.insert(self.mechanical_world); + world.insert(self.geometrical_world); + + if let Some(stepper_res) = self.stepper_res { + world.insert(stepper_res); + world.insert(Step::default()); } + + // TODO: These would be defaulted when converted to specs Storages. + world.insert(JointConstraintSetRes::::new()); + world.insert(ForceGeneratorSetRes::::new()); + + // Add PhysicsStepperSystem after all other Systems that write data to the + // nphysics World and has to depend on them; this System is used to progress the + // nphysics World for all existing objects. + builder.add( + PhysicsStepperSystem::::default(), + "physics_stepper_system", + self.stepper_deps + .iter() + .map(|s| s.as_ref()) + .collect::>() + .as_slice(), + ); + + // Add PhysicsPoseSystem last as it handles the + // synchronisation between nphysics World bodies and the Position + // components; this depends on the PhysicsStepperSystem. + builder.add( + PhysicsPoseSystem::::default(), + "physics_pose_system", + &["physics_stepper_system"], + ); } +} - (inserted, modified, removed) +#[cfg(feature = "amethyst")] +impl<'a, 'b, N: RealField, P: Position> amethyst::core::SystemBundle<'a, 'b> + for PhysicsBundle +{ + fn build( + self, + world: &mut World, + builder: &mut DispatcherBuilder, + ) -> Result<(), amethyst::error::Error> { + Ok(self.register(world, builder)) + } +} + +impl> Default for PhysicsBundle { + fn default() -> Self { + Self::new(Vector::::y() * na_convert::(-9.81)) + } } diff --git a/src/systems/physics_stepper.rs b/src/systems/physics_stepper.rs deleted file mode 100644 index 9bd0721..0000000 --- a/src/systems/physics_stepper.rs +++ /dev/null @@ -1,144 +0,0 @@ -use std::marker::PhantomData; - -use specs::{world::Index, Entities, Entity, Read, System, SystemData, World, Write, WriteExpect}; - -use crate::events::{ContactEvent, ContactEvents, ContactType, ProximityEvent, ProximityEvents}; -use nalgebra::RealField; -use ncollide::{events::ContactEvent as NContactEvent, world::CollisionObjectHandle}; -use nphysics::world::ColliderWorld; -use crate::parameters::TimeStep; -use crate::Physics; - -/// The `PhysicsStepperSystem` progresses the nphysics `World`. -pub struct PhysicsStepperSystem { - n_marker: PhantomData, -} - -impl<'s, N: RealField> System<'s> for PhysicsStepperSystem { - type SystemData = ( - Entities<'s>, - Option>>, - Write<'s, ContactEvents>, - Write<'s, ProximityEvents>, - WriteExpect<'s, Physics>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (entities, time_step, mut contact_events, mut proximity_events, mut physics) = data; - - // if a TimeStep resource exits, set the timestep for the nphysics integration - // accordingly; this should not be required if the Systems are executed in a - // fixed interval - if let Some(time_step) = time_step { - // only update timestep if it actually differs from the current nphysics World - // one; keep in mind that changing the Resource will destabilize the simulation - if physics.world.timestep() != time_step.0 { - warn!( - "TimeStep and world.timestep() differ, changing worlds timestep from {} to: {:?}", - physics.world.timestep(), - time_step.0 - ); - physics.world.set_timestep(time_step.0); - } - } - - physics.world.step(); - - let collider_world = physics.world.collider_world(); - - // map occurred ncollide ContactEvents to a custom ContactEvent type; this - // custom type contains data that is more relevant for Specs users than - // CollisionObjectHandles, such as the Entities that took part in the collision - contact_events.iter_write(collider_world.contact_events().iter().map(|contact_event| { - debug!("Got ContactEvent: {:?}", contact_event); - // retrieve CollisionObjectHandles from ContactEvent and map the ContactEvent - // type to our own custom ContactType - let (handle1, handle2, contact_type) = match contact_event { - NContactEvent::Started(handle1, handle2) => { - (*handle1, *handle2, ContactType::Started) - } - NContactEvent::Stopped(handle1, handle2) => { - (*handle1, *handle2, ContactType::Stopped) - } - }; - - // create our own ContactEvent from the extracted data; mapping the - // CollisionObjectHandles to Entities is error prone but should work as intended - // as long as we're the only ones working directly with the nphysics World - ContactEvent { - collider1: entity_from_collision_object_handle(&entities, handle1, &collider_world), - collider2: entity_from_collision_object_handle(&entities, handle2, &collider_world), - contact_type, - } - })); - - // map occurred ncollide ProximityEvents to a custom ProximityEvent type; see - // ContactEvents for reasoning - proximity_events.iter_write(collider_world.proximity_events().iter().map( - |proximity_event| { - debug!("Got ProximityEvent: {:?}", proximity_event); - // retrieve CollisionObjectHandles and Proximity statuses from the ncollide - // ProximityEvent - let (handle1, handle2, prev_status, new_status) = ( - proximity_event.collider1, - proximity_event.collider2, - proximity_event.prev_status, - proximity_event.new_status, - ); - - // create our own ProximityEvent from the extracted data; mapping - // CollisionObjectHandles to Entities is once again error prone, but yeah... - // ncollides Proximity types are mapped to our own types - ProximityEvent { - collider1: entity_from_collision_object_handle( - &entities, - handle1, - &collider_world, - ), - collider2: entity_from_collision_object_handle( - &entities, - handle2, - &collider_world, - ), - prev_status, - new_status, - } - }, - )); - } - - fn setup(&mut self, res: &mut World) { - info!("PhysicsStepperSystem.setup"); - Self::SystemData::setup(res); - - // initialise required resources - res.entry::>().or_insert_with(Physics::default); - } -} - -impl Default for PhysicsStepperSystem -where - N: RealField, -{ - fn default() -> Self { - Self { - n_marker: PhantomData, - } - } -} - -fn entity_from_collision_object_handle( - entities: &Entities, - collision_object_handle: CollisionObjectHandle, - collider_world: &ColliderWorld, -) -> Entity { - entities.entity( - *collider_world - .collider(collision_object_handle) - .unwrap() - .user_data() - .unwrap() - .downcast_ref::() - .unwrap(), - ) -} diff --git a/src/systems/pose.rs b/src/systems/pose.rs new file mode 100644 index 0000000..b0ce915 --- /dev/null +++ b/src/systems/pose.rs @@ -0,0 +1,29 @@ +use crate::{nalgebra::RealField, position::Position, world::BodyComponent}; +use specs::{Join, ReadStorage, System, WriteStorage}; +use std::marker::PhantomData; + +/// The `SyncBodiesFromPhysicsSystem` synchronised the updated position of +/// the `RigidBody`s in the nphysics `World` with their Specs counterparts. This +/// affects the `Position` `Component` related to the `Entity`. +pub struct PhysicsPoseSystem>(PhantomData<(N, P)>); + +impl<'s, N: RealField, P: Position> System<'s> for PhysicsPoseSystem { + type SystemData = (WriteStorage<'s, P>, ReadStorage<'s, BodyComponent>); + + fn run(&mut self, (mut positions, bodies): Self::SystemData) { + // iterate over all PhysicBody components joined with their Positions + for (position, body) in (&mut positions, &bodies).join() { + // if a RigidBody exists in the nphysics World we fetch it and update the + // Position component accordingly + if let Some(part) = body.part(0) { + *position.isometry_mut() = part.position(); + } + } + } +} + +impl> Default for PhysicsPoseSystem { + fn default() -> Self { + Self(PhantomData) + } +} diff --git a/src/systems/stepper.rs b/src/systems/stepper.rs new file mode 100644 index 0000000..e3dad9f --- /dev/null +++ b/src/systems/stepper.rs @@ -0,0 +1,62 @@ +use crate::{ + nalgebra::{convert as na_convert, RealField}, + stepper::Step, + world::{ + BodySet, ColliderSet, ForceGeneratorSetRes, GeometricalWorldRes, JointConstraintSetRes, + MechanicalWorldRes, + }, +}; + +use specs::{Read, System, WriteExpect}; +use std::marker::PhantomData; + +/// This system steps the physics world once when called. +/// To ensure the visual motion of the simulation matches the speeds within the +/// simulation, you will want to +pub struct PhysicsStepperSystem(PhantomData); + +impl<'s, N: RealField> System<'s> for PhysicsStepperSystem { + type SystemData = ( + WriteExpect<'s, MechanicalWorldRes>, + WriteExpect<'s, GeometricalWorldRes>, + BodySet<'s, N>, + ColliderSet<'s, N>, + WriteExpect<'s, JointConstraintSetRes>, + WriteExpect<'s, ForceGeneratorSetRes>, + Option>, + ); + + fn run(&mut self, data: Self::SystemData) { + let ( + mut mechanical_world, + mut geometrical_world, + mut body_set, + mut collider_set, + mut joint_constraint_set, + mut force_generator_set, + step, + ) = data; + + // If we've added a batch time step resource to the world, check if we need to + // update our timestep from that resource. + if let Some(step_data) = step { + if step_data.step_dirty() { + mechanical_world.set_timestep(na_convert(step_data.delta().as_secs_f64())); + } + } + + mechanical_world.step( + &mut *geometrical_world, + &mut body_set, + &mut collider_set, + &mut *joint_constraint_set, + &mut *force_generator_set, + ); + } +} + +impl Default for PhysicsStepperSystem { + fn default() -> Self { + Self(PhantomData) + } +} diff --git a/src/systems/sync_bodies_from_physics.rs b/src/systems/sync_bodies_from_physics.rs deleted file mode 100644 index 70edcc5..0000000 --- a/src/systems/sync_bodies_from_physics.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::marker::PhantomData; - -use specs::{Join, ReadExpect, System, SystemData, World, WriteStorage}; - -use crate::bodies::PhysicsBody; -use crate::positon::Position; -use crate::Physics; -use nalgebra::RealField; - -/// The `SyncBodiesFromPhysicsSystem` synchronised the updated position of -/// the `RigidBody`s in the nphysics `World` with their Specs counterparts. This -/// affects the `Position` `Component` related to the `Entity`. -pub struct SyncBodiesFromPhysicsSystem { - n_marker: PhantomData, - p_marker: PhantomData

, -} - -impl<'s, N, P> System<'s> for SyncBodiesFromPhysicsSystem -where - N: RealField, - P: Position, -{ - type SystemData = ( - ReadExpect<'s, Physics>, - WriteStorage<'s, PhysicsBody>, - WriteStorage<'s, P>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (physics, mut physics_bodies, mut positions) = data; - - // iterate over all PhysicBody components joined with their Positions - for (physics_body, position) in (&mut physics_bodies, &mut positions).join() { - // if a RigidBody exists in the nphysics World we fetch it and update the - // Position component accordingly - if let Some(rigid_body) = physics.world.rigid_body(physics_body.handle.unwrap()) { - *position.isometry_mut() = *rigid_body.position(); - physics_body.update_from_physics_world(rigid_body); - } - } - } - - fn setup(&mut self, res: &mut World) { - info!("SyncBodiesFromPhysicsSystem.setup"); - Self::SystemData::setup(res); - - // initialise required resources - res.entry::>().or_insert_with(Physics::default); - } -} - -impl Default for SyncBodiesFromPhysicsSystem -where - N: RealField, - P: Position, -{ - fn default() -> Self { - Self { - n_marker: PhantomData, - p_marker: PhantomData, - } - } -} diff --git a/src/systems/sync_bodies_to_physics.rs b/src/systems/sync_bodies_to_physics.rs deleted file mode 100644 index 8a0adb6..0000000 --- a/src/systems/sync_bodies_to_physics.rs +++ /dev/null @@ -1,243 +0,0 @@ -use std::marker::PhantomData; - -use specs::{ - storage::ComponentEvent, - world::Index, - BitSet, - Join, - ReadStorage, - ReaderId, - System, - SystemData, - World, - WriteExpect, - WriteStorage, -}; - -use nalgebra::RealField; -use crate::bodies::PhysicsBody; -use crate::positon::Position; -use crate::Physics; - -use super::iterate_component_events; - -/// The `SyncBodiesToPhysicsSystem` handles the synchronisation of `PhysicsBody` -/// `Component`s into the physics `World`. -pub struct SyncBodiesToPhysicsSystem { - positions_reader_id: Option>, - physics_bodies_reader_id: Option>, - - n_marker: PhantomData, - p_marker: PhantomData

, -} - -impl<'s, N, P> System<'s> for SyncBodiesToPhysicsSystem -where - N: RealField, - P: Position, -{ - type SystemData = ( - ReadStorage<'s, P>, - WriteExpect<'s, Physics>, - WriteStorage<'s, PhysicsBody>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (positions, mut physics, mut physics_bodies) = data; - - // collect all ComponentEvents for the Position storage - let (inserted_positions, modified_positions, removed_positions) = - iterate_component_events(&positions, self.positions_reader_id.as_mut().unwrap()); - - // collect all ComponentEvents for the PhysicsBody storage - let (inserted_physics_bodies, modified_physics_bodies, removed_physics_bodies) = - iterate_component_events( - &physics_bodies, - self.physics_bodies_reader_id.as_mut().unwrap(), - ); - - // iterate over PhysicsBody and Position components with an id/Index that - // exists in either of the collected ComponentEvent BitSets - for (position, mut physics_body, id) in ( - &positions, - &mut physics_bodies, - &inserted_positions - | &modified_positions - | &removed_positions - | &inserted_physics_bodies - | &modified_physics_bodies - | &removed_physics_bodies, - ) - .join() - { - // handle inserted events - if inserted_positions.contains(id) || inserted_physics_bodies.contains(id) { - debug!("Inserted PhysicsBody with id: {}", id); - add_rigid_body::(id, &position, &mut physics, &mut physics_body); - } - - // handle modified events - if modified_positions.contains(id) || modified_physics_bodies.contains(id) { - debug!("Modified PhysicsBody with id: {}", id); - update_rigid_body::( - id, - &position, - &mut physics, - &mut physics_body, - &modified_positions, - &modified_physics_bodies, - ); - } - - // handle removed events - if removed_positions.contains(id) || removed_physics_bodies.contains(id) { - debug!("Removed PhysicsBody with id: {}", id); - remove_rigid_body::(id, &mut physics); - } - } - } - - fn setup(&mut self, res: &mut World) { - info!("SyncBodiesToPhysicsSystem.setup"); - Self::SystemData::setup(res); - - // initialise required resources - res.entry::>().or_insert_with(Physics::default); - - // register reader id for the Position storage - let mut position_storage: WriteStorage

= SystemData::fetch(&res); - self.positions_reader_id = Some(position_storage.register_reader()); - - // register reader id for the PhysicsBody storage - let mut physics_body_storage: WriteStorage> = SystemData::fetch(&res); - self.physics_bodies_reader_id = Some(physics_body_storage.register_reader()); - } -} - -impl Default for SyncBodiesToPhysicsSystem -where - N: RealField, - P: Position, -{ - fn default() -> Self { - Self { - positions_reader_id: None, - physics_bodies_reader_id: None, - n_marker: PhantomData, - p_marker: PhantomData, - } - } -} - -fn add_rigid_body( - id: Index, - position: &P, - physics: &mut Physics, - physics_body: &mut PhysicsBody, -) where - N: RealField, - P: Position, -{ - // remove already existing bodies for this inserted component; - // this technically should never happen but we need to keep the list of body - // handles clean - if let Some(body_handle) = physics.body_handles.remove(&id) { - warn!("Removing orphaned body handle: {:?}", body_handle); - physics.world.remove_bodies(&[body_handle]); - } - - // create a new RigidBody in the PhysicsWorld and store its - // handle for later usage - let handle = physics_body - .to_rigid_body_desc() - .position(*position.isometry()) - .user_data(id) - .build(&mut physics.world) - .handle(); - - physics_body.handle = Some(handle); - physics.body_handles.insert(id, handle); - - info!( - "Inserted rigid body to world with values: {:?}", - physics_body - ); -} - -fn update_rigid_body( - id: Index, - position: &P, - physics: &mut Physics, - physics_body: &mut PhysicsBody, - modified_positions: &BitSet, - modified_physics_bodies: &BitSet, -) where - N: RealField, - P: Position, -{ - if let Some(rigid_body) = physics.world.rigid_body_mut(physics_body.handle.unwrap()) { - // the PhysicsBody was modified, update everything but the position - if modified_physics_bodies.contains(id) { - physics_body.apply_to_physics_world(rigid_body); - } - - // the Position was modified, update the position directly - if modified_positions.contains(id) { - rigid_body.set_position(*position.isometry()); - } - - trace!( - "Updated rigid body in world with values: {:?}", - physics_body - ); - } -} - -fn remove_rigid_body(id: Index, physics: &mut Physics) -where - N: RealField, - P: Position, -{ - if let Some(handle) = physics.body_handles.remove(&id) { - // remove body if it still exists in the PhysicsWorld - physics.world.remove_bodies(&[handle]); - info!("Removed rigid body from world with id: {}", id); - } -} - -#[cfg(all(test, feature = "physics3d"))] -mod tests { - use nalgebra::Isometry3; - use nphysics::object::BodyStatus; - use crate::{systems::SyncBodiesToPhysicsSystem, Physics, PhysicsBodyBuilder, SimplePosition}; - - use specs::prelude::*; - - #[test] - fn add_rigid_body() { - let mut world = World::new(); - let mut dispatcher = DispatcherBuilder::new() - .with( - SyncBodiesToPhysicsSystem::>::default(), - "sync_bodies_to_physics_system", - &[], - ) - .build(); - dispatcher.setup(&mut world); - - // create an Entity with the PhysicsBody component and execute the dispatcher - world - .create_entity() - .with(SimplePosition::(Isometry3::::translation( - 1.0, 1.0, 1.0, - ))) - .with(PhysicsBodyBuilder::::from(BodyStatus::Dynamic).build()) - .build(); - dispatcher.dispatch(&world); - - // fetch the Physics instance and check for new bodies - let physics = world.read_resource::>(); - assert_eq!(physics.body_handles.len(), 1); - assert_eq!(physics.world.bodies().count(), 1); - } -} diff --git a/src/systems/sync_colliders_to_physics.rs b/src/systems/sync_colliders_to_physics.rs deleted file mode 100644 index 37e6903..0000000 --- a/src/systems/sync_colliders_to_physics.rs +++ /dev/null @@ -1,296 +0,0 @@ -use std::marker::PhantomData; - -use specs::{ - storage::ComponentEvent, - world::Index, - Join, - ReadStorage, - ReaderId, - System, - SystemData, - World, - WriteExpect, - WriteStorage, -}; - -use crate::positon::Position; -use crate::colliders::PhysicsCollider; -use crate::{Physics, PhysicsParent}; -use nalgebra::RealField; -use nphysics::object::{BodyPartHandle, ColliderDesc}; - -use super::iterate_component_events; - -/// The `SyncCollidersToPhysicsSystem` handles the synchronisation of -/// `PhysicsCollider` `Component`s into the physics `World`. -pub struct SyncCollidersToPhysicsSystem { - positions_reader_id: Option>, - physics_colliders_reader_id: Option>, - - n_marker: PhantomData, - p_marker: PhantomData

, -} - -impl<'s, N, P> System<'s> for SyncCollidersToPhysicsSystem -where - N: RealField, - P: Position, -{ - type SystemData = ( - ReadStorage<'s, P>, - ReadStorage<'s, PhysicsParent>, - WriteExpect<'s, Physics>, - WriteStorage<'s, PhysicsCollider>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (positions, parent_entities, mut physics, mut physics_colliders) = data; - - // collect all ComponentEvents for the Position storage - let (inserted_positions, ..) = - iterate_component_events(&positions, self.positions_reader_id.as_mut().unwrap()); - - // collect all ComponentEvents for the PhysicsCollider storage - let (inserted_physics_colliders, modified_physics_colliders, removed_physics_colliders) = - iterate_component_events( - &physics_colliders, - self.physics_colliders_reader_id.as_mut().unwrap(), - ); - - // iterate over PhysicsCollider and Position components with an id/Index that - // exists in either of the collected ComponentEvent BitSets - for (position, parent_entity, mut physics_collider, id) in ( - &positions, - parent_entities.maybe(), - &mut physics_colliders.restrict_mut(), - &inserted_positions - | &inserted_physics_colliders - | &modified_physics_colliders - | &removed_physics_colliders, - ) - .join() - { - // handle inserted events - if inserted_positions.contains(id) || inserted_physics_colliders.contains(id) { - debug!("Inserted PhysicsCollider with id: {}", id); - add_collider::( - id, - parent_entity, - &position, - &mut physics, - physics_collider.get_mut_unchecked(), - ); - } - - // handle modified events - if modified_physics_colliders.contains(id) { - debug!("Modified PhysicsCollider with id: {}", id); - update_collider::(id, &mut physics, physics_collider.get_unchecked()); - } - - // handle removed events - if removed_physics_colliders.contains(id) { - debug!("Removed PhysicsCollider with id: {}", id); - remove_collider::(id, &mut physics); - } - } - - // Drain update triggers caused by inserts - let event_iter = physics_colliders - .channel() - .read(self.physics_colliders_reader_id.as_mut().unwrap()); - for _ in event_iter {} - } - - fn setup(&mut self, res: &mut World) { - info!("SyncCollidersToPhysicsSystem.setup"); - Self::SystemData::setup(res); - - // initialise required resources - res.entry::>().or_insert_with(Physics::default); - - // register reader id for the Position storage - let mut position_storage: WriteStorage

= SystemData::fetch(&res); - self.positions_reader_id = Some(position_storage.register_reader()); - - // register reader id for the PhysicsBody storage - let mut physics_collider_storage: WriteStorage> = - SystemData::fetch(&res); - self.physics_colliders_reader_id = Some(physics_collider_storage.register_reader()); - } -} - -impl Default for SyncCollidersToPhysicsSystem -where - N: RealField, - P: Position, -{ - fn default() -> Self { - Self { - positions_reader_id: None, - physics_colliders_reader_id: None, - n_marker: PhantomData, - p_marker: PhantomData, - } - } -} - -fn add_collider( - id: Index, - parent_entity: Option<&PhysicsParent>, - position: &P, - physics: &mut Physics, - physics_collider: &mut PhysicsCollider, -) where - N: RealField, - P: Position, -{ - // remove already existing colliders for this inserted event - if let Some(handle) = physics.collider_handles.remove(&id) { - warn!("Removing orphaned collider handle: {:?}", handle); - physics.world.remove_colliders(&[handle]); - } - - // attempt to find an existing RigidBody for this Index; if one exists we'll - // fetch its BodyPartHandle and use it as the Colliders parent in the - // nphysics World - let parent_part_handle = match physics.body_handles.get(&id) { - Some(parent_handle) => physics - .world - .rigid_body(*parent_handle) - .map_or(BodyPartHandle::ground(), |body| body.part_handle()), - None => { - // if BodyHandle was found for the current Entity/Index, check for a potential - // parent Entity and repeat the first step - if let Some(parent_entity) = parent_entity { - match physics.body_handles.get(&parent_entity.entity.id()) { - Some(parent_handle) => physics - .world - .rigid_body(*parent_handle) - .map_or(BodyPartHandle::ground(), |body| body.part_handle()), - None => { - // ultimately default to BodyPartHandle::ground() - BodyPartHandle::ground() - } - } - } else { - // no parent Entity exists, default to BodyPartHandle::ground() - BodyPartHandle::ground() - } - } - }; - - // translation based on parent handle; if we did not have a valid parent and - // ended up defaulting to BodyPartHandle::ground(), we'll need to take the - // Position into consideration - let translation = if parent_part_handle.is_ground() { - // let scale = 1.0; may be added later - let iso = &mut position.isometry().clone(); - iso.translation.vector += - iso.rotation * physics_collider.offset_from_parent.translation.vector; //.component_mul(scale); - iso.rotation *= physics_collider.offset_from_parent.rotation; - *iso - } else { - physics_collider.offset_from_parent - }; - - // create the actual Collider in the nphysics World and fetch its handle - let handle = ColliderDesc::new(physics_collider.shape_handle()) - .position(translation) - .density(physics_collider.density) - .material(physics_collider.material.clone()) - .margin(physics_collider.margin) - .collision_groups(physics_collider.collision_groups) - .linear_prediction(physics_collider.linear_prediction) - .angular_prediction(physics_collider.angular_prediction) - .sensor(physics_collider.sensor) - .user_data(id) - .build_with_parent(parent_part_handle, &mut physics.world) - .unwrap() - .handle(); - - physics_collider.handle = Some(handle); - physics.collider_handles.insert(id, handle); - - info!( - "Inserted collider to world with values: {:?}", - physics_collider - ); -} - -fn update_collider(id: Index, physics: &mut Physics, physics_collider: &PhysicsCollider) -where - N: RealField, - P: Position, -{ - debug!("Modified PhysicsCollider with id: {}", id); - let collider_handle = physics_collider.handle.unwrap(); - let collider_world = physics.world.collider_world_mut(); - - // update collision groups - collider_world.set_collision_groups(collider_handle, physics_collider.collision_groups); - - info!( - "Updated collider in world with values: {:?}", - physics_collider - ); -} - -fn remove_collider(id: Index, physics: &mut Physics) -where - N: RealField, - P: Position, -{ - debug!("Removed PhysicsCollider with id: {}", id); - if let Some(handle) = physics.collider_handles.remove(&id) { - // we have to check if the collider still exists in the nphysics World before - // attempting to delete it as removing a collider that does not exist anymore - // causes the nphysics World to panic; colliders are implicitly removed when a - // parent body is removed so this is actually a valid scenario - if physics.world.collider(handle).is_some() { - physics.world.remove_colliders(&[handle]); - } - - info!("Removed collider from world with id: {}", id); - } -} - -#[cfg(all(test, feature = "physics3d"))] -mod tests { - use specs::prelude::*; - - use nalgebra::Isometry3; - use crate::{ - colliders::Shape, systems::SyncCollidersToPhysicsSystem, Physics, - PhysicsColliderBuilder, SimplePosition, - }; - - #[test] - fn add_collider() { - let mut world = World::new(); - let mut dispatcher = DispatcherBuilder::new() - .with( - SyncCollidersToPhysicsSystem::>::default(), - "sync_colliders_to_physics_system", - &[], - ) - .build(); - dispatcher.setup(&mut world); - - // create an Entity with the PhysicsCollider component and execute the - // dispatcher - world - .create_entity() - .with(SimplePosition::(Isometry3::::translation( - 1.0, 1.0, 1.0, - ))) - .with(PhysicsColliderBuilder::::from(Shape::Ball { radius: 5.0 }).build()) - .build(); - dispatcher.dispatch(&world); - - // fetch the Physics instance and check for new colliders - let physics = world.read_resource::>(); - assert_eq!(physics.collider_handles.len(), 1); - assert_eq!(physics.world.colliders().count(), 1); - } -} diff --git a/src/systems/sync_parameters_to_physics.rs b/src/systems/sync_parameters_to_physics.rs deleted file mode 100644 index 2ce5859..0000000 --- a/src/systems/sync_parameters_to_physics.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::marker::PhantomData; - -use specs::{Read, System, SystemData, World, WriteExpect}; - -use nalgebra::RealField; -use crate::{ - parameters::{Gravity, PhysicsIntegrationParameters, PhysicsProfilingEnabled}, - Physics, -}; - -/// The `SyncParametersToPhysicsSystem` synchronises the simulation parameters -/// with the nphysics `World`. -pub struct SyncParametersToPhysicsSystem { - n_marker: PhantomData, -} - -impl<'s, N: RealField> System<'s> for SyncParametersToPhysicsSystem { - type SystemData = ( - Option>>, - Option>, - Option>>, - WriteExpect<'s, Physics>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (gravity, profiling, integration_params, mut physics) = data; - - // if a Gravity resource exists, synchronise its values with the nphysics World - if let Some(gravity) = gravity { - if gravity.0 != *physics.gravity() { - info!( - "Global physics gravity modified from {}, updating to {}.", - physics.gravity(), - gravity.0 - ); - physics.world.set_gravity(gravity.0); - } - } - - if let Some(enable_profiling) = profiling { - if enable_profiling.0 != physics.performance_counters().enabled() { - if enable_profiling.0 { - info!("Physics performance counters enabled."); - physics.world.enable_performance_counters(); - } else { - info!("Physics performance counters disabled."); - physics.world.disable_performance_counters(); - } - } - } - - if let Some(params) = integration_params { - if *params != *physics.integration_parameters() { - params.apply(physics.world.integration_parameters_mut()); - info!("Integration parameters have been updated."); - } - } - } - - fn setup(&mut self, res: &mut World) { - info!("SyncParametersToPhysicsSystem.setup"); - Self::SystemData::setup(res); - - // initialise required resources - res.entry::>().or_insert_with(Physics::default); - } -} - -impl Default for SyncParametersToPhysicsSystem -where - N: RealField, -{ - fn default() -> Self { - Self { - n_marker: PhantomData, - } - } -} - -#[cfg(all(test, feature = "physics3d"))] -mod tests { - use approx::assert_ulps_eq; - use specs::prelude::*; - - use nalgebra::Vector3; - use crate::{parameters::Gravity, systems::SyncParametersToPhysicsSystem, Physics}; - - #[test] - fn update_gravity() { - let mut world = World::new(); - let mut dispatcher = DispatcherBuilder::new() - .with( - SyncParametersToPhysicsSystem::::default(), - "sync_parameters_to_physics_system", - &[], - ) - .build(); - dispatcher.setup(&mut world); - - world.insert(Gravity(Vector3::::new(1.0, 2.0, 3.0))); - dispatcher.dispatch(&world); - - let physics = world.read_resource::>(); - assert_ulps_eq!(physics.world.gravity().x, 1.0); - assert_ulps_eq!(physics.world.gravity().y, 2.0); - assert_ulps_eq!(physics.world.gravity().z, 3.0); - } -} diff --git a/src/world/body_set.rs b/src/world/body_set.rs new file mode 100644 index 0000000..31eebbf --- /dev/null +++ b/src/world/body_set.rs @@ -0,0 +1,236 @@ +use crate::{ + nalgebra::RealField, + nphysics::object::{Body, BodySet as NBodySet, Ground, Multibody, RigidBody}, +}; + +use specs::{ + shred::{Fetch, FetchMut, MetaTable, ResourceId}, + storage::{AnyStorage, ComponentEvent, MaskedStorage, TryDefault}, + world::EntitiesRes, + BitSet, Component, DenseVecStorage, Entity, FlaggedStorage, Join, ReaderId, SystemData, World, + WorldExt, WriteStorage, +}; + +/// Handle component to a generic BodyPart, consisting of two members. +/// The first item, Entity, points at the BodyComponent for this Part. +/// The second item, usize, is the index of the Part in that Body. +pub struct BodyPartHandle(Entity, usize); + +impl Component for BodyPartHandle { + type Storage = DenseVecStorage; +} + +/// Component that marks the Body on this Entity as a RigidBody. +/// Do not attach more than one kind of Body Marker type to a Body entity. +pub struct RigidBodyMarker; + +impl Component for RigidBodyMarker { + type Storage = NullStorage; +} + +/// Component that marks the Body on this Entity as a Multibody. +/// Do not attach more than one kind of Body Marker type to a Body entity. +pub struct MultibodyMarker; + +impl Component for RigidBodyMarker { + type Storage = NullStorage; +} + +/// Component that marks the Body on this Entity as Ground. +/// Do not attach more than one kind of Body Marker type to a Body entity. +pub struct GroundMarker; + +impl Component for RigidBodyMarker { + type Storage = NullStorage; +} + +/// The handle type of Bodies passed to nphysics API type parameters receiving +/// "BodyHandle". +pub type BodyHandleType = Entity; + +/// The component type of all physics bodies. Attaching this component to your +/// entity with a `Position` component will make the syncing system synchronize +/// the isometry of the first part in that Body to your `Position` (or simply +/// the position of the body if it is a single part body). This relationship is +/// one-way, however. If you'd like to update the position of a Body, **do so +/// via this component, and not from the `Position` component**. +/// +/// If you'd like to synchronize individual parts of a body to a `Position`, you +/// should not attach a `Position` to the entity with this Component, and should +/// instead attach a `BodyPartHandle`, which points to the multipart body, to +/// the entity with the `Position` for a single part. +// Ouch! Bad allocation story here with DenseVecStorage>. +// However, this is hard if not impossible to avoid due to nphysics API limitations. +#[derive(Shrinkwrap)] +#[shrinkwrap(mutable)] +pub struct BodyComponent { + #[shrinkwrap(main_field)] + pub body: Box>, + // Bitset of entities with a BodyPartHandle to this Body + handles: BitSet, +} + +impl Component for BodyComponent { + type Storage = FlaggedStorage>; +} + +impl BodyComponent { + /// Creates a new Body Component. + /// This is made useful by inserting the returned struct + /// into the BodyComponent's Storage. + pub fn new>(body: B) -> Self { + Self { + body: Box::new(body), + handles: BitSet::new(), + } + } + + /// Attempts to cast this Body to a RigidBody. + /// Just sugar for `Body::downcast_ref()`. + pub fn as_rigid_body(&self) -> Option<&RigidBody> { + self.body.downcast_ref() + } + + /// Attempts to mutably cast this Body to a RigidBody. + /// Just sugar for `Body::downcast_mut()`. + pub fn as_rigid_body_mut(&mut self) -> Option<&mut RigidBody> { + self.body.downcast_mut() + } + + /// Attempts to cast this Body to a Multibody. + /// Just sugar for `Body::downcast_ref()`. + pub fn as_multi_body(&self) -> Option<&Multibody> { + self.body.downcast_ref() + } + + /// Attempts to mutably cast this Body to a Multibody. + /// Just sugar for `Body::downcast_mut()`. + pub fn as_multi_body_mut(&mut self) -> Option<&mut Multibody> { + self.body.downcast_mut() + } + + /// Attempts to cast this Body to Ground. + /// Just sugar for `Body::downcast_ref()`. + pub fn as_ground(&self) -> Option<&Ground> { + self.body.downcast_ref() + } + + /// Attempts to mutably cast this Body to Ground. + /// Just sugar for `Body::downcast_mut()`. + pub fn as_ground_mut(&mut self) -> Option<&mut Ground> { + self.body.downcast_mut() + } +} + +/// List of removals used by `BodySet` so that nphysics may `pop` single removal +/// events. +#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Shrinkwrap)] +#[shrinkwrap(mutable)] +pub(crate) struct BodyRemovalRes(pub Vec); + +/// Reader resource used by `BodySet` during fetching to populate +/// `BodyRemovalRes` with removal events. +#[derive(Debug, Shrinkwrap)] +#[shrinkwrap(mutable)] +pub(crate) struct BodyReaderRes(pub ReaderId); + +/// This structure is only used to pass the BodyComponent storage to nphysics +/// API's. You probably don't want to use it. unless you're using your own +/// system for stepping. +pub struct BodySet<'f, N: RealField> { + entities: Fetch<'f, EntitiesRes>, + storage: WriteStorage<'f, BodyComponent>, + removals: FetchMut<'f, BodyRemovalRes>, +} + +impl<'f, N: RealField> SystemData<'f> for BodySet<'f, N> { + fn setup(world: &mut World) { + // Setup storage for body component. + world + .entry::>>() + .or_insert_with(|| { + MaskedStorage::new( + < as Component>::Storage as TryDefault>::unwrap_default(), + ) + }); + world + .fetch_mut::>() + .register(&*world.fetch::>>()); + + // Setup resource for removal buffer. + world + .entry::() + .or_insert_with(|| BodyRemovalRes::default()); + + // Setup ComponentEvent reader resource. + if !world.has_value::() { + let reader = world.write_storage::>().register_reader(); + world.insert(BodyReaderRes(reader)); + } + } + + fn fetch(world: &'f World) -> Self { + let entities = world.read_resource::(); + let storage = world.write_storage::>(); + let mut reader = world.write_resource::(); + let mut removals = world.write_resource::(); + + for event in storage.channel().read(&mut reader) { + if let ComponentEvent::Removed(index) = event { + // Is grabbing the current entity for this index logically wrong? Maybe. + // Is doing this in SystemData::fetch morally wrong? Yes. + removals.push(entities.entity(*index)); + } + } + + Self { + entities, + storage, + removals, + } + } + + fn reads() -> Vec { + vec![ResourceId::new::()] + } + + fn writes() -> Vec { + vec![ + ResourceId::new::>>(), + ResourceId::new::(), + ResourceId::new::(), + ] + } +} + +impl<'f, N: RealField> NBodySet for BodySet<'f, N> { + type Handle = BodyHandleType; + + fn get(&self, handle: Self::Handle) -> Option<&dyn Body> { + self.storage.get(handle).map(|x| x.body.as_ref()) + } + + fn get_mut(&mut self, handle: Self::Handle) -> Option<&mut dyn Body> { + self.storage.get_mut(handle).map(|x| x.body.as_mut()) + } + + fn contains(&self, handle: Self::Handle) -> bool { + self.storage.contains(handle) + } + + fn foreach(&self, f: &mut dyn FnMut(Self::Handle, &dyn Body)) { + for (handle, body) in (&self.entities, &self.storage).join() { + f(handle, body.body.as_ref()); + } + } + + fn foreach_mut(&mut self, f: &mut dyn FnMut(Self::Handle, &mut dyn Body)) { + for (handle, body) in (&self.entities, &mut self.storage).join() { + f(handle, body.body.as_mut()); + } + } + + fn pop_removal_event(&mut self) -> Option { + self.removals.pop() + } +} diff --git a/src/world/collider_set.rs b/src/world/collider_set.rs new file mode 100644 index 0000000..6a21fee --- /dev/null +++ b/src/world/collider_set.rs @@ -0,0 +1,206 @@ +use crate::{ + nalgebra::RealField, + ncollide::pipeline::CollisionObjectSet, + nphysics::object::{Collider, ColliderRemovalData, ColliderSet as NColliderSet}, +}; + +use specs::{ + shred::{Fetch, FetchMut, MetaTable, ResourceId}, + storage::{AnyStorage, ComponentEvent, MaskedStorage, TryDefault, UnprotectedStorage}, + world::EntitiesRes, + Component, DenseVecStorage, Entity, FlaggedStorage, Join, ReaderId, SystemData, World, + WorldExt, WriteStorage, +}; + +use super::BodyHandleType; + +pub type ColliderHandleType = Entity; + +#[derive(Shrinkwrap)] +#[shrinkwrap(mutable)] +pub struct ColliderComponent(pub Collider); + +impl Component for ColliderComponent { + type Storage = FlaggedStorage>; +} + +#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Shrinkwrap)] +#[shrinkwrap(mutable)] +pub struct ColliderInsertionRes(pub Vec); + +#[derive(Shrinkwrap)] +#[shrinkwrap(mutable)] +pub struct ColliderRemovalRes( + pub Vec<(ColliderHandleType, ColliderRemovalData)>, +); + +impl Default for ColliderRemovalRes { + fn default() -> Self { + Self(Vec::new()) + } +} + +#[derive(Debug, Shrinkwrap)] +#[shrinkwrap(mutable)] +pub struct ColliderReaderRes(pub ReaderId); + +pub struct ColliderSet<'f, N: RealField> { + entities: Fetch<'f, EntitiesRes>, + storage: WriteStorage<'f, ColliderComponent>, + insertions: FetchMut<'f, ColliderInsertionRes>, + removals: FetchMut<'f, ColliderRemovalRes>, +} + +impl<'f, N: RealField> SystemData<'f> for ColliderSet<'f, N> { + fn setup(world: &mut World) { + // Setup storage for collider component. + world + .entry::>>() + .or_insert_with(|| { + MaskedStorage::new( + < as Component>::Storage as TryDefault>::unwrap_default(), + ) + }); + world + .fetch_mut::>() + .register(&*world.fetch::>>()); + + // Setup resources for insertion and removal buffers. + world + .entry::() + .or_insert_with(|| ColliderInsertionRes::default()); + world + .entry::>() + .or_insert_with(|| ColliderRemovalRes::::default()); + + // Setup ComponentEvent reader resource. + if !world.has_value::() { + let reader = world + .write_storage::>() + .register_reader(); + world.insert(ColliderReaderRes(reader)); + } + } + + fn fetch(world: &'f World) -> Self { + let entities = world.read_resource::(); + let storage = world.write_storage::>(); + let mut reader = world.write_resource::(); + let mut insertions = world.write_resource::(); + let mut removals = world.write_resource::>(); + + for event in storage.channel().read(&mut reader) { + match event { + ComponentEvent::Removed(index) => { + // Don't panic please! (I'm asking the computer) + unsafe { + // Ok this can be even more incredibly wrong than what we did in body_set. + // Only one way to find out + // (that isn't actually investigating the code underneath) + if let Some(removal_data) = UnprotectedStorage::>::get( + storage.unprotected_storage(), + *index, + ) + .removal_data() + { + removals.push((entities.entity(*index), removal_data)); + } + } + } + ComponentEvent::Inserted(index) => { + insertions.push(entities.entity(*index)); + } + _ => {} + } + } + + Self { + entities, + storage, + insertions, + removals, + } + } + + fn reads() -> Vec { + vec![ResourceId::new::()] + } + + fn writes() -> Vec { + vec![ + ResourceId::new::>>(), + ResourceId::new::(), + ResourceId::new::(), + ResourceId::new::>(), + ] + } +} + +impl<'f, N: RealField> CollisionObjectSet for ColliderSet<'f, N> { + type CollisionObject = Collider; + type CollisionObjectHandle = ColliderHandleType; + + fn collision_object( + &self, + handle: Self::CollisionObjectHandle, + ) -> Option<&Self::CollisionObject> { + self.storage.get(handle).map(|x| &x.0) + } + + fn foreach(&self, mut f: impl FnMut(Self::CollisionObjectHandle, &Self::CollisionObject)) { + for (handle, collider) in (&self.entities, &self.storage).join() { + f(handle, &collider.0); + } + } +} + +impl<'f, N: RealField> NColliderSet for ColliderSet<'f, N> { + type Handle = ColliderHandleType; + + fn get(&self, handle: Self::Handle) -> Option<&Collider> { + self.storage.get(handle).map(|x| &x.0) + } + + fn get_mut(&mut self, handle: Self::Handle) -> Option<&mut Collider> { + self.storage.get_mut(handle).map(|x| &mut x.0) + } + + fn contains(&self, handle: Self::Handle) -> bool { + self.storage.contains(handle) + } + + fn foreach(&self, mut f: impl FnMut(Self::Handle, &Collider)) { + for (handle, collider) in (&self.entities, &self.storage).join() { + f(handle, &collider.0); + } + } + + fn foreach_mut(&mut self, mut f: impl FnMut(Self::Handle, &mut Collider)) { + for (handle, collider) in (&self.entities, &mut self.storage).join() { + f(handle, &mut collider.0); + } + } + + fn pop_insertion_event(&mut self) -> Option { + self.insertions.pop() + } + + fn pop_removal_event( + &mut self, + ) -> Option<(Self::Handle, ColliderRemovalData)> { + self.removals.pop() + } + + fn remove( + &mut self, + to_remove: Self::Handle, + ) -> Option<&mut ColliderRemovalData> { + let collider = self.storage.remove(to_remove)?; + if let Some(removal_data) = collider.removal_data() { + self.removals.push((to_remove, removal_data)); + self.removals.last_mut().map(|r| &mut r.1) + } else { + None + } + } +} diff --git a/src/world/mod.rs b/src/world/mod.rs new file mode 100644 index 0000000..8897ebf --- /dev/null +++ b/src/world/mod.rs @@ -0,0 +1,33 @@ +use crate::nphysics::{ + force_generator::DefaultForceGeneratorSet, + joint::DefaultJointConstraintSet, + world::{GeometricalWorld, MechanicalWorld}, +}; + +pub(crate) mod body_set; +pub(crate) mod collider_set; + +pub use body_set::{BodyComponent, BodyHandleType, BodySet}; +pub use collider_set::{ColliderComponent, ColliderHandleType, ColliderSet}; + +/// This is an alias for the nphysics mechanical world type stored in the specs +/// world. You can fetch this type from the world with +/// `ReadExpect<'_, MechanicalWorldRes>` or +/// `WriteExpect<'_, MechanicalWorldRes>`. +pub type MechanicalWorldRes = MechanicalWorld; + +/// This is an alias for the nphysics geometrical world type stored in the specs +/// world. You can fetch this type from the world with +/// `ReadExpect<'_, MechanicalWorldRes>` or +/// `WriteExpect<'_, MechanicalWorldRes>`. +pub type GeometricalWorldRes = GeometricalWorld; + +// TODO: Can probably make the JointConstraintSet a Storage. +pub type JointConstraintSetRes = DefaultJointConstraintSet; + +// TODO: Can probably make ForceGeneratorSet a Storage +// Although the usefulness may be somewhat limited? +// Investigating batch dispatch in relation to modifications in nphysics for +// execution of force generators seems a possible path forward. +// Do note, force generators may be executed in *substeps* +pub type ForceGeneratorSetRes = DefaultForceGeneratorSet; From 37d0663e02138aa3683e517226a4c5e68109ae34 Mon Sep 17 00:00:00 2001 From: Rob Gries Date: Tue, 12 Nov 2019 00:08:00 -0500 Subject: [PATCH 3/7] Added amethyst example with physics for cubes and the camera --- Cargo.toml | 2 +- examples/amethyst.rs | 0 examples/amethyst/assets/display.ron | 3 + examples/amethyst/assets/input_config.ron | 19 ++ examples/amethyst/assets/loading.ron | 18 ++ examples/amethyst/assets/mesh/cube.obj | 39 ++++ examples/amethyst/assets/mesh/rectangle.obj | 15 ++ .../amethyst/assets/prefab/renderable.ron | 166 ++++++++++++++ examples/amethyst/assets/texture/logo.png | Bin 0 -> 13943 bytes examples/amethyst/assets/ui/loading.ron | 18 ++ examples/amethyst/main.rs | 143 ++++++++++++ examples/amethyst/prefab.rs | 210 ++++++++++++++++++ examples/amethyst/systems.rs | 110 +++++++++ 13 files changed, 742 insertions(+), 1 deletion(-) delete mode 100644 examples/amethyst.rs create mode 100644 examples/amethyst/assets/display.ron create mode 100644 examples/amethyst/assets/input_config.ron create mode 100644 examples/amethyst/assets/loading.ron create mode 100644 examples/amethyst/assets/mesh/cube.obj create mode 100644 examples/amethyst/assets/mesh/rectangle.obj create mode 100644 examples/amethyst/assets/prefab/renderable.ron create mode 100644 examples/amethyst/assets/texture/logo.png create mode 100644 examples/amethyst/assets/ui/loading.ron create mode 100644 examples/amethyst/main.rs create mode 100644 examples/amethyst/prefab.rs create mode 100644 examples/amethyst/systems.rs diff --git a/Cargo.toml b/Cargo.toml index 2909431..b790a91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,4 +43,4 @@ features = ["vulkan"] [dev-dependencies] simple_logger = "1.3" approx = "0.3" - +serde = { version = "1.0", features = ["derive"] } diff --git a/examples/amethyst.rs b/examples/amethyst.rs deleted file mode 100644 index e69de29..0000000 diff --git a/examples/amethyst/assets/display.ron b/examples/amethyst/assets/display.ron new file mode 100644 index 0000000..2e91a88 --- /dev/null +++ b/examples/amethyst/assets/display.ron @@ -0,0 +1,3 @@ +( + title: "amethyst_example", +) diff --git a/examples/amethyst/assets/input_config.ron b/examples/amethyst/assets/input_config.ron new file mode 100644 index 0000000..eafbf4d --- /dev/null +++ b/examples/amethyst/assets/input_config.ron @@ -0,0 +1,19 @@ +( + axes: { + "move_x": Emulated( + pos: Key(D), + neg: Key(A), + ), + "move_y": Emulated( + pos: Key(E), + neg: Key(Q), + ), + "move_z": Emulated( + pos: Key(S), + neg: Key(W), + ), + }, + actions: { + + }, +) diff --git a/examples/amethyst/assets/loading.ron b/examples/amethyst/assets/loading.ron new file mode 100644 index 0000000..437c545 --- /dev/null +++ b/examples/amethyst/assets/loading.ron @@ -0,0 +1,18 @@ +#![enable(implicit_some)] +Label( + transform: ( + id: "loading", + anchor: Middle, + x: 0., + y: 0., + width: 200., + height: 50., + transparent: true, + ), + text: ( + text: "Loading", + font_size: 25., + color: (1., 1., 1., 1.), + font: File("font/square.ttf", ("TTF", ())), + ), +) diff --git a/examples/amethyst/assets/mesh/cube.obj b/examples/amethyst/assets/mesh/cube.obj new file mode 100644 index 0000000..579e47a --- /dev/null +++ b/examples/amethyst/assets/mesh/cube.obj @@ -0,0 +1,39 @@ +v -1.000000 -1.000000 1.000000 +v 1.000000 -1.000000 1.000000 +v -1.000000 1.000000 1.000000 +v 1.000000 1.000000 1.000000 +v -1.000000 1.000000 -1.000000 +v 1.000000 1.000000 -1.000000 +v -1.000000 -1.000000 -1.000000 +v 1.000000 -1.000000 -1.000000 + +vt 0.000000 0.000000 +vt 1.000000 0.000000 +vt 0.000000 1.000000 +vt 1.000000 1.000000 + +vn 0.000000 0.000000 1.000000 +vn 0.000000 1.000000 0.000000 +vn 0.000000 0.000000 -1.000000 +vn 0.000000 -1.000000 0.000000 +vn 1.000000 0.000000 0.000000 +vn -1.000000 0.000000 0.000000 + +s 1 +f 1/1/1 2/2/1 3/3/1 +f 3/3/1 2/2/1 4/4/1 +s 2 +f 3/1/2 4/2/2 5/3/2 +f 5/3/2 4/2/2 6/4/2 +s 3 +f 5/4/3 6/3/3 7/2/3 +f 7/2/3 6/3/3 8/1/3 +s 4 +f 7/1/4 8/2/4 1/3/4 +f 1/3/4 8/2/4 2/4/4 +s 5 +f 2/1/5 8/2/5 4/3/5 +f 4/3/5 8/2/5 6/4/5 +s 6 +f 7/1/6 1/2/6 5/3/6 +f 5/3/6 1/2/6 3/4/6 diff --git a/examples/amethyst/assets/mesh/rectangle.obj b/examples/amethyst/assets/mesh/rectangle.obj new file mode 100644 index 0000000..70c206e --- /dev/null +++ b/examples/amethyst/assets/mesh/rectangle.obj @@ -0,0 +1,15 @@ +v -1.000000 -1.000000 0.000000 +v 1.000000 -1.000000 0.000000 +v -1.000000 1.000000 0.000000 +v 1.000000 1.000000 0.000000 + +vt 0.000000 0.000000 +vt 1.000000 0.000000 +vt 0.000000 1.000000 +vt 1.000000 1.000000 + +vn 0.000000 0.000000 1.000000 + +s 1 +f 1/1/1 2/2/1 3/3/1 +f 3/3/1 2/2/1 4/4/1 diff --git a/examples/amethyst/assets/prefab/renderable.ron b/examples/amethyst/assets/prefab/renderable.ron new file mode 100644 index 0000000..27cc42a --- /dev/null +++ b/examples/amethyst/assets/prefab/renderable.ron @@ -0,0 +1,166 @@ +#![enable(implicit_some)] +/*! + @import /amethyst_assets/src/prefab/mod.rs#Prefab + @import ../../renderable/main.rs#MyPrefabData + Prefab +*/ + +Prefab ( + entities: [ + ( + data: ( + physics: ( + transform: ( + translation: (5.0, 1.0, 5.0), + ), + ), + graphics: ( + mesh: Asset(File("mesh/lid.obj", ("OBJ", ()))), + material: ( + albedo: Generate(Srgba(0.0, 0.0, 1.0, 1.0)), + ), + ), + ), + ), + ( + data: ( + physics: ( + transform: ( + translation: (5.0, 1.0, 5.0), + ), + ), + graphics: ( + mesh: Asset(File("mesh/teapot.obj", ("OBJ", ()))), + material: ( + albedo: Generate(Srgba(0.0, 1.0, 0.0, 1.0)), + ), + ), + ), + ), + ( + data: ( + physics: ( + collider: Cuboid((2., 2., 2.)), + mass: 1.0, + transform: ( + translation: (5.0, 5.0, -5.0), + scale: (2.0, 2.0, 2.0), + ), + ), + graphics: ( + mesh: Asset(File("mesh/cube.obj", ("OBJ", ()))), + material: ( + albedo: File("texture/logo.png", ( + "IMAGE", ( + sampler_info: ( + min_filter: Linear, + mag_filter: Linear, + mip_filter: Linear, + wrap_mode: (Tile, Tile, Tile), + lod_bias: (0), + lod_range: ( + start: ( 0 ), + end: ( 8000 ), + ), + comparison: None, + border: (0), + anisotropic: On(8), + normalized: true, + ), + ) + )), + ), + ), + ), + ), + ( + data: ( + physics: ( + transform: ( + translation: (-5.0, 0.0, -5.0), + scale: (2.0, 2.0, 2.0), + rotation: (-0.707, 0, 0, 0.707), + ), + ), + graphics: ( + mesh: Asset(File("mesh/cone.obj", ("OBJ", ()))), + material: ( + albedo: Generate(Srgba(1.0, 1.0, 1.0, 1.0)), + ), + ), + ), + ), + ( + data: ( + physics: ( + collider: Cuboid((2., 2., 2.)), + mass: 1.0, + transform: ( + translation: (-5., 5., 5.), + scale: (2.0, 2.0, 2.0), + ), + ), + graphics: ( + mesh: Asset(File("mesh/cube.obj", ("OBJ", ()))), + material: ( + albedo: Generate(Srgba(1.0, 0.0, 0.0, 1.0)), + ), + ), + ), + ), + ( + data: ( + physics: ( + transform: ( + translation: (0.0, 1.0, 0.0), + rotation: (0, 0.5, 0.5, 0), + scale: (10.0, 10.0, 10.0), + ), + status: Static, + collider: Cuboid((10., 10.0, 0.01)), + ), + graphics: ( + mesh: Asset(File("mesh/rectangle.obj", ("OBJ", ()))), + material: ( + albedo: Generate(Srgba(1.0, 1.0, 1.0, 1.0)), + ), + ), + ), + ), + ( + data: ( + physics: ( + transform: ( + translation: (0.0, 20.0, 0.0), + ), + ), + light: ( + light: Point(( + intensity: 50.0, + color: (1., 1., 1.), + )), + ), + ), + ), + ( + data: ( + physics: ( + transform: ( + translation: (0.0, 20.0, 20.0), + rotation: (-0.4, 0.0, 0.0, 0.862), + ), + collider: Cuboid((5., 5., 10.)), + ), + camera: ( + active_camera: true, + projection: Perspective( + aspect: 1.3, + fovy: 1.0471975512, + znear: 0.1, + zfar: 2000.0, + ), + ), + ), + ), + ], +) diff --git a/examples/amethyst/assets/texture/logo.png b/examples/amethyst/assets/texture/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..560535306b2ae5ea24d26ceec4c089e95dec04f5 GIT binary patch literal 13943 zcmZv@2RK~O*Dii$3^V!!(aVhJ5xv(zBtoJK(L2#wln`cgq9sI>U_|c(ksyp{(Gn3| zL>DAcBRco^efK{1e&7G^c|3E@KIg2xc3W$`Yri{ASNk?488aCG0F-w$Zt4R71iXX* z2omte+^5tL{DJwZ-Z4age<29_Sn!_IOXGnr0Qj?B{zFPW25x|bOn&Mneg>Y7egQT< z4nROafY3t^S6@3DF9#t{AE)e11!e$10e5by7zV!jlN0ocuKVahdDeGx@N=$U?EvL# z0$D7oDcp(EC85VG)!CLRG3(~NVI&c=!VqUBZr%ZJP5aK0Ho zK=VcLsiRg@;r;G!#$g7YQMtm$M`_bx{%+F`N z*QcMO%&PtO1h!f^)5m(ECMRX{Ske_!^Rl4oMY{HxR8iH-H}_nXDhy3;@avk<))Ymn zV8rEKm><_sKpQ0S69XuGMcm7Cdt^$9HFEfd#;uCnUFxH znTLNlAa%!X0>A^x2&|)@!27>;OfcumnpCi*aV1riZMH!o5x_lC3TS!v*Kw?+a0He! zed2yE3Z>J5!keYs0-y5tXdt%Zy~rdfpjltbmPe8ukoYRd2TKy%xpJVbOcXN-WN6J0 zY?c^e_&*{5;3F3*0?6SRYoLJAtyJ*ZBk`I%=4`KqFEb^uw{Nmm>>BF!V!t;wkLwNx+GM~kwAjTNFcM}65&iR4bjYK z>sALRNy;q`Z8C0{VA_ zR$mN(^s$FP)3jAyF#mTIK8e8ey~xV%_Wid$xlMw$hQqr%79vqbl&tGq2Wfr+cCinQ z=t@Viuj#xaCDPB2X&*YslKAPQ^6R&p2i8^)qR~cDhK8~t_K`$IYJd0y8avFo3TnHe zKiaae3Q@7=Q3BQhKy|pxMFftKbKzZEZZ)5(3S{<=o?k%M#V?nqud77Ei#u&3#-hK5 z$b?&?Zk67js}XGE_YslJ|F>6Cj?;9%^Cg}LmSy^Iebm!n`tka7gT;+>6~Zf|mz=EO zJuln8&x8^bKE^$xUAj2dbWdwEim$-NRG`kAnqoa1-qlOSXDO<%PSf5T+C)-qlD+o# zMCPLiC}04CZF&58LN!c8h1@+V-@iqFq%0wStW1ySc$e5g@NA!VgshN2_lU=U?!}Bq z^>kv~x7iGt0$_=!#X6vPq--$vW&w1AI=ar)0BDgqu>RQtWVv?6`(-*CEL;1i{`sk6 z^xw?LhI=zL@Y>v3jjk}u zb;7xDup~Ic3R$4vzZ^dXD4?cSA|kNghzI36W7L1r+YEBKCa;U%IBEag4Hs^*`(u`K zwnwaQgVf-K9YZkMgQ%1Q4K`?-J_~*5KuoHzz^yyogqQ2PdU7+b54ZQJT2IVbrn5NT zn2_N7R4`D$$Cdp z2LM^o2#lC2C|`_4yQP7##N2$)@>D{LOK`p*%x}K&w&_hpC<(4Z6^r;U$&nJ{23<>o z*R|>nTRHzNI+=izbnC-#v^2YCRJ%AC_^Jx#Dqzn>0qsLTS`#}z);~G?XH*m+*6r}T z*)QPc&UC?zz^xX+5 zmQ&C^vdT@=kOOa$PNT3jgVB3omeE04WM#WzeMxSI0SxzNA0#KeEfssNf|0y&75F5# zOAW2p-dP+eTHzD4nSCN2-!2}{u<7{`Ui0?)+%*!&Yp)694M-y$t*Q?zl>4ew$VEDt z;g{>fcYC{7-#FxOBv+RjOjC;>__(-B@a1jVZ4R%ex2{bMc%n1k5}vkL zvBd<<2|m9i@lEt8`n{Xg8mCes}xb_OY0t!c2*2)&p*OqQJL90I=P0{ZHV( z+*|98*T!krUbZTeFs>A9z! zuI&8=)6U1xGuv#%-Q%mwcOHR?3sT|iw6_Q{U9`sRNtd}PeW zgKhqKQZKgUP3+4bz_1bWswNlr83b<06HYpT$!&E#h< zQG2y|O}nQhD)}dD(H9^~Th`IR2gXuLE0{iPxxQb<0K#~yKI>4ddmnI9IYnxLs~+8v3cRki+Y z&)^YaFVi1wR<9+9Ae7HZ+Z?PeyD3w`0P6#W4Rcp} zGRB>jp2Z+_tLcA2U_76_mR?d*Ml?+}KV1=4o($iwXrWtZK*rvJRfaZG4hS7GTZPhV z1fNxMf@WV@S&VF~{v;Y3L;}g(?CxhnfHRTsh?*i|#&!1PMZtvJPL0#_lJb9iNfuR< z_m{E8z>pxKX8DHID?Y~G+1aI)L}K96RfB*<>?%>$< zp%nsj#JhJa-d~IAmO9<|a!pT+t0^(caE_uvjj*!dwf{j)@BYJ~N1>>;@r&hnp5Ma{ ziU%ZMwtA3@^I-XOD<4E~)9M$Y&UgX1$Gb4DR~~)E528$%pjmmhDpavQaU4G^abM{h zx3xa~qHeV9Y+rL+5vPOz&WCad0eP+^^B+12HG>9D$}`YPa&x)>@3g`aKuYIv7QgcEGz6=yyVd*Iq|kl1YEY9H061OLvexTdk&W%Ug4piI8`7l`C4>`dMXAi& zUcd7Dkp6615v|Z%c||l+FZTfoNk!^hHqdSf^g@*1H_}||SxI+7XY$uRLeC4#Z>P4z zm`ryyH7bRlR9D=_%Z~@tl_{{5f$lcu;OE2}Yt`?T>=U!%oU;4~6Lu)fz}i)H{`8lY zXh>zmo11%g2ppXe6Vmt(cmA$R^TT$%*-_|gyzji$_tlDU^wHRO{@O;A*I*p`;6-|) z&c@WSH?@mDZCfQzB*r55A3)>NPs}ajUsw6z0UeaNm@tmT>>yc$KDJB?gJ(U2iQ{aBw5my@xOK5}b zJF$_dGqo=p+SdZj(B3Hq5^Oqi> zbxl4ly>iv!Z_Ko6<(BGusN@)fIfF=10N5=dw&+Fuk2ZDmIBc|muCLmBILm;piOX4a zm}($2Q|Dvhi4m8vsjsT_E~~U-uFE~?AEx?-)u+!hu0oI6 zXi(`xmDQf*ybsXW(jyR1-2jm%o!HJ3)puv{%nDC}X&(pMZU^*iPi}48 zXxyuX|;PMM@~c=s*!= zDD`iIZ8vw!_6Skmwf^NBJBjTLwg-;C2H`Rmte;y|-LgD95XgHvVBd@^NP*MN*dRcaoKki zB4p`Ay;k1m$Sf_-N-%EuyC|Zs~Dv-*?*l<-+uAwr{5qP@`C@-9%eprFGN=PpHA%1Xa3J3BH}Pk8^}G$ zW35Mx!D{c$c1>HqAB=HKY7rKuTC#QPc{Q}1#k`w?UWKTTqI=Ad!zKpe>`*G+n939ShaI$@255D zJ@OuB)4-MMUPJ71Ic}JFi036_FZN8{5n`Pzqf%R>9eW@8A#(bQ@{z#|oyjwUYwlUW z(4*Q-9h$zBUVBB{YcZB;OIZrzS;@P*yX2~;744@VnfnIv;-p?6{(*~O&67pYMxb?# zwaOB(w%56Yt^S|ysuCU$Y*x$0m8M$%&5rLOG6^q~ z6%(8`-BfFhkE9%FpjPe;(Pi7=;_-z}q{wbY#F`B8*!@g?#9b>x>vbX}l51UO0EsHW z*rn4bfVxv>io)z!KbLwqj_y^sOT}FVSn&;kPvk9Ok~{0~ z;a|jHnlU`T>cP-P!PiOrF=-MeA)0XUW2+~o#(%>_S`!4+;`CSbmAFx6NR6-jrEmNF zF!LF-d%nBEXetKY%nK13XltTc-P&IkMQX)cYV+@dv%CWHf^uH42e_ODdW)-Ir8jWT zQ2#-n;$bZpq|bC<%*42n{E>PxQ+{(lCQ_Vk4K~I@tfp-HtFJ<>d|ZHrnix4GN)6el zFoh*t5jXd0-fKF^Lq0#Mzk=jrmY=GIB6T$7(MT-xHXf}(MmcVRP^f#|k!BfHBCI=2#7GJW=dTr|uC$M(RJ)x$ zQ!Q`C$p&SFNfkr^ShNiU3`er45-ruE-`y` zbSy(y-%!X(Sc(K%q?StAST7szd{g&cMUC`{`$k0~WOP$8FmBb3OaHux*jrnD|&=&`^<5tyN;&Sg%(mr+#o;@^2i+(sw>Ood~= zb$48!-koWOTB8xHieFv@YxZdSbSx}elOUXTMa?dwO6*Xsrg*>m&Wj3ce+WyqJHxRKO{=AjEh3@j}m%aik9^A$DCyY!%D$e+| z)C-G8j**zLz>;-p@;e&ch~4kTQfSmJnHvQx{yHO|@mhuh_{0qBx1YhzFcr1Q9(gmTRYIo?T%UnZg43dIU9m`bh{@{JAMf=Z;m}zg}S$ zBq}g(Pza{bO#yJl%|M2Qe-_e0eT=Ooa96e<91V}Y#17pghz7*#P+2&HT4Zwz4|8o0S2 zv$M_Df6^YBa%+{2lKKVZF5#P?3TlBjCr&>NTsK6J9Rck3tM4A5FVlvd zJthN%3f|E4XUJhG;?Gdn?{Skik~cL@AYzn|q|{9!RN}=D0#`;}xxWvm0YIa0Tufk! z#@U$lo@AE^QLL z^~vgMaQ^+3z?V9jiem`n;0piI$*D-{^g{8TX^^jH+| zhs+7T3M!yL9r)Drf~T;|AN3=>NSTBjGUi)aPmCLvlM66766lk~4ys`k;PalzoUB|o zzGe|Y$LKB3wKNReO(ccPki+J7{Uk6`YH`3h&QDF(5v?mw57C`?C)7jtL?CcGC|eiX52KGX?S37s-lj$kvvq!^RBAur3Nl8f_2Qv)?n-d_&`)%R_J4}W9oeEaCb zX(W8Ha|4=BZK_Cp7o)9(j5-bMzv5WT&bo?eZ&%ZI& zyKVvP(iYdXhBi}{oAi(=)^mJ_(XFQ_W{8{YBKloDcl-^Gp7z~fDdH>KH%e$x!(%kv zsqO9^kHV@`CYO!xd08La8eveQ7egS@D2&!285Gbl->SK@%?w5IgZD*Q+HuJfs8EZu)EhRhHC)p8!`{(|z!qgsy4Ne)q zv_9y;7nY=XAY!Y4D8}I*g-;;2RIKB>|6D*WVM?Ttf{gVVLW9882(Dv zeer|RdM^v0@}O2h>W5ehAP?7yJQ8w$cfI1a1hDnk&g4zz@RqP)EXMe*;t2|+%gGG& zdwc6jBA0=2Lc>zw)A-u+A&-y`;zuj}Qn2V`Z@g{feoI{N+rK>*x*yJet6+6_^f&ON zcO8D{Y5hE}8qbEy{54|qJbh0De;bAROZ{*(YBRTo$nsqUrPY>3o7>fb--jK4R><~@ z$9PMQD?f31!}K{IaO=NZyh2u@uyCJF<+j<{n&*wF zM%k3p{KscMLmoGpZrQi_~BI99c6en8}+;LE6b7%rjKaDL8na$GUPf@ehq)NCT zhD>I!LAbgwh~ZEjKO#6e?sGaLGqtI!?6Bick-dh%fJLnpP6Efv{DRqam4$myAy_i?+1Ij99&!)fPNuWB(V|@Y69Ywm(|a4pr+L zu^$b&#v>rbmc$B{2F%vxVm=91z0*@m{?d~$*SF}w?v4}*EBW0ZsL{Y@ZKm= zm$7ZW?uz_Gd~#up8gGK)_vAnj*2-yq+X&2-l9Er>e|`J``(SSSuTB~_*DMl2jO~3X zrIijTxgkL4dnwTVWB0eVbj;G1{e=Kw!-Z*h%;H*9|I4xh*?4Jom);?#QQNV2`caOQ z>i&jN_3h(}xM)rVucAM(uB6(7x$8An3SSJCH~Lp;KNqa+5N|A= z!_;luW4CL*0yAnx4;#{Urw*7RY5(LZoovwDMW;~$>O~5l-@RdM5BosIaMnzxM!HAR z82s_@gu}G%^i^k|lM(MJv$m;nXc3E5XjX_)XOGNwRbE=DC8l}WPza0(=i4vRJrBje zjAIGjuUw26=gx6py4!#UQxSB+9rI|gzfkv;e>_WA#SM5~za3<>yTUFvowQdi+~Hnh zY}5Bb{W;Tl0m@CethtphY5%iM6yz*q^kruyY*Yi~Q8)b4r?PR|=NxpO?h;Qf~>~ z-Vc!u^$#QV2+F5tEBra?jJpI%Qyf)p+YAHF5uQ z@xKqFu|uD2cD=`~&S!SLsiB3w^biooQU0f;5S*|QVvScUbCQkK@zuydQNIWb1_^Xj zzrJz6+q+)CE9*V>;cA9LZ&d~(gyx=%Tb8~&C<;~E62;a)@yw5 z&uwz=1JF6{E*~Wryc;`RcBfkG!>I3wl0fCORn`rQSfDdXWaW9;j<%T}ZZ{ur>jp~suSl|lpF1)9>nbIe)zNItiQ9H&3UQGza;vNl3Ue`WwrV*Njb-MO zr?hKHHUp;0>H}YtEUt9gTVs`l9 zKuwnSucJpMd#c8HOIIXwh$1k-26Qz_C^oW?t)>;PtFb;s`S%{2{pA3_ohsvt-M3RH z%sO3KHP3n^q2!|WhX?>T4!A+Ze$XN`f`R1O(S(y~m#*<-BBV1_-qn~w1a9Bc-|mFB z@27-)B->AO1A%)Iu~J5cj-90iub3rkqeK2S(~UTWW5~enaCt6-7~Lc*@PE1QkN9k< zDpSiD0m6}I%qZ4r6=_@+6{0H!TJ=1iKsvX>uk)MDIy+0O3G2LO&pmbFfQ<1^%?WeX z@<(BEEksFSN1!FiqkM598cWopuq zqR*nK7LjUtmE2p(bGX{p15YtT?0@7m3LltwM5KTY_Al*HqVl&(lyv%hskojTm$b@f z*=#IF_s;D>(07@5Rh&ZU)vTc{pN#sh*qy2%ja+Iqsm-q?6HdB*I|FVvp| zVPxvQz&X{C7k>-#(5=zRHs3_wrt|$oI@Q{7RonT+4olA3CX%DZQ5%5-m4M@@bN8>k zUQ{2e6kA{{=~7ZQKyJt1hcwUFMG8(u5$Xfp;wx@U(LV6Xw4*u?9ve{ z)lJ4@f?iNA6>|$tjZj0PC^0Q=R0Iq{AuJ$!dGg@B(__&`Hjuw4g7>)P#l?I#mEZGM zq48aJk;Ai&4m^m~rhc7_HL)g**(eZ|?LIjlF#;rWz&$5pGrBK7ra0r;msj7kp`XgZ z%6Vk;^lgUD!;D?&Je7;i_Zgl(Kw%ocj0hqTe%;%jmmbdu3~`HV3#A0r-{XKSI}&DH z?qf+*tOssGzzyvw-TC8@^R&XE3f&jST;@F$>SR&d>nw_;^~2ta z9>OsN$uFfh{$eoh2y0Nq+BZfc+_}EZ485<`)^+&5c#Q2_cH; zg+1P$3phPnP+yTU546kV;)c(cKJb{#DQlHmP1*N+v{aF(A26qTIznG?X@veGven<} zrG5I;;Kv=p;J4A{w?r}6KpK%@HnlWsK?iz-Li?M0KFf!d-xM;upZkT6#?M}C{^PR@ z#Y>(?{JOAe&tUtnw2nAE6qm&K83SYJNIhBTSnBcX=OFrlHqws7}5RD2*kIT&p5SwDhY1#9g4-u5b@HSl(fLkLL< zFH!e%W>}XRw)$;`X?2>PBQsR^$G7NzJ1I&ztLIcG*{jk#AQ!uW7Mdp(<|@tx&{p-`gDIYQ52>j6xGdVVg`GVO-M)dntsMLQEMsd9B}=*XiF}vV>eWMXQT& zK*85coSAyPrM!s-HhK?tgADh_HRA9~ePj_6ZJ+m>F)R*)siW2XMmEfdnEcJUD!8+l z-fp~Da$smZ`fNOShjw?I_RnNP@$C;516=~h+NWUxO0QVqBR|tJ(oJA+T-tLy#-c+3L(%O=yM1EYW1#e~o+@h5$JV91I$ zHg1XNY^`sV*$-Aqqihfx9PXi93Y?w;dn@eImmT<%M0e4Ot$~1Rt6T)Fq}6_@LHZn z$cG2I1f?!!G@7rI4rfBmkkAD@wj}*+LDBsg`KNkfzp~l4BM&0zp|)07RSJY(`4Q2e z0?eG4mELNtv#Q2uaWPxdNJ{B}P8Rj;?VnasoTZELLJTO3Cz+cH;&8?;(B)yvCygG3 z#p_GnqizS-UQhPAE4s#a-&~b6L-K9!nCy0Po}MQPygC@5p`-3gWZ;O^DOWmt8eJ$% zG`8yMj&PO%Ggzk7P)ayw;Ayn~-{YYs36U*9YBwqm3=*jWr8$7uBcV~;3ZIW3ih2Zp zpwS97tjtKRZH6`51rj*b7`WIYWgg^N_V@I0gu@1> z^k!%~jXR~@=oWQBXc&z0*GM>I?*6yN1zl^CzLS1%EtoW0c+6`i9 zg#{vKvdx5_sDdd<_U8O6R$CN$bKe4MC3#66IX(0thQi-Q?+m{2H{ZF?m4?yRX@XtxcEowH7O@_ z26v8BjWi!foL5}Rw_NS~Uf-Iolpk{+IxBj5^fGbsL|vCI%QRb`ckn3Tg}|_d3lT8ONC#YI9%JHj{G)WoYt`#jo|(NweF+8lfS;afrs(Ct1Y_ioK+NvPI0m4So!)A~>`-k_W`IhJccTFZYZN#?bjU!SBn&k9XorgkW0~;h&{;JLM=D){wfxnA+T92Gt z{Oojc)iB+X_d-=c27CxjSA?>QN@wU^3xl01?$~*7 zO?1TQY;12$HA`VAPMbTR+fq5WM|yw(4y2E6%XF^N1W5 z3Y0opBT*!5qTg`!HP=;FV82d2)V~@1O}4ahGF=;N=NbRSDpQODje=lZK$fyCvX?v~ z$Q1JVl!=Aj-VEGOs0@8W%h5JE_LHh=3oVM^vu%_R0PtBj6%4lqMg^Um+WxaU+*h&q zK-~HfDKRYM(1+oN#aL9X$NTFN|HT811$2i9C_D|_MHuMg)fs~8wiT#Zg-F8vG$jNt zkwM_ej<%nKtNT1iIlZ-}2C_HH`P0-cb<@x#$Tlbhbtlxgl1C=+ZM2>=1;m?dpt!lG6yU-c*9Jfesf)o=O6+ZKDX zUN(QSOm_H)!e7m&!YKjH<<>}zJ0)TtHzbp@y+AD&Q zq`g)l$3JUmz^`KOozg%s2E-8rvx$G4tfR+Nq6p_vkVX%My_*wqLAQEPKwU)FBQdJxmQ-Wpvl(wEqvGn#i{)Cm za%dT*@-_rK6{%r{F(Fw@%e}{YuZEWeBY~qf6VigIdoRB&0i%cPZP6#MWi9rF$-<1Q9 z4*Y+G#KL)08)o}#4ng0ZTJb zCu{WX?rzYAJ}faJw?___LY^E!7o0`3pQ$jtW-34wDhl4w;=LV-eLS?h{w9nfQ!KXn z?X;ZiO;xB%8`;5^blwp;bJl(VcfN{*JaH2Vkm;_^{)3Yp47h&SvQsg@n_d$25=qHV zC{G|M5Fj@ap@(m}jVUz-gF%Y(q{A=}Q~61#e>G)L*Rp_B#au(g7+R{%2A4nKaGs$% zV;c+f;a3bRx^G+2ub(|q3Q_0pL&#f1F~LS{6R^PgBQp326|EwEb3hk$s(xHXBY|x$ zCl*EMjwWb!SflZabZOHQagQnX1Pf^-5W*s?vLI*Q$fwZnta+O6k;_ZWOz~gIhwICR zKvMkBZ#r!iCKw}( zq%u*THv)VAE)N=GDQ}C^Pyz3fq%?Ct2DoTuI8c+7rh*~{@8tfENGP<`k(&{wKir4H z7yR#!o6&?{KLkl|bbexpHRpm+@X^&&&;&1jr+`vGyTAhsek&EulH#yHZ?Pdw5kw(A zRRwdKrAV6uQkCI{!M^1ERtby1kjXK@{2KH&z=4cs5`-ZJu;3{cNl@kMdD{Q`4Oblnk;9G<-S8HIm9L;`8vUH?Ba5HP=14H1O%I93H)9U|wR z23F8us~?4J=Ee(3M*tCkD9GxzI1(^TjK;qG|CB`nHCJvA4uR(jFoPW^jB#81{Q?xY z>0P$!hy*R>>ZN8@>J?Xzx_6=Ac^Xjf|5N7r7L73{2A_|CXEOfxx8vaP8J&DeS_2V; z2mboa?BtIV^)!GXhOfJacOL#|d$VSyT>2kt8)bpLM$f;&YZCyfyt zHVyD_kslcM^PNI2rKQ+1!PdH5kn?fU2`X5n8=`Fgw`JlGkXj)Ufl&aD-Ovr7FueD1 wAfNHS6=1Q^|E<6=1|%o`zi&8##1%N}l?cMs2$leF6CJptu6?sw)jIrt0k?YSg#Z8m literal 0 HcmV?d00001 diff --git a/examples/amethyst/assets/ui/loading.ron b/examples/amethyst/assets/ui/loading.ron new file mode 100644 index 0000000..437c545 --- /dev/null +++ b/examples/amethyst/assets/ui/loading.ron @@ -0,0 +1,18 @@ +#![enable(implicit_some)] +Label( + transform: ( + id: "loading", + anchor: Middle, + x: 0., + y: 0., + width: 200., + height: 50., + transparent: true, + ), + text: ( + text: "Loading", + font_size: 25., + color: (1., 1., 1., 1.), + font: File("font/square.ttf", ("TTF", ())), + ), +) diff --git a/examples/amethyst/main.rs b/examples/amethyst/main.rs new file mode 100644 index 0000000..0e832b4 --- /dev/null +++ b/examples/amethyst/main.rs @@ -0,0 +1,143 @@ +//! Demonstrates renderable objects with physics properties +use amethyst::{ + assets::{ + Completion, Handle, HotReloadBundle, Prefab, PrefabLoader, PrefabLoaderSystemDesc, + ProgressCounter, RonFormat, + }, + controls::{CursorHideSystemDesc, MouseFocusUpdateSystemDesc}, + core::transform::{Transform, TransformBundle}, + input::{ + is_close_requested, is_key_down, InputBundle, StringBindings, + VirtualKeyCode, + }, + prelude::*, + renderer::{ + plugins::{RenderShaded3D, RenderToWindow}, + rendy::mesh::PosNormTex, + types::DefaultBackend, + RenderingBundle, + }, + ui::{RenderUi, UiBundle, UiCreator, UiFinder}, + utils::application_root_dir, + Error, +}; +use specs_physics::{ + nphysics::math::Vector, + systems::PhysicsBundle, +}; + +use crate::{ + prefab::CustomScenePrefab, + systems::{CameraMovementSystemDesc, CameraRotationSystemDesc, CollisionDetectionSystemDesc}, +}; + +type MyPrefabData = CustomScenePrefab>; + +#[derive(Default)] +struct Loading { + progress: ProgressCounter, + prefab: Option>>, +} + +struct Example { + scene: Handle>, +} + +impl SimpleState for Loading { + fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) { + self.prefab = Some(data.world.exec(|loader: PrefabLoader<'_, MyPrefabData>| { + loader.load("prefab/renderable.ron", RonFormat, &mut self.progress) + })); + + data.world.exec(|mut creator: UiCreator<'_>| { + creator.create("ui/loading.ron", &mut self.progress); + }); + } + + fn update(&mut self, data: &mut StateData<'_, GameData<'_, '_>>) -> SimpleTrans { + match self.progress.complete() { + Completion::Failed => { + println!("Failed loading assets: {:?}", self.progress.errors()); + Trans::Quit + } + Completion::Complete => { + println!("Assets loaded, swapping state"); + if let Some(entity) = data + .world + .exec(|finder: UiFinder<'_>| finder.find("loading")) + { + let _ = data.world.delete_entity(entity); + } + Trans::Switch(Box::new(Example { + scene: self.prefab.as_ref().unwrap().clone(), + })) + } + Completion::Loading => Trans::None, + } + } +} + +impl SimpleState for Example { + fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) { + let StateData { world, .. } = data; + + world.create_entity().with(self.scene.clone()).build(); + } + + fn handle_event( + &mut self, + _data: StateData<'_, GameData<'_, '_>>, + event: StateEvent, + ) -> SimpleTrans { + if let StateEvent::Window(event) = &event { + // Exit if user hits Escape or closes the window + if is_close_requested(&event) || is_key_down(&event, VirtualKeyCode::Escape) { + return Trans::Quit; + } + } + Trans::None + } +} + +fn main() -> Result<(), Error> { + amethyst::start_logger(Default::default()); + + let app_root = application_root_dir()?; + + // Add our meshes directory to the asset loader. + let assets_dir = app_root.join("examples").join("amethyst").join("assets"); + + let display_config_path = assets_dir + .join("display.ron"); + + let input_config_path = assets_dir + .join("input_config.ron"); + + let game_data = GameDataBuilder::default() + .with_system_desc(PrefabLoaderSystemDesc::::default(), "", &[]) + .with_bundle(TransformBundle::new())? + .with_bundle(UiBundle::::new())? + .with_bundle(HotReloadBundle::default())? + .with_bundle(InputBundle::::new().with_bindings_from_file(&input_config_path)?)? + .with_system_desc(CameraMovementSystemDesc::new(5.), "", &[]) + .with_system_desc(CameraRotationSystemDesc::new(0.25, 0.25), "", &[]) + .with_system_desc(CursorHideSystemDesc::default(), "", &[]) + .with_system_desc(MouseFocusUpdateSystemDesc::default(), "", &[]) + .with_bundle(PhysicsBundle::::new(Vector::y() * -9.81))? + .with_system_desc(CollisionDetectionSystemDesc::default(), "", &[]) + .with_bundle( + RenderingBundle::::new() + .with_plugin( + RenderToWindow::from_config_path(display_config_path)? + .with_clear([0.34, 0.36, 0.52, 1.0]), + ) + .with_plugin(RenderShaded3D::default()) + .with_plugin(RenderUi::default()), + )?; + let mut game = Application::build(assets_dir, Loading::default())?.build(game_data)?; + game.run(); + Ok(()) +} + +mod prefab; +mod systems; \ No newline at end of file diff --git a/examples/amethyst/prefab.rs b/examples/amethyst/prefab.rs new file mode 100644 index 0000000..cb08373 --- /dev/null +++ b/examples/amethyst/prefab.rs @@ -0,0 +1,210 @@ +use amethyst::{ + assets::{PrefabData, ProgressCounter}, + core::Transform, + derive::PrefabData, + ecs::prelude::*, + error::Error, + renderer::{ + camera::{ActiveCamera, Camera, Projection}, + formats::GraphicsPrefab, + light::LightPrefab, + rendy::mesh::MeshBuilder, + shape::FromShape, + }, + utils::removal::Removal, +}; +use serde::{Deserialize, Serialize}; +use specs_physics::{ + ncollide::shape::{Cuboid, ShapeHandle}, + nphysics::{ + math::{Vector, Velocity}, + object::{BodyPartHandle, BodyStatus, ColliderDesc, RigidBodyDesc} + }, + BodyComponent, + ColliderComponent, +}; +use std::fmt::Debug; + +#[derive(Deserialize, Debug, Serialize, PrefabData)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct CustomScenePrefab +where + R: PartialEq + Debug + Clone + Send + Sync + 'static, + V: FromShape + Into>, +{ + camera: Option, + graphics: Option>, + light: Option, + physics: Option, + removal: Option>, +} + +impl Default for CustomScenePrefab +where + R: PartialEq + Debug + Clone + Send + Sync + 'static, + V: FromShape + Into>, +{ + fn default() -> Self { + CustomScenePrefab { + camera: None, + graphics: None, + light: None, + physics: None, + removal: None, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CustomCameraPrefab { + #[serde(default)] + active_camera: bool, + projection: CameraProjection, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +enum CameraProjection { + Orthographic { + left: f32, + right: f32, + bottom: f32, + top: f32, + znear: f32, + zfar: f32, + }, + Perspective { + aspect: f32, + fovy: f32, + znear: f32, + zfar: f32, + }, +} + +impl<'a> PrefabData<'a> for CustomCameraPrefab { + type SystemData = ( + Write<'a, ActiveCamera>, + WriteStorage<'a, Camera>, + ); + type Result = (); + + fn add_to_entity( + &self, + entity: Entity, + data: &mut Self::SystemData, + _: &[Entity], + _: &[Entity], + ) -> Result<(), Error> { + let (ref mut active_camera, ref mut camera_storage) = data; + camera_storage.insert( + entity, + match self.projection { + CameraProjection::Orthographic { + left, + right, + bottom, + top, + znear, + zfar, + } => Camera::from(Projection::orthographic(left, right, bottom, top, znear, zfar)), + CameraProjection::Perspective { + aspect, + fovy, + znear, + zfar, + } => Camera::from(Projection::perspective(aspect, fovy, znear, zfar)), + }, + )?; + if self.active_camera { + active_camera.entity = Some(entity); + } + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum ColliderShape { + Cuboid((f32, f32, f32)), + None, +} + +impl Default for ColliderShape { + fn default() -> Self { + ColliderShape::None + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum BodyStatusPrefab { + Disabled, + Static, + Dynamic, + Kinematic, +} + +impl Default for BodyStatusPrefab { + fn default() -> Self { + BodyStatusPrefab::Dynamic + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct PhysicsPrefab { + #[serde(default)] + collider: ColliderShape, + #[serde(default)] + mass: f32, + #[serde(default)] + status: BodyStatusPrefab, + #[serde(default)] + transform: Transform, + #[serde(default)] + velocity: [f32; 3], +} + +impl<'a> PrefabData<'a> for PhysicsPrefab { + type SystemData = ( + WriteStorage<'a, BodyComponent>, + WriteStorage<'a, ColliderComponent>, + >::SystemData, + ); + type Result = (); + + fn add_to_entity( + &self, + entity: Entity, + system_data: &mut Self::SystemData, + entities: &[Entity], + children: &[Entity], + ) -> Result<(), Error> { + let (ref mut bodies, ref mut colliders, inner_transform) = system_data; + match self.collider { + ColliderShape::Cuboid((x, y, z)) => { + let shape = ShapeHandle::new(Cuboid::new(Vector::new(x/2., y/2., z/2.))); + colliders.insert(entity, + ColliderComponent( + ColliderDesc::new(shape) + .build(BodyPartHandle(entity, 0)) + ) + )?; + }, + ColliderShape::None => (), + }; + bodies.insert(entity, + BodyComponent::new(RigidBodyDesc::new() + .mass(self.mass) + .position(*self.transform.isometry()) + .status(match self.status { + BodyStatusPrefab::Disabled => BodyStatus::Disabled, + BodyStatusPrefab::Static => BodyStatus::Static, + BodyStatusPrefab::Dynamic => BodyStatus::Dynamic, + BodyStatusPrefab::Kinematic => BodyStatus::Kinematic, + }) + .velocity(Velocity::linear(self.velocity[0], self.velocity[1], self.velocity[2])) + .build() + ))?; + self.transform.add_to_entity(entity, inner_transform, entities, children)?; + Ok(()) + } +} \ No newline at end of file diff --git a/examples/amethyst/systems.rs b/examples/amethyst/systems.rs new file mode 100644 index 0000000..b4f5e86 --- /dev/null +++ b/examples/amethyst/systems.rs @@ -0,0 +1,110 @@ +use amethyst::{ + controls::{HideCursor, WindowFocus}, + core::Transform, + derive::SystemDesc, + ecs::prelude::*, + input::{get_input_axis_simple, InputHandler, StringBindings}, + renderer::ActiveCamera, + shrev::{EventChannel, ReaderId}, + winit::{DeviceEvent, Event}, +}; +use specs_physics::{ + BodyComponent, + nalgebra::Unit, + nphysics::math::{Vector, Velocity}, + world::GeometricalWorldRes, +}; + +#[derive(Default, SystemDesc)] +#[system_desc(name(CollisionDetectionSystemDesc))] +pub struct CollisionDetectionSystem; + +// Here is a system that can perform additional logic when a collision is detected +impl<'a> System<'a> for CollisionDetectionSystem { + type SystemData = ReadExpect<'a, GeometricalWorldRes>; + + fn run(&mut self, geo_world: Self::SystemData) { + for contact in geo_world.contact_events() { + println!("CONTACT = {:?}", contact); + } + } +} + +#[derive(SystemDesc)] +#[system_desc(name(CameraMovementSystemDesc))] +pub struct CameraMovementSystem { + speed: f32, +} + +// Here is a system that sets the velocity of the camera's RigidBody component depending on user input +impl<'a> System<'a> for CameraMovementSystem { + type SystemData = ( + Read<'a, ActiveCamera>, + WriteStorage<'a, BodyComponent>, + Read<'a, InputHandler>, + ); + + fn run(&mut self, (active_camera, mut bodies, input): Self::SystemData) { + let x = get_input_axis_simple(&Some(String::from("move_x")), &input); + let y = get_input_axis_simple(&Some(String::from("move_y")), &input); + let z = get_input_axis_simple(&Some(String::from("move_z")), &input); + + if let Some(camera) = active_camera.entity { + if let Some(body) = bodies.get_mut(camera) { + let b = body.as_rigid_body_mut().expect("Camera must be RigidBody"); + // Sets the velocity local to the object's orientation if WASD is pressed + if let Some(dir) = Unit::try_new(Vector::new(x, y, z), 1.0e-6) { + b.set_linear_velocity(b.position().rotation * dir.as_ref() * self.speed); + } else { + b.set_velocity(Velocity::zero()); + } + } + } + } +} + +#[derive(SystemDesc)] +#[system_desc(name(CameraRotationSystemDesc))] +pub struct CameraRotationSystem { + sensitivity_x: f32, + sensitivity_y: f32, + #[system_desc(event_channel_reader)] + event_reader: ReaderId, +} + +// This system updates the camera's orientation using mouse motion as input +impl<'a> System<'a> for CameraRotationSystem { + type SystemData = ( + Read<'a, ActiveCamera>, + WriteStorage<'a, BodyComponent>, + Read<'a, EventChannel>, + Read<'a, HideCursor>, + Read<'a, WindowFocus>, + ); + + fn run(&mut self, (active_camera, mut bodies, event_channel, hide, window_focus): Self::SystemData) { + for event in event_channel.read(&mut self.event_reader) { + if window_focus.is_focused && hide.hide { + if let Event::DeviceEvent { ref event, .. } = *event { + if let DeviceEvent::MouseMotion { delta: (x, y) } = *event { + if let Some(entity) = active_camera.entity { + if let Some(body) = bodies.get_mut(entity) { + let body = body + .as_rigid_body_mut() + .expect("Active camera must be RigidBody"); + // Update camera rotation using cleaner `Tansform` api instead of manually applying maths to `Isometry` + body.set_position({ + let isometry = body.position(); + let mut transform = Transform::new(isometry.translation, isometry.rotation, Vector::zeros()); + transform.append_rotation_x_axis((-y as f32 * self.sensitivity_y).to_radians()); + transform.prepend_rotation(Vector::y_axis(), (-x as f32 * self.sensitivity_x).to_radians()); + *transform.isometry() + }); + } + } + } + } + } + } + } +} \ No newline at end of file From 7999424b3a3c212fd0d0b2548bed396f0fb0ec3e Mon Sep 17 00:00:00 2001 From: Kel Date: Fri, 29 Nov 2019 03:50:14 -0500 Subject: [PATCH 4/7] 0.4.0 rewrite continued. --- Cargo.toml | 3 +- LICENSE | 2 +- README.md | 279 ++++++++-- .../amethyst/assets/prefab/renderable.ron | 6 +- examples/amethyst/main.rs | 48 +- examples/basic.rs | 11 +- examples/batch.rs | 10 +- src/bodies/components.rs | 96 ++++ src/bodies/marker.rs | 234 ++++++++ src/bodies/mod.rs | 14 + src/bodies/set.rs | 129 +++++ src/builder.rs | 109 ++-- src/bundle.rs | 154 ++++++ src/colliders/components.rs | 11 + src/colliders/mod.rs | 9 + .../collider_set.rs => colliders/set.rs} | 84 +-- src/joints/components.rs | 9 + src/joints/mod.rs | 9 + src/joints/set.rs | 182 +++++++ src/lib.rs | 362 +++++-------- src/pose.rs | 85 +++ src/position.rs | 79 --- src/stepper.rs | 499 ------------------ src/stepper/mod.rs | 78 +++ src/stepper/resource.rs | 174 ++++++ src/stepper/semi_fixed_step.rs | 165 ++++++ src/systems/batch.rs | 12 +- src/systems/mod.rs | 142 +---- src/systems/pose.rs | 14 +- src/systems/stepper.rs | 21 +- src/world.rs | 77 +++ src/world/body_set.rs | 236 --------- src/world/mod.rs | 33 -- 33 files changed, 1976 insertions(+), 1400 deletions(-) create mode 100644 src/bodies/components.rs create mode 100644 src/bodies/marker.rs create mode 100644 src/bodies/mod.rs create mode 100644 src/bodies/set.rs create mode 100644 src/bundle.rs create mode 100644 src/colliders/components.rs create mode 100644 src/colliders/mod.rs rename src/{world/collider_set.rs => colliders/set.rs} (68%) create mode 100644 src/joints/components.rs create mode 100644 src/joints/mod.rs create mode 100644 src/joints/set.rs create mode 100644 src/pose.rs delete mode 100644 src/position.rs delete mode 100644 src/stepper.rs create mode 100644 src/stepper/mod.rs create mode 100644 src/stepper/resource.rs create mode 100644 src/stepper/semi_fixed_step.rs create mode 100644 src/world.rs delete mode 100644 src/world/body_set.rs delete mode 100644 src/world/mod.rs diff --git a/Cargo.toml b/Cargo.toml index b790a91..e5cf509 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,7 @@ license = "MIT" readme = "README.md" documentation = "https://docs.rs/specs-physics" description = "Integration with nphysics for the SPECS Parallel ECS." - -keywords = ["specs", "nphysics", "nphysics3d"] +keywords = ["specs", "physics", "real-time"] [features] default = ["dim3", "nightly", "amethyst"] diff --git a/LICENSE b/LICENSE index d6a7188..0e63ca9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Benjamin Amling +Copyright (c) 2019 Amethyst Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 84be3be..85bde0a 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,276 @@ # specs-physics -[![Build Status][bi]][bl] [![Crates.io][ci]][cl] ![MIT/Apache][li] [![Docs.rs][di]][dl] +[![Build Status][bi]][bl] +[![Crates.io][ci]][cl] +[![MIT/Apache][li]][ll] +[![Docs.rs][di]][dl] -[bi]: https://travis-ci.com/bamling/specs-physics.svg?branch=master -[bl]: https://travis-ci.com/bamling/specs-physics +[bi]: https://travis-ci.com/amethyst/specs-physics.svg?branch=master +[bl]: https://travis-ci.com/amethyst/specs-physics [ci]: https://img.shields.io/crates/v/specs-physics.svg [cl]: https://crates.io/crates/specs-physics/ [li]: https://img.shields.io/crates/l/specs-physics.svg +[ll]: https://github.com/amethyst/specs-physics/blob/master/LICENSE [di]: https://docs.rs/specs-physics/badge.svg [dl]: https://docs.rs/specs-physics/ -**specs-physics** aims to be an easily usable and extendable [nphysics](https://www.nphysics.org/) physics engine integration for applications and games that utilise the [Specs ECS](https://slide-rs.github.io/specs/). +**For when you want some [nphysics] in your [Specs]!** +***Somewhat*** **better than sliced bread!** -The dream is to *simply* create `Entity`s with a set of configurable `Component`s and have most of your physics covered, be it collision/proximity detection, velocity and acceleration or gravity. +Remember those "FooBarDefault" types in the [nphysics tutorial]? +**specs-physics** provides [ECS Component]-based implementations for nphysics data sets, +as well as faculties for synchronizing pose data to your position type of choice, +and stepping functionality for stepping your simulations to a real good beat. +[Specs]: https://slide-rs.github.io/specs/ +[nphysics]: https://www.nphysics.org/ +[nphysics tutorial]: https://www.nphysics.org/rigid_body_simulations_with_contacts/#basic-setup +[ECS Component]: https://amethyst.github.io/specs/01_intro.html#whats-an-ecs -### Examples +## Usage -Full examples can be found under [src/examples](https://github.com/bamling/specs-physics/tree/master/examples). If anything is missing or unclear, feel free to open an issue or give me a poke! +To use **specs-physics** with a 3D nphysics world, +add the following dependency to your project's *[Cargo.toml]*: -## Contributing +```toml +[dependencies] +specs-physics = { version = "0.4.0", features = ["dim3"] } +``` -I'd appreciate any kind of contribution to this project, be it feature requests, bugs/issues, pull requests, documentation, tests or examples! +For 2D nphysics, replace `dim3` with `dim2`. +You **must** enable one of these two features, and you can *only* enable one of them! -Please just try to format any code changes according to the [rustfmt.toml](https://github.com/bamling/specs-physics/blob/master/rustfmt.toml) rules. They're not exactly set in stone and I'm open for suggestions, but let's try to keep things tidy! +Also available is an `amethyst` feature, +which adds synchronization support for [Amethyst] +through `amethyst_core`'s [`Transform`] type +as well as a [`SystemBundle`] trait impl for [`PhysicsBundle`]. +Usage is explained further below. -## Current Roadmap +[Cargo.toml]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html +[Amethyst]: https://amethyst.rs/ +[`Transform`]: https://docs.amethyst.rs/stable/amethyst_core/transform/components/struct.Transform.html +[`SystemBundle`]: https://docs.amethyst.rs/stable/amethyst_core/bundle/trait.SystemBundle.html +[`PhysicsBundle`]: struct.PhysicsBundle.html -Full *TODO* sheet can be found in [this nphysics issue][todo] +### Dispatching -- [x] RigidBody Components -- [x] Collider Components -- [x] Proximity and Contact EventChannels -- [x] External force property -- [x] `log` based logging -- [ ] Handling Body Activation & Sleeping -- [ ] Multibody-based Component Joints -- [ ] Force generator inversion of control -- [ ] Time scale and simulation pausing -Investigating: -- [ ] Proximity & Curve-based external force utility -- [ ] Constraint-based Joints -- [ ] Kinematics +### Generic types -## License +All `System`s and `Component`s provided by this crate require between one +and two type parameters to function properly. These were explicitly +introduced to keep this integration as generic as possible and allow +compatibility with as many external crates and game engines as possible. -Distributed under the MIT License. See [LICENSE](https://github.com/bamling/specs-physics/blob/master/LICENSE) for more information. +#### `N: RealField` -## Acknowledgments +[nphysics] is built upon [nalgebra] and uses various types and +structures from this crate. **specs-physics** builds up on this even further +and utilises the same structures, which all work with any type that +implements `nalgebra::RealField`. `nalgebra::RealField` is by default +implemented for various standard types, such as `f32` and`f64`. `nalgebra` +is re-exported under `specs_physics::nalgebra`. -This project is heavily inspired by [nphysics-ecs-dumb](https://github.com/distransient/nphysics-ecs-dumb); they did most of the heavy lifting, I'm just building up on what they have started! +#### `P: Position` -**Special thanks to:** -- [distransient](https://github.com/distransient) -- [jojolepro](https://github.com/jojolepro) \ No newline at end of file +a type parameter which implements the `specs_physics::bodies::Position` +*trait*, requiring also a `Component` implementation with a +`FlaggedStorage`. This `Position` `Component` is used to initially place a +[RigidBody] in the [nphysics] world and later used to synchronise the +updated translation and rotation of these bodies back into the [Specs] +world. + +Example for a `Position` `Component`, simply using the "Isometry" type (aka +combined translation and rotation structure) directly: + +```rust,ignore +use specs::{Component, DenseVecStorage, FlaggedStorage}; +use specs_physics::{bodies::Position, nalgebra::Isometry3}; + +struct Pos(pub Isometry3); + +impl Component for Pos { + type Storage = FlaggedStorage>; +} + +impl Position for Pos { + fn isometry(&self) -> &Isometry3 { + &self.0 + } + + fn isometry_mut(&mut self) -> &mut Isometry3 { + &mut self.0 + } +} +``` + +If you're using [Amethyst], you can enable the "amethyst" feature for this +crate which provides a `Position` impl for `Transform`. + +```toml +[dependencies] +specs-physics = { version = "0.3", features = ["amethyst"] } +``` + +### Components + +##### PhysicsBody + +The `specs_physics::PhysicsBody` `Component` is used to define [RigidBody] +from the comforts of your [Specs] world. Changes to the `PhysicsBody` will +automatically be synchronised with [nphysics]. + +Example: + +```rust,ignore +use specs_physics::{ + nalgebra::{Matrix3, Point3}, + nphysics::{algebra::Velocity3, object::BodyStatus}, + PhysicsBodyBuilder, +}; + +let physics_body = PhysicsBodyBuilder::from(BodyStatus::Dynamic) + .gravity_enabled(true) + .velocity(Velocity3::linear(1.0, 1.0, 1.0)) + .angular_inertia(Matrix3::from_diagonal_element(3.0)) + .mass(1.3) + .local_center_of_mass(Point3::new(0.0, 0.0, 0.0)) + .build(); +``` + +##### PhysicsCollider + +`specs_physics::PhysicsCollider`s are the counterpart to `PhysicsBody`s. +They can exist on their own or as a part of a `PhysicsBody` +`PhysicsCollider`s are used to define and create [Collider]'s in +[nphysics]. + +Example: + +```rust,ignore +use specs_physics::{ + colliders::Shape, + nalgebra::{Isometry3, Vector3}, + ncollide::world::CollisionGroups, + nphysics::material::{BasicMaterial, MaterialHandle}, + PhysicsColliderBuilder, +}; + +let physics_collider = PhysicsColliderBuilder::from( + Shape::Cuboid{ half_extents: Vector3::new(10.0, 10.0, 1.0) }) + .offset_from_parent(Isometry3::identity()) + .density(1.2) + .material(MaterialHandle::new(BasicMaterial::default())) + .margin(0.02) + .collision_groups(CollisionGroups::default()) + .linear_prediction(0.001) + .angular_prediction(0.0) + .sensor(true) + .build(); +``` + +To assign multiple [Collider]'s the the same body, [Entity hierarchy] +can be used. This utilises [specs-hierarchy]. + +### Systems + +The following `System`s currently exist and should be added to your +`Dispatcher` in order: + +1. `specs_physics::systems::SyncBodiesToPhysicsSystem` - handles the +creation, modification and removal of [RigidBody]'s based on the +`PhysicsBody` `Component` and an implementation of the `Position` +*trait*. + +2. `specs_physics::systems::SyncCollidersToPhysicsSystem` - handles +the creation, modification and removal of [Collider]'s based on the +`PhysicsCollider` `Component`. This `System` depends on +`SyncBodiesToPhysicsSystem` as [Collider] can depend on [RigidBody]. + +3. `specs_physics::systems::SyncParametersToPhysicsSystem` - handles the +modification of the [nphysics] `World`s parameters. + +4. `specs_physics::systems::PhysicsStepperSystem` - handles the progression +of the [nphysics] `World` and causes objects to actually move and +change their position. This `System` is the backbone for collision +detection. + +5. `specs_physics::systems::SyncBodiesFromPhysicsSystem` - +handles the synchronisation of [RigidBody] positions and dynamics back +into the [Specs] `Component`s. This `System` also utilises the +`Position` *trait* implementation. + +An example `Dispatcher` with all required `System`s: + +```rust,no_run +use specs::DispatcherBuilder; +use specs_physics::{ + systems::{ + PhysicsStepperSystem, + PhysicsPoseSystem, + SyncBodiesToPhysicsSystem, + SyncCollidersToPhysicsSystem, + SyncParametersToPhysicsSystem, + }, + SimplePosition, +}; + +let dispatcher = DispatcherBuilder::new() + .with( + SyncBodiesToPhysicsSystem::>::default(), + "sync_bodies_to_physics_system", + &[], + ) + .with( + SyncCollidersToPhysicsSystem::>::default(), + "sync_colliders_to_physics_system", + &["sync_bodies_to_physics_system"], + ) + .with( + SyncParametersToPhysicsSystem::::default(), + "sync_gravity_to_physics_system", + &[], + ) + .with( + PhysicsStepperSystem::::default(), + "physics_stepper_system", + &[ + "sync_bodies_to_physics_system", + "sync_colliders_to_physics_system", + "sync_gravity_to_physics_system", + ], + ) + .with( + PhysicsPoseSystem::>::default(), + "sync_bodies_from_physics_system", + &["physics_stepper_system"], + ) + .build(); +``` + +If you're using [Amethyst] Transforms directly, you'd pass the generic +arguments like so: + +```rust,ignore +use amethyst::core::{Float, Transform}; +use specs_physics::systems::SyncBodiesToPhysicsSystem; +SyncBodiesToPhysicsSystem::::default(); +``` + +Alternatively to building your own `Dispatcher`, you can always fall back on +the convenience function `specs_physics::physics_dispatcher()`, which +returns a configured *default* `Dispatcher` for you or +`specs_physics::register_physics_systems()` which takes a +`DispatcherBuilder` as an argument and registers the required `System`s for +you. + +[nalgebra]: https://nalgebra.org/ +[RigidBody]: https://www.nphysics.org/rigid_body_simulations_with_contacts/#rigid-bodies +[Collider]: https://www.nphysics.org/rigid_body_simulations_with_contacts/#colliders +[Amethyst]: https://amethyst.rs/ +[Entity hierarchy]: https://github.com/bamling/specs-physics/blob/master/examples/hierarchy.rs +[specs-hierarchy]: https://github.com/rustgd/specs-hierarchy \ No newline at end of file diff --git a/examples/amethyst/assets/prefab/renderable.ron b/examples/amethyst/assets/prefab/renderable.ron index 27cc42a..2c9a001 100644 --- a/examples/amethyst/assets/prefab/renderable.ron +++ b/examples/amethyst/assets/prefab/renderable.ron @@ -1,9 +1,5 @@ #![enable(implicit_some)] -/*! - @import /amethyst_assets/src/prefab/mod.rs#Prefab - @import ../../renderable/main.rs#MyPrefabData - Prefab -*/ + Prefab ( entities: [ diff --git a/examples/amethyst/main.rs b/examples/amethyst/main.rs index 0e832b4..dd574a8 100644 --- a/examples/amethyst/main.rs +++ b/examples/amethyst/main.rs @@ -6,10 +6,7 @@ use amethyst::{ }, controls::{CursorHideSystemDesc, MouseFocusUpdateSystemDesc}, core::transform::{Transform, TransformBundle}, - input::{ - is_close_requested, is_key_down, InputBundle, StringBindings, - VirtualKeyCode, - }, + input::{is_close_requested, is_key_down, InputBundle, StringBindings, VirtualKeyCode}, prelude::*, renderer::{ plugins::{RenderShaded3D, RenderToWindow}, @@ -21,10 +18,7 @@ use amethyst::{ utils::application_root_dir, Error, }; -use specs_physics::{ - nphysics::math::Vector, - systems::PhysicsBundle, -}; +use specs_physics::{nphysics::math::Vector, systems::PhysicsBundle}; use crate::{ prefab::CustomScenePrefab, @@ -39,7 +33,8 @@ struct Loading { prefab: Option>>, } -struct Example { +struct Playing { + fixed_dispatcher: Dispatcher, scene: Handle>, } @@ -68,16 +63,14 @@ impl SimpleState for Loading { { let _ = data.world.delete_entity(entity); } - Trans::Switch(Box::new(Example { - scene: self.prefab.as_ref().unwrap().clone(), - })) + Trans::Switch(Box::new(Playing::new(self.prefab.as_ref().unwrap().clone()))) } Completion::Loading => Trans::None, } } } -impl SimpleState for Example { +impl SimpleState for Playing { fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) { let StateData { world, .. } = data; @@ -97,6 +90,21 @@ impl SimpleState for Example { } Trans::None } + + fn fixed_update(&mut self, data: &mut StateData<'_, GameData<'_, '_>>) -> SimpleTrans { + self.fixed_dispatcher.dispatch(data.world); + Trans::None + } +} + +impl Playing { + fn new(scene: Handle>) -> Box { + let fixed_dispatcher = DispatcherBuilder + Self { + fixed_dispatcher: + scene, + } + } } fn main() -> Result<(), Error> { @@ -106,19 +114,17 @@ fn main() -> Result<(), Error> { // Add our meshes directory to the asset loader. let assets_dir = app_root.join("examples").join("amethyst").join("assets"); - - let display_config_path = assets_dir - .join("display.ron"); - - let input_config_path = assets_dir - .join("input_config.ron"); + let display_config_path = assets_dir.join("display.ron"); + let input_config_path = assets_dir.join("input_config.ron"); let game_data = GameDataBuilder::default() .with_system_desc(PrefabLoaderSystemDesc::::default(), "", &[]) .with_bundle(TransformBundle::new())? .with_bundle(UiBundle::::new())? .with_bundle(HotReloadBundle::default())? - .with_bundle(InputBundle::::new().with_bindings_from_file(&input_config_path)?)? + .with_bundle( + InputBundle::::new().with_bindings_from_file(&input_config_path)?, + )? .with_system_desc(CameraMovementSystemDesc::new(5.), "", &[]) .with_system_desc(CameraRotationSystemDesc::new(0.25, 0.25), "", &[]) .with_system_desc(CursorHideSystemDesc::default(), "", &[]) @@ -140,4 +146,4 @@ fn main() -> Result<(), Error> { } mod prefab; -mod systems; \ No newline at end of file +mod systems; diff --git a/examples/basic.rs b/examples/basic.rs index 75c2c9c..73217cf 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -8,9 +8,7 @@ use specs_physics::{ math::Vector, object::{ColliderDesc, Ground, RigidBody, RigidBodyDesc}, }, - systems::PhysicsBundle, - world::MechanicalWorldRes, - BodyComponent, EntityBuilderExt, SimplePosition, + BodyComponent, EntityBuilderExt, MechanicalWorldRes, PhysicsBundle, SimplePosition, }; /* @@ -25,6 +23,7 @@ fn main() { /* * 1: Specs Setup */ + // ANCHOR: dispatcher // Initialise the Specs world // This will contain our Resources and Entities let mut world = World::new(); @@ -35,8 +34,7 @@ fn main() { // Attach the specs-physics systems to the dispatcher, // with our pre-physics-stepping systems as a dependency - PhysicsBundle::>::new(Vector::y() * -9.81) - .with_deps(&["my_physics_system"]) + PhysicsBundle::>::new(Vector::y() * -9.81, &["my_physics_system"]) .register(&mut world, &mut dispatcher_builder); // Build our dispatcher for use in the application loop @@ -52,6 +50,7 @@ fn main() { // Sets up all resources for our dispatcher. dispatcher.setup(&mut world); + // ANCHOR_END: dispatcher /* * 2: Add the ground body @@ -146,7 +145,7 @@ impl<'s> System<'s> for MyRenderingSystem { ); fn run(&mut self, (positions, mechanical_world): Self::SystemData) { - for pos in (&positions,).join() { + for (pos,) in (&positions,).join() { println!( "Body Position (X: {:?}, Y: {:?}, Z: {:?}) @ {:?}s", pos.0.translation.vector.x, diff --git a/examples/batch.rs b/examples/batch.rs index f1fcdac..3ba43ac 100644 --- a/examples/batch.rs +++ b/examples/batch.rs @@ -5,9 +5,8 @@ use specs::{ use specs_physics::{ ncollide::shape::{Ball, ShapeHandle}, nphysics::object::{ColliderDesc, RigidBody, RigidBodyDesc}, - systems::{PhysicsBatchSystem, PhysicsBundle}, - world::MechanicalWorldRes, - BodyComponent, EntityBuilderExt, SimplePosition, + systems::PhysicsBatchSystem, + BodyComponent, EntityBuilderExt, MechanicalWorldRes, PhysicsBundle, SimplePosition, }; fn main() { @@ -22,7 +21,8 @@ fn main() { // we must be sure to construct our bundle with a value for its stepper // Refer to the PhysicsBundle documentation to see its various constructor // options - PhysicsBundle::>::stepper(60) + PhysicsBundle::>::default() + .with_fixed_stepper(60) .with_deps(&["my_physics_system"]) .register(&mut world, &mut physics_builder); @@ -72,7 +72,7 @@ impl<'s> System<'s> for MyRenderingSystem { ); fn run(&mut self, (positions, mechanical_world): Self::SystemData) { - for pos in (&positions,).join() { + for (pos,) in (&positions,).join() { println!( "Body Position (X: {:?}, Y: {:?}, Z: {:?}) @ {:?}s", pos.0.translation.vector.x, diff --git a/src/bodies/components.rs b/src/bodies/components.rs new file mode 100644 index 0000000..22c2914 --- /dev/null +++ b/src/bodies/components.rs @@ -0,0 +1,96 @@ +//! Component data of the bodies themselves + +use crate::{ + nalgebra::RealField, + nphysics::object::{Body, Ground, Multibody, RigidBody}, +}; + +use specs::{Component, DenseVecStorage, Entity, FlaggedStorage}; + +/** +Component designating component as a `usize` index body part of `Entity`. + +Attaching this component to your entity with a [`Pose`] component will make the syncing system +synchronize the isometry of the `usize` indexed part on the [`BodyComponent`] of `Entity`. This +relationship is one-way, however. If you'd like to update the position of a Body, **do so via that +Body's `BodyComponent`, and not the component used for `Pose` synchronization**. + +[`Pose`]: ../trait.Pose.html +*/ +#[derive(Copy, Clone, Debug)] +pub struct BodyPartHandle(pub Entity, pub usize); + +impl Component for BodyPartHandle { + type Storage = DenseVecStorage; +} + +/** +The component type of all physics bodies. + +Attaching this component to your entity with a [`Pose`] component will make the syncing system +synchronize the isometry of the first part in that Body to your `Pose` (or simply the position of +the body if it is a single part body). This relationship is one-way, however. If you'd like to +update the position of a Body, **do so via this component, and not from the component used for +`Pose` synchronization**. + +If you'd like to synchronize individual parts of a body to a [`Pose`], you should not attach a +`Pose` to the entity with this Component, and should instead attach a `BodyPartHandle`, which points +to the multipart body, to the entity with the `Pose` for a single part. + +[`Pose`]: ../trait.Pose.html +*/ +#[derive(Shrinkwrap)] +#[shrinkwrap(mutable)] +// Ouch! Bad allocation story here with DenseVecStorage>. +// However, this is hard if not impossible to avoid due to nphysics API +// limitations. +pub struct BodyComponent(pub Box>); + +impl Component for BodyComponent { + type Storage = FlaggedStorage>; +} + +impl BodyComponent { + /// Creates a new Body Component. + /// This is made useful by inserting the returned struct into the + /// BodyComponent's Storage. + pub fn new>(body: B) -> Self { + Self(Box::new(body)) + } + + /// Attempts to cast this Body to a RigidBody. + /// Just sugar for `Body::downcast_ref()`. + pub fn as_rigid_body(&self) -> Option<&RigidBody> { + self.0.downcast_ref() + } + + /// Attempts to mutably cast this Body to a RigidBody. + /// Just sugar for `Body::downcast_mut()`. + pub fn as_rigid_body_mut(&mut self) -> Option<&mut RigidBody> { + self.0.downcast_mut() + } + + /// Attempts to cast this Body to a Multibody. + /// Just sugar for `Body::downcast_ref()`. + pub fn as_multi_body(&self) -> Option<&Multibody> { + self.0.downcast_ref() + } + + /// Attempts to mutably cast this Body to a Multibody. + /// Just sugar for `Body::downcast_mut()`. + pub fn as_multi_body_mut(&mut self) -> Option<&mut Multibody> { + self.0.downcast_mut() + } + + /// Attempts to cast this Body to Ground. + /// Just sugar for `Body::downcast_ref()`. + pub fn as_ground(&self) -> Option<&Ground> { + self.0.downcast_ref() + } + + /// Attempts to mutably cast this Body to Ground. + /// Just sugar for `Body::downcast_mut()`. + pub fn as_ground_mut(&mut self) -> Option<&mut Ground> { + self.0.downcast_mut() + } +} diff --git a/src/bodies/marker.rs b/src/bodies/marker.rs new file mode 100644 index 0000000..226f767 --- /dev/null +++ b/src/bodies/marker.rs @@ -0,0 +1,234 @@ +/*! +Markers used to simplify join operations +*/ + +use crate::{ + bodies::BodyComponent, + nalgebra::RealField, + nphysics::object::{Body, Ground, Multibody, RigidBody}, +}; + +use specs::{ + hibitset::BitSetAnd, + shred::{Fetch, FetchMut, MetaTable, ResourceId}, + storage::{AnyStorage, MaskedStorage, TryDefault, UnprotectedStorage}, + BitSet, Component, Join, NullStorage, ReadStorage, Storage, SystemData, World, WorldExt, +}; + +use std::{ + marker::PhantomData, + ops::{Deref, DerefMut}, +}; + +/// Component that marks the Body on this Entity as a RigidBody. +/// Do not attach more than one kind of Body Marker type to a Body entity. +#[derive(Default, Copy, Clone, Debug)] +pub struct RigidBodyMarker; + +impl Component for RigidBodyMarker { + type Storage = NullStorage; +} + +/// Put this type in your SystemData to get a storage which joins immutably over +/// RigidBodies. +pub type ReadRigidBodies<'f, N> = BodyMarkerStorage< + 'f, + N, + RigidBody, + RigidBodyMarker, + Fetch<'f, MaskedStorage>>, +>; + +/// Put this type in your SystemData to get a storage which joins mutably over +/// RigidBodies. +pub type WriteRigidBodies<'f, N> = BodyMarkerStorage< + 'f, + N, + RigidBody, + RigidBodyMarker, + FetchMut<'f, MaskedStorage>>, +>; + +/// Component that marks the Body on this Entity as a Multibody. +/// Do not attach more than one kind of Body Marker type to a Body entity. +#[derive(Default, Copy, Clone, Debug)] +pub struct MultibodyMarker; + +impl Component for MultibodyMarker { + type Storage = NullStorage; +} + +/// Put this type in your SystemData to get a storage which joins immutably over +/// Multibodies. +pub type ReadMultiBodies<'f, N> = BodyMarkerStorage< + 'f, + N, + Multibody, + MultibodyMarker, + Fetch<'f, MaskedStorage>>, +>; + +/// Put this type in your SystemData to get a storage which joins mutably over +/// Multibodies. +pub type WriteMultiBodies<'f, N> = BodyMarkerStorage< + 'f, + N, + Multibody, + MultibodyMarker, + FetchMut<'f, MaskedStorage>>, +>; + +/// Component that marks the Body on this Entity as Ground. +/// Do not attach more than one kind of Body Marker type to a Body entity. +#[derive(Default, Copy, Clone, Debug)] +pub struct GroundMarker; + +impl Component for GroundMarker { + type Storage = NullStorage; +} + +/// Put this type in your SystemData to get a storage which joins immutably over +/// Ground type bodies. +pub type ReadGroundBodies<'f, N> = + BodyMarkerStorage<'f, N, Ground, GroundMarker, Fetch<'f, MaskedStorage>>>; + +/// Put this type in your SystemData to get a storage which joins mutably over +/// Ground type bodies. +pub type WriteGroundBodies<'f, N> = BodyMarkerStorage< + 'f, + N, + Ground, + GroundMarker, + FetchMut<'f, MaskedStorage>>, +>; + +/// Used by the marker types in this module as a sort of Specs `Storage` +/// meta-type. +pub struct BodyMarkerStorage<'f, N: RealField, B: Body, M: Component, D> { + body_storage: Storage<'f, BodyComponent, D>, + marker_storage: ReadStorage<'f, M>, + phantom: PhantomData<(N, B)>, +} + +impl<'a, 'f, N, B, M, D> Join for &'a BodyMarkerStorage<'f, N, B, M, D> +where + N: RealField, + B: Body, + M: Component, + D: Deref>>, +{ + type Mask = BitSetAnd<&'a BitSet, &'a BitSet>; + type Type = &'a B; + type Value = &'a as Component>::Storage; + + // SAFETY: No unsafe code and no invariants to fulfill. + unsafe fn open(self) -> (Self::Mask, Self::Value) { + ( + self.body_storage.mask() & self.marker_storage.mask(), + self.body_storage.unprotected_storage(), + ) + } + + // SAFETY: Since we require that the mask was checked, an element for `id` must + // have been inserted without being removed. + unsafe fn get(value: &mut Self::Value, id: u32) -> Self::Type { + value + .get(id) + .downcast_ref() + .expect("Incorrect marker on Body.") + } +} + +impl<'a, 'f, N, B, M, D> Join for &'a mut BodyMarkerStorage<'f, N, B, M, D> +where + N: RealField, + B: Body, + M: Component, + D: DerefMut>>, +{ + type Mask = BitSetAnd<&'a BitSet, &'a BitSet>; + type Type = &'a mut B; + type Value = &'a mut as Component>::Storage; + + // SAFETY: No unsafe code and no invariants to fulfill. + unsafe fn open(self) -> (Self::Mask, Self::Value) { + let bodies = Join::open(&mut self.body_storage); + (bodies.0 & self.marker_storage.mask(), bodies.1) + } + + // TODO: audit unsafe + unsafe fn get(value: &mut Self::Value, id: u32) -> Self::Type { + // From Specs: + // This is horribly unsafe. Unfortunately, Rust doesn't provide a way + // to abstract mutable/immutable state at the moment, so we have to hack + // our way through it. + let value: *mut Self::Value = value as *mut Self::Value; + + (*value) + .get_mut(id) + .downcast_mut() + .expect("Incorrect marker on Body.") + } +} + +impl<'f, N: RealField, B: Body, M: Component> SystemData<'f> + for BodyMarkerStorage<'f, N, B, M, Fetch<'f, MaskedStorage>>> +{ + fn setup(world: &mut World) { + setup_component::>(world); + setup_component::(world); + } + + fn fetch(world: &'f World) -> Self { + Self { + body_storage: world.read_storage(), + marker_storage: world.read_storage(), + phantom: PhantomData, + } + } + + fn reads() -> Vec { + vec![ + ResourceId::new::>>(), + ResourceId::new::>(), + ] + } + + fn writes() -> Vec { + vec![] + } +} + +impl<'f, N: RealField, B: Body, M: Component> SystemData<'f> + for BodyMarkerStorage<'f, N, B, M, FetchMut<'f, MaskedStorage>>> +{ + fn setup(world: &mut World) { + setup_component::>(world); + setup_component::(world); + } + + fn fetch(world: &'f World) -> Self { + Self { + body_storage: world.write_storage(), + marker_storage: world.read_storage(), + phantom: PhantomData, + } + } + + fn reads() -> Vec { + vec![ResourceId::new::>()] + } + + fn writes() -> Vec { + vec![ResourceId::new::>>()] + } +} + +fn setup_component(world: &mut World) { + world.entry::>().or_insert_with(|| { + MaskedStorage::new(<::Storage as TryDefault>::unwrap_default()) + }); + world + .fetch_mut::>() + .register(&*world.fetch::>()); +} diff --git a/src/bodies/mod.rs b/src/bodies/mod.rs new file mode 100644 index 0000000..e701f24 --- /dev/null +++ b/src/bodies/mod.rs @@ -0,0 +1,14 @@ +/*! +Storage, set, and marker types for Bodies, storing the bulk of the state for your simulation. +*/ + +mod components; +mod marker; +mod set; + +pub use components::{BodyComponent, BodyPartHandle}; +pub use marker::{ + BodyMarkerStorage, GroundMarker, MultibodyMarker, ReadGroundBodies, ReadMultiBodies, + ReadRigidBodies, RigidBodyMarker, WriteGroundBodies, WriteMultiBodies, WriteRigidBodies, +}; +pub use set::BodySet; diff --git a/src/bodies/set.rs b/src/bodies/set.rs new file mode 100644 index 0000000..ab7dd8c --- /dev/null +++ b/src/bodies/set.rs @@ -0,0 +1,129 @@ +use crate::{ + bodies::{marker::ReadMultiBodies, BodyComponent, ReadGroundBodies, ReadRigidBodies}, + nalgebra::RealField, + nphysics::object::{Body, BodySet as NBodySet}, +}; + +use specs::{ + shred::{Fetch, FetchMut, MetaTable, ResourceId}, + storage::{AnyStorage, ComponentEvent, MaskedStorage, TryDefault}, + world::EntitiesRes, + Component, Entity, Join, ReaderId, SystemData, World, WorldExt, WriteStorage, +}; + +// List of removals used by `BodySet` so that nphysics may `pop` single removal +// events. +struct BodyRemovalRes(Vec); + +// Reader resource used by `BodySet` during fetching to populate +// `BodyRemovalRes` with removal events. +struct BodyReaderRes(ReaderId); + +/// This structure is only used to pass the BodyComponent storage to nphysics +/// API's. You probably don't want to use it. unless you're using your own +/// system for stepping. +pub struct BodySet<'f, N: RealField> { + pub storage: WriteStorage<'f, BodyComponent>, + + entities: Fetch<'f, EntitiesRes>, + removals: FetchMut<'f, BodyRemovalRes>, +} + +impl<'f, N: RealField> SystemData<'f> for BodySet<'f, N> { + fn setup(world: &mut World) { + // Setup storage for body component. + world + .entry::>>() + .or_insert_with(|| { + MaskedStorage::new( + < as Component>::Storage as TryDefault>::unwrap_default(), + ) + }); + world + .fetch_mut::>() + .register(&*world.fetch::>>()); + + // Setup resource for removal buffer. + world + .entry::() + .or_insert_with(|| BodyRemovalRes(Vec::default())); + + // Setup ComponentEvent reader resource. + // No worries about race condition here due to mut exclusive World reference. + // Entry cannot be used since mut reference isn't passed to closure. + if !world.has_value::() { + let id = world.write_storage::>().register_reader(); + world.insert(BodyReaderRes(id)); + } + + // Setup marker component storages. + ReadRigidBodies::::setup(world); + ReadMultiBodies::::setup(world); + ReadGroundBodies::::setup(world); + } + + fn fetch(world: &'f World) -> Self { + let entities = world.read_resource::(); + let storage = world.write_storage::>(); + let mut reader = world.write_resource::(); + let mut removals = world.write_resource::(); + + for event in storage.channel().read(&mut reader.0) { + if let ComponentEvent::Removed(index) = event { + // Is grabbing the current entity for this index logically wrong? Maybe. + // Is doing this in SystemData::fetch morally wrong? Yes. + removals.0.push(entities.entity(*index)); + } + } + + Self { + entities, + storage, + removals, + } + } + + fn reads() -> Vec { + vec![ResourceId::new::()] + } + + fn writes() -> Vec { + vec![ + ResourceId::new::>>(), + ResourceId::new::(), + ResourceId::new::(), + ] + } +} + +impl<'f, N: RealField> NBodySet for BodySet<'f, N> { + type Handle = Entity; + + fn get(&self, handle: Self::Handle) -> Option<&dyn Body> { + self.storage.get(handle).map(|x| x.0.as_ref()) + } + + fn get_mut(&mut self, handle: Self::Handle) -> Option<&mut dyn Body> { + self.storage.get_mut(handle).map(|x| x.0.as_mut()) + } + + fn contains(&self, handle: Self::Handle) -> bool { + self.storage.contains(handle) + } + + fn foreach(&self, f: &mut dyn FnMut(Self::Handle, &dyn Body)) { + for (handle, body) in (&self.entities, &self.storage).join() { + f(handle, body.0.as_ref()); + } + } + + fn foreach_mut(&mut self, f: &mut dyn FnMut(Self::Handle, &mut dyn Body)) { + for (handle, body) in (&self.entities, &mut self.storage).join() { + f(handle, body.0.as_mut()); + } + } + + fn pop_removal_event(&mut self) -> Option { + self.removals.0.pop() + } +} diff --git a/src/builder.rs b/src/builder.rs index 1846ca5..7855aae 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,61 +1,90 @@ use crate::{ + bodies::{BodyComponent, GroundMarker, MultibodyMarker, RigidBodyMarker}, + colliders::ColliderComponent, nalgebra::RealField, nphysics::object::{Body, BodyPartHandle, ColliderDesc}, - world::{BodyComponent, ColliderComponent}, }; use specs::{world::Builder, EntityBuilder, WorldExt}; -/// Provides methods on `EntityBuilder` that make building -/// physics bodies easier. -/// -/// # Usage -/// -/// ``` -/// use specs::Builder; -/// use specs_physics::{ -/// ncollide::shape::{Ball, ShapeHandle}, -/// nphysics::{math::Vector, object::{ColliderDesc, RigidBodyDesc}}, -/// EntityBuilderExt, SimplePosition, -/// }; -/// -/// # use specs::WorldExt; -/// # let mut world = specs::World::new(); -/// # world.register::>(); -/// # world.register::>(); -/// # world.register::>(); -/// let body = RigidBodyDesc::new().translation(Vector::x() * 2.0).build(); -/// let shape = ShapeHandle::new(Ball::new(1.6)); -/// let collider_desc = ColliderDesc::new(shape); -/// -/// let entity = world -/// .create_entity() -/// .with(SimplePosition::::default()) -/// .with_body::(body) -/// .with_collider::(&collider_desc) -/// .build(); -/// ``` +/** +Provides extension methods to build physics objects with [`EntityBuilder`]. + +# Usage + +``` +use specs::Builder; +use specs_physics::{ + ncollide::shape::{Ball, ShapeHandle}, + nphysics::{math::Vector, object::{ColliderDesc, RigidBodyDesc}}, + EntityBuilderExt, SimplePosition, +}; + +# use specs::WorldExt; +# let mut world = specs::World::new(); +# world.register::>(); +# world.register::>(); +# world.register::>(); +let body = RigidBodyDesc::new().translation(Vector::x() * 2.0).build(); +let shape = ShapeHandle::new(Ball::new(1.6)); +let collider_desc = ColliderDesc::new(shape); + +let entity = world + .create_entity() + .with(SimplePosition::::default()) + .with_body::(body) + .with_collider::(&collider_desc) + .build(); +``` +*/ pub trait EntityBuilderExt { /// Attaches `body` to this entity. fn with_body>(self, body: B) -> Self; + /// Builds a `collider` to point at the body part of index `0` on this + /// entity. So, the body itself for bodies without parts, such as + /// Ground's or RigidBody's. fn with_collider(self, collider: &ColliderDesc) -> Self; } impl EntityBuilderExt for EntityBuilder<'_> { fn with_body>(self, body: B) -> Self { - self.with(BodyComponent::new(body)) - } + let component = BodyComponent::new(body); - fn with_collider(self, collider: &ColliderDesc) -> Self { - { - let mut storage = self.world.write_storage::>(); - storage - .insert( - self.entity, - ColliderComponent(collider.build(BodyPartHandle(self.entity, 0))), - ) + // Reflect on the component type and add relevant markers + // Branches should be optimized away at compilation time + if component.as_rigid_body().is_some() { + self.world + .write_storage::() + .insert(self.entity, RigidBodyMarker) + // Guaranteed to not fail by the lifetime in the EntityBuilder. + .unwrap(); + } else if component.as_multi_body().is_some() { + self.world + .write_storage::() + .insert(self.entity, MultibodyMarker) + // Ditto. + .unwrap(); + } else if component.as_ground().is_some() { + self.world + .write_storage::() + .insert(self.entity, GroundMarker) + // Ditto. .unwrap(); } + + self.with(component) + } + + fn with_collider(self, collider: &ColliderDesc) -> Self { + self.world + .write_storage::>() + .insert( + self.entity, + ColliderComponent(collider.build(BodyPartHandle(self.entity, 0))), + ) + // Guaranteed to not fail by the lifetime in the EntityBuilder. + .unwrap(); + self } } diff --git a/src/bundle.rs b/src/bundle.rs new file mode 100644 index 0000000..9340ccc --- /dev/null +++ b/src/bundle.rs @@ -0,0 +1,154 @@ +use crate::{ + nalgebra::{convert as na_convert, RealField}, + nphysics::math::Vector, + pose::Pose, + stepper::StepperRes, + systems::{PhysicsPoseSystem, PhysicsStepperSystem}, + ForceGeneratorSetRes, GeometricalWorldRes, MechanicalWorldRes, +}; +use specs::{DispatcherBuilder, World}; +use std::marker::PhantomData; + +/// Bundle used to construct and insert all specs-physics Systems and Resources +/// into a Dispatcher and its World. +#[must_use] +pub struct PhysicsBundle> { + mechanical_world: MechanicalWorldRes, + geometrical_world: GeometricalWorldRes, + stepper_res: Option, + // Exercising superfluous allocations at init-time + // is better than figuring out the lifetimes + // for the slice version of this at programming-time. + stepper_deps: Vec>, + marker: PhantomData

, +} + +impl> PhysicsBundle { + /// Constructs a new physics bundle with a given gravity vector and system + /// dependencies for [`PhysicsStepperSystem`]. Omits data for the + /// [`PhysicsBatchSystem`] stepper. + /// + /// [`PhysicsBatchSystem`]: ../systems/system.PhysicsBatchSystem.html + pub fn new(gravity: Vector, dep: &[&str]) -> Self { + Self::from_parts( + MechanicalWorldRes::::new(gravity), + GeometricalWorldRes::::new(), + None, + dep, + ) + } + + /// For fine-grained control over initialization, constructs a bundle given + /// - `mechanical_world`, for configuring the mechanics of the simulation + /// - `geometrical_world`, for configuring collision + /// - optionally `stepper_res`, for configuring [`PhysicsBatchSystem`] if + /// you're using it. + /// - `dep`, to configure which systems should execute before the + /// [`PhysicsStepperSystem`]. + /// + /// [`PhysicsBatchSystem`]: ../systems/system.PhysicsBatchSystem.html + pub fn from_parts( + mechanical_world: MechanicalWorldRes, + geometrical_world: GeometricalWorldRes, + stepper_res: Option, + dep: &[&str], + ) -> Self { + Self { + mechanical_world, + geometrical_world, + stepper_res, + stepper_deps: dep.iter().map(|s| Box::from(*s)).collect(), + marker: PhantomData, + } + } + + /// *Adds* to the dependency list configuring which systems should execute + /// before the [`PhysicsStepperSystem`]. + /// + /// [`PhysicsBatchSystem`]: ../systems/system.PhysicsBatchSystem.html + pub fn with_deps(mut self, dep: &[&str]) -> Self { + self.stepper_deps = [ + self.stepper_deps + .iter() + .map(|s| s.as_ref()) + .collect::>() + .as_slice(), + dep, + ] + .concat() + .iter() + .map(|s| Box::from(*s)) + .collect(); + self + } + + /// Adds fixed stepper [`StepperRes`] data for [`PhysicsBatchSystem`] at + /// `interval` hz + /// + /// [`PhysicsBatchSystem`]: ../systems/system.PhysicsBatchSystem.html + pub fn with_fixed_stepper(mut self, interval: u32) -> Self { + self.stepper_res = Some(StepperRes::new_fixed(interval)); + self + } + + /// Adds fixed stepper [`StepperRes`] data for [`PhysicsBatchSystem`]. + /// + /// [`PhysicsBatchSystem`]: ../systems/system.PhysicsBatchSystem.html + pub fn with_stepper_instance(mut self, stepper: StepperRes) -> Self { + self.stepper_res = Some(stepper); + self + } + + /// Registers this bundle data to a `world` and dispatcher `builder`. + pub fn register(self, world: &mut World, builder: &mut DispatcherBuilder) { + world.insert(self.mechanical_world); + world.insert(self.geometrical_world); + + if let Some(stepper_res) = self.stepper_res { + world.insert(stepper_res); + } + + world.insert(ForceGeneratorSetRes::::new()); + + // Add PhysicsStepperSystem after all other Systems that write data to the + // nphysics World and has to depend on them; this System is used to progress the + // nphysics World for all existing objects. + builder.add( + PhysicsStepperSystem::::default(), + "physics_stepper_system", + self.stepper_deps + .iter() + .map(|s| s.as_ref()) + .collect::>() + .as_slice(), + ); + + // Add PhysicsPoseSystem last as it handles the + // synchronisation between nphysics World bodies and the Position + // components; this depends on the PhysicsStepperSystem. + builder.add( + PhysicsPoseSystem::::default(), + "physics_pose_system", + &["physics_stepper_system"], + ); + } +} + +#[cfg(feature = "amethyst")] +impl<'a, 'b, N: RealField, P: Pose> amethyst::core::SystemBundle<'a, 'b> + for PhysicsBundle +{ + fn build( + self, + world: &mut World, + builder: &mut DispatcherBuilder, + ) -> Result<(), amethyst::error::Error> { + Ok(self.register(world, builder)) + } +} + +impl> Default for PhysicsBundle { + fn default() -> Self { + Self::new(Vector::::y() * na_convert::(-9.81), &[]) + } +} diff --git a/src/colliders/components.rs b/src/colliders/components.rs new file mode 100644 index 0000000..96af747 --- /dev/null +++ b/src/colliders/components.rs @@ -0,0 +1,11 @@ +use crate::{nalgebra::RealField, nphysics::object::Collider}; +use specs::{Component, DenseVecStorage, Entity, FlaggedStorage}; + +/// The component type of all physics colliders. +#[derive(Shrinkwrap)] +#[shrinkwrap(mutable)] +pub struct ColliderComponent(pub Collider); + +impl Component for ColliderComponent { + type Storage = FlaggedStorage>; +} diff --git a/src/colliders/mod.rs b/src/colliders/mod.rs new file mode 100644 index 0000000..61f8726 --- /dev/null +++ b/src/colliders/mod.rs @@ -0,0 +1,9 @@ +/*! +Storage and set types for your collision meshes and shapes. +*/ + +mod components; +mod set; + +pub use components::ColliderComponent; +pub use set::ColliderSet; diff --git a/src/world/collider_set.rs b/src/colliders/set.rs similarity index 68% rename from src/world/collider_set.rs rename to src/colliders/set.rs index 6a21fee..c34f329 100644 --- a/src/world/collider_set.rs +++ b/src/colliders/set.rs @@ -1,4 +1,5 @@ use crate::{ + colliders::ColliderComponent, nalgebra::RealField, ncollide::pipeline::CollisionObjectSet, nphysics::object::{Collider, ColliderRemovalData, ColliderSet as NColliderSet}, @@ -8,45 +9,19 @@ use specs::{ shred::{Fetch, FetchMut, MetaTable, ResourceId}, storage::{AnyStorage, ComponentEvent, MaskedStorage, TryDefault, UnprotectedStorage}, world::EntitiesRes, - Component, DenseVecStorage, Entity, FlaggedStorage, Join, ReaderId, SystemData, World, - WorldExt, WriteStorage, + Component, Entity, Join, ReaderId, SystemData, World, WorldExt, WriteStorage, }; -use super::BodyHandleType; +struct ColliderInsertionRes(Vec); -pub type ColliderHandleType = Entity; +struct ColliderRemovalRes(Vec<(Entity, ColliderRemovalData)>); -#[derive(Shrinkwrap)] -#[shrinkwrap(mutable)] -pub struct ColliderComponent(pub Collider); - -impl Component for ColliderComponent { - type Storage = FlaggedStorage>; -} - -#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Shrinkwrap)] -#[shrinkwrap(mutable)] -pub struct ColliderInsertionRes(pub Vec); - -#[derive(Shrinkwrap)] -#[shrinkwrap(mutable)] -pub struct ColliderRemovalRes( - pub Vec<(ColliderHandleType, ColliderRemovalData)>, -); - -impl Default for ColliderRemovalRes { - fn default() -> Self { - Self(Vec::new()) - } -} - -#[derive(Debug, Shrinkwrap)] -#[shrinkwrap(mutable)] -pub struct ColliderReaderRes(pub ReaderId); +struct ColliderReaderRes(ReaderId); +/// The `set` type needed by nphysics for colliders. pub struct ColliderSet<'f, N: RealField> { + pub storage: WriteStorage<'f, ColliderComponent>, entities: Fetch<'f, EntitiesRes>, - storage: WriteStorage<'f, ColliderComponent>, insertions: FetchMut<'f, ColliderInsertionRes>, removals: FetchMut<'f, ColliderRemovalRes>, } @@ -68,12 +43,14 @@ impl<'f, N: RealField> SystemData<'f> for ColliderSet<'f, N> { // Setup resources for insertion and removal buffers. world .entry::() - .or_insert_with(|| ColliderInsertionRes::default()); + .or_insert_with(|| ColliderInsertionRes(Vec::default())); world .entry::>() - .or_insert_with(|| ColliderRemovalRes::::default()); + .or_insert_with(|| ColliderRemovalRes::(Vec::default())); // Setup ComponentEvent reader resource. + // No worries about race condition here due to mut exclusive World reference. + // Entry cannot be used since mut reference isn't passed to closure. if !world.has_value::() { let reader = world .write_storage::>() @@ -89,7 +66,7 @@ impl<'f, N: RealField> SystemData<'f> for ColliderSet<'f, N> { let mut insertions = world.write_resource::(); let mut removals = world.write_resource::>(); - for event in storage.channel().read(&mut reader) { + for event in storage.channel().read(&mut reader.0) { match event { ComponentEvent::Removed(index) => { // Don't panic please! (I'm asking the computer) @@ -103,12 +80,12 @@ impl<'f, N: RealField> SystemData<'f> for ColliderSet<'f, N> { ) .removal_data() { - removals.push((entities.entity(*index), removal_data)); + removals.0.push((entities.entity(*index), removal_data)); } } } ComponentEvent::Inserted(index) => { - insertions.push(entities.entity(*index)); + insertions.0.push(entities.entity(*index)); } _ => {} } @@ -137,8 +114,8 @@ impl<'f, N: RealField> SystemData<'f> for ColliderSet<'f, N> { } impl<'f, N: RealField> CollisionObjectSet for ColliderSet<'f, N> { - type CollisionObject = Collider; - type CollisionObjectHandle = ColliderHandleType; + type CollisionObject = Collider; + type CollisionObjectHandle = Entity; fn collision_object( &self, @@ -154,14 +131,14 @@ impl<'f, N: RealField> CollisionObjectSet for ColliderSet<'f, N> { } } -impl<'f, N: RealField> NColliderSet for ColliderSet<'f, N> { - type Handle = ColliderHandleType; +impl<'f, N: RealField> NColliderSet for ColliderSet<'f, N> { + type Handle = Entity; - fn get(&self, handle: Self::Handle) -> Option<&Collider> { + fn get(&self, handle: Self::Handle) -> Option<&Collider> { self.storage.get(handle).map(|x| &x.0) } - fn get_mut(&mut self, handle: Self::Handle) -> Option<&mut Collider> { + fn get_mut(&mut self, handle: Self::Handle) -> Option<&mut Collider> { self.storage.get_mut(handle).map(|x| &mut x.0) } @@ -169,36 +146,31 @@ impl<'f, N: RealField> NColliderSet for ColliderSet<'f, N> { self.storage.contains(handle) } - fn foreach(&self, mut f: impl FnMut(Self::Handle, &Collider)) { + fn foreach(&self, mut f: impl FnMut(Self::Handle, &Collider)) { for (handle, collider) in (&self.entities, &self.storage).join() { f(handle, &collider.0); } } - fn foreach_mut(&mut self, mut f: impl FnMut(Self::Handle, &mut Collider)) { + fn foreach_mut(&mut self, mut f: impl FnMut(Self::Handle, &mut Collider)) { for (handle, collider) in (&self.entities, &mut self.storage).join() { f(handle, &mut collider.0); } } fn pop_insertion_event(&mut self) -> Option { - self.insertions.pop() + self.insertions.0.pop() } - fn pop_removal_event( - &mut self, - ) -> Option<(Self::Handle, ColliderRemovalData)> { - self.removals.pop() + fn pop_removal_event(&mut self) -> Option<(Self::Handle, ColliderRemovalData)> { + self.removals.0.pop() } - fn remove( - &mut self, - to_remove: Self::Handle, - ) -> Option<&mut ColliderRemovalData> { + fn remove(&mut self, to_remove: Self::Handle) -> Option<&mut ColliderRemovalData> { let collider = self.storage.remove(to_remove)?; if let Some(removal_data) = collider.removal_data() { - self.removals.push((to_remove, removal_data)); - self.removals.last_mut().map(|r| &mut r.1) + self.removals.0.push((to_remove, removal_data)); + self.removals.0.last_mut().map(|r| &mut r.1) } else { None } diff --git a/src/joints/components.rs b/src/joints/components.rs new file mode 100644 index 0000000..4a99cb0 --- /dev/null +++ b/src/joints/components.rs @@ -0,0 +1,9 @@ +use crate::{nalgebra::RealField, nphysics::joint::JointConstraint}; +use specs::{Component, DenseVecStorage, Entity, FlaggedStorage}; + +/// The component type of all constraint joints. +pub struct JointComponent(pub Box>); + +impl Component for JointComponent { + type Storage = FlaggedStorage>; +} diff --git a/src/joints/mod.rs b/src/joints/mod.rs new file mode 100644 index 0000000..9a561b2 --- /dev/null +++ b/src/joints/mod.rs @@ -0,0 +1,9 @@ +/*! +Storage and set types for constraint-based joints. +*/ + +mod components; +mod set; + +pub use components::JointComponent; +pub use set::JointConstraintSet; diff --git a/src/joints/set.rs b/src/joints/set.rs new file mode 100644 index 0000000..78700d5 --- /dev/null +++ b/src/joints/set.rs @@ -0,0 +1,182 @@ +use crate::{ + joints::JointComponent, + nalgebra::RealField, + nphysics::{ + joint::{JointConstraint, JointConstraintSet as NJointConstraintSet}, + object::BodyPartHandle, + }, +}; + +use specs::{ + shred::{Fetch, FetchMut, MetaTable, ResourceId}, + storage::{AnyStorage, ComponentEvent, MaskedStorage, TryDefault}, + world::EntitiesRes, + Component, Entity, Join, ReaderId, SystemData, World, WorldExt, WriteStorage, +}; + +struct JointEvent { + handle: Entity, + part_one: BodyPartHandle, + part_two: BodyPartHandle, +} + +// Reader resource used by `BodySet` during fetching to populate +// `BodyRemovalRes` with removal events. +struct JointReaderRes(ReaderId); + +struct JointInsertionRes(Vec); + +struct JointRemovalRes(Vec); + +/// The `set` type needed by nphysics for constraint joints. +pub struct JointConstraintSet<'f, N: RealField> { + pub storage: WriteStorage<'f, JointComponent>, + entities: Fetch<'f, EntitiesRes>, + insertions: FetchMut<'f, JointInsertionRes>, + removals: FetchMut<'f, JointRemovalRes>, +} + +impl<'f, N: RealField> SystemData<'f> for JointConstraintSet<'f, N> { + fn setup(world: &mut World) { + // Setup storage for joint component. + world + .entry::>>() + .or_insert_with(|| { + MaskedStorage::new( + < as Component>::Storage as TryDefault>::unwrap_default(), + ) + }); + world + .fetch_mut::>() + .register(&*world.fetch::>>()); + + // Setup resource for insertion/removal buffers. + world + .entry::() + .or_insert_with(|| JointInsertionRes(Vec::default())); + world + .entry::() + .or_insert_with(|| JointRemovalRes(Vec::default())); + + // Setup ComponentEvent reader resource. + // No worries about race condition here due to mut exclusive World reference. + // Entry cannot be used since mut reference isn't passed to closure. + if !world.has_value::() { + let id = world.write_storage::>().register_reader(); + world.insert(JointReaderRes(id)); + } + } + + fn fetch(world: &'f World) -> Self { + let entities = world.read_resource::(); + let storage = world.write_storage::>(); + + let mut reader = world.write_resource::(); + let mut insertions = world.write_resource::(); + let mut removals = world.write_resource::(); + + for event in storage.channel().read(&mut reader.0) { + match event { + ComponentEvent::Removed(index) => { + let entity = entities.entity(*index); + if let Some(joint) = storage.get(entity) { + let anchors = joint.0.anchors(); + removals.0.push(JointEvent { + handle: entities.entity(*index), + part_one: anchors.0, + part_two: anchors.1, + }); + } else { + error!("Failed to record anchors of removed Joint {:?}", entity); + } + } + ComponentEvent::Inserted(index) => { + let entity = entities.entity(*index); + if let Some(joint) = storage.get(entity) { + let anchors = joint.0.anchors(); + insertions.0.push(JointEvent { + handle: entities.entity(*index), + part_one: anchors.0, + part_two: anchors.1, + }); + } else { + error!("Failed to record anchors of inserted Joint {:?}", entity); + } + } + // No need for modified events. + _ => {} + } + } + + Self { + entities, + storage, + insertions, + removals, + } + } + + fn reads() -> Vec { + vec![ResourceId::new::()] + } + + fn writes() -> Vec { + vec![ + ResourceId::new::>>(), + ResourceId::new::(), + ResourceId::new::(), + ResourceId::new::(), + ] + } +} + +impl<'f, N: RealField> NJointConstraintSet for JointConstraintSet<'f, N> { + type Handle = Entity; + type JointConstraint = dyn JointConstraint; + + fn get(&self, handle: Entity) -> Option<&dyn JointConstraint> { + self.storage.get(handle).map(|x| x.0.as_ref()) + } + + fn get_mut(&mut self, handle: Entity) -> Option<&mut dyn JointConstraint> { + self.storage.get_mut(handle).map(|x| x.0.as_mut()) + } + + fn contains(&self, handle: Entity) -> bool { + self.storage.contains(handle) + } + + fn foreach(&self, mut f: impl FnMut(Entity, &dyn JointConstraint)) { + for (entity, joint) in (&self.entities, &self.storage).join() { + f(entity, joint.0.as_ref()) + } + } + + fn foreach_mut(&mut self, mut f: impl FnMut(Entity, &mut dyn JointConstraint)) { + for (entity, joint) in (&self.entities, &mut self.storage).join() { + f(entity, joint.0.as_mut()) + } + } + + fn pop_insertion_event( + &mut self, + ) -> Option<(Self::Handle, BodyPartHandle, BodyPartHandle)> { + self.insertions + .0 + .pop() + .map(|e| (e.handle, e.part_one, e.part_two)) + } + + fn pop_removal_event( + &mut self, + ) -> Option<(Entity, BodyPartHandle, BodyPartHandle)> { + self.removals + .0 + .pop() + .map(|e| (e.handle, e.part_one, e.part_two)) + } + + fn remove(&mut self, to_remove: Entity) { + let _ = self.storage.remove(to_remove); + } +} diff --git a/src/lib.rs b/src/lib.rs index fd2cc9a..b0d11f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,262 +1,157 @@ /*! -# specs-physics +**For when you want some [nphysics] in your [Specs]!** *Somewhat better than sliced bread!* -**For when you want some [nphysics] in your [SPECS Parallel ECS][Specs]!** -***Somewhat*** **better than sliced bread!** +Remember all those "Default" types in the [nphysics tutorial], like `DefaultBodySet` and +`DefaultColliderSet`? **specs-physics** provides [ECS Component]-based implementations for +*nphysics* data sets, as well as faculties for synchronizing pose data to your position type of +choice and stepping functionality for stepping your simulations to a real good beat. -Remember those "default" types in the [nphysics tutorial]? -**specs-physics** provides [ECS Component]-based implementations for nphysics data sets, -as well as faculties for synchronizing pose data to your position type of choice, -and stepping functionality for stepping your simulations to a good beat. - -[Specs]: https://slide-rs.github.io/specs/ +[Specs]: https://github.com/amethyst/specs +[ECS Component]: https://amethyst.github.io/specs/01_intro.html#whats-an-ecs [nphysics]: https://www.nphysics.org/ [nphysics tutorial]: https://www.nphysics.org/rigid_body_simulations_with_contacts/#basic-setup -[ECS Component]: https://amethyst.github.io/specs/01_intro.html#whats-an-ecs -## Usage +# Usage -To use **specs-physics** with 3D nphysics, -add the following dependency to your project's *[Cargo.toml]*: +To use **specs-physics** with [nphysics3d], add the following dependency to your project's +*[Cargo.toml]*: ```toml [dependencies] specs-physics = { version = "0.4.0", features = ["dim3"] } ``` -For 2D nphysics, replace `dim3` with `dim2`. -You **must** enable one of these two features, and you can *only* enable one of them! - -Also available is an `amethyst` feature, for [Amethyst], -which adds synchronization support for Amethyst's [`Transform`] type -as well as a [`SystemBundle`] trait impl for [`PhysicsBundle`]. -Usage is explained further below. - -[Cargo.toml]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html -[Amethyst]: https://amethyst.rs/ -[`Transform`]: https://docs.amethyst.rs/stable/amethyst_core/transform/components/struct.Transform.html -[`SystemBundle`]: https://docs.amethyst.rs/stable/amethyst_core/bundle/trait.SystemBundle.html -[`PhysicsBundle`]: struct.PhysicsBundle.html +For [nphysics2d], replace `dim3` with `dim2`. You **must** enable one of these two features, and +you can *only* enable one of them! Note that these docs are built by default with the 3D feature, +but content is the same with the exception of the `nphysics3d` and `ncollide3d` re-exports below. +Also available is an `amethyst` feature, which adds synchronization support for [Amethyst] through +`amethyst_core`'s [`Transform`] type as well as a [`SystemBundle`] trait impl for [`PhysicsBundle`]. -### Dispatching +And then for a basic working example, the following code goes in your `main.rs`, aspects of which +are explained in the sections below. +``` +use specs::{DispatcherBuilder, Join, System, World, WorldExt}; +use specs_physics::{ + bodies::WriteRigidBodies, + nphysics::{ + math::{Force, ForceType, Vector}, + object::Body + }, + PhysicsBundle, + SimplePosition +}; +fn main() { + // Initialize world and dispatcher + let mut world = World::new(); + let mut dispatcher_builder = DispatcherBuilder::new() + .with(MyPhysicsSystem, "my_physics_system", &[]); -### Generic types - -All `System`s and `Component`s provided by this crate require between one -and two type parameters to function properly. These were explicitly -introduced to keep this integration as generic as possible and allow -compatibility with as many external crates and game engines as possible. - -#### `N: RealField` - -[nphysics] is built upon [nalgebra] and uses various types and -structures from this crate. **specs-physics** builds up on this even further -and utilises the same structures, which all work with any type that -implements `nalgebra::RealField`. `nalgebra::RealField` is by default -implemented for various standard types, such as `f32` and`f64`. `nalgebra` -is re-exported under `specs_physics::nalgebra`. - -#### `P: Position` - -a type parameter which implements the `specs_physics::bodies::Position` -*trait*, requiring also a `Component` implementation with a -`FlaggedStorage`. This `Position` `Component` is used to initially place a -[RigidBody] in the [nphysics] world and later used to synchronise the -updated translation and rotation of these bodies back into the [Specs] -world. - -Example for a `Position` `Component`, simply using the "Isometry" type (aka -combined translation and rotation structure) directly: - -```rust,ignore -use specs::{Component, DenseVecStorage, FlaggedStorage}; -use specs_physics::{bodies::Position, nalgebra::Isometry3}; + // Create a new physics bundle + PhysicsBundle::>::new(Vector::y() * -9.81, &["my_physics_system"]) + .register(&mut world, &mut dispatcher_builder); -struct Pos(pub Isometry3); + let mut dispatcher = dispatcher_builder.build(); + dispatcher.setup(&mut world); -impl Component for Pos { - type Storage = FlaggedStorage>; + // In your application loop + dispatcher.dispatch(&world); } -impl Position for Pos { - fn isometry(&self) -> &Isometry3 { - &self.0 - } +struct MyPhysicsSystem; - fn isometry_mut(&mut self) -> &mut Isometry3 { - &mut self.0 +impl<'s> System<'s> for MyPhysicsSystem { + type SystemData = WriteRigidBodies<'s, f32>; + + fn run(&mut self, mut rigid_bodies: Self::SystemData) { + let force = Force::::linear(Vector::x()); + for (body,) in (&mut rigid_bodies,).join() { + body.apply_force(0, &force, ForceType::AccelerationChange, true); + } } } ``` -If you're using [Amethyst], you can enable the "amethyst" feature for this -crate which provides a `Position` impl for `Transform`. - -```toml -[dependencies] -specs-physics = { version = "0.3", features = ["amethyst"] } -``` - -### Components - -##### PhysicsBody - -The `specs_physics::PhysicsBody` `Component` is used to define [RigidBody] -from the comforts of your [Specs] world. Changes to the `PhysicsBody` will -automatically be synchronised with [nphysics]. - -Example: - -```rust,ignore -use specs_physics::{ - nalgebra::{Matrix3, Point3}, - nphysics::{algebra::Velocity3, object::BodyStatus}, - PhysicsBodyBuilder, -}; - -let physics_body = PhysicsBodyBuilder::from(BodyStatus::Dynamic) - .gravity_enabled(true) - .velocity(Velocity3::linear(1.0, 1.0, 1.0)) - .angular_inertia(Matrix3::from_diagonal_element(3.0)) - .mass(1.3) - .local_center_of_mass(Point3::new(0.0, 0.0, 0.0)) - .build(); -``` - -##### PhysicsCollider +[Cargo.toml]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html +[nphysics2d]: https://www.nphysics.org/rustdoc/nphysics2d/index.html +[nphysics3d]: https://www.nphysics.org/rustdoc/nphysics3d/index.html +[Amethyst]: https://amethyst.rs/ +[`Transform`]: https://docs.amethyst.rs/stable/amethyst_core/transform/components/struct.Transform.html +[`SystemBundle`]: https://docs.amethyst.rs/stable/amethyst_core/bundle/trait.SystemBundle.html -`specs_physics::PhysicsCollider`s are the counterpart to `PhysicsBody`s. -They can exist on their own or as a part of a `PhysicsBody` -`PhysicsCollider`s are used to define and create [Collider]'s in -[nphysics]. +# Universal Generic Parameters -Example: +**specs-physics** relies on the following generic parameters for configuration, each of which should +only be given a single value throughout your codebase. Failing to use the same values for these +parameters will cause you to reference incorrect instances of your desired types! -```rust,ignore -use specs_physics::{ - colliders::Shape, - nalgebra::{Isometry3, Vector3}, - ncollide::world::CollisionGroups, - nphysics::material::{BasicMaterial, MaterialHandle}, - PhysicsColliderBuilder, -}; +## `N: RealField` -let physics_collider = PhysicsColliderBuilder::from( - Shape::Cuboid{ half_extents: Vector3::new(10.0, 10.0, 1.0) }) - .offset_from_parent(Isometry3::identity()) - .density(1.2) - .material(MaterialHandle::new(BasicMaterial::default())) - .margin(0.02) - .collision_groups(CollisionGroups::default()) - .linear_prediction(0.001) - .angular_prediction(0.0) - .sensor(true) - .build(); -``` +The `N`: [`RealField`] parameter determines the precision of the numeric type used for simulation. +Valid values for `N` are [`f32`] and [`f64`], although if you're using Amethyst only `f32` is +*currently* implemented for interoperability with [`Transform`]. Be careful about type inference in +the Rust language itself, because `f64` may be inferred for floating point numbers where a type +isn't stated and `f32` can't be inferred, so if you're using `f32` play it safe and always be +explicit with your `N` parameters. -To assign multiple [Collider]'s the the same body, [Entity hierarchy] -can be used. This utilises [specs-hierarchy]. +## `P: Pose` -### Systems +The second common parameter is the `P`: [`Pose`] parameter which determines the **Specs** Component +that physics positions are synchronized to. The mechanism for synchronization is explained in +further detail on the documentation for the [`Pose`] trait. Your `Pose` type should implement +`Pose` for the same `N` that you've chosen. [`SimplePosition`] is provided as an easy utility +transform type which implements `Pose`, although for Amethyst, you may just pass [`Transform`] to +this parameter. -The following `System`s currently exist and should be added to your -`Dispatcher` in order: +[`RealField`]: https://nalgebra.org/rustdoc/nalgebra/trait.RealField.html -1. `specs_physics::systems::SyncBodiesToPhysicsSystem` - handles the -creation, modification and removal of [RigidBody]'s based on the -`PhysicsBody` `Component` and an implementation of the `Position` -*trait*. +# PhysicsBundle and its Systems -2. `specs_physics::systems::SyncCollidersToPhysicsSystem` - handles -the creation, modification and removal of [Collider]'s based on the -`PhysicsCollider` `Component`. This `System` depends on -`SyncBodiesToPhysicsSystem` as [Collider] can depend on [RigidBody]. +Besides providing nice storage types for *nphysics*, **specs-physics** takes on two major +responsibilities. -3. `specs_physics::systems::SyncParametersToPhysicsSystem` - handles the -modification of the [nphysics] `World`s parameters. +First is the stepping of the simulation, which is done simply once per run in +[`PhysicsStepperSystem`] and an advanced fixed stepper is provided in [`PhysicsBatchSystem`]. +Second is the synchronization to the [`Pose`] type mentioned above, which is performed in the +[`PhysicsPoseSystem`]. -4. `specs_physics::systems::PhysicsStepperSystem` - handles the progression -of the [nphysics] `World` and causes objects to actually move and -change their position. This `System` is the backbone for collision -detection. +The single step and pose synchronization systems are set up along with the Resources they depend on +by [`PhysicsBundle`], which is the best way to initiate your **specs-physics** simulation. You can +check out the documentation on that type's page for more details, and you can find examples for +different cases in the [examples directory]. -5. `specs_physics::systems::SyncBodiesFromPhysicsSystem` - -handles the synchronisation of [RigidBody] positions and dynamics back -into the [Specs] `Component`s. This `System` also utilises the -`Position` *trait* implementation. +[`PhysicsStepperSystem`]: systems/struct.PhysicsStepperSystem.html +[`PhysicsBatchSystem`]: systems/struct.PhysicsBatchSystem.html +[`PhysicsPoseSystem`]: systems/struct.PhysicsPoseSystem.html +[examples directory]: https://github.com/amethyst/specs-physics/tree/master/examples -An example `Dispatcher` with all required `System`s: +# Interacting with the storages -```rust,no_run -use specs::DispatcherBuilder; -use specs_physics::{ - systems::{ - PhysicsStepperSystem, - PhysicsPoseSystem, - SyncBodiesToPhysicsSystem, - SyncCollidersToPhysicsSystem, - SyncParametersToPhysicsSystem, - }, - SimplePosition, -}; +So while in vanilla *nphysics* you may access your bodies, colliders, or constraint joints via +`DefaultBodySet`, `DefaultColliderSet`, or `DefaultJointConstraintSet`, this data is instead held in +[`BodyComponent`], [`ColliderComponent`], and [`JointComponent`] storages indexed with [`Entity`] +when using **specs-physics**. Sugar for inserting a new entity with a Body or Collider is provided +in [`EntityBuilderExt`] which also adds a marker component for some Body types to that body's +Entity, so that you may join over only the bodies of a specific type with automatic downcasting via +[`ReadRigidBodies`], [`WriteRigidBodies`], and [`ReadGroundBodies`], etc. -let dispatcher = DispatcherBuilder::new() - .with( - SyncBodiesToPhysicsSystem::>::default(), - "sync_bodies_to_physics_system", - &[], - ) - .with( - SyncCollidersToPhysicsSystem::>::default(), - "sync_colliders_to_physics_system", - &["sync_bodies_to_physics_system"], - ) - .with( - SyncParametersToPhysicsSystem::::default(), - "sync_gravity_to_physics_system", - &[], - ) - .with( - PhysicsStepperSystem::::default(), - "physics_stepper_system", - &[ - "sync_bodies_to_physics_system", - "sync_colliders_to_physics_system", - "sync_gravity_to_physics_system", - ], - ) - .with( - PhysicsPoseSystem::>::default(), - "sync_bodies_from_physics_system", - &["physics_stepper_system"], - ) - .build(); -``` +[`BodyComponent`]: bodies/struct.BodyComponent.html +[`ColliderComponent`]: colliders/struct.ColliderComponent.html +[`JointComponent`]: joints/struct.JointComponent.html +[`Entity`]: https://docs.rs/specs/latest/specs/struct.Entity.html +[`ReadRigidBodies`]: bodies/type.ReadRigidBodies.html +[`WriteRigidBodies`]: bodies/type.WriteRigidBodies.html +[`ReadGroundBodies`]: bodies/type.ReadGroundBodies.html -If you're using [Amethyst] Transforms directly, you'd pass the generic -arguments like so: +# Getting help -```rust,ignore -use amethyst::core::{Float, Transform}; -use specs_physics::systems::SyncBodiesToPhysicsSystem; -SyncBodiesToPhysicsSystem::::default(); -``` +If you find any bugs or would like to contribut a pull request, visit our [Github repository]. -Alternatively to building your own `Dispatcher`, you can always fall back on -the convenience function `specs_physics::physics_dispatcher()`, which -returns a configured *default* `Dispatcher` for you or -`specs_physics::register_physics_systems()` which takes a -`DispatcherBuilder` as an argument and registers the required `System`s for -you. +If you'd like to ask for help, feel free to visit the `#physics` channel on our [Discord server]! -[nalgebra]: https://nalgebra.org/ -[RigidBody]: https://www.nphysics.org/rigid_body_simulations_with_contacts/#rigid-bodies -[Collider]: https://www.nphysics.org/rigid_body_simulations_with_contacts/#colliders -[Amethyst]: https://amethyst.rs/ -[Entity hierarchy]: https://github.com/bamling/specs-physics/blob/master/examples/hierarchy.rs -[specs-hierarchy]: https://github.com/rustgd/specs-hierarchy +[Github repository]: https://github.com/amethyst/specs-physics +[Discord server]: https://discord.gg/amethyst */ #[cfg(any( @@ -274,6 +169,24 @@ extern crate log; #[macro_use] extern crate shrinkwraprs; +pub mod bodies; +pub mod colliders; +pub mod joints; +pub mod stepper; +pub mod systems; + +mod builder; +mod bundle; +mod pose; +mod world; + +pub use self::{ + builder::EntityBuilderExt, + bundle::PhysicsBundle, + pose::{Pose, SimplePosition}, + world::{ForceGeneratorSetRes, GeometricalWorldRes, MechanicalWorldRes}, +}; + pub use nalgebra; #[cfg(feature = "dim3")] @@ -286,14 +199,15 @@ pub use ncollide2d as ncollide; #[cfg(feature = "dim2")] pub use nphysics2d as nphysics; -mod builder; -mod position; -pub mod stepper; -pub mod systems; -pub mod world; +// These are separated so they appear in a relevant order. +#[doc(no_inline)] +pub use bodies::ReadRigidBodies; -pub use self::{ - builder::EntityBuilderExt, - position::{Position, SimplePosition}, - world::{BodyComponent, ColliderComponent}, -}; +#[doc(no_inline)] +pub use bodies::WriteRigidBodies; + +#[doc(no_inline)] +pub use bodies::BodyComponent; + +#[doc(no_inline)] +pub use colliders::ColliderComponent; diff --git a/src/pose.rs b/src/pose.rs new file mode 100644 index 0000000..d7cf479 --- /dev/null +++ b/src/pose.rs @@ -0,0 +1,85 @@ +use crate::{ + nalgebra::{ + base::{ + dimension::{DimNameSum, U1}, + MatrixN, + }, + RealField, + }, + nphysics::math::{Dim, Isometry, Point}, +}; + +use specs::{Component, DenseVecStorage}; + +/** +Implement this for the Component type you want to sync transformation data to from the simulation. + +If an entity shares this Component and a [`BodyPartHandle`], [`PhysicsPoseSystem`] will call `sync` +with the transform data of the Body part pointed at by the handle. Otherwise, if this Component is +on an entity with a [`BodyComponent`], the first part on that Body (for non-multipart bodies, the +Body itself) will be passed to `sync`. [`SimplePosition`] is provided as a simpl utility type which +implements this trait. +*/ +pub trait Pose: Component + Send + Sync { + fn sync(&mut self, pose: &Isometry); +} + +// TODO: 64 bit implementation for amethyst +#[cfg(all(feature = "amethyst", feature = "dim3"))] +impl Pose for amethyst::core::Transform { + fn sync(&mut self, pose: &Isometry) { + *self.isometry_mut() = *pose; + } +} + +#[cfg(all(feature = "amethyst", feature = "dim2"))] +impl Pose for amethyst::core::Transform { + fn sync(&mut self, pose: &Isometry) { + let euler = self.rotation().euler_angles(); + self.set_rotation_euler(euler.0, euler.1, pose.rotation.angle()); + self.set_translation_x(pose.translation.x); + self.set_translation_y(pose.translation.y); + } +} + +/// A utility type you may use for synchronizing poses from the simulation. +/// +/// Like any `Pose` data, you should not modify this component +/// directly when it is being synchronized to, +/// but should instead modify the `Body` relating to this component. +#[derive(Copy, Clone, Debug, PartialEq, Hash)] +pub struct SimplePosition(pub Isometry); + +impl SimplePosition { + /// Sugar for retreiving the transformation matrix of the position. + pub fn matrix(&self) -> MatrixN> { + self.0.to_homogeneous() + } + + /// Sugar for retreiving the translational component of the position. + pub fn translation(&self) -> Point { + self.0.translation.vector.into() + } + + /// In 2D, the rotation angle within the range `[-pi, pi]`. + /// In 3D, the rotation angle of the unit quaternion in the range `[0, pi]`. + pub fn angle(&self) -> N { + self.0.rotation.angle() + } +} + +impl Pose for SimplePosition { + fn sync(&mut self, pose: &Isometry) { + self.0 = *pose; + } +} + +impl Component for SimplePosition { + type Storage = DenseVecStorage; +} + +impl Default for SimplePosition { + fn default() -> Self { + Self(Isometry::identity()) + } +} diff --git a/src/position.rs b/src/position.rs deleted file mode 100644 index ed636e3..0000000 --- a/src/position.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::{ - nalgebra::RealField, - nphysics::math::{Isometry, Point}, -}; - -use specs::{Component, DenseVecStorage, FlaggedStorage}; - -/// An implementation of the `Position` trait is required for the -/// synchronisation of the position of Specs and nphysics objects. -/// -/// Initially, it is used to position bodies in the nphysics `World`. Then after -/// progressing the `World` it is used to synchronise the updated positions back -/// towards Specs. -pub trait Pose: Component + Send + Sync { - fn sync(&mut self, pose: &Isometry); -} - -#[cfg(all(feature = "amethyst", feature = "dim3"))] -impl Pose for amethyst::core::Transform { - fn sync(&mut self, pose: &Isometry) { - *self.isometry_mut() = pose; - } -} - -#[cfg(all(feature = "amethyst", feature = "dim2"))] -impl Pose for amethyst::core::Transform { - fn sync(&mut self, pose: &Isometry) { - let euler = self.rotation().euler_angles(); - self.set_rotation_euler(euler.0, euler.1, pose.rotation.angle()); - self.set_translation_x(pose.translation.x); - self.set_translation_y(pose.translation.y); - } -} - -// TODO: 64 bit implementation for amethyst - -#[derive(Copy, Clone, Debug, PartialEq, Hash, Shrinkwrap)] -#[shrinkwrap(mutable)] -pub struct SimplePosition(pub Isometry); - -impl SimplePosition { - /// Helper function to extract the location of this `Position`. Using - /// `Position::isometry()` is preferable, but can be harder to work - /// with. The translation of this `Position` can be set using `Position: - /// :isometry_mut()`. - fn translation(&self) -> Point { - self.0.translation.vector.into() - } - - /// Helper function to extract the rotation of this `Position`. Using - /// `Position::isometry()` is preferable, but can be harder to work - /// with. The rotation of this `Position` can be set using `Position:: - /// isometry_mut()`. This is only available when the `physics2d` feature is - /// enabled. - #[cfg(feature = "dim2")] - fn angle(&self) -> N { - self.0.rotation.angle() - } -} - -impl Position for SimplePosition { - fn isometry(&self) -> &Isometry { - &self.0 - } - - fn isometry_mut(&mut self) -> &mut Isometry { - &mut self.0 - } -} - -impl Component for SimplePosition { - type Storage = FlaggedStorage>; -} - -impl Default for SimplePosition { - fn default() -> Self { - Self(Isometry::identity()) - } -} diff --git a/src/stepper.rs b/src/stepper.rs deleted file mode 100644 index 13a7e89..0000000 --- a/src/stepper.rs +++ /dev/null @@ -1,499 +0,0 @@ -/*! -# Stepper Data Module - -This module contains types exclusively used for the specs-physics -fixed stepping implementation. If you choose to use some other way to -perform fixed stepping, such as using Amethyst's fixed dispatcher instead, -you can simply ignore this module. - - -*/ -use std::{ - fmt, - time::{Duration, Instant}, -}; - -#[derive(Copy, Clone, Debug)] -pub enum TimeError { - WrongStepType, - OutOfSemiStepBounds, -} - -impl fmt::Display for TimeError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - formatter, - "{}", - match self { - TimeError::WrongStepType => - r#"Operation performed on wrong step type. -Did you use a fixed timestep when you intended to use a semi-fixed timstep?"#, - TimeError::OutOfSemiStepBounds => "Step index not in bounds of step list.", - } - ) - } -} - -pub struct StepperRes { - /// When set to Some, the Batch system will only execute that many steps in - /// one dispatch/frame. Useful for preventing death spirals in the stepper - /// when your application does not require synchronization. In such cases - /// where synchronization of physics state is performed, you should - /// instead watch the value of `BatchTimeRes::accumulator()` and - /// invalidate local state after crossing a certain threshold. - pub max_steps_per_frame: Option, - - /// When set to Some, the Batch system will postpone remaining steps to the - /// next dispatch/frame after stepping for this time or longer. Useful for - /// preventing death spirals in the stepper when your application does - /// not require synchronization. In such cases where synchronization of - /// physics state is performed, you should instead watch the value of - /// `BatchTimeRes::accumulator()` and invalidate local state after - /// crossing a certain threshold. - pub frame_time_limit: Option, - - // Tracks how far "behind" physics time we are - accumulator: Duration, - - // Timestep interval state data - time_step: TimeStep, - - // Whether the MechanicalWorld timestep should be updated - time_step_dirty: bool, - - // Number of steps since start - // Safety: Given a liberal timestep of 120hz, this would take 4.8b years to saturate. - // A u32 on the other hand would take 414 days to saturate. War Boys voice MEDIOCRE! - global_steps: u64, - - // Steps in current iteration - // Safety: Let's pray that nobody has a frame that takes 414 days to render. WITNESS ME! - frame_steps: u32, - - frame_start: Option, -} - -impl StepperRes { - pub fn new_fixed(interval: u32) -> Self { - Self::new_fixed_exact(Duration::from_secs(1) / interval, None, None) - } - - pub fn new_fixed_exact( - step_delta: Duration, - max_steps_per_frame: Option, - frame_time_limit: Option, - ) -> Self { - Self { - max_steps_per_frame, - frame_time_limit, - accumulator: Duration::default(), - time_step: TimeStep::Fixed(step_delta), - time_step_dirty: true, - global_steps: 0, - frame_steps: 0, - frame_start: None, - } - } - - pub fn set_fixed_time_step(&mut self, interval: u32) { - self.time_step = TimeStep::Fixed(Duration::from_secs(1) / interval); - self.time_step_dirty = true; - } - - /// Creates a new resource instance utilizing a semi-fixed timestep method. - pub fn new_semi_fixed( - steps: &[u32], - minimum_time_running_slow: Option, - minimum_time_running_fast: Option, - max_steps_per_frame: Option, - frame_time_limit: Option, - ) -> Self { - Self::new_semi_fixed_exact( - steps.iter().map(|x| Duration::from_secs(1) / *x).collect(), - minimum_time_running_slow, - minimum_time_running_fast, - max_steps_per_frame, - frame_time_limit, - ) - } - - pub fn new_semi_fixed_exact( - steps: Vec, - minimum_time_running_slow: Option, - minimum_time_running_fast: Option, - max_steps_per_frame: Option, - frame_time_limit: Option, - ) -> Self { - assert!(steps.len() > 0, "No steps provided for semi-fixed timer."); - Self { - max_steps_per_frame, - frame_time_limit, - accumulator: Duration::default(), - time_step: TimeStep::SemiFixed(SemiFixedStep { - minimum_time_running_slow, - minimum_time_running_fast, - steps, - active_step: 0, - first_slow_step_in_last_series: None, - last_slow_step: None, - }), - time_step_dirty: true, - global_steps: 0, - frame_steps: 0, - frame_start: None, - } - } - - /// When utilizing the semi-fixed timestep method, attempts to switch to the - /// step at `index` in the list of steps, optionally changing the state of - /// the qualifier for switching steps. - /// - /// The following are the options for `qualifier_mod`: - /// - `None` - Do nothing. If there were timestamps for when slow frames - /// were occuring they are left alone. - /// - `SemiFixedQualifierState::NoPostponement` - Remove any timestamps for - /// slow frames, setting the qualifier state as though no frames have - /// recently been postponed. This is what is done internally when the - /// `minimum_time_running_fast` duration is hit, if it is set, and the - /// `index` is subsequently brought to zero. - /// - `SemiFixedQualifierState::StartPostponementNow(u64)` - Update - /// timestamps for slow frames to begin new slow period right now, stamped - /// with the u64 frame number. This is what is done internally when the - /// `minimum_time_running_slow` duration is hit, if it is set. - pub fn semi_fixed_switch_to_step( - &mut self, - index: usize, - qualifier_mod: Option, - ) -> Result { - match &mut self.time_step { - TimeStep::Fixed(_) => Err(TimeError::WrongStepType), - TimeStep::SemiFixed(step_data) => { - if index < step_data.steps.len() { - step_data.active_step = index; - self.time_step_dirty = true; - match qualifier_mod { - Some(SemiFixedQualifierState::NoPostponement) => { - step_data.last_slow_step = None; - step_data.first_slow_step_in_last_series = None; - } - Some(SemiFixedQualifierState::StartPostponementNow(frame)) => { - let tuple = Some((frame, Instant::now())); - step_data.last_slow_step = tuple.clone(); - step_data.first_slow_step_in_last_series = tuple; - } - _ => {} - } - Ok(step_data.steps[index]) - } else { - Err(TimeError::OutOfSemiStepBounds) - } - } - } - } - - pub fn accumulator(&self) -> Duration { - self.accumulator - } - - pub fn time_step(&self) -> Duration { - match &self.time_step { - TimeStep::Fixed(duration) => *duration, - TimeStep::SemiFixed(data) => data.steps[data.active_step], - } - } - - pub fn frame_steps(&self) -> u32 { - self.frame_steps - } - - pub fn global_steps(&self) -> u64 { - self.global_steps - } -} - -// That's right, I'm clever. -impl Iterator for StepperRes { - type Item = Step; - - fn next(&mut self) -> Option { - self.time_step_dirty = false; - - // Initialize frame. - if self.frame_start.is_none() || self.frame_steps == 0 { - self.frame_steps = 0; - self.accumulator += match self.frame_start { - Some(instant) => instant.elapsed(), - None => self.time_step(), - }; - self.frame_start = Some(Instant::now()); - } - - let current_frame_delta = self.time_step(); - if let Some(slow_frame_error) = SlowFrameError::check_for( - self.frame_steps, - self.frame_start - .expect( - "Frame start stamp should've been initialized at the beginning of this frame.", - ) - .elapsed(), - self.max_steps_per_frame, - self.frame_time_limit, - ) { - // We're running slow and the user's requested - // we postpone steps to the next frame. - if let TimeStep::SemiFixed(step_data) = &mut self.time_step { - if step_data.degrade_from_being_slow(self.global_steps, slow_frame_error) { - self.time_step_dirty = true; - } - } else { - warn!("Physics stepping is falling behind. {}", slow_frame_error); - } - - // Signal end of stepping due to postponement. - self.frame_steps = 0; - None - } else if self.accumulator >= current_frame_delta { - // Drain the accumulator by the length of our timestep. - self.frame_steps += 1; - self.global_steps += 1; - self.accumulator -= current_frame_delta; - - Some(Step { - delta: current_frame_delta, - step_dirty: self.time_step_dirty, - accumulator_remaining: self.accumulator, - global_step_number: self.global_steps, - frame_step_number: self.frame_steps, - }) - } else { - if let TimeStep::SemiFixed(step_data) = &mut self.time_step { - if step_data.upgrade_from_being_fast(self.global_steps) { - self.time_step_dirty = true; - } - } - - // Signal end of stepping due to exhaustion of accumulator. - None - } - } -} - -impl Default for StepperRes { - fn default() -> Self { - StepperRes::new_fixed(60) - } -} - -/// Affect to be applied to state of rate change qualifier via -/// `BatchTimeRes::semi_fixed_switch_to_step`. -#[derive(Debug, Clone, Copy)] -pub enum SemiFixedQualifierState { - /// Remove any timestamps for slow frames, setting the qualifier state as - /// though no frames have recently been postponed. This is what is done - /// internally when the `minimum_time_running_fast` duration is hit, if it - /// is set, and the `index` is subsequently brought to zero. - NoPostponement, - /// Update timestamps for slow frames to begin new slow period right now, - /// stamped with the u64 frame number. This is what is done internally when - /// the `minimum_time_running_slow` duration is hit, if it is set. - StartPostponementNow(u64), -} - -/// Data associated with a single step performed by `::next`. -#[derive(Debug, Default, Clone)] -pub struct Step { - delta: Duration, - step_dirty: bool, - accumulator_remaining: Duration, - global_step_number: u64, - frame_step_number: u32, -} - -impl Step { - /// Delta time of the current physics step. - /// If you're using a fixed step, this will always be the same. - pub fn delta(&self) -> Duration { - self.delta - } - - /// Time left in the delta accumulator for the stepper. - pub fn accumulator_remaining(&self) -> Duration { - self.accumulator_remaining - } - - /// Whether the delta of the step has been modified. - pub fn step_dirty(&self) -> bool { - self.step_dirty - } - - /// How many physics steps have occured globally - pub fn global_step_number(&self) -> u64 { - self.global_step_number - } - - /// How many physics steps have occured in this frame. - pub fn frame_step_number(&self) -> u32 { - self.frame_step_number - } - - pub(crate) fn update(&mut self, other: Self) { - self.delta = other.delta; - self.step_dirty = other.step_dirty; - self.accumulator_remaining = other.accumulator_remaining; - self.global_step_number = other.global_step_number; - self.frame_step_number = other.frame_step_number; - } -} - -enum TimeStep { - Fixed(Duration), - SemiFixed(SemiFixedStep), -} - -struct SemiFixedStep { - minimum_time_running_slow: Option, - minimum_time_running_fast: Option, - steps: Vec, - active_step: usize, - last_slow_step: Option<(u64, Instant)>, - first_slow_step_in_last_series: Option<(u64, Instant)>, -} - -impl SemiFixedStep { - fn degrade_from_being_slow( - &mut self, - step_number: u64, - slow_frame_error: SlowFrameError, - ) -> bool { - let tuple = Some((step_number, Instant::now())); - - if self.first_slow_step_in_last_series.is_none() { - warn!( - "Physics stepping is starting to fall behind. {}", - slow_frame_error - ); - - self.first_slow_step_in_last_series = tuple.clone(); - self.last_slow_step = tuple; - - false - } else if self.last_slow_step.is_some() - && self.minimum_time_running_slow.map_or(false, |minimum| { - self.last_slow_step.unwrap().1.elapsed() > minimum - }) - { - if self.active_step >= self.steps.len() - 1 { - error!( - r#"Physics stepping has fallen beyond the threshold -for setting the step to a lower level. However, -there are no lower level step rates to fall back to. {}"#, - slow_frame_error - ); - - self.last_slow_step = tuple; - - false - } else { - warn!( - r#"Physics stepping has fallen beyond the threshold -and is lowering the step rate to the next level. {}"#, - slow_frame_error - ); - - self.first_slow_step_in_last_series = tuple.clone(); - self.last_slow_step = tuple; - self.active_step += 1; - - true - } - } else { - // We've fallen behind, but not yet triggered a threshold to change step rate. - warn!("Physics stepping has fallen behind. {}", slow_frame_error); - - self.last_slow_step = tuple; - - false - } - } - - fn upgrade_from_being_fast(&mut self, step_number: u64) -> bool { - if self.minimum_time_running_fast.is_some() - && self.active_step > 0 - && (self.last_slow_step.is_none() - || self.last_slow_step.unwrap().1.elapsed() - > self.minimum_time_running_fast.unwrap()) - { - self.active_step -= 1; - - if self.active_step == 0 { - info!("Physics stepping has resumed base stepping rate."); - - self.last_slow_step = None; - self.first_slow_step_in_last_series = None; - } else { - info!( - "Physics stepping has upgraded to the stepping rate index {}.", - self.active_step - ); - - let tuple = Some((step_number, Instant::now())); - self.first_slow_step_in_last_series = tuple.clone(); - self.last_slow_step = tuple; - } - - true - } else { - false - } - } -} - -struct SlowFrameError(bool, Option); - -impl SlowFrameError { - fn check_for( - frame_steps: u32, - frame_duration: Duration, - max_steps_per_frame: Option, - frame_time_limit: Option, - ) -> Option { - // Check if we've ran past the frame step limit - let max_steps_failure = max_steps_per_frame.map_or(false, |max| frame_steps >= max); - - // Check if we've ran past the frame time limit, and calculate how much by - let frame_time_limit_failure = frame_time_limit.and_then(|limit| { - if frame_duration > limit { - Some(frame_duration - limit) - } else { - None - } - }); - - if max_steps_failure || frame_time_limit_failure.is_some() { - Some(SlowFrameError(max_steps_failure, frame_time_limit_failure)) - } else { - None - } - } -} - -impl fmt::Display for SlowFrameError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - SlowFrameError(true, Some(duration)) => write!( - f, - r#"The stepper has hit the frame step limit -and exceeded the frame time limit by {:#?}."#, - duration - ), - SlowFrameError(true, None) => write!(f, "The stepper has hit the frame step limit."), - SlowFrameError(false, Some(duration)) => write!( - f, - "The stepper has exceeded the frame time limit by {:#?}.", - duration - ), - SlowFrameError(false, None) => write!(f, "Haha, just kidding."), - } - } -} diff --git a/src/stepper/mod.rs b/src/stepper/mod.rs new file mode 100644 index 0000000..3d0a338 --- /dev/null +++ b/src/stepper/mod.rs @@ -0,0 +1,78 @@ +/*! +Data types used for the optional `PhysicsBatchSystem` stepper implementation. + +If you choose to use some other way to perform fixed stepping, such as using Amethyst's fixed +dispatcher instead of [`PhysicsBatchSystem`], you can simply ignore this module. + +[`PhysicsBatchSystem`]: ../systems/struct.PhysicsBatchSystem.html +*/ + +mod resource; +mod semi_fixed_step; + +pub use resource::StepperRes; +pub use semi_fixed_step::{OutOfBoundsError, SemiFixedQualifierState, SemiFixedStep}; + +use std::{fmt, mem::drop, time::Duration}; + +/// Provides a constant fixed timestep for the stepper. +#[derive(Debug, Copy, Clone)] +pub struct FixedTimeStep(pub Duration); + +impl Default for FixedTimeStep { + fn default() -> Self { + FixedTimeStep(Duration::from_secs(1) / 60) + } +} + +impl TimeStep for FixedTimeStep { + fn current_time_step(&self) -> Duration { + self.0 + } +} + +/// A stepping implementation which decides what the timestep should be. +pub trait TimeStep: Send + Sync { + /// Returns the delta for this timestep. + fn current_time_step(&self) -> Duration; + + /// Called when the simulation is exhausting the aggregator at the indicated + /// step. + fn fast_at_step(&mut self, global_step_number: u64) { + // Used to avoid underscore prefix lol. + drop(global_step_number); + } + + /// Called when the simulation is hitting frame limits and falling behind at + /// the indicated step. + fn degraded_at_step(&mut self, global_step_number: u64, info: SlowFrameError) { + warn!( + "Physics stepping has been postponed at step {} due to slowness. {}", + global_step_number, info, + ); + } +} + +/// Error struct for slow frames. +// Max frame steps exceeded / Frame time limit excess +pub struct SlowFrameError(bool, Option); + +impl fmt::Display for SlowFrameError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SlowFrameError(true, Some(duration)) => write!( + f, + r#"The stepper has hit the frame step limit +and exceeded the frame time limit by {:#?}."#, + duration + ), + SlowFrameError(true, None) => write!(f, "The stepper has hit the frame step limit."), + SlowFrameError(false, Some(duration)) => write!( + f, + "The stepper has exceeded the frame time limit by {:#?}.", + duration + ), + SlowFrameError(false, None) => write!(f, "Haha, just kidding."), + } + } +} diff --git a/src/stepper/resource.rs b/src/stepper/resource.rs new file mode 100644 index 0000000..12d72ca --- /dev/null +++ b/src/stepper/resource.rs @@ -0,0 +1,174 @@ +use super::{FixedTimeStep, SlowFrameError, TimeStep}; + +use std::time::{Duration, Instant}; + +/** +Resource used for fixed stepping within [`PhysicsBatchSystem`]. + +Should be inserted into the *Specs* World before `PhysicsBatchSystem` is called. + +[`PhysicsBatchSystem`]: ../systems/struct.PhysicsBatchSystem.html +*/ +pub struct StepperRes { + /// When set to Some, the Batch system will only execute that many steps in + /// one dispatch/frame. Useful for preventing death spirals in the stepper + /// when your application does not require net synchronization. + pub max_steps_per_frame: Option, + + /// When set to Some, the Batch system will postpone remaining steps to the + /// next dispatch/frame after stepping for this time or longer. Useful for + /// preventing death spirals in the stepper when your application does + /// not require net synchronization. + pub frame_time_limit: Option, + + // Timestep interval state data + pub time_step: Box, + + // Tracks how far "behind" physics time we are + accumulator: Duration, + + is_dirty: bool, + last_delta: Duration, + + // Number of steps since start + // Safety: Given a liberal timestep of 120hz, this would take 4.8b years to saturate. + // A u32 on the other hand would take 414 days to saturate. War Boys voice MEDIOCRE! + global_steps: u64, + + // Steps in current iteration + // Safety: Let's pray that nobody has a frame that takes 414 days to render. WITNESS ME! + frame_steps: u32, + + // Tracks when the current frame began, + frame_start: Option, +} + +impl StepperRes { + pub fn new(time_step: T) -> Self { + Self::new_with_limits(time_step, None, None) + } + + pub fn new_fixed(interval: u32) -> Self { + Self::new(FixedTimeStep(Duration::from_secs(1) / interval)) + } + + pub fn new_with_limits( + time_step: T, + max_steps_per_frame: Option, + frame_time_limit: Option, + ) -> Self { + Self { + max_steps_per_frame, + frame_time_limit, + time_step: Box::new(time_step), + accumulator: Duration::default(), + is_dirty: true, + // This forces updating on first iteration + last_delta: Duration::from_millis(0), + global_steps: 0, + frame_steps: 0, + frame_start: None, + } + } + + pub fn accumulator(&self) -> Duration { + self.accumulator + } + + pub fn current_time_step(&self) -> Duration { + self.time_step.current_time_step() + } + + pub fn frame_steps(&self) -> u32 { + self.frame_steps + } + + pub fn global_steps(&self) -> u64 { + self.global_steps + } + + pub fn is_dirty(&self) -> bool { + self.is_dirty + } + + fn check_if_frame_slow(&self) -> Option { + // Check if we've ran past the frame step limit + let max_steps_failure = self + .max_steps_per_frame + .map_or(false, |max| self.frame_steps >= max); + + // Check if we've ran past the frame time limit, and calculate how much by + let frame_time_limit_failure = + if let Some(frame_duration) = self.frame_start.map(|x| x.elapsed()) { + self.frame_time_limit.and_then(|limit| { + if frame_duration > limit { + Some(frame_duration - limit) + } else { + None + } + }) + } else { + None + }; + + if max_steps_failure || frame_time_limit_failure.is_some() { + Some(SlowFrameError(max_steps_failure, frame_time_limit_failure)) + } else { + None + } + } +} + +// That's right, I'm clever. +impl Iterator for StepperRes { + type Item = (); + + fn next(&mut self) -> Option { + let current_frame_delta = self.current_time_step(); + self.is_dirty = false; + + // First step in frame, initialize. + if self.frame_steps == 0 || self.frame_start.is_none() { + if let Some(last_frame) = self.frame_start { + self.accumulator += last_frame.elapsed(); + } + + self.frame_steps = 0; + self.frame_start = Some(Instant::now()); + } + + if let Some(slow_frame_error) = self.check_if_frame_slow() { + // We've exhausted frame stepping limits and are running slow + + self.time_step + .degraded_at_step(self.global_steps, slow_frame_error); + + // Signal end of stepping due to postponement. + self.frame_steps = 0; + None + } else if self.accumulator >= current_frame_delta { + // We may step the simulation once, drain the accumulator. + self.frame_steps += 1; + self.global_steps += 1; + self.accumulator -= current_frame_delta; + + self.is_dirty = current_frame_delta != self.last_delta; + self.last_delta = current_frame_delta; + + Some(()) + } else { + // We've exhausted the accumulator. + self.time_step.fast_at_step(self.global_steps); + + // Signal end of stepping. + self.frame_steps = 0; + None + } + } +} + +impl Default for StepperRes { + fn default() -> Self { + Self::new(FixedTimeStep::default()) + } +} diff --git a/src/stepper/semi_fixed_step.rs b/src/stepper/semi_fixed_step.rs new file mode 100644 index 0000000..b705683 --- /dev/null +++ b/src/stepper/semi_fixed_step.rs @@ -0,0 +1,165 @@ +use std::{ + fmt, + time::{Duration, Instant}, +}; + +use super::{SlowFrameError, TimeStep}; + +/// A variable stepping algorithm for single player games or gibs. WIP. +pub struct SemiFixedStep { + minimum_time_running_slow: Option, + minimum_time_running_fast: Option, + steps: Vec, + active_step: usize, + last_slow_step: Option<(u64, Instant)>, + first_slow_step_in_last_series: Option<(u64, Instant)>, +} + +impl TimeStep for SemiFixedStep { + fn current_time_step(&self) -> Duration { + self.steps[self.active_step] + } + + fn fast_at_step(&mut self, global_step_number: u64) { + if self.minimum_time_running_fast.is_some() + && self.active_step > 0 + && (self.last_slow_step.is_none() + || self.last_slow_step.unwrap().1.elapsed() + > self.minimum_time_running_fast.unwrap()) + { + self.active_step -= 1; + + if self.active_step == 0 { + info!("Physics stepping has resumed base stepping rate."); + + self.last_slow_step = None; + self.first_slow_step_in_last_series = None; + } else { + info!( + "Physics stepping has upgraded to the stepping rate index {}.", + self.active_step + ); + + let tuple = Some((global_step_number, Instant::now())); + self.first_slow_step_in_last_series = tuple.clone(); + self.last_slow_step = tuple; + } + } + } + + fn degraded_at_step(&mut self, global_step_number: u64, info: SlowFrameError) { + let tuple = Some((global_step_number, Instant::now())); + + if self.first_slow_step_in_last_series.is_none() { + warn!("Physics stepping is starting to fall behind. {}", info); + + self.first_slow_step_in_last_series = tuple.clone(); + self.last_slow_step = tuple; + } else if self.last_slow_step.is_some() + && self.minimum_time_running_slow.map_or(false, |minimum| { + self.last_slow_step.unwrap().1.elapsed() > minimum + }) + { + if self.active_step >= self.steps.len() - 1 { + error!( + r#"Physics stepping has fallen beyond the threshold +for setting the step to a lower level. However, +there are no lower level step rates to fall back to. {}"#, + info + ); + + self.last_slow_step = tuple; + } else { + warn!( + r#"Physics stepping has fallen beyond the threshold +and is lowering the step rate to the next level. {}"#, + info + ); + + self.first_slow_step_in_last_series = tuple.clone(); + self.last_slow_step = tuple; + self.active_step += 1; + } + } else { + // We've fallen behind, but not yet triggered a threshold to change step rate. + warn!("Physics stepping has fallen behind. {}", info); + + self.last_slow_step = tuple; + } + } +} + +/// When utilizing the semi-fixed timestep method, attempts to switch to the +/// step at `index` in the list of steps, optionally changing the state of +/// the qualifier for switching steps. +/// +/// The following are the options for `qualifier_mod`: +/// - `None` - Do nothing. If there were timestamps for when slow frames were +/// occuring they are left alone. +/// - `SemiFixedQualifierState::NoPostponement` - Remove any timestamps for slow +/// frames, setting the qualifier state as though no frames have recently been +/// postponed. This is what is done internally when the +/// `minimum_time_running_fast` duration is hit, if it is set, and the `index` +/// is subsequently brought to zero. +/// - `SemiFixedQualifierState::StartPostponementNow(u64)` - Update timestamps +/// for slow frames to begin new slow period right now, stamped with the u64 +/// frame number. This is what is done internally when the +/// `minimum_time_running_slow` duration is hit, if it is set. +impl SemiFixedStep { + pub fn switch_to_step( + &mut self, + index: usize, + qualifier_mod: Option, + ) -> Result { + if index < self.steps.len() { + self.active_step = index; + match qualifier_mod { + Some(SemiFixedQualifierState::NoPostponement) => { + self.last_slow_step = None; + self.first_slow_step_in_last_series = None; + } + Some(SemiFixedQualifierState::StartPostponementNow(frame)) => { + let tuple = Some((frame, Instant::now())); + self.last_slow_step = tuple.clone(); + self.first_slow_step_in_last_series = tuple; + } + _ => {} + } + Ok(self.steps[index]) + } else { + Err(OutOfBoundsError) + } + } +} + +/** +Effect to be applied to state of rate change qualifier via +[`SemiFixedStep::switch_to_step`]. +*/ +#[derive(Debug, Clone, Copy)] +pub enum SemiFixedQualifierState { + /// Remove any timestamps for slow frames, setting the qualifier state as + /// though no frames have recently been postponed. This is what is done + /// internally when the `minimum_time_running_fast` duration is hit, if it + /// is set, and the `index` is subsequently brought to zero. + NoPostponement, + /// Update timestamps for slow frames to begin new slow period right now, + /// stamped with the u64 frame number. This is what is done internally when + /// the `minimum_time_running_slow` duration is hit, if it is set. + StartPostponementNow(u64), +} + +/// Error when indexing a step in [`SemiFixedStep`] which doesn't exist. +pub struct OutOfBoundsError; + +impl fmt::Debug for OutOfBoundsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Selected index is out of step bounds.") + } +} + +impl fmt::Display for OutOfBoundsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Selected index is out of step bounds.") + } +} diff --git a/src/systems/batch.rs b/src/systems/batch.rs index 2ba00db..2d634e4 100644 --- a/src/systems/batch.rs +++ b/src/systems/batch.rs @@ -1,7 +1,4 @@ -use crate::{ - nalgebra::RealField, - stepper::{Step, StepperRes}, -}; +use crate::{nalgebra::RealField, stepper::StepperRes}; use specs::{ AccessorCow, BatchAccessor, BatchController, BatchUncheckedWorld, Dispatcher, RunningTime, @@ -10,6 +7,8 @@ use specs::{ use std::{marker::PhantomData, ops::DerefMut}; +/// Provides a batch system which uses `StepperRes` to keep a contained +/// dispatcher up to time pub struct PhysicsBatchSystem<'a, 'b, N: RealField> { accessor: BatchAccessor, dispatcher: Dispatcher<'a, 'b>, @@ -17,7 +16,7 @@ pub struct PhysicsBatchSystem<'a, 'b, N: RealField> { } impl<'a, 'b, N: RealField> BatchController<'a, 'b> for PhysicsBatchSystem<'a, 'b, N> { - type BatchSystemData = (WriteExpect<'a, StepperRes>, WriteExpect<'a, Step>); + type BatchSystemData = WriteExpect<'a, StepperRes>; unsafe fn create(accessor: BatchAccessor, dispatcher: Dispatcher<'a, 'b>) -> Self { PhysicsBatchSystem { @@ -32,8 +31,7 @@ impl<'a, 'b, 's, N: RealField> System<'s> for PhysicsBatchSystem<'a, 'b, N> { type SystemData = BatchUncheckedWorld<'s>; fn run(&mut self, data: Self::SystemData) { - for step in data.0.fetch_mut::().deref_mut() { - data.0.fetch_mut::().update(step); + for _ in data.0.fetch_mut::().deref_mut() { self.dispatcher.dispatch(data.0); } } diff --git a/src/systems/mod.rs b/src/systems/mod.rs index a238928..bde3519 100644 --- a/src/systems/mod.rs +++ b/src/systems/mod.rs @@ -1,143 +1,11 @@ -use crate::{ - nalgebra::{convert as na_convert, RealField}, - nphysics::math::Vector, - position::Position, - stepper::{Step, StepperRes}, - world::{ForceGeneratorSetRes, GeometricalWorldRes, JointConstraintSetRes, MechanicalWorldRes}, -}; -use specs::{DispatcherBuilder, World}; -use std::marker::PhantomData; +/*! +Specs [`System`]s for stepping and synchronizing the simulation. + +[`System`]: https://docs.rs/specs/latest/specs/trait.System.html +*/ mod batch; mod pose; mod stepper; pub use self::{batch::PhysicsBatchSystem, pose::PhysicsPoseSystem, stepper::PhysicsStepperSystem}; - -pub struct PhysicsBundle> { - mechanical_world: MechanicalWorldRes, - geometrical_world: GeometricalWorldRes, - stepper_res: Option, - // Exercising superfluous allocations at init-time - // is better than figuring out the lifetimes - // for the slice version of this at programming-time. - stepper_deps: Vec>, - marker: PhantomData

, -} - -impl> PhysicsBundle { - pub fn new(gravity: Vector) -> Self { - Self::from_deps(gravity, &[]) - } - - pub fn stepper(interval: u32) -> Self { - Self::default().with_stepper(interval) - } - - pub fn from_deps(gravity: Vector, dep: &[&str]) -> Self { - Self::from_parts( - MechanicalWorldRes::::new(gravity), - GeometricalWorldRes::::new(), - None, - dep, - ) - } - - pub fn from_parts( - mechanical_world: MechanicalWorldRes, - geometrical_world: GeometricalWorldRes, - stepper_res: Option, - dep: &[&str], - ) -> Self { - Self { - mechanical_world, - geometrical_world, - stepper_res, - stepper_deps: dep.iter().map(|s| Box::from(*s)).collect(), - marker: PhantomData, - } - } - - /// Add dependencies which will be attached to the stepper, - /// to be executed before physics stepping. - pub fn with_deps(mut self, dep: &[&str]) -> Self { - self.stepper_deps = [ - self.stepper_deps - .iter() - .map(|s| s.as_ref()) - .collect::>() - .as_slice(), - dep, - ] - .concat() - .iter() - .map(|s| Box::from(*s)) - .collect(); - self - } - - pub fn with_stepper(mut self, interval: u32) -> Self { - self.stepper_res = Some(StepperRes::new_fixed(interval)); - self - } - - pub fn with_stepper_instance(mut self, stepper: StepperRes) -> Self { - self.stepper_res = Some(stepper); - self - } - - pub fn register(self, world: &mut World, builder: &mut DispatcherBuilder) { - world.insert(self.mechanical_world); - world.insert(self.geometrical_world); - - if let Some(stepper_res) = self.stepper_res { - world.insert(stepper_res); - world.insert(Step::default()); - } - - // TODO: These would be defaulted when converted to specs Storages. - world.insert(JointConstraintSetRes::::new()); - world.insert(ForceGeneratorSetRes::::new()); - - // Add PhysicsStepperSystem after all other Systems that write data to the - // nphysics World and has to depend on them; this System is used to progress the - // nphysics World for all existing objects. - builder.add( - PhysicsStepperSystem::::default(), - "physics_stepper_system", - self.stepper_deps - .iter() - .map(|s| s.as_ref()) - .collect::>() - .as_slice(), - ); - - // Add PhysicsPoseSystem last as it handles the - // synchronisation between nphysics World bodies and the Position - // components; this depends on the PhysicsStepperSystem. - builder.add( - PhysicsPoseSystem::::default(), - "physics_pose_system", - &["physics_stepper_system"], - ); - } -} - -#[cfg(feature = "amethyst")] -impl<'a, 'b, N: RealField, P: Position> amethyst::core::SystemBundle<'a, 'b> - for PhysicsBundle -{ - fn build( - self, - world: &mut World, - builder: &mut DispatcherBuilder, - ) -> Result<(), amethyst::error::Error> { - Ok(self.register(world, builder)) - } -} - -impl> Default for PhysicsBundle { - fn default() -> Self { - Self::new(Vector::::y() * na_convert::(-9.81)) - } -} diff --git a/src/systems/pose.rs b/src/systems/pose.rs index b0ce915..bceab96 100644 --- a/src/systems/pose.rs +++ b/src/systems/pose.rs @@ -1,28 +1,28 @@ -use crate::{nalgebra::RealField, position::Position, world::BodyComponent}; +use crate::{bodies::BodyComponent, nalgebra::RealField, pose::Pose}; use specs::{Join, ReadStorage, System, WriteStorage}; use std::marker::PhantomData; /// The `SyncBodiesFromPhysicsSystem` synchronised the updated position of /// the `RigidBody`s in the nphysics `World` with their Specs counterparts. This /// affects the `Position` `Component` related to the `Entity`. -pub struct PhysicsPoseSystem>(PhantomData<(N, P)>); +pub struct PhysicsPoseSystem>(PhantomData<(N, P)>); -impl<'s, N: RealField, P: Position> System<'s> for PhysicsPoseSystem { +impl<'s, N: RealField, P: Pose> System<'s> for PhysicsPoseSystem { type SystemData = (WriteStorage<'s, P>, ReadStorage<'s, BodyComponent>); - fn run(&mut self, (mut positions, bodies): Self::SystemData) { + fn run(&mut self, (mut poses, bodies): Self::SystemData) { // iterate over all PhysicBody components joined with their Positions - for (position, body) in (&mut positions, &bodies).join() { + for (pose, body) in (&mut poses, &bodies).join() { // if a RigidBody exists in the nphysics World we fetch it and update the // Position component accordingly if let Some(part) = body.part(0) { - *position.isometry_mut() = part.position(); + pose.sync(&part.position()); } } } } -impl> Default for PhysicsPoseSystem { +impl> Default for PhysicsPoseSystem { fn default() -> Self { Self(PhantomData) } diff --git a/src/systems/stepper.rs b/src/systems/stepper.rs index e3dad9f..11d640c 100644 --- a/src/systems/stepper.rs +++ b/src/systems/stepper.rs @@ -1,10 +1,10 @@ use crate::{ + bodies::BodySet, + colliders::ColliderSet, + joints::JointConstraintSet, nalgebra::{convert as na_convert, RealField}, - stepper::Step, - world::{ - BodySet, ColliderSet, ForceGeneratorSetRes, GeometricalWorldRes, JointConstraintSetRes, - MechanicalWorldRes, - }, + stepper::StepperRes, + world::{ForceGeneratorSetRes, GeometricalWorldRes, MechanicalWorldRes}, }; use specs::{Read, System, WriteExpect}; @@ -21,9 +21,9 @@ impl<'s, N: RealField> System<'s> for PhysicsStepperSystem { WriteExpect<'s, GeometricalWorldRes>, BodySet<'s, N>, ColliderSet<'s, N>, - WriteExpect<'s, JointConstraintSetRes>, + JointConstraintSet<'s, N>, WriteExpect<'s, ForceGeneratorSetRes>, - Option>, + Option>, ); fn run(&mut self, data: Self::SystemData) { @@ -40,8 +40,9 @@ impl<'s, N: RealField> System<'s> for PhysicsStepperSystem { // If we've added a batch time step resource to the world, check if we need to // update our timestep from that resource. if let Some(step_data) = step { - if step_data.step_dirty() { - mechanical_world.set_timestep(na_convert(step_data.delta().as_secs_f64())); + if step_data.is_dirty() { + mechanical_world + .set_timestep(na_convert(step_data.current_time_step().as_secs_f64())); } } @@ -49,7 +50,7 @@ impl<'s, N: RealField> System<'s> for PhysicsStepperSystem { &mut *geometrical_world, &mut body_set, &mut collider_set, - &mut *joint_constraint_set, + &mut joint_constraint_set, &mut *force_generator_set, ); } diff --git a/src/world.rs b/src/world.rs new file mode 100644 index 0000000..0035c76 --- /dev/null +++ b/src/world.rs @@ -0,0 +1,77 @@ +//! Resource type aliases for accessing the geometrical and mechanical worlds in +//! *Specs*. + +use crate::nphysics::{ + force_generator::DefaultForceGeneratorSet, + world::{GeometricalWorld, MechanicalWorld}, +}; + +use specs::Entity; + +/** +The [`MechanicalWorld`] type stored as a Resource. + +You can fetch this type in a system with `ReadExpect<'_, MechanicalWorldRes>` or +`WriteExpect<'_, MechanicalWorldRes>`. + +# Usage + +``` +# use specs::{DispatcherBuilder, World, WorldExt}; +# use specs_physics::{MechanicalWorldRes, PhysicsBundle, SimplePosition}; +# let mut world = World::new(); +# let mut builder = DispatcherBuilder::new(); +# PhysicsBundle::>::default().register(&mut world, &mut builder); +# builder.build().setup(&mut world); +let mechanical_world = world.fetch::>(); + +assert!(mechanical_world.timestep() > 0.0) +``` +*/ +pub type MechanicalWorldRes = MechanicalWorld; + +/** +The [`GeometricalWorld`] type stored as a Resource. + +You can fetch this type in a system with `ReadExpect<'_, GeometricalWorldRes>` or +`WriteExpect<'_, GeometricalWorldRes>`. + +# Usage + +``` +# use specs::{DispatcherBuilder, World, WorldExt}; +# use specs_physics::{GeometricalWorldRes, PhysicsBundle, SimplePosition}; +# let mut world = World::new(); +# let mut builder = DispatcherBuilder::new(); +# PhysicsBundle::>::default().register(&mut world, &mut builder); +# builder.build().setup(&mut world); +let geometrical_world = world.fetch::>(); + +assert!(geometrical_world.contact_events().len() >= 0) +``` +*/ +pub type GeometricalWorldRes = GeometricalWorld; + +/** +The [`DefaultForceGeneratorSet`] type stored as a Resource. (Not Recommended) + +Force generators are only sugar for executing a series of actions over the bodyset on each step, +and aren't executed on substeps. Therefor in specs usage it's probably better to just excute +force generation systems per step like your other physics systems instead of dealing with the +force generator set. + +# Usage + +``` +# use specs::{DispatcherBuilder, World, WorldExt}; +# use specs_physics::{ForceGeneratorSetRes, PhysicsBundle, SimplePosition}; +# let mut world = World::new(); +# let mut builder = DispatcherBuilder::new(); +# PhysicsBundle::>::default().register(&mut world, &mut builder); +# builder.build().setup(&mut world); +let force_gen_set = world.fetch::>(); + +assert!(force_gen_set.len() >= 0) +``` +*/ +pub type ForceGeneratorSetRes = DefaultForceGeneratorSet; diff --git a/src/world/body_set.rs b/src/world/body_set.rs deleted file mode 100644 index 31eebbf..0000000 --- a/src/world/body_set.rs +++ /dev/null @@ -1,236 +0,0 @@ -use crate::{ - nalgebra::RealField, - nphysics::object::{Body, BodySet as NBodySet, Ground, Multibody, RigidBody}, -}; - -use specs::{ - shred::{Fetch, FetchMut, MetaTable, ResourceId}, - storage::{AnyStorage, ComponentEvent, MaskedStorage, TryDefault}, - world::EntitiesRes, - BitSet, Component, DenseVecStorage, Entity, FlaggedStorage, Join, ReaderId, SystemData, World, - WorldExt, WriteStorage, -}; - -/// Handle component to a generic BodyPart, consisting of two members. -/// The first item, Entity, points at the BodyComponent for this Part. -/// The second item, usize, is the index of the Part in that Body. -pub struct BodyPartHandle(Entity, usize); - -impl Component for BodyPartHandle { - type Storage = DenseVecStorage; -} - -/// Component that marks the Body on this Entity as a RigidBody. -/// Do not attach more than one kind of Body Marker type to a Body entity. -pub struct RigidBodyMarker; - -impl Component for RigidBodyMarker { - type Storage = NullStorage; -} - -/// Component that marks the Body on this Entity as a Multibody. -/// Do not attach more than one kind of Body Marker type to a Body entity. -pub struct MultibodyMarker; - -impl Component for RigidBodyMarker { - type Storage = NullStorage; -} - -/// Component that marks the Body on this Entity as Ground. -/// Do not attach more than one kind of Body Marker type to a Body entity. -pub struct GroundMarker; - -impl Component for RigidBodyMarker { - type Storage = NullStorage; -} - -/// The handle type of Bodies passed to nphysics API type parameters receiving -/// "BodyHandle". -pub type BodyHandleType = Entity; - -/// The component type of all physics bodies. Attaching this component to your -/// entity with a `Position` component will make the syncing system synchronize -/// the isometry of the first part in that Body to your `Position` (or simply -/// the position of the body if it is a single part body). This relationship is -/// one-way, however. If you'd like to update the position of a Body, **do so -/// via this component, and not from the `Position` component**. -/// -/// If you'd like to synchronize individual parts of a body to a `Position`, you -/// should not attach a `Position` to the entity with this Component, and should -/// instead attach a `BodyPartHandle`, which points to the multipart body, to -/// the entity with the `Position` for a single part. -// Ouch! Bad allocation story here with DenseVecStorage>. -// However, this is hard if not impossible to avoid due to nphysics API limitations. -#[derive(Shrinkwrap)] -#[shrinkwrap(mutable)] -pub struct BodyComponent { - #[shrinkwrap(main_field)] - pub body: Box>, - // Bitset of entities with a BodyPartHandle to this Body - handles: BitSet, -} - -impl Component for BodyComponent { - type Storage = FlaggedStorage>; -} - -impl BodyComponent { - /// Creates a new Body Component. - /// This is made useful by inserting the returned struct - /// into the BodyComponent's Storage. - pub fn new>(body: B) -> Self { - Self { - body: Box::new(body), - handles: BitSet::new(), - } - } - - /// Attempts to cast this Body to a RigidBody. - /// Just sugar for `Body::downcast_ref()`. - pub fn as_rigid_body(&self) -> Option<&RigidBody> { - self.body.downcast_ref() - } - - /// Attempts to mutably cast this Body to a RigidBody. - /// Just sugar for `Body::downcast_mut()`. - pub fn as_rigid_body_mut(&mut self) -> Option<&mut RigidBody> { - self.body.downcast_mut() - } - - /// Attempts to cast this Body to a Multibody. - /// Just sugar for `Body::downcast_ref()`. - pub fn as_multi_body(&self) -> Option<&Multibody> { - self.body.downcast_ref() - } - - /// Attempts to mutably cast this Body to a Multibody. - /// Just sugar for `Body::downcast_mut()`. - pub fn as_multi_body_mut(&mut self) -> Option<&mut Multibody> { - self.body.downcast_mut() - } - - /// Attempts to cast this Body to Ground. - /// Just sugar for `Body::downcast_ref()`. - pub fn as_ground(&self) -> Option<&Ground> { - self.body.downcast_ref() - } - - /// Attempts to mutably cast this Body to Ground. - /// Just sugar for `Body::downcast_mut()`. - pub fn as_ground_mut(&mut self) -> Option<&mut Ground> { - self.body.downcast_mut() - } -} - -/// List of removals used by `BodySet` so that nphysics may `pop` single removal -/// events. -#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Shrinkwrap)] -#[shrinkwrap(mutable)] -pub(crate) struct BodyRemovalRes(pub Vec); - -/// Reader resource used by `BodySet` during fetching to populate -/// `BodyRemovalRes` with removal events. -#[derive(Debug, Shrinkwrap)] -#[shrinkwrap(mutable)] -pub(crate) struct BodyReaderRes(pub ReaderId); - -/// This structure is only used to pass the BodyComponent storage to nphysics -/// API's. You probably don't want to use it. unless you're using your own -/// system for stepping. -pub struct BodySet<'f, N: RealField> { - entities: Fetch<'f, EntitiesRes>, - storage: WriteStorage<'f, BodyComponent>, - removals: FetchMut<'f, BodyRemovalRes>, -} - -impl<'f, N: RealField> SystemData<'f> for BodySet<'f, N> { - fn setup(world: &mut World) { - // Setup storage for body component. - world - .entry::>>() - .or_insert_with(|| { - MaskedStorage::new( - < as Component>::Storage as TryDefault>::unwrap_default(), - ) - }); - world - .fetch_mut::>() - .register(&*world.fetch::>>()); - - // Setup resource for removal buffer. - world - .entry::() - .or_insert_with(|| BodyRemovalRes::default()); - - // Setup ComponentEvent reader resource. - if !world.has_value::() { - let reader = world.write_storage::>().register_reader(); - world.insert(BodyReaderRes(reader)); - } - } - - fn fetch(world: &'f World) -> Self { - let entities = world.read_resource::(); - let storage = world.write_storage::>(); - let mut reader = world.write_resource::(); - let mut removals = world.write_resource::(); - - for event in storage.channel().read(&mut reader) { - if let ComponentEvent::Removed(index) = event { - // Is grabbing the current entity for this index logically wrong? Maybe. - // Is doing this in SystemData::fetch morally wrong? Yes. - removals.push(entities.entity(*index)); - } - } - - Self { - entities, - storage, - removals, - } - } - - fn reads() -> Vec { - vec![ResourceId::new::()] - } - - fn writes() -> Vec { - vec![ - ResourceId::new::>>(), - ResourceId::new::(), - ResourceId::new::(), - ] - } -} - -impl<'f, N: RealField> NBodySet for BodySet<'f, N> { - type Handle = BodyHandleType; - - fn get(&self, handle: Self::Handle) -> Option<&dyn Body> { - self.storage.get(handle).map(|x| x.body.as_ref()) - } - - fn get_mut(&mut self, handle: Self::Handle) -> Option<&mut dyn Body> { - self.storage.get_mut(handle).map(|x| x.body.as_mut()) - } - - fn contains(&self, handle: Self::Handle) -> bool { - self.storage.contains(handle) - } - - fn foreach(&self, f: &mut dyn FnMut(Self::Handle, &dyn Body)) { - for (handle, body) in (&self.entities, &self.storage).join() { - f(handle, body.body.as_ref()); - } - } - - fn foreach_mut(&mut self, f: &mut dyn FnMut(Self::Handle, &mut dyn Body)) { - for (handle, body) in (&self.entities, &mut self.storage).join() { - f(handle, body.body.as_mut()); - } - } - - fn pop_removal_event(&mut self) -> Option { - self.removals.pop() - } -} diff --git a/src/world/mod.rs b/src/world/mod.rs deleted file mode 100644 index 8897ebf..0000000 --- a/src/world/mod.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::nphysics::{ - force_generator::DefaultForceGeneratorSet, - joint::DefaultJointConstraintSet, - world::{GeometricalWorld, MechanicalWorld}, -}; - -pub(crate) mod body_set; -pub(crate) mod collider_set; - -pub use body_set::{BodyComponent, BodyHandleType, BodySet}; -pub use collider_set::{ColliderComponent, ColliderHandleType, ColliderSet}; - -/// This is an alias for the nphysics mechanical world type stored in the specs -/// world. You can fetch this type from the world with -/// `ReadExpect<'_, MechanicalWorldRes>` or -/// `WriteExpect<'_, MechanicalWorldRes>`. -pub type MechanicalWorldRes = MechanicalWorld; - -/// This is an alias for the nphysics geometrical world type stored in the specs -/// world. You can fetch this type from the world with -/// `ReadExpect<'_, MechanicalWorldRes>` or -/// `WriteExpect<'_, MechanicalWorldRes>`. -pub type GeometricalWorldRes = GeometricalWorld; - -// TODO: Can probably make the JointConstraintSet a Storage. -pub type JointConstraintSetRes = DefaultJointConstraintSet; - -// TODO: Can probably make ForceGeneratorSet a Storage -// Although the usefulness may be somewhat limited? -// Investigating batch dispatch in relation to modifications in nphysics for -// execution of force generators seems a possible path forward. -// Do note, force generators may be executed in *substeps* -pub type ForceGeneratorSetRes = DefaultForceGeneratorSet; From 4a082c4591a19e8771becf385fbd71e30d2fa188 Mon Sep 17 00:00:00 2001 From: Kel Date: Sun, 29 Dec 2019 19:12:40 -0500 Subject: [PATCH 5/7] Add pose syncing --- src/systems/pose.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/systems/pose.rs b/src/systems/pose.rs index bceab96..26da37c 100644 --- a/src/systems/pose.rs +++ b/src/systems/pose.rs @@ -1,4 +1,4 @@ -use crate::{bodies::BodyComponent, nalgebra::RealField, pose::Pose}; +use crate::{bodies::{BodyComponent, BodyPartHandle}, nalgebra::RealField, pose::Pose}; use specs::{Join, ReadStorage, System, WriteStorage}; use std::marker::PhantomData; @@ -7,12 +7,26 @@ use std::marker::PhantomData; /// affects the `Position` `Component` related to the `Entity`. pub struct PhysicsPoseSystem>(PhantomData<(N, P)>); +// TODO: Add logging to me! impl<'s, N: RealField, P: Pose> System<'s> for PhysicsPoseSystem { - type SystemData = (WriteStorage<'s, P>, ReadStorage<'s, BodyComponent>); + type SystemData = ( + WriteStorage<'s, P>, + ReadStorage<'s, BodyComponent>, + ReadStorage<'s, BodyPartHandle>, + ); - fn run(&mut self, (mut poses, bodies): Self::SystemData) { - // iterate over all PhysicBody components joined with their Positions - for (pose, body) in (&mut poses, &bodies).join() { + fn run(&mut self, (mut poses, bodies, handles): Self::SystemData) { + // Iterate over all BodyPartHandles and apply their pose. + for (pose, handle) in (&mut poses, &handles).join() { + if let Some(body) = bodies.get(handle.0) { + if let Some(part) = body.part(handle.1) { + pose.sync(&part.position()); + } + } + } + + // Iterate over all Body Components without Handles and apply their pose. + for (pose, body, _) in (&mut poses, &bodies, !&handles).join() { // if a RigidBody exists in the nphysics World we fetch it and update the // Position component accordingly if let Some(part) = body.part(0) { From 8c4de549cc339a2a504c0f02bca6bfa45134ce4e Mon Sep 17 00:00:00 2001 From: Kel Date: Sat, 4 Jan 2020 22:21:43 -0500 Subject: [PATCH 6/7] Don't push dead collider insertions --- Cargo.toml | 2 +- src/colliders/set.rs | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e5cf509..50af025 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ description = "Integration with nphysics for the SPECS Parallel ECS." keywords = ["specs", "physics", "real-time"] [features] -default = ["dim3", "nightly", "amethyst"] +default = [] dim3 = ["ncollide3d", "nphysics3d"] dim2 = ["ncollide2d", "nphysics2d"] nightly = ["specs/nightly"] diff --git a/src/colliders/set.rs b/src/colliders/set.rs index c34f329..23b39c8 100644 --- a/src/colliders/set.rs +++ b/src/colliders/set.rs @@ -85,7 +85,13 @@ impl<'f, N: RealField> SystemData<'f> for ColliderSet<'f, N> { } } ComponentEvent::Inserted(index) => { - insertions.0.push(entities.entity(*index)); + let entity = entities.entity(*index); + + if entities.is_alive(entity) { + insertions.0.push(entity); + } else { + error!("Dropping collider insertion event for dead entity {}", entity); + } } _ => {} } From 5802a8ce972698c7fbd2e960239da22f3ce67c8b Mon Sep 17 00:00:00 2001 From: Kel Date: Sat, 4 Jan 2020 22:43:36 -0500 Subject: [PATCH 7/7] This is why you test before pushing --- src/colliders/set.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/colliders/set.rs b/src/colliders/set.rs index 23b39c8..6d146b5 100644 --- a/src/colliders/set.rs +++ b/src/colliders/set.rs @@ -90,7 +90,7 @@ impl<'f, N: RealField> SystemData<'f> for ColliderSet<'f, N> { if entities.is_alive(entity) { insertions.0.push(entity); } else { - error!("Dropping collider insertion event for dead entity {}", entity); + error!("Dropping collider insertion event for dead entity {:?}", entity); } } _ => {}