specs-physics

[Fork] Integration of Amethyst, SPECS and Nphysics together.
git clone https://git.jojolepro.com/specs-physics.git
Log | Files | Refs | README | LICENSE

commit 9cc60f724946f91bbe1b917d3a0b394054a5c6ce
parent 564580643eb36c3f7e5397ce3bc604f98ca889f8
Author: Benjamin Amling <b.amling@tarent.de>
Date:   Thu, 30 May 2019 11:36:39 +0200

Initial commit
Diffstat:
D.gitignore | 5-----
DCargo.toml | 21---------------------
ALICENSE | 21+++++++++++++++++++++
MREADME.md | 52++--------------------------------------------------
Dexamples/amethyst.rs | 264-------------------------------------------------------------------------------
Dsrc/bodies.rs | 67-------------------------------------------------------------------
Dsrc/colliders.rs | 74--------------------------------------------------------------------------
Dsrc/lib.rs | 31-------------------------------
Dsrc/systems/mod.rs | 92-------------------------------------------------------------------------------
Dsrc/systems/physics_stepper.rs | 229-------------------------------------------------------------------------------
Dsrc/systems/sync_bodies_from_physics.rs | 125-------------------------------------------------------------------------------
Dsrc/systems/sync_bodies_to_physics.rs | 216-------------------------------------------------------------------------------
Dsrc/systems/sync_colliders_to_physics.rs | 232-------------------------------------------------------------------------------
Dsrc/systems/sync_gravity_to_physics.rs | 29-----------------------------
Dsrc/time_step.rs | 217-------------------------------------------------------------------------------
15 files changed, 23 insertions(+), 1652 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,5 +0,0 @@ -/target -**/*.rs.bk -*.iml -/.idea -Cargo.lock diff --git a/Cargo.toml b/Cargo.toml @@ -1,21 +0,0 @@ -[package] -name = "nphysics-ecs-dumb" -version = "0.1.0" -authors = ["kel <distransient@protonmail.com>"] -edition = "2018" - -#[features] -# default = [ "nphysics3d" ] - -[dependencies] -num-traits = "0.2" -derive-new = "0.5.6" -derive_builder = "0.7.0" -serde = { version = "1.0", features = ["derive"] } -log = "*" -amethyst = { git = "https://github.com/amethyst/amethyst", branch = "master", features = ["nightly"] } - -#sertmp = ser branch + ncollide3d override to changes branch -nphysics3d = { git = "https://github.com/jojolepro/nphysics", branch = "sertmp", features = ["serde"] } -ncollide3d = { git = "https://github.com/jojolepro/ncollide", branch = "changes" } -nalgebra = "0.17" diff --git a/LICENSE b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Benjamin Amling + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md @@ -1,50 +1,2 @@ -# Nphysics - Amethyst connector - -Don't use. Work in progress. Many things are incomplete! - -Currently specific to 3d Nphysics and Amethyst due to Amethyst handling of Transforms and to keep iteration quick, -although I currently plan on allowing 3d and 2d nphysics implementations to be used to configuration settings, and -both amethyst and plain specs interfaces to be exposed, also behind configuration settings. - -## System Sequence - -I'll update this as I go along. - -1. - - `"sync_bodies_to_physics_system"` - Synchronize changes to dynamics bodies to physics world - - `"sync_gravity_to_physics_system"` - Update gravity of physics world from resource -1. `"sync_colliders_to_physics_system"` - Synchronize collision items to physics world -1. `"physics_stepper_system"` - Step physics world simulation -1. `"sync_bodies_from_physics_system"` - Synchronize physics world changes back to components - - -## Current Roadmap - -Full *TODO* sheet can be found in [this nphysics issue][todo] - -Ongoing work: - -- [x] RigidBody Components -- [x] Collider Components [#2] -- [x] Proximity and Contact EventChannels [#2] -- [x] External force property [#3] -- [x] `log` based logging [#4] -- [ ] Handling Body Activation & Sleeping [#9] -- [ ] Multibody-based Component Joints [#10] -- [ ] Force generator inversion of control [#11] -- [ ] Time scale and simulation pausing [#12] - -Investigating: - -- [ ] Proximity & Curve-based external force utility -- [ ] Constraint-based Joints -- [ ] Kinematics - -[todo]: https://github.com/rustsim/nphysics/issues/149 -[#2]: https://github.com/distransient/nphysics-ecs-dumb/pull/2 -[#3]: https://github.com/distransient/nphysics-ecs-dumb/pull/3 -[#4]: https://github.com/distransient/nphysics-ecs-dumb/pull/4 -[#9]: https://github.com/distransient/nphysics-ecs-dumb/issues/9 -[#10]: https://github.com/distransient/nphysics-ecs-dumb/issues/10 -[#11]: https://github.com/distransient/nphysics-ecs-dumb/issues/11 -[#12]: https://github.com/distransient/nphysics-ecs-dumb/issues/12 +# specs_nphysics +nphysics integration for the Specs entity component system diff --git a/examples/amethyst.rs b/examples/amethyst.rs @@ -1,264 +0,0 @@ -use amethyst::assets::{Handle, Loader}; -use amethyst::core::math::{Matrix3, Vector3}; -use amethyst::core::shrev::{EventChannel, ReaderId}; -use amethyst::core::ecs::world::Builder; -use amethyst::core::ecs::Join; -use amethyst::core::{GlobalTransform, Transform, TransformBundle}; -use amethyst::input::{is_close_requested, is_key_down}; -use amethyst::renderer::{ - AmbientColor, Camera, DisplayConfig, DrawShaded, Light, Material, MaterialDefaults, MeshData, - MeshHandle, Pipeline, PointLight, PosNormTex, RenderBundle, Rgba, ScreenDimensions, Shape, - Stage, Texture, VirtualKeyCode, -}; -use amethyst::{ - Application, GameData, GameDataBuilder, SimpleState, SimpleTrans, StateData, StateEvent, Trans, -}; -use nphysics_ecs_dumb::ncollide::shape::{Ball, ShapeHandle}; -use nphysics_ecs_dumb::nphysics::material::BasicMaterial as PhysicsMaterial; -use nphysics_ecs_dumb::nphysics::math::Velocity; -use nphysics_ecs_dumb::nphysics::volumetric::Volumetric; -use nphysics_ecs_dumb::*; -use num_traits::identities::One; -use std::time::Duration; - -#[derive(Default)] -struct GameState { - pub collision_reader: Option<ReaderId<EntityContactEvent>>, -} - -impl SimpleState for GameState { - fn on_start(&mut self, data: StateData<GameData>) { - data.world.register::<DynamicBody>(); - data.world.register::<MeshData>(); - data.world.register::<Handle<Texture>>(); - - // Create a texture for using. - let texture = data - .world - .read_resource::<Loader>() - .load_from_data::<Texture, ()>( - [170.0, 170.0, 255.0, 1.0].into(), - (), - &data.world.read_resource(), - ); - - let material = Material { - albedo: texture, - ..data.world.read_resource::<MaterialDefaults>().0.clone() - }; - - // Get resolution of the screen. - let (x, y) = { - let resolution = data.world.res.fetch::<ScreenDimensions>(); - (resolution.width(), resolution.height()) - }; - - let camera_transform = Transform::from(Vector3::new(0.0, 5.0, 5.0)); - - self.collision_reader = Some( - data.world - .write_resource::<EventChannel<EntityContactEvent>>() - .register_reader(), - ); - - // Add Camera - data.world - .create_entity() - .with(Camera::standard_3d(x, y)) - .with(camera_transform) - .build(); - - // Add Light - data.world.add_resource(AmbientColor(Rgba::from([0.2; 3]))); - data.world - .create_entity() - .with(Light::Point(PointLight { - intensity: 50.0, - color: Rgba::white(), - radius: 5.0, - smoothness: 4.0, - })) - .with(Transform::from(Vector3::new(2.0, 2.0, -2.0))) - .build(); - - let sphere_shape = Shape::Sphere(32, 32).generate::<Vec<PosNormTex>>(None); - let sphere_handle: MeshHandle = data.world.read_resource::<Loader>().load_from_data( - sphere_shape, - (), - &data.world.read_resource(), - ); - - let ball = ShapeHandle::new(Ball::new(1.0)); - - // Add Sphere (todo: add many, add rigidbodies and colliders) - data.world - .create_entity() - .with(sphere_handle.clone()) - .with(material.clone()) - .with(Transform::from(Vector3::new(0.0, 15.0, -10.0))) - .with(GlobalTransform::default()) - .with(DynamicBody::new_rigidbody_with_velocity( - Velocity::linear(0.0, 1.0, 0.0), - 10.0, - Matrix3::one(), - ball.center_of_mass(), - )) - .with( - ColliderBuilder::from(ball.clone()) - .physics_material(PhysicsMaterial::default()) - .build() - .unwrap(), - ) - .build(); - - // Add ground - data.world - .create_entity() - .with(sphere_handle) - .with(material) - .with(Transform::from(Vector3::new(0.0, 0.0, -10.0))) - .with(GlobalTransform::default()) - .with( - //ColliderBuilder::from(ShapeHandle::new(Cuboid::new(Vector3::new(5.0, 1.0, 5.0)))) - ColliderBuilder::from(ball) - .physics_material(PhysicsMaterial::default()) - .build() - .unwrap(), - ) - .build(); - - //---------------------------------------------------- nphysics's ball3.rs adapted - - /*let mut physics_world = data.world.write_resource::<PhysicsWorld>(); - physics_world.set_gravity(Vector3::new(0.0, -9.81, 0.0)); - - // Material for all objects. - let material = PhysicsMaterial::default(); - let ground_shape = - ShapeHandle::new(Cuboid::new(Vector3::repeat(1.0 - 0.01))); - let ground_pos = Isometry3::new(Vector3::new(0.0, -0.5, -15.0), nalgebra::zero()); - - physics_world.add_collider( - 0.01, - ground_shape, - BodyHandle::ground(), - ground_pos, - material.clone(), - ); - let geom = ShapeHandle::new(Ball::new(1.0 - 0.01)); - let inertia = geom.inertia(1.0); - let center_of_mass = geom.center_of_mass(); - - - let pos = Isometry3::new(Vector3::new(0.0, 5.0, -15.0), nalgebra::zero()); - let handle = physics_world.add_rigid_body(pos, inertia, center_of_mass); - physics_world.add_collider( - 0.01, - geom.clone(), - handle, - Isometry3::identity(), - material.clone(), - );*/ - } - - fn handle_event( - &mut self, - data: StateData<'_, GameData<'_, '_>>, - event: StateEvent, - ) -> SimpleTrans { - if let StateEvent::Window(event) = &event { - for _ in data - .world - .read_resource::<EventChannel<EntityContactEvent>>() - .read(self.collision_reader.as_mut().unwrap()) - { - println!("Collision Event Detected."); - } - - // Exit if user hits Escape or closes the window - if is_close_requested(&event) || is_key_down(&event, VirtualKeyCode::Escape) { - return Trans::Quit; - } - - // - if is_key_down(&event, VirtualKeyCode::T) { - *data.world.write_resource::<TimeStep>() = TimeStep::Fixed(1. / 120.); - println!("Setting timestep to 1./120."); - } - - if is_key_down(&event, VirtualKeyCode::Y) { - *data.world.write_resource::<TimeStep>() = TimeStep::Fixed(1. / 60.); - println!("Setting timestep to 1./60."); - } - - if is_key_down(&event, VirtualKeyCode::S) { - *data.world.write_resource::<TimeStep>() = - TimeStep::SemiFixed(TimeStepConstraint::new( - vec![1. / 240., 1. / 120., 1. / 60.], - 0.4, - Duration::from_millis(50), - Duration::from_millis(500), - )) - } - - // Reset the example - if is_key_down(&event, VirtualKeyCode::Space) { - *( - &mut data.world.write_storage::<Transform>(), - &data.world.read_storage::<DynamicBody>(), - ) - .join() - .next() - .unwrap() - .0 - .translation_mut() = Vector3::new(0.0, 15.0, -10.0); - } - } - Trans::None - } -} - -fn main() -> amethyst::Result<()> { - amethyst::start_logger(Default::default()); - - let display_config = DisplayConfig { - title: "Amethyst + Nphysics".to_string(), - fullscreen: false, - dimensions: Some((800, 400)), - min_dimensions: Some((800, 400)), - max_dimensions: None, - icon: None, - vsync: true, - multisampling: 0, // Must be multiple of 2, use 0 to disable - visibility: true, - always_on_top: false, - decorations: true, - maximized: false, - multitouch: true, - resizable: true, - transparent: false, - loaded_icon: None, - }; - let pipe = Pipeline::build().with_stage( - Stage::with_backbuffer() - .clear_target([0.1, 0.1, 0.1, 1.0], 1.0) - .with_pass(DrawShaded::<PosNormTex>::new()), - ); - - let game_data = GameDataBuilder::default() - .with_bundle(TransformBundle::new())? - .with_bundle( - PhysicsBundle::new() - .with_dep(&["transform_system"]) - .with_timestep_iter_limit(20), - )? - .with_bundle(RenderBundle::new(pipe, Some(display_config)))?; - - let application = Application::new("./", GameState::default(), game_data); - - assert_eq!(application.is_ok(), true); - - application.ok().unwrap().run(); - - Ok(()) -} diff --git a/src/bodies.rs b/src/bodies.rs @@ -1,67 +0,0 @@ -use amethyst::ecs::{Component, FlaggedStorage}; -use nalgebra::Matrix3; -use nphysics3d::math::{Force, Point, Velocity}; -use nphysics3d::object::{BodyHandle, BodyStatus}; - -/// Rigid physics body, for use in `PhysicsBody` Component. -/// Currently only the velocity is read and updated at runtime. -/// The properties of mass are only written at physics body creation time. -#[derive(Serialize, Deserialize, Clone, Copy, Debug, new)] -pub struct DynamicBody { - #[serde(skip)] - #[new(default)] - pub(crate) handle: Option<BodyHandle>, - pub velocity: Velocity<f32>, - - // TODO: update these in the physics system below. - pub mass: f32, - pub angular_mass: Matrix3<f32>, - pub center_of_mass: Point<f32>, - - pub external_forces: Force<f32>, - #[serde(skip)] - pub body_status: BodyStatus, -} - -impl DynamicBody { - pub fn new_rigidbody( - mass: f32, - angular_mass: Matrix3<f32>, - center_of_mass: Point<f32>, - ) -> Self { - DynamicBody { - handle: None, - velocity: Velocity::<f32>::zero(), - mass, - angular_mass, - center_of_mass, - external_forces: Force::<f32>::zero(), - body_status: BodyStatus::Dynamic, - } - } - - pub fn new_rigidbody_with_velocity( - velocity: Velocity<f32>, - mass: f32, - angular_mass: Matrix3<f32>, - center_of_mass: Point<f32>, - ) -> Self { - DynamicBody { - handle: None, - velocity, - mass, - angular_mass, - center_of_mass, - external_forces: Force::<f32>::zero(), - body_status: BodyStatus::Dynamic, - } - } - - pub fn handle(&self) -> Option<BodyHandle> { - self.handle - } -} - -impl Component for DynamicBody { - type Storage = FlaggedStorage<Self>; -} diff --git a/src/colliders.rs b/src/colliders.rs @@ -1,74 +0,0 @@ -use amethyst::ecs::{Component, DenseVecStorage, FlaggedStorage}; -use nalgebra::Isometry3; -use ncollide::{shape::ShapeHandle, world::GeometricQueryType}; -use ncollide3d::world::CollisionGroups; -use nphysics::material::BasicMaterial; -use nphysics::object::ColliderHandle; - -#[derive(Clone, Serialize, Deserialize, Debug, new)] -pub enum ColliderType { - Collider, - Trigger, -} - -impl Default for ColliderType { - fn default() -> Self { - ColliderType::Collider - } -} - -impl ColliderType { - pub fn to_geometric_query_type( - &self, - margin: f32, - prediction: f32, - angular_prediction: f32, - ) -> GeometricQueryType<f32> { - match *self { - ColliderType::Collider => { - GeometricQueryType::Contacts(margin + prediction * 0.5, angular_prediction) - } - ColliderType::Trigger => GeometricQueryType::Proximity(prediction * 0.5), - } - } -} - -#[derive(new, Clone, Builder)] -#[builder(pattern = "owned")] -pub struct Collider { - #[new(default)] - //#[serde(skip)] - #[builder(default)] - pub(crate) handle: Option<ColliderHandle>, - /// Warning: Changing the margin after inserting the entity will have no effect. - pub margin: f32, - pub shape: ShapeHandle<f32>, - pub offset_from_parent: Isometry3<f32>, - pub physics_material: BasicMaterial<f32>, - pub collision_group: CollisionGroups, - pub query_type: ColliderType, -} - -impl From<ShapeHandle<f32>> for ColliderBuilder { - fn from(shape: ShapeHandle<f32>) -> ColliderBuilder { - ColliderBuilder::default() - .margin(0.01) - .shape(shape) - .offset_from_parent(Isometry3::identity()) - .query_type(ColliderType::default()) - .physics_material(BasicMaterial::default()) - .collision_group(CollisionGroups::default()) - } -} - -impl ColliderBuilder { - pub fn trigger(mut self) -> Self { - // query type = trigger - self.query_type = Some(ColliderType::Trigger); - self - } -} - -impl Component for Collider { - type Storage = FlaggedStorage<Self, DenseVecStorage<Self>>; -} diff --git a/src/lib.rs b/src/lib.rs @@ -1,31 +0,0 @@ -//! nphysics-ecs-dumb -//! Straight forward wrapper around nphysics to allow its usage inside of Amethyst's ECS: Specs - -pub extern crate ncollide3d as ncollide; -pub extern crate nphysics3d as nphysics; -#[macro_use] -extern crate derive_builder; -#[macro_use] -extern crate derive_new; -#[macro_use] -extern crate serde; - -#[macro_use] -extern crate log; - -pub mod bodies; -pub mod colliders; -pub mod systems; -pub mod time_step; - -pub use self::bodies::*; -pub use self::colliders::*; -pub use self::systems::*; -pub use self::time_step::*; - -/// The Physical World containing all physical objects. -pub type PhysicsWorld = self::nphysics::world::World<f32>; - -/// Gravity is a type alias for a Vector of dimension 3. -/// It represents a constant acceleration affecting all physical objects in the scene. -pub type Gravity = self::nphysics::math::Vector<f32>; diff --git a/src/systems/mod.rs b/src/systems/mod.rs @@ -1,92 +0,0 @@ -mod physics_stepper; -mod sync_bodies_from_physics; -mod sync_bodies_to_physics; -mod sync_colliders_to_physics; -mod sync_gravity_to_physics; - -use amethyst::core::bundle::SystemBundle; -use amethyst::core::ecs::DispatcherBuilder; -use amethyst::error::Error; -use core::result::Result; - -pub use self::physics_stepper::*; -pub use self::sync_bodies_from_physics::*; -pub use self::sync_bodies_to_physics::SyncBodiesToPhysicsSystem; -pub use self::sync_colliders_to_physics::SyncCollidersToPhysicsSystem; -pub use self::sync_gravity_to_physics::SyncGravityToPhysicsSystem; - -pub const SYNC_BODIES_TO_PHYSICS_SYSTEM: &str = "sync_bodies_to_physics_system"; -pub const SYNC_GRAVITY_TO_PHYSICS_SYSTEM: &str = "sync_gravity_to_physics_system"; -pub const SYNC_COLLIDERS_TO_PHYSICS_SYSTEM: &str = "sync_colliders_to_physics_system"; -pub const PHYSICS_STEPPER_SYSTEM: &str = "physics_stepper_system"; -pub const SYNC_BODIES_FROM_PHYSICS_SYSTEM: &str = "sync_bodies_from_physics_system"; - -pub struct PhysicsBundle<'a> { - dep: &'a [&'a str], - timestep_iter_limit: i32, -} - -impl Default for PhysicsBundle<'_> { - fn default() -> Self { - Self { - dep: Default::default(), - timestep_iter_limit: 10, - } - } -} - -impl<'a> PhysicsBundle<'a> { - pub fn new() -> Self { - Default::default() - } - - pub fn with_dep(mut self, dep: &'a [&'a str]) -> Self { - self.dep = dep; - self - } - - /// Set the maximum number of physics timesteps to simulate in a single run of the `PhysicsStepperSystem` - pub fn with_timestep_iter_limit(mut self, timestep_iter_limit: i32) -> Self { - self.timestep_iter_limit = timestep_iter_limit; - self - } -} - -impl<'a, 'b, 'c> SystemBundle<'a, 'b> for PhysicsBundle<'c> { - fn build(self, builder: &mut DispatcherBuilder<'a, 'b>) -> Result<(), Error> { - builder.add( - SyncBodiesToPhysicsSystem::new(), - SYNC_BODIES_TO_PHYSICS_SYSTEM, - self.dep, - ); - builder.add( - SyncGravityToPhysicsSystem::new(), - SYNC_GRAVITY_TO_PHYSICS_SYSTEM, - self.dep, - ); - - builder.add( - SyncCollidersToPhysicsSystem::new(), - SYNC_COLLIDERS_TO_PHYSICS_SYSTEM, - &[SYNC_BODIES_TO_PHYSICS_SYSTEM], - ); - - builder.add( - PhysicsStepperSystem::new(self.timestep_iter_limit), - PHYSICS_STEPPER_SYSTEM, - &[ - SYNC_BODIES_TO_PHYSICS_SYSTEM, - SYNC_GRAVITY_TO_PHYSICS_SYSTEM, - SYNC_COLLIDERS_TO_PHYSICS_SYSTEM, - ], - ); - - builder.add( - SyncBodiesFromPhysicsSystem::new(), - SYNC_BODIES_FROM_PHYSICS_SYSTEM, - &[PHYSICS_STEPPER_SYSTEM], - ); - - Ok(()) - } -} diff --git a/src/systems/physics_stepper.rs b/src/systems/physics_stepper.rs @@ -1,229 +0,0 @@ -use crate::time_step::TimeStep; -use crate::PhysicsWorld; -use amethyst::core::Time; -use amethyst::ecs::{Entity, Read, System, Write, WriteExpect}; -use amethyst::shrev::EventChannel; -use ncollide3d::events::{ContactEvent, ProximityEvent}; -use std::f32::EPSILON; -use std::time::Instant; - -// TODO: why is this here -// Might want to replace by better types. -pub type EntityContactEvent = (Entity, Entity, ContactEvent); -pub type EntityProximityEvent = (Entity, Entity, ProximityEvent); - -/// Falloff factor for calculating the moving average step time. -const AVERAGE_STEP_TIME_FALLOFF: f32 = 0.33; -/// Factor to apply to available physics time before decreasing the timestep. Makes sure that the -/// timestep isn't switched too eagerly. -const TIME_STEP_DECREASE_HYSTERESIS: f32 = 1.5; - -/// Simulates a step of the physics world. -pub struct PhysicsStepperSystem { - timestep_iter_limit: i32, - time_accumulator: f32, - avg_step_time: Option<f32>, -} - -impl Default for PhysicsStepperSystem { - fn default() -> Self { - PhysicsStepperSystem { - timestep_iter_limit: 10, - time_accumulator: 0., - avg_step_time: None, - } - } -} - -impl PhysicsStepperSystem { - pub fn new(timestep_iter_limit: i32) -> Self { - PhysicsStepperSystem { - timestep_iter_limit, - time_accumulator: 0., - avg_step_time: None, - } - } -} - -impl<'a> System<'a> for PhysicsStepperSystem { - type SystemData = ( - WriteExpect<'a, PhysicsWorld>, - Read<'a, Time>, - Write<'a, TimeStep>, - Write<'a, EventChannel<EntityContactEvent>>, - Write<'a, EventChannel<EntityProximityEvent>>, - ); - - // Simulate world using the current time frame - fn run(&mut self, data: Self::SystemData) { - let ( - mut physical_world, - time, - mut intended_timestep, - mut contact_events, - mut proximity_events, - ) = data; - - let (timestep, mut change_timestep) = match &mut *intended_timestep { - TimeStep::Fixed(timestep) => (*timestep, false), - TimeStep::SemiFixed(constraint) => { - let mut timestep = (constraint.current_timestep(), false); - if let Some(avg_step) = self.avg_step_time { - // If the timestep is smaller than it takes to simulate that step, we have a problem. - // As simulated time is affected by the time scale, simulated time step / time scale - // is the maximum real time the step may take, so we take that into account here. We - // also take into account the maximum fraction of time physics are allowed to take - let adjusted_step_time = - avg_step * time.time_scale() / constraint.max_physics_time_fraction(); - constraint.set_running_slow(constraint.current_timestep() < adjusted_step_time); - if constraint.should_increase_timestep() { - match constraint.increase_timestep() { - Err(error) => { - warn!("Failed to increase physics timestep! Error: {}", error); - } - Ok(new_timestep) => { - info!("Increasing physics timestep to {:.8} seconds", new_timestep); - timestep = (new_timestep, true); - } - } - } else if let Some(smaller_timestep) = constraint.smaller_timestep() { - // Check if we have enough time to simulate with a smaller timestep. - constraint.set_running_fast( - smaller_timestep > adjusted_step_time * TIME_STEP_DECREASE_HYSTERESIS, - ); - if constraint.should_decrease_timestep() { - match constraint.decrease_timestep() { - Err(error) => { - warn!("Failed to decrease physics timestep! Error: {}", error); - } - Ok(new_timestep) => { - info!( - "Decreasing physics timestep to {:.8} seconds", - new_timestep - ); - timestep = (new_timestep, true); - } - } - } - } - } - timestep - } - }; - - if (physical_world.timestep() - timestep).abs() > EPSILON && !change_timestep { - warn!("Physics world timestep out of sync with intended timestep! Physics timestep: {}, Requested timestep: {}", physical_world.timestep(), timestep); - change_timestep = true; - } - - if change_timestep { - trace!("Changing physics timestep to {}", timestep); - // reset average when changing timestep - self.avg_step_time = None; - physical_world.set_timestep(timestep); - } - - self.time_accumulator += time.delta_seconds(); - let mut steps = 0; - - while steps <= self.timestep_iter_limit && self.time_accumulator >= timestep { - let physics_time = Instant::now(); - - trace!( - "Stepping physics system. Step: {}, Timestep: {}, Time accumulator: {}", - steps, - timestep, - self.time_accumulator - ); - - physical_world.step(); - - trace!("iterating collision events."); - - let collision_world = physical_world.collider_world(); - - let contact_ev = collision_world.contact_events().iter().cloned().flat_map(|ev| { - trace!("Emitting contact event: {:?}", ev); - - let (handle1, handle2) = match ev { - ContactEvent::Started(h1, h2) => (h1, h2), - ContactEvent::Stopped(h1, h2) => (h1, h2), - }; - let coll1 = physical_world.collider(handle1); - let coll2 = physical_world.collider(handle2); - if let (Some(c1), Some(c2)) = (coll1, coll2) { - // TODO: Check if the data is in fact the one we want. There might be - // user-inserted one. - let e1 = c1.user_data().map(|data| data.downcast_ref::<Entity>().unwrap()); - let e2 = c2.user_data().map(|data| data.downcast_ref::<Entity>().unwrap()); - if let (Some(e1), Some(e2)) = (e1, e2) { - Some((*e1, *e2, ev)) - } else { - error!("Failed to find entity for collider during proximity event iteration. Was the entity removed?"); - None - } - } else { - error!("Failed to fetch the rigid body from the physical world using the collider handle of the collision event. Was the entity removed?."); - None - } - }).collect::<Vec<_>>(); - - contact_events.iter_write(contact_ev.into_iter()); - - let proximity_ev = collision_world - .proximity_events() - .iter() - .cloned() - .flat_map(|ev| { - trace!("Emitting proximity event: {:?}", ev); - println!("hello there"); - let coll1 = physical_world.collider(ev.collider1); - let coll2 = physical_world.collider(ev.collider2); - if let (Some(c1), Some(c2)) = (coll1, coll2) { - // TODO: Check if the data is in fact the one we want. There might be - // user-inserted one. - let e1 = c1.user_data().map(|data| data.downcast_ref::<Entity>().unwrap()); - let e2 = c2.user_data().map(|data| data.downcast_ref::<Entity>().unwrap()); - if let (Some(e1), Some(e2)) = (e1, e2) { - Some((*e1, *e2, ev)) - } else { - error!("Failed to find entity for collider during proximity event iteration. Was the entity removed?"); - None - } - } else { - error!("Failed to fetch the rigid body from the physical world using the collider handle of the collision event. Was the entity removed?."); - None - } - }).collect::<Vec<_>>(); - - proximity_events.iter_write(proximity_ev.into_iter()); - - let physics_time = physics_time.elapsed(); - let physics_time = - physics_time.as_secs() as f32 + physics_time.subsec_nanos() as f32 * 1e-9; - self.avg_step_time = Some(match self.avg_step_time { - None => physics_time, - Some(avg) => { - // calculate exponentially weighted moving average - // basic formula: AVG_n = alpha * value_n + (1 - alpha) * AVG_n-1 - avg + AVERAGE_STEP_TIME_FALLOFF * (physics_time - avg) - } - }); - self.time_accumulator -= timestep; - steps += 1; - } - - trace!( - "Average time per physics step: {:.8} seconds", - self.avg_step_time.unwrap_or_default() - ); - - if steps > self.timestep_iter_limit { - // This shouldn't normally happen. If it does, one of the following might be true: - // - TimeStep::Fixed was chosen too small - // - TimeStep::SemiFixed can't increase the timestep - // - Game itself is running slow, not leaving enough time for physics - warn!("Physics running slow!"); - } - } -} diff --git a/src/systems/sync_bodies_from_physics.rs b/src/systems/sync_bodies_from_physics.rs @@ -1,125 +0,0 @@ -use crate::bodies::DynamicBody; -use crate::colliders::Collider; -use crate::PhysicsWorld; -use amethyst::core::{GlobalTransform, Transform}; -use amethyst::ecs::world::EntitiesRes; -use amethyst::ecs::{Entities, Entity, Join, ReadExpect, ReadStorage, System, WriteStorage}; -use nalgebra::Vector3; -use nphysics3d::object::{Body, BodyPart, ColliderHandle}; - -#[derive(Default)] -pub struct SyncBodiesFromPhysicsSystem; - -impl SyncBodiesFromPhysicsSystem { - pub fn new() -> Self { - Default::default() - } -} - -impl<'a> System<'a> for SyncBodiesFromPhysicsSystem { - type SystemData = ( - Entities<'a>, - ReadExpect<'a, PhysicsWorld>, - WriteStorage<'a, GlobalTransform>, - WriteStorage<'a, DynamicBody>, - WriteStorage<'a, Transform>, - ); - - fn run(&mut self, data: Self::SystemData) { - let ( - _entities, - physical_world, - mut global_transforms, - mut physics_bodies, - mut local_transforms, - ) = data; - - trace!("Synchronizing bodies from physical world."); - - // Apply the updated values of the simulated world to our Components - #[allow(unused_mut)] - for (mut global_transform, mut body, mut local_transform) in ( - &mut global_transforms, - &mut physics_bodies, - (&mut local_transforms).maybe(), - ) - .join() - { - if let Some(updated_body_handle) = body.handle() { - if let Some(updated_body) = physical_world.rigid_body(updated_body_handle) { - if !updated_body.is_active() || updated_body.is_static() { - trace!( - "Skipping synchronizing data from non-dynamic body: {:?}", - updated_body.handle() - ); - continue; - } - - trace!( - "Synchronizing RigidBody from handle: {:?}", - updated_body.handle() - ); - - trace!( - "Synchronized RigidBody's updated position: {:?}", - updated_body.position() - ); - - global_transform.0 = updated_body - .position() - .to_homogeneous() - .prepend_nonuniform_scaling( - &local_transform - .as_ref() - .map(|tr| *tr.scale()) - .unwrap_or_else(|| Vector3::new(1.0, 1.0, 1.0)), - ); - - if let Some(ref mut local_transform) = local_transform { - *local_transform.isometry_mut() = *updated_body.position(); - } - - trace!( - "Synchronized RigidBody's updated velocity: {:?}", - updated_body.velocity() - ); - body.velocity = *updated_body.velocity(); - - trace!( - "Synchronized RigidBody's updated inertia: {:?}", - updated_body.inertia() - ); - let inertia = updated_body.inertia(); - body.mass = inertia.linear; - body.angular_mass = inertia.angular; - - trace!( - "Synchronized RigidBody's updated center of mass: {:?}", - updated_body.center_of_mass() - ); - body.center_of_mass = updated_body.center_of_mass(); - } else { - error!("Found body without pair in physics world!"); - } - } else { - error!("Found body without handle!"); - } - } - } -} - -// TODO: why is this here -pub fn entity_from_handle( - entities: &EntitiesRes, - colliders: &ReadStorage<Collider>, - handle: ColliderHandle, -) -> Option<Entity> { - (&*entities, colliders) - .join() - .find(|(_, c)| { - c.handle - .expect("Collider has no handle and wasn't removed.") - == handle - }) - .map(|(e, _)| e) -} diff --git a/src/systems/sync_bodies_to_physics.rs b/src/systems/sync_bodies_to_physics.rs @@ -1,216 +0,0 @@ -use crate::bodies::DynamicBody; -use crate::PhysicsWorld; -use amethyst::core::GlobalTransform; -use amethyst::ecs::storage::{ComponentEvent, GenericReadStorage, MaskedStorage}; -use amethyst::ecs::{ - BitSet, Component, Entities, Join, ReadStorage, ReaderId, Resources, Storage, System, - SystemData, Tracked, WriteExpect, WriteStorage, -}; -use core::ops::Deref; -use nalgebra::try_convert; -use nalgebra::Isometry3; -use nphysics3d::math::Isometry; - -use nphysics3d::object::{Body, RigidBodyDesc}; - -#[derive(Default)] -pub struct SyncBodiesToPhysicsSystem { - transforms_reader_id: Option<ReaderId<ComponentEvent>>, - physics_bodies_reader_id: Option<ReaderId<ComponentEvent>>, -} - -impl SyncBodiesToPhysicsSystem { - pub fn new() -> Self { - Default::default() - } -} - -impl<'a> System<'a> for SyncBodiesToPhysicsSystem { - type SystemData = ( - WriteExpect<'a, PhysicsWorld>, - Entities<'a>, - ReadStorage<'a, GlobalTransform>, - WriteStorage<'a, DynamicBody>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (mut physical_world, entities, transforms, mut physics_bodies) = data; - - let mut inserted_transforms = BitSet::new(); - let mut modified_transforms = BitSet::new(); - let mut inserted_physics_bodies = BitSet::new(); - let mut modified_physics_bodies = BitSet::new(); - - // Get change flag events for transforms, removing deleted ones from the physics world. - trace!("Iterating transform storage events."); - iterate_events( - &transforms, - self.transforms_reader_id.as_mut().unwrap(), - &mut inserted_transforms, - &mut modified_transforms, - &mut physical_world, - &entities, - &physics_bodies, - ); - - // Get change flag events for physics bodies, removing deleted ones from the physics world. - trace!("Iterating physics body storage events."); - iterate_events( - &physics_bodies, - self.physics_bodies_reader_id.as_mut().unwrap(), - &mut inserted_physics_bodies, - &mut modified_physics_bodies, - &mut physical_world, - &entities, - &physics_bodies, - ); - - // Update simulation world with the value of Components flagged as changed - #[allow(unused_mut)] - for (entity, transform, mut body, id) in ( - &entities, - &transforms, - &mut physics_bodies, - &modified_transforms - | &inserted_transforms - | &modified_physics_bodies - | &inserted_physics_bodies, - ) - .join() - { - if inserted_transforms.contains(id) || inserted_physics_bodies.contains(id) { - trace!("Detected inserted dynamics body with id {}", id); - - // Just inserted. Remove old one and insert new. - if let Some(handle) = body.handle { - if physical_world.rigid_body(handle).is_some() { - trace!("Removing body marked as inserted that already exists with handle: {:?}", handle); - physical_world.remove_bodies(&[handle]); - } - } - - /*body.handle = Some(physical_world.add_rigid_body( - try_convert(transform.0).unwrap(), - Inertia::new(body.mass, body.angular_mass), - body.center_of_mass, - ));*/ - - let iso: Isometry3<f32> = try_convert(transform.0).unwrap(); - - body.handle = Some( - RigidBodyDesc::new() - .position(iso) - //.gravity_enabled(false) - .status(body.body_status) - //.name("my rigid body".to_owned()) - .velocity(body.velocity) - //.angular_inertia(3.0) - .mass(1.2) - //.local_inertia(Inertia::new(1.0, 3.0)) - .local_center_of_mass(body.center_of_mass) - //.sleep_threshold(None) - //.kinematic_translations(Vector2::new(true, false)) - //.kinematic_rotation(true) - .user_data(entity) - .build(&mut physical_world) - .handle(), - ); - - trace!("Inserted rigid body to world with values: {:?}", body); - - //let physical_body = physical_world.rigid_body_mut(body.handle.unwrap()).unwrap(); - - //physical_body.set_velocity(body.velocity); - - // TODO - //physical_body.apply_force(&body.external_forces); - //body.external_forces = Force::<f32>::zero(); - - trace!("Velocity and external forces applied, external forces reset to zero, for body with handle: {:?}", body.handle); - } else if modified_transforms.contains(id) || modified_physics_bodies.contains(id) { - trace!("Detected changed dynamics body with id {}", id); - if let Some(physical_body) = physical_world.rigid_body_mut(body.handle.unwrap()) { - // if you changed the mass properties at all... too bad! - match try_convert(transform.0) { - Some(p) => { - let position: Isometry<f32> = p; - trace!( - "Updating rigid body in physics world with isometry: {}", - position - ); - physical_body.set_position(position); - - physical_body.set_velocity(body.velocity); - - // TODO - //physical_body.apply_force(&body.external_forces); - //body.external_forces = Force::<f32>::zero(); - - physical_body.set_status(body.body_status); - } - None => error!( - "Failed to convert entity position from `Transform` to physics systems" - ), - } - } - } - } - } - - fn setup(&mut self, res: &mut Resources) { - Self::SystemData::setup(res); - - let mut transform_storage: WriteStorage<GlobalTransform> = SystemData::fetch(&res); - self.transforms_reader_id = Some(transform_storage.register_reader()); - - let mut physics_body_storage: WriteStorage<DynamicBody> = SystemData::fetch(&res); - self.physics_bodies_reader_id = Some(physics_body_storage.register_reader()); - } -} - -fn iterate_events<T, D, S>( - tracked_storage: &Storage<T, D>, - reader: &mut ReaderId<ComponentEvent>, - inserted: &mut BitSet, - modified: &mut BitSet, - world: &mut PhysicsWorld, - entities: &Entities, - bodies: &S, -) where - T: Component, - T::Storage: Tracked, - D: Deref<Target = MaskedStorage<T>>, - S: GenericReadStorage<Component = DynamicBody>, -{ - let events = tracked_storage.channel().read(reader); - - for event in events { - match event { - ComponentEvent::Modified(id) => { - modified.add(*id); - } - ComponentEvent::Inserted(id) => { - inserted.add(*id); - } - ComponentEvent::Removed(id) => { - match bodies.get(entities.entity(*id)) { - Some(body) => { - match body.handle() { - Some(handle) => { - trace!("Removing body with id: {}", id); - - world.remove_bodies(&[handle]); - } - None => { - error!("Missing handle in body: {}", id); - } - }; - } - None => { - error!("Missing body with id: {}", id); - } - }; - } - }; - } -} diff --git a/src/systems/sync_colliders_to_physics.rs b/src/systems/sync_colliders_to_physics.rs @@ -1,232 +0,0 @@ -use crate::bodies::DynamicBody; -use crate::Collider; -use crate::PhysicsWorld; -use amethyst::core::Transform; -use amethyst::ecs::storage::{ComponentEvent, GenericReadStorage, MaskedStorage}; -use amethyst::ecs::{ - BitSet, Component, Entities, Join, ReadStorage, ReaderId, Resources, Storage, System, - SystemData, Tracked, WriteExpect, WriteStorage, -}; -use core::ops::Deref; -use nphysics::material::MaterialHandle; -use nphysics::object::{BodyHandle, BodyPartHandle, ColliderDesc}; - -#[derive(Default, new)] -pub struct SyncCollidersToPhysicsSystem { - #[new(default)] - colliders_reader_id: Option<ReaderId<ComponentEvent>>, -} - -impl<'a> System<'a> for SyncCollidersToPhysicsSystem { - type SystemData = ( - WriteExpect<'a, PhysicsWorld>, - Entities<'a>, - ReadStorage<'a, Transform>, - ReadStorage<'a, DynamicBody>, - WriteStorage<'a, Collider>, - ); - - fn run(&mut self, data: Self::SystemData) { - let (mut physical_world, entities, transforms, rigid_bodies, mut colliders) = data; - // TODO: Check for inserted/removed rigid_bodies. parent sync https://nphysics.org/rustdoc/nphysics3d/world/struct.World.html?search=#method.add_collider - let mut inserted_colliders = BitSet::new(); - let mut modified_colliders = BitSet::new(); - - iterate_events( - &colliders, - self.colliders_reader_id.as_mut().unwrap(), - &mut inserted_colliders, - &mut modified_colliders, - &mut physical_world, - &entities, - &colliders, - ); - - for (entity, mut collider, id, tr) in ( - &entities, - &mut colliders, - &inserted_colliders | &modified_colliders, - &transforms, - ) - .join() - { - if inserted_colliders.contains(id) { - trace!("Detected inserted collider with id {:?}", id); - // Just inserted. Remove old one and insert new. - if collider.handle.is_some() - && physical_world.collider(collider.handle.unwrap()).is_some() - { - physical_world.remove_colliders(&[collider.handle.unwrap()]); - } - - let parent = if let Some(rb) = rigid_bodies.get(entity) { - trace!("Attaching inserted collider to rigid body: {:?}", entity); - - rb.handle.expect( - "You should normally have a body handle at this point. This is a bug.", - ) - } else { - BodyHandle::ground() - }; - let position = if parent.is_ground() { - tr.isometry() * collider.offset_from_parent - } else { - collider.offset_from_parent - }; - - //trace!("Inserted collider to world with values: {:?}", collider); - - let prediction = physical_world.prediction(); - let angular_prediction = 0.09; - - let parent_part_handle = physical_world - .rigid_body(parent) - .map(|body| body.part_handle()) - .unwrap_or_else(BodyPartHandle::ground); - - collider.handle = Some( - ColliderDesc::new(collider.shape.clone()) - .user_data(entity) - .margin(collider.margin) - .position(position) - .material(MaterialHandle::new(collider.physics_material)) - .build_with_parent(parent_part_handle, &mut physical_world) - .unwrap() - .handle(), - ); - - let collision_world = physical_world.collider_world_mut(); - - collision_world.as_collider_world_mut().set_query_type( - collider.handle.unwrap(), - collider.query_type.to_geometric_query_type( - collider.margin, - prediction, - angular_prediction, - ), - ); - - let collider_object = collision_world - .collider_mut(collider.handle.unwrap()) - .unwrap(); - - let collider_handle = collider_object.handle(); - - collision_world.set_collision_groups(collider_handle, collider.collision_group); - } else if modified_colliders.contains(id) || modified_colliders.contains(id) { - trace!("Detected changed collider with id {:?}", id); - - let prediction = physical_world.prediction(); - let angular_prediction = 0.09; - - let collision_world = physical_world.collider_world_mut(); - let collider_handle = collision_world - .collider(collider.handle.unwrap()) - .unwrap() - .handle(); - - collision_world.set_collision_groups(collider_handle, collider.collision_group); - collision_world - .as_collider_world_mut() - .set_shape(collider_handle, collider.shape.clone()); - - let collider_object = collision_world - .collider_mut(collider.handle.unwrap()) - .unwrap(); - - let parent = if let Some(rb) = rigid_bodies.get(entity) { - trace!("Updating collider to rigid body: {:?}", entity); - - rb.handle().expect( - "You should normally have a body handle at this point. This is a bug.", - ) - } else { - trace!("Updating collider to ground."); - - BodyHandle::ground() - }; - - let position = if parent.is_ground() { - tr.isometry() * collider.offset_from_parent - } else { - collider.offset_from_parent - }; - - collider_object.set_position(position); - - collision_world.as_collider_world_mut().set_query_type( - collider.handle.unwrap(), - collider.query_type.to_geometric_query_type( - collider.margin, - prediction, - angular_prediction, - ), - ); - - // TODO: Material changes & more complex mats than BasicMaterial - /*collider_object - .user_data_mut() - .set_material(collider.physics_material.clone());*/ - } - } - - colliders - .channel() - .read(&mut self.colliders_reader_id.as_mut().unwrap()) - .for_each(|_| ()); - } - - fn setup(&mut self, res: &mut Resources) { - Self::SystemData::setup(res); - - let mut collider_storage: WriteStorage<Collider> = SystemData::fetch(&res); - self.colliders_reader_id = Some(collider_storage.register_reader()); - } -} - -fn iterate_events<T, D, S>( - tracked_storage: &Storage<T, D>, - reader: &mut ReaderId<ComponentEvent>, - inserted: &mut BitSet, - modified: &mut BitSet, - world: &mut PhysicsWorld, - entities: &Entities, - colliders: &S, -) where - T: Component, - T::Storage: Tracked, - D: Deref<Target = MaskedStorage<T>>, - S: GenericReadStorage<Component = Collider>, -{ - let events = tracked_storage.channel().read(reader); - - for event in events { - match event { - ComponentEvent::Modified(id) => { - modified.add(*id); - } - ComponentEvent::Inserted(id) => { - inserted.add(*id); - } - ComponentEvent::Removed(id) => { - match colliders.get(entities.entity(*id)) { - Some(collider) => { - match collider.handle { - Some(handle) => { - trace!("Removing collider with id: {}", id); - - world.remove_colliders(&[handle]); - } - None => { - error!("Missing handle in collider: {}", id); - } - }; - } - None => { - error!("Missing collider with id: {}", id); - } - }; - } - }; - } -} diff --git a/src/systems/sync_gravity_to_physics.rs b/src/systems/sync_gravity_to_physics.rs @@ -1,29 +0,0 @@ -use crate::{Gravity, PhysicsWorld}; -use amethyst::ecs::{ReadExpect, Resources, System, SystemData, WriteExpect}; - -#[derive(Default)] -pub struct SyncGravityToPhysicsSystem; - -impl SyncGravityToPhysicsSystem { - pub fn new() -> Self { - Default::default() - } -} - -impl<'a> System<'a> for SyncGravityToPhysicsSystem { - type SystemData = (WriteExpect<'a, PhysicsWorld>, ReadExpect<'a, Gravity>); - - fn run(&mut self, (mut world, gravity): Self::SystemData) { - world.set_gravity(*gravity); - } - - fn setup(&mut self, res: &mut Resources) { - Self::SystemData::setup(res); - - res.entry::<Gravity>() - .or_insert_with(|| Gravity::new(0.0, -9.80665, 0.0)); - - res.entry::<PhysicsWorld>() - .or_insert_with(PhysicsWorld::new); - } -} diff --git a/src/time_step.rs b/src/time_step.rs @@ -1,217 +0,0 @@ -use std::{ - cmp::Ordering, - time::{Duration, Instant}, -}; - -/// The type of time step to use for the physics simulation. -pub enum TimeStep { - /// Physics will always use the given timestep. - Fixed(f32), - /// Physics use one of the given timesteps, changing when physics are falling behind. - SemiFixed(TimeStepConstraint), -} - -impl Default for TimeStep { - fn default() -> Self { - TimeStep::Fixed(1. / 120.) - } -} - -/// Error when trying to change the actual timestep for a semi-fixed timestep. -#[derive(Debug)] -pub enum TimeStepChangeError { - /// No smaller timestep available. - MinimumTimestepReached, - /// No larger timestep available. - MaximumTimestepReached, -} - -impl std::fmt::Display for TimeStepChangeError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - TimeStepChangeError::MaximumTimestepReached => { - write!(f, "No larger timestep available!") - } - TimeStepChangeError::MinimumTimestepReached => { - write!(f, "No smaller timestep available!") - } - } - } -} - -/// Constraints for a semi-fixed timestep. -pub struct TimeStepConstraint { - /// Vector of possible timesteps to use. - time_steps: Vec<f32>, - /// Index of the currently used timestep. - current_index: usize, - /// Fraction of frame time physics are allowed to take. - max_physics_time_fraction: f32, - /// Minimum time the simulation has to be running slow before the timestep is changed. - minimum_time_running_slow: Duration, - /// Minimum time the simulation has to be running fast before the timestep is changed. - minimum_time_running_fast: Duration, - /// Time when the simulation started running slow. - running_slow_since: Option<Instant>, - /// Time when the simulation started running fast. - running_fast_since: Option<Instant>, -} - -impl TimeStepConstraint { - /// Creates a new `TimeStepConstraint` from the specified timesteps to use, the maximum physics - /// time fraction and the minimum times before changing timestep. - /// - /// If physics take more than the specified fraction of simulated time, the timestep will be - /// increased. If physics with a smaller timestep would still take less than the specified - /// fraction of simulated time, the timestep will be decreased. Timesteps will only be changed - /// if physics are running fast/slow for the specified durations. - /// - /// # Examples - /// - /// ``` - /// let game_data = GameDataBuilder::default() - /// .with_bundle( - /// PhysicsBundle::new() - /// .with_timestep(TimeStep::SemiFixed(TimeStepConstraint::new( - /// vec![1. / 240., 1. / 120., 1. / 60.], - /// 0.4, - /// Duration::from_millis(50), - /// Duration::from_millis(500), - /// ))) - /// )? - /// ... - /// ); - /// ``` - /// Here physics will start out with a timestep of 1/240 seconds. If physics take more than 40% of - /// this timestep to simulate, it's running slowly. If it's running slowly for 50ms in a row, the - /// timestep will be increased to 1/120 seconds. If physics are running fast for 500ms in a row, the - /// timestep will be decreased again. - /// - /// # Panics - /// - /// This constructor will panic if no timesteps are given or if any negative timesteps are specified. - pub fn new( - time_steps: impl Into<Vec<f32>>, - max_physics_time_fraction: f32, - minimum_time_running_slow: Duration, - minimum_time_running_fast: Duration, - ) -> Self { - let mut time_steps = time_steps.into(); - assert!( - !time_steps.is_empty(), - "No timesteps given in TimeStepConstraint" - ); - time_steps.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); - time_steps.dedup(); - assert!(time_steps[0] > 0., "Negative timesteps are not allowed"); - - Self { - time_steps, - current_index: 0, - max_physics_time_fraction, - minimum_time_running_slow, - minimum_time_running_fast, - running_slow_since: None, - running_fast_since: None, - } - } - - /// Increase the timestep. This corresponds to fewer updates per second. - /// - /// Shouldn't be called from outside the `PhysicsStepperSystem`, otherwise bad things may happen. - pub fn increase_timestep(&mut self) -> Result<f32, TimeStepChangeError> { - if self.current_index >= self.time_steps.len() - 1 { - return Err(TimeStepChangeError::MaximumTimestepReached); - } - self.current_index += 1; - self.running_slow_since = None; - self.running_fast_since = None; - Ok(self.current_timestep()) - } - - /// Decrease the timestep. This corresponds to more updates per second. - /// - /// Shouldn't be called from outside the `PhysicsStepperSystem`, otherwise bad things may happen. - pub fn decrease_timestep(&mut self) -> Result<f32, TimeStepChangeError> { - if self.current_index == 0 { - return Err(TimeStepChangeError::MinimumTimestepReached); - } - self.current_index -= 1; - self.running_slow_since = None; - self.running_fast_since = None; - Ok(self.current_timestep()) - } - - /// Get the currently used timestep. - pub fn current_timestep(&self) -> f32 { - self.time_steps[self.current_index] - } - - /// Get next smaller timestep. - pub fn smaller_timestep(&self) -> Option<f32> { - if self.current_index == 0 { - None - } else { - Some(self.time_steps[self.current_index - 1]) - } - } - - /// Get the fraction of frame time physics are allowed to take. - pub fn max_physics_time_fraction(&self) -> f32 { - self.max_physics_time_fraction - } - - /// Set whether physics are currently running slow or not. Intended to be called every frame as changing - /// values only happens when `is_running_slow` changes between calls. - /// - /// Shouldn't be called from outside the `PhysicsStepperSystem`, otherwise bad things may happen. - pub fn set_running_slow(&mut self, is_running_slow: bool) { - self.running_slow_since = match (self.running_slow_since, is_running_slow) { - (None, true) => { - warn!("Physics seem to be running slow! Timestep will be changed if we keep running slow."); - Some(Instant::now()) - } - (Some(_), false) => { - debug!("Physics aren't running slow anymore."); - None - } - (_, _) => self.running_slow_since, - }; - } - - /// Set whether physics are currently running fast or not. Intended to be called every frame as changing - /// values only happens when `is_running_fast` changes between calls. - /// - /// Shouldn't be called from outside the `PhysicsStepperSystem`, otherwise bad things may happen. - pub fn set_running_fast(&mut self, is_running_fast: bool) { - self.running_fast_since = match (self.running_fast_since, is_running_fast) { - (None, true) => { - debug!("Physics seem to be running fast. Timestep will be changed if we keep running fast."); - Some(Instant::now()) - } - (Some(_), false) => { - debug!("Physics aren't running fast anymore."); - None - } - (_, _) => self.running_fast_since, - }; - } - - /// Get whether physics have been running slow for the specified minimum time and thus the timestep should - /// be increased. - pub fn should_increase_timestep(&self) -> bool { - match self.running_slow_since { - None => false, - Some(time) => time.elapsed() > self.minimum_time_running_slow, - } - } - - /// Get whether physics have been running fast for the specified minimum time and thus the timestep should - /// be decreased. - pub fn should_decrease_timestep(&self) -> bool { - match self.running_fast_since { - None => false, - Some(time) => time.elapsed() > self.minimum_time_running_fast, - } - } -}