diff --git a/bevy_rapier2d/Cargo.toml b/bevy_rapier2d/Cargo.toml index ddce9481..5a8ac4c7 100644 --- a/bevy_rapier2d/Cargo.toml +++ b/bevy_rapier2d/Cargo.toml @@ -55,6 +55,7 @@ serde = { version = "1", features = ["derive"], optional = true } bevy = { version = "0.14", default-features = false, features = [ "x11", "bevy_state", + "bevy_debug_stepping", ] } oorandom = "11" approx = "0.5.1" diff --git a/bevy_rapier3d/Cargo.toml b/bevy_rapier3d/Cargo.toml index b0b5a495..a413cdae 100644 --- a/bevy_rapier3d/Cargo.toml +++ b/bevy_rapier3d/Cargo.toml @@ -57,6 +57,7 @@ bevy = { version = "0.14", default-features = false, features = [ "x11", "tonemapping_luts", "bevy_state", + "bevy_debug_stepping", ] } approx = "0.5.1" glam = { version = "0.27", features = ["approx"] } diff --git a/src/pipeline/events.rs b/src/pipeline/events.rs index 71e1240d..897d8b02 100644 --- a/src/pipeline/events.rs +++ b/src/pipeline/events.rs @@ -125,7 +125,7 @@ impl<'a> EventHandler for EventQueue<'a> { #[cfg(test)] mod test { - use bevy::time::TimePlugin; + use bevy::time::{TimePlugin, TimeUpdateStrategy}; use systems::tests::HeadlessRenderPlugin; use crate::{plugin::*, prelude::*}; @@ -175,10 +175,14 @@ mod test { // } // app.finish(); // app.cleanup(); - let mut time = app.world_mut().get_resource_mut::>().unwrap(); - time.set_relative_speed(1000f32); - for _ in 0..300 { - // FIXME: advance by set durations to avoid being at the mercy of the CPU. + + // Simulates 60 updates per seconds + app.insert_resource(TimeUpdateStrategy::ManualDuration( + std::time::Duration::from_secs_f32(1f32 / 60f32), + )); + // 2 seconds should be plenty of time for the cube to fall on the + // lowest collider. + for _ in 0..120 { app.update(); } let saved_collisions = app diff --git a/src/plugin/plugin.rs b/src/plugin/plugin.rs index 3cdbbd12..91df431a 100644 --- a/src/plugin/plugin.rs +++ b/src/plugin/plugin.rs @@ -335,3 +335,155 @@ pub fn setup_rapier_simulation_to_render_time( commands.entity(e).insert(SimulationToRenderTime::default()); } } + +#[cfg(test)] +mod test { + + use bevy::{ + ecs::schedule::Stepping, + prelude::Component, + time::{TimePlugin, TimeUpdateStrategy}, + }; + use rapier::{data::Index, dynamics::RigidBodyHandle}; + use systems::tests::HeadlessRenderPlugin; + + use crate::{plugin::*, prelude::*}; + + #[cfg(feature = "dim3")] + fn cuboid(hx: Real, hy: Real, hz: Real) -> Collider { + Collider::cuboid(hx, hy, hz) + } + #[cfg(feature = "dim2")] + fn cuboid(hx: Real, hy: Real, _hz: Real) -> Collider { + Collider::cuboid(hx, hy) + } + + #[derive(Component)] + pub struct TestMarker; + + #[test] + pub fn hierarchy_link_propagation() { + return main(); + + use bevy::prelude::*; + + fn run_test(app: &mut App) { + app.insert_resource(TimeUpdateStrategy::ManualDuration( + std::time::Duration::from_secs_f32(1f32 / 60f32), + )); + + app.add_systems(Update, setup_physics); + + // while app.plugins_state() == bevy::app::PluginsState::Adding { + // #[cfg(not(target_arch = "wasm32"))] + // bevy::tasks::tick_global_task_pools_on_main_thread(); + // } + // app.finish(); + // app.cleanup(); + + let mut stepping = Stepping::new(); + + app.update(); + + stepping + .add_schedule(PostUpdate) + .add_schedule(Update) + .enable() + .set_breakpoint(PostUpdate, systems::on_add_entity_with_parent) + .set_breakpoint(PostUpdate, systems::init_rigid_bodies) + .set_breakpoint(PostUpdate, systems::on_change_world) + .set_breakpoint(PostUpdate, systems::sync_removals) + .set_breakpoint(Update, setup_physics); + + app.insert_resource(stepping); + + let mut stepping = app.world_mut().resource_mut::(); + // Advancing once to get the context. + stepping.continue_frame(); + app.update(); + // arbitrary hardcoded amount to run the simulation for a few frames. + // This test uses stepping so the actual amount of frames is this `number / breakpoints` + for i in 0..20 { + let world = app.world_mut(); + let stepping = world.resource_mut::(); + if let Some(cursor) = &stepping.cursor() { + let system = world + .resource::() + .get(cursor.0) + .unwrap() + .systems() + .unwrap() + .find(|s| s.0 == cursor.1) + .unwrap(); + println!( + "next system: {}", + system + .1 + .name() + .to_string() + .split_terminator("::") + .last() + .unwrap() + ); + } else { + println!("no cursor, new frame!"); + } + let mut stepping = world.resource_mut::(); + stepping.continue_frame(); + app.update(); + let context = app + .world_mut() + .query::<&RapierContext>() + .get_single(&app.world()) + .unwrap(); + + println!("{:?}", &context.entity2body); + } + let context = app + .world_mut() + .query::<&RapierContext>() + .get_single(&app.world()) + .unwrap(); + + assert_eq!( + context.entity2body.iter().next().unwrap().1, + // assert the generation is 0, that means we didn't modify it twice (due to change world detection) + &RigidBodyHandle(Index::from_raw_parts(0, 0)) + ); + } + + fn main() { + let mut app = App::new(); + app.add_plugins(( + HeadlessRenderPlugin, + TransformPlugin, + TimePlugin, + RapierPhysicsPlugin::::default(), + )); + run_test(&mut app); + } + + pub fn setup_physics(mut commands: Commands, mut counter: Local) { + // run on the 3rd iteration: I believe current logic is: + // - 1st is to setup bevy internals + // - 2nd is to wait for having stepping enabled, as I couldnt get it to work for the first update. + // - 3rd, we're looking to test adding a rapier entity while playing, opposed to a Startup function, + // which most examples are focused on. + let run_at = 3; + if *counter == run_at { + return; + } + *counter += 1; + if *counter < run_at { + return; + } + + commands.spawn(( + TransformBundle::from(Transform::from_xyz(0.0, 13.0, 0.0)), + RigidBody::Dynamic, + cuboid(0.5, 0.5, 0.5), + TestMarker, + )); + } + } +} diff --git a/src/plugin/systems/multiple_rapier_contexts.rs b/src/plugin/systems/multiple_rapier_contexts.rs index 40aa5ad5..68fe3b43 100644 --- a/src/plugin/systems/multiple_rapier_contexts.rs +++ b/src/plugin/systems/multiple_rapier_contexts.rs @@ -7,13 +7,17 @@ use crate::geometry::RapierColliderHandle; use crate::plugin::{RapierContext, RapierContextEntityLink}; use bevy::prelude::*; -/// If an entity is turned into the child of something with a physics world, the child should become a part of that physics world +/// If an entity is turned into the child of something with a physics context link, +/// the child should become a part of that physics context /// /// If this fails to happen, weirdness will ensue. pub fn on_add_entity_with_parent( q_add_entity_without_parent: Query< (Entity, &Parent), - (With, Changed), + ( + With, + Or<(Changed, Changed)>, + ), >, q_parent: Query<&Parent>, q_physics_world: Query<&RapierContextEntityLink>, @@ -27,7 +31,6 @@ pub fn on_add_entity_with_parent( remove_old_physics(ent, &mut commands); break; } - parent = q_parent.get(parent_entity).ok().map(|x| x.get()); } } @@ -58,31 +61,23 @@ pub fn on_change_world( ) { for (entity, new_physics_world) in &q_changed_worlds { let context = q_context.get(new_physics_world.0); - if new_physics_world.is_added() { - continue; - } // Ensure the world actually changed before removing them from the world if !context .map(|x| { // They are already apart of this world if any of these are true - x.entity2impulse_joint.contains_key(&entity) - || x.entity2multibody_joint.contains_key(&entity) - || x.entity2collider.contains_key(&entity) + x.entity2collider.contains_key(&entity) || x.entity2body.contains_key(&entity) + || x.entity2impulse_joint.contains_key(&entity) + || x.entity2multibody_joint.contains_key(&entity) }) .unwrap_or(false) { - remove_old_physics(entity, &mut commands); - + remove_old_physics(dbg!(entity), &mut commands); bubble_down_world_change( &mut commands, entity, &q_children, - *new_physics_world, - &q_physics_world, - ); - } - } + *new_physics_world, } fn bubble_down_world_change(