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 854e5fd..50af025 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,47 +9,37 @@ edition = "2018" license = "MIT" readme = "README.md" documentation = "https://docs.rs/specs-physics" -description = "nphysics integration for the Specs entity component system" - -keywords = ["specs", "nphysics", "nphysics3d"] +description = "Integration with nphysics for the SPECS Parallel ECS." +keywords = ["specs", "physics", "real-time"] [features] default = [] - -amethyst = ["amethyst_core"] +dim3 = ["ncollide3d", "nphysics3d"] +dim2 = ["ncollide2d", "nphysics2d"] +nightly = ["specs/nightly"] [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 } -objekt = "0.1.2" - -[dev-dependencies] -simple_logger = "1.2.0" -approx = "0.3.2" - -[[example]] -name = "basic" -path = "examples/basic.rs" +log = "0.4" +objekt = "0.1" +shrinkwraprs = "0.2" -[[example]] -name = "hierarchy" -path = "examples/hierarchy.rs" +specs = "0.15" +specs-hierarchy = "0.5" +nalgebra = "0.19" -[[example]] -name = "positions" -path = "examples/positions.rs" +ncollide2d = { version = "0.21", optional = true } +ncollide3d = { version = "0.21", optional = true } -[[example]] -name = "collision" -path = "examples/collision.rs" +nphysics2d = { version = "0.13", optional = true } +nphysics3d = { version = "0.13", optional = true } -[[example]] -name = "events" -path = "examples/events.rs" +[dependencies.amethyst] +git = "https://github.com/amethyst/amethyst" +optional = true +default-features = false +features = ["vulkan"] +[dev-dependencies] +simple_logger = "1.3" +approx = "0.3" +serde = { version = "1.0", features = ["derive"] } 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/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/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..2c9a001 --- /dev/null +++ b/examples/amethyst/assets/prefab/renderable.ron @@ -0,0 +1,162 @@ +#![enable(implicit_some)] + + +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 0000000..5605353 Binary files /dev/null and b/examples/amethyst/assets/texture/logo.png differ 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..dd574a8 --- /dev/null +++ b/examples/amethyst/main.rs @@ -0,0 +1,149 @@ +//! 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 Playing { + fixed_dispatcher: Dispatcher, + 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(Playing::new(self.prefab.as_ref().unwrap().clone()))) + } + Completion::Loading => Trans::None, + } + } +} + +impl SimpleState for Playing { + 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 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> { + 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; 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 diff --git a/examples/basic.rs b/examples/basic.rs index d1d6137..73217cf 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,44 +1,158 @@ -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}, + }, + BodyComponent, EntityBuilderExt, MechanicalWorldRes, PhysicsBundle, 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 + */ + // ANCHOR: dispatcher + // 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, &["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); + // ANCHOR_END: dispatcher - // 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..3ba43ac --- /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, + BodyComponent, EntityBuilderExt, MechanicalWorldRes, PhysicsBundle, 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::>::default() + .with_fixed_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 58c33c5..0000000 --- a/src/bodies.rs +++ /dev/null @@ -1,233 +0,0 @@ -use specs::{Component, DenseVecStorage, FlaggedStorage}; - -use crate::{ - nalgebra::{Isometry3, Matrix3, Point3, RealField}, - nphysics::{ - algebra::{Force3, ForceType, Velocity3}, - object::{Body, BodyHandle, BodyPart, BodyStatus, RigidBody, RigidBodyDesc}, - }, -}; - -pub mod util { - use specs::{Component, DenseVecStorage, FlaggedStorage}; - - use crate::{ - bodies::Position, - nalgebra::{Isometry3, RealField}, - }; - - 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) - } -} - -/// 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: Velocity3, - pub angular_inertia: Matrix3, - pub mass: N, - pub local_center_of_mass: Point3, - external_forces: Force3, -} - -impl Component for PhysicsBody { - type Storage = FlaggedStorage>; -} - -impl PhysicsBody { - pub fn check_external_force(&self) -> &Force3 { - &self.external_forces - } - - pub fn apply_external_force(&mut self, force: &Force3) -> &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) -> Force3 { - let value = self.external_forces; - self.external_forces = Force3::::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 -/// 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: Velocity3, - angular_inertia: Matrix3, - mass: N, - local_center_of_mass: Point3, -} - -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: Velocity3::zero(), - angular_inertia: Matrix3::zeros(), - mass: N::from_f32(1.2).unwrap(), - local_center_of_mass: Point3::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: Velocity3) -> Self { - self.velocity = velocity; - self - } - - /// Sets the `angular_inertia` value of the `PhysicsBodyBuilder`. - pub fn angular_inertia(mut self, angular_inertia: Matrix3) -> 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: Point3) -> 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: Force3::zero(), - } - } -} 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 new file mode 100644 index 0000000..7855aae --- /dev/null +++ b/src/builder.rs @@ -0,0 +1,90 @@ +use crate::{ + bodies::{BodyComponent, GroundMarker, MultibodyMarker, RigidBodyMarker}, + colliders::ColliderComponent, + nalgebra::RealField, + nphysics::object::{Body, BodyPartHandle, ColliderDesc}, +}; + +use specs::{world::Builder, EntityBuilder, WorldExt}; + +/** +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 { + let component = BodyComponent::new(body); + + // 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.rs b/src/colliders.rs deleted file mode 100644 index b13b808..0000000 --- a/src/colliders.rs +++ /dev/null @@ -1,324 +0,0 @@ -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, - }, -}; - -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<(Isometry3, Shape)>, - }, - ConvexHull { - points: Vec>, - }, - Cuboid { - half_extents: Vector3, - }, - HeightField { - heights: DMatrix, - scale: Vector3, - }, - Plane { - normal: Unit>, - }, - Polyline { - points: Vec>, - indices: Option>>, - }, - Segment { - a: Point3, - b: Point3, - }, - TriMesh { - handle: Box>, - }, - Triangle { - a: Point3, - b: Point3, - c: Point3, - }, -} - -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(), - )), - 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)), - Shape::TriMesh { handle } => { - let data = handle.points(); - ShapeHandle::new(TriMesh::new(data.0, data.1, data.2)) - } - Shape::Triangle { a, b, c } => ShapeHandle::new(Triangle::new(*a, *b, *c)), - } - } -} - -/// 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: Isometry3, - 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 -/// 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(); -/// ``` -pub struct PhysicsColliderBuilder { - shape: Shape, - offset_from_parent: Isometry3, - 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: Isometry3::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: Isometry3) -> 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/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/colliders/set.rs b/src/colliders/set.rs new file mode 100644 index 0000000..6d146b5 --- /dev/null +++ b/src/colliders/set.rs @@ -0,0 +1,184 @@ +use crate::{ + colliders::ColliderComponent, + 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, Entity, Join, ReaderId, SystemData, World, WorldExt, WriteStorage, +}; + +struct ColliderInsertionRes(Vec); + +struct ColliderRemovalRes(Vec<(Entity, ColliderRemovalData)>); + +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>, + 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(Vec::default())); + world + .entry::>() + .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::>() + .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.0) { + 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.0.push((entities.entity(*index), removal_data)); + } + } + } + ComponentEvent::Inserted(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); + } + } + _ => {} + } + } + + 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 = Entity; + + 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 = Entity; + + 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.0.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> { + let collider = self.storage.remove(to_remove)?; + if let Some(removal_data) = collider.removal_data() { + self.removals.0.push((to_remove, removal_data)); + self.removals.0.last_mut().map(|r| &mut r.1) + } else { + None + } + } +} diff --git a/src/events.rs b/src/events.rs deleted file mode 100644 index c32cfb3..0000000 --- a/src/events.rs +++ /dev/null @@ -1,42 +0,0 @@ -use specs::Entity; - -use crate::{ncollide::query::Proximity, 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/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 ceaccdc..b0d11f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,450 +1,213 @@ -//! ## 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 -//! 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 -//! } -//! -//! 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 -//! 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 -//! 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 -//! 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 -//! 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: -//! -//! ``` -//! use amethyst_core::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 - -#[macro_use] -extern crate log; - -pub use nalgebra; -pub use ncollide3d as ncollide; -pub use nphysics3d as nphysics; -pub use shrev; - -use std::collections::HashMap; - -use specs::{ - world::Index, - Component, - DenseVecStorage, - Dispatcher, - DispatcherBuilder, - Entity, - FlaggedStorage, -}; -use specs_hierarchy::Parent; - -pub use self::{ - bodies::{util::SimplePosition, PhysicsBody, PhysicsBodyBuilder}, - colliders::{PhysicsCollider, PhysicsColliderBuilder}, -}; - -use self::{ - bodies::Position, - nalgebra::{RealField, Vector3}, +/*! +**For when you want some [nphysics] in your [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. + +[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 + +# Usage + +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 [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`]. + +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::{ - counters::Counters, - material::MaterialsCoefficientsTable, - object::{BodyHandle, ColliderHandle}, - solver::IntegrationParameters, - world::World, - }, - systems::{ - PhysicsStepperSystem, - SyncBodiesFromPhysicsSystem, - SyncBodiesToPhysicsSystem, - SyncCollidersToPhysicsSystem, - SyncParametersToPhysicsSystem, + math::{Force, ForceType, Vector}, + object::Body }, + PhysicsBundle, + SimplePosition }; -pub mod bodies; -pub mod colliders; -pub mod events; -pub mod parameters; -pub mod systems; +fn main() { + // Initialize world and dispatcher + let mut world = World::new(); + let mut dispatcher_builder = DispatcherBuilder::new() + .with(MyPhysicsSystem, "my_physics_system", &[]); -/// 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, + // Create a new physics bundle + PhysicsBundle::>::new(Vector::y() * -9.81, &["my_physics_system"]) + .register(&mut world, &mut dispatcher_builder); + + let mut dispatcher = dispatcher_builder.build(); + dispatcher.setup(&mut world); + + // In your application loop + dispatcher.dispatch(&world); } -// Some non-mutating methods for diagnostics and testing -impl Physics { - /// Creates a new instance of the physics structure. - pub fn new() -> Self { - Self::default() - } +struct MyPhysicsSystem; - /// Reports the internal value for the timestep. - /// See also `TimeStep` for setting this value. - pub fn timestep(&self) -> N { - self.world.timestep() - } +impl<'s> System<'s> for MyPhysicsSystem { + type SystemData = WriteRigidBodies<'s, f32>; - /// Reports the internal value for the gravity. - /// See also `Gravity` for setting this value. - pub fn gravity(&self) -> &Vector3 { - self.world.gravity() + 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); + } } +} +``` + +[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 + +# Universal Generic Parameters + +**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! + +## `N: RealField` + +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. + +## `P: Pose` + +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. + +[`RealField`]: https://nalgebra.org/rustdoc/nalgebra/trait.RealField.html + +# PhysicsBundle and its Systems + +Besides providing nice storage types for *nphysics*, **specs-physics** takes on two major +responsibilities. + +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`]. + +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]. + +[`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 + +# Interacting with the storages + +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. + +[`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 + +# Getting help + +If you find any bugs or would like to contribut a pull request, visit our [Github repository]. + +If you'd like to ask for help, feel free to visit the `#physics` channel on our [Discord server]! + +[Github repository]: https://github.com/amethyst/specs-physics +[Discord server]: https://discord.gg/amethyst +*/ + +#[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."# +); - /// 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() - } +#[macro_use] +extern crate log; - /// 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() - } +#[macro_use] +extern crate shrinkwraprs; - /// Retrieves the internal parameters for integration. - /// See also `PhysicsIntegrationParameters` for setting these parameters. - pub fn integration_parameters(&self) -> &IntegrationParameters { - self.world.integration_parameters() - } +pub mod bodies; +pub mod colliders; +pub mod joints; +pub mod stepper; +pub mod systems; - /// 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() - } -} +mod builder; +mod bundle; +mod pose; +mod world; -impl Default for Physics { - fn default() -> Self { - Self { - world: World::new(), - body_handles: HashMap::new(), - collider_handles: HashMap::new(), - } - } -} +pub use self::{ + builder::EntityBuilderExt, + bundle::PhysicsBundle, + pose::{Pose, SimplePosition}, + world::{ForceGeneratorSetRes, GeometricalWorldRes, MechanicalWorldRes}, +}; -/// 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, -} +pub use nalgebra; -impl Component for PhysicsParent { - type Storage = FlaggedStorage>; -} +#[cfg(feature = "dim3")] +pub use ncollide3d as ncollide; +#[cfg(feature = "dim3")] +pub use nphysics3d as nphysics; -impl Parent for PhysicsParent { - fn parent_entity(&self) -> Entity { - self.entity - } -} +#[cfg(feature = "dim2")] +pub use ncollide2d as ncollide; +#[cfg(feature = "dim2")] +pub use nphysics2d as nphysics; -/// Convenience function for configuring and building a `Dispatcher` with all -/// required physics related `System`s. -/// -/// # Examples -/// ``` -/// use specs_physics::bodies::util::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() -} +// These are separated so they appear in a relevant order. +#[doc(no_inline)] +pub use bodies::ReadRigidBodies; -/// 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(), - "sync_bodies_to_physics_system", - &[], - ); - - // add SyncCollidersToPhysicsSystem next with SyncBodiesToPhysicsSystem as its - // dependency - dispatcher_builder.add( - 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", - &[], - ); - - // 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(), - "physics_stepper_system", - &[ - "sync_bodies_to_physics_system", - "sync_colliders_to_physics_system", - "sync_parameters_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(), - "sync_bodies_from_physics_system", - &["physics_stepper_system"], - ); -} +#[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/parameters.rs b/src/parameters.rs deleted file mode 100644 index df8c58a..0000000 --- a/src/parameters.rs +++ /dev/null @@ -1,204 +0,0 @@ -//! # Parameters module -//! Resources for modifying the various simulation parameters of the -//! nphysics World. - -use std::ops::{Deref, DerefMut}; - -use crate::{ - nalgebra::{self as na, RealField, Scalar, Vector3}, - nphysics::solver::IntegrationParameters, -}; - -/// 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(na::convert(1.0 / 60.0)) - } -} - -/// `Gravity` is a newtype for `Vector3`. It represents a constant -/// acceleration affecting all physical objects in the scene. -#[derive(Debug, PartialEq)] -pub struct Gravity(pub Vector3); - -impl Deref for Gravity { - type Target = Vector3; - - 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(Vector3::::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: 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), - max_velocity_iterations: 8, - max_position_iterations: 3, - } - } -} 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/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 new file mode 100644 index 0000000..2d634e4 --- /dev/null +++ b/src/systems/batch.rs @@ -0,0 +1,52 @@ +use crate::{nalgebra::RealField, stepper::StepperRes}; + +use specs::{ + AccessorCow, BatchAccessor, BatchController, BatchUncheckedWorld, Dispatcher, RunningTime, + System, World, WriteExpect, +}; + +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>, + marker: PhantomData, +} + +impl<'a, 'b, N: RealField> BatchController<'a, 'b> for PhysicsBatchSystem<'a, 'b, N> { + type BatchSystemData = WriteExpect<'a, StepperRes>; + + 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 _ in data.0.fetch_mut::().deref_mut() { + 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..bde3519 100644 --- a/src/systems/mod.rs +++ b/src/systems/mod.rs @@ -1,56 +1,11 @@ -use std::ops::Deref; +/*! +Specs [`System`]s for stepping and synchronizing the simulation. -use specs::{ - storage::{ComponentEvent, MaskedStorage}, - BitSet, - Component, - ReaderId, - Storage, - Tracked, -}; +[`System`]: https://docs.rs/specs/latest/specs/trait.System.html +*/ -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); - } - } - } - - (inserted, modified, removed) -} +pub use self::{batch::PhysicsBatchSystem, pose::PhysicsPoseSystem, stepper::PhysicsStepperSystem}; diff --git a/src/systems/physics_stepper.rs b/src/systems/physics_stepper.rs deleted file mode 100644 index e63e868..0000000 --- a/src/systems/physics_stepper.rs +++ /dev/null @@ -1,146 +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}, - nalgebra::RealField, - ncollide::{events::ContactEvent as NContactEvent, world::CollisionObjectHandle}, - nphysics::world::ColliderWorld, - parameters::TimeStep, - 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..26da37c --- /dev/null +++ b/src/systems/pose.rs @@ -0,0 +1,43 @@ +use crate::{bodies::{BodyComponent, BodyPartHandle}, 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)>); + +// TODO: Add logging to me! +impl<'s, N: RealField, P: Pose> System<'s> for PhysicsPoseSystem { + type SystemData = ( + WriteStorage<'s, P>, + ReadStorage<'s, BodyComponent>, + ReadStorage<'s, BodyPartHandle>, + ); + + 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) { + pose.sync(&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..11d640c --- /dev/null +++ b/src/systems/stepper.rs @@ -0,0 +1,63 @@ +use crate::{ + bodies::BodySet, + colliders::ColliderSet, + joints::JointConstraintSet, + nalgebra::{convert as na_convert, RealField}, + stepper::StepperRes, + world::{ForceGeneratorSetRes, GeometricalWorldRes, 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>, + JointConstraintSet<'s, N>, + 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.is_dirty() { + mechanical_world + .set_timestep(na_convert(step_data.current_time_step().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 c88ffe0..0000000 --- a/src/systems/sync_bodies_from_physics.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::marker::PhantomData; - -use specs::{Join, ReadExpect, System, SystemData, World, WriteStorage}; - -use crate::{ - bodies::{PhysicsBody, Position}, - nalgebra::RealField, - Physics, -}; - -/// 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.set_isometry(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 befc8b0..0000000 --- a/src/systems/sync_bodies_to_physics.rs +++ /dev/null @@ -1,249 +0,0 @@ -use std::marker::PhantomData; - -use specs::{ - storage::ComponentEvent, - world::Index, - BitSet, - Join, - ReadStorage, - ReaderId, - System, - SystemData, - World, - WriteExpect, - WriteStorage, -}; - -use crate::{ - bodies::{PhysicsBody, Position}, - nalgebra::RealField, - 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(test)] -mod tests { - use crate::{ - nalgebra::Isometry3, - nphysics::object::BodyStatus, - 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 cab49e0..0000000 --- a/src/systems/sync_colliders_to_physics.rs +++ /dev/null @@ -1,302 +0,0 @@ -use std::marker::PhantomData; - -use specs::{ - storage::ComponentEvent, - world::Index, - Join, - ReadStorage, - ReaderId, - System, - SystemData, - World, - WriteExpect, - WriteStorage, -}; - -use crate::{ - bodies::Position, - colliders::PhysicsCollider, - nalgebra::RealField, - nphysics::object::{BodyPartHandle, ColliderDesc}, - Physics, - PhysicsParent, -}; - -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(test)] -mod tests { - use specs::prelude::*; - - use crate::{ - colliders::Shape, - nalgebra::Isometry3, - 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 cfa862a..0000000 --- a/src/systems/sync_parameters_to_physics.rs +++ /dev/null @@ -1,112 +0,0 @@ -use std::marker::PhantomData; - -use specs::{Read, System, SystemData, World, WriteExpect}; - -use crate::{ - nalgebra::RealField, - 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(test)] -mod tests { - use approx::assert_ulps_eq; - use specs::prelude::*; - - use crate::{ - nalgebra::Vector3, - 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.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;