Skip to main content

bevy_rapier2d/plugin/
plugin.rs

1use crate::pipeline::{CollisionEvent, ContactForceEvent};
2use crate::prelude::*;
3use crate::reflect::{IntegrationParametersWrapper, SpringCoefficientsWrapper};
4use bevy::app::DynEq;
5use bevy::ecs::{
6    intern::Interned,
7    schedule::{IntoScheduleConfigs, ScheduleConfigs, ScheduleLabel},
8    system::{ScheduleSystem, SystemParamItem},
9};
10use bevy::platform::collections::HashSet;
11use bevy::{prelude::*, transform::TransformSystems};
12use rapier::dynamics::IntegrationParameters;
13use std::marker::PhantomData;
14
15use super::context::DefaultRapierContext;
16
17#[cfg(doc)]
18use crate::plugin::context::systemparams::RapierContext;
19
20/// No specific user-data is associated to the hooks.
21pub type NoUserData = ();
22
23/// A plugin responsible for setting up a full Rapier physics simulation pipeline and resources.
24///
25/// This will automatically setup all the resources needed to run a physics simulation with the
26/// Rapier physics engine.
27pub struct RapierPhysicsPlugin<PhysicsHooks = ()> {
28    schedule: Interned<dyn ScheduleLabel>,
29    default_system_setup: bool,
30    /// Read during [`RapierPhysicsPlugin::build()`],
31    /// to help initializing [`RapierContextInitialization`] resource.
32    /// This will be ignored if that resource already exists.
33    default_world_setup: RapierContextInitialization,
34    /// Controls whether given `PhysicsSets` systems are injected into the scheduler.
35    ///
36    /// This is useful to opt out of default plugin behaviour, for example if you need to reorganize
37    /// the systems in different schedules.
38    ///
39    /// If passing an empty set, the plugin will still add the Physics Sets to the plugin schedule,
40    /// but no systems will be added automatically.
41    enabled_physics_schedules: HashSet<PhysicsSet>,
42    _phantom: PhantomData<PhysicsHooks>,
43}
44
45impl<PhysicsHooks> RapierPhysicsPlugin<PhysicsHooks>
46where
47    PhysicsHooks: 'static + BevyPhysicsHooks,
48    for<'w, 's> SystemParamItem<'w, 's, PhysicsHooks>: BevyPhysicsHooks,
49{
50    /// Specifies a scale ratio between the physics world and the bevy transforms.
51    ///
52    /// This affects the size of every elements in the physics engine, by multiplying
53    /// all the length-related quantities by the `length_unit` factor. This should
54    /// likely always be 1.0 in 3D. In 2D, this is useful to specify a "pixels-per-meter"
55    /// conversion ratio.
56    pub fn with_length_unit(mut self, length_unit: f32) -> Self {
57        self.default_world_setup =
58            RapierContextInitialization::default_with_length_unit(length_unit);
59        self
60    }
61
62    /// Specifies a default world initialization strategy.
63    ///
64    /// The default is to initialize a [`RapierContext`] with a length unit of 1.
65    pub fn with_custom_initialization(
66        mut self,
67        default_world_initialization: RapierContextInitialization,
68    ) -> Self {
69        self.default_world_setup = default_world_initialization;
70        self
71    }
72
73    /// Specifies whether the plugin should setup each of its [`PhysicsSet`]
74    /// (`true`), or if the user will set them up later (`false`).
75    ///
76    /// The default value is `true`.
77    pub fn with_default_system_setup(mut self, default_system_setup: bool) -> Self {
78        self.default_system_setup = default_system_setup;
79        self
80    }
81
82    /// Specifies how many pixels on the 2D canvas equal one meter on the physics world.
83    ///
84    /// This conversion unit assumes that the 2D camera uses an unscaled projection.
85    #[cfg(feature = "dim2")]
86    pub fn pixels_per_meter(pixels_per_meter: f32) -> Self {
87        Self {
88            default_system_setup: true,
89            default_world_setup: RapierContextInitialization::default_with_length_unit(
90                pixels_per_meter,
91            ),
92            ..default()
93        }
94    }
95
96    /// Controls whether given `PhysicsSets` systems are injected into the scheduler.
97    ///
98    /// This is useful to opt out of default plugin behaviour, for example if you need to reorganize
99    /// the systems in different schedules.
100    ///
101    /// If passing an empty set, the plugin will still add the Physics Sets to the plugin schedule,
102    /// but no systems will be added automatically.
103    pub fn with_physics_sets_systems(
104        mut self,
105        enabled_physics_schedules: HashSet<PhysicsSet>,
106    ) -> Self {
107        self.enabled_physics_schedules = enabled_physics_schedules;
108        self
109    }
110
111    /// Adds the physics systems to the `FixedUpdate` schedule rather than `PostUpdate`.
112    pub fn in_fixed_schedule(self) -> Self {
113        self.in_schedule(FixedUpdate)
114    }
115
116    /// Adds the physics systems to the provided schedule rather than `PostUpdate`.
117    pub fn in_schedule(mut self, schedule: impl ScheduleLabel) -> Self {
118        self.schedule = schedule.intern();
119        self
120    }
121
122    /// Provided for use when staging systems outside of this plugin using
123    /// [`with_default_system_setup(false)`](Self::with_default_system_setup).
124    /// See [`PhysicsSet`] for a description of these systems.
125    pub fn get_systems(set: PhysicsSet) -> ScheduleConfigs<ScheduleSystem> {
126        match set {
127            PhysicsSet::SyncBackend => (
128                (
129                    // Initialize the rapier configuration.
130                    // A good candidate for required component or hook components.
131                    // The configuration is needed for following systems, so it should be chained.
132                    setup_rapier_configuration,
133                    // Run the character controller before the manual transform propagation.
134                    systems::update_character_controls,
135                )
136                    .chain()
137                    .in_set(PhysicsSet::SyncBackend),
138                // Run Bevy transform propagation additionally to sync [`GlobalTransform`]
139                (
140                    bevy::transform::systems::sync_simple_transforms,
141                    bevy::transform::systems::propagate_parent_transforms,
142                )
143                    .chain()
144                    .in_set(RapierTransformPropagateSet),
145                (
146                    (
147                        systems::on_add_entity_with_parent,
148                        systems::on_change_context,
149                        systems::sync_removals,
150                        #[cfg(all(feature = "dim3", feature = "async-collider"))]
151                        systems::init_async_scene_colliders,
152                        #[cfg(all(feature = "dim3", feature = "async-collider"))]
153                        systems::init_async_colliders,
154                        systems::init_rigid_bodies,
155                        systems::init_colliders,
156                        systems::init_joints,
157                    )
158                        .chain()
159                        .in_set(PhysicsSet::SyncBackend),
160                    (
161                        (
162                            systems::apply_scale.in_set(RapierBevyComponentApply),
163                            systems::apply_collider_user_changes.in_set(RapierBevyComponentApply),
164                        )
165                            .chain(),
166                        systems::apply_joint_user_changes.in_set(RapierBevyComponentApply),
167                    ),
168                    // TODO: joints and colliders might be parallelizable.
169                    systems::apply_initial_rigid_body_impulses.in_set(RapierBevyComponentApply),
170                    systems::apply_rigid_body_user_changes.in_set(RapierBevyComponentApply),
171                )
172                    .chain(),
173            )
174                .chain()
175                .into_configs(),
176            PhysicsSet::StepSimulation => (systems::step_simulation::<PhysicsHooks>)
177                .in_set(PhysicsSet::StepSimulation)
178                .into_configs(),
179            PhysicsSet::Writeback => (
180                systems::update_colliding_entities,
181                systems::writeback_rigid_bodies,
182                // Each writeback write to different properties.
183                systems::writeback_mass_properties.ambiguous_with(systems::writeback_rigid_bodies),
184            )
185                .in_set(PhysicsSet::Writeback)
186                .into_configs(),
187        }
188    }
189}
190
191/// A set for rapier's copying bevy_rapier's Bevy components back into rapier.
192#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
193pub struct RapierBevyComponentApply;
194
195/// A set for rapier's copy of Bevy's transform propagation systems.
196///
197/// See [`TransformSystems`](bevy::transform::TransformSystems::Propagate).
198#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
199pub struct RapierTransformPropagateSet;
200
201impl<PhysicsHooksSystemParam> Default for RapierPhysicsPlugin<PhysicsHooksSystemParam> {
202    fn default() -> Self {
203        Self {
204            schedule: PostUpdate.intern(),
205            default_system_setup: true,
206            default_world_setup: Default::default(),
207            enabled_physics_schedules: HashSet::from_iter([
208                PhysicsSet::SyncBackend,
209                PhysicsSet::StepSimulation,
210                PhysicsSet::Writeback,
211            ]),
212            _phantom: PhantomData,
213        }
214    }
215}
216
217/// [`SystemSet`] for each phase of the plugin.
218#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
219pub enum PhysicsSet {
220    /// This set runs the systems responsible for synchronizing (and
221    /// initializing) backend data structures with current component state.
222    /// These systems typically run at the after [`Update`].
223    SyncBackend,
224    /// The systems responsible for advancing the physics simulation, and
225    /// updating the internal state for scene queries.
226    /// These systems typically run immediately after [`PhysicsSet::SyncBackend`].
227    StepSimulation,
228    /// The systems responsible for updating
229    /// [`crate::geometry::CollidingEntities`] and writing
230    /// the result of the last simulation step into our `bevy_rapier`
231    /// components and the [`GlobalTransform`] component.
232    /// These systems typically run immediately after [`PhysicsSet::StepSimulation`].
233    Writeback,
234}
235
236impl<PhysicsHooks> Plugin for RapierPhysicsPlugin<PhysicsHooks>
237where
238    PhysicsHooks: 'static + BevyPhysicsHooks,
239    for<'w, 's> SystemParamItem<'w, 's, PhysicsHooks>: BevyPhysicsHooks,
240{
241    fn build(&self, app: &mut App) {
242        // Register components as reflectable.
243        app.register_type::<RigidBody>()
244            .register_type::<Velocity>()
245            .register_type::<AdditionalMassProperties>()
246            .register_type::<MassProperties>()
247            .register_type::<LockedAxes>()
248            .register_type::<ExternalForce>()
249            .register_type::<ExternalImpulse>()
250            .register_type::<Sleeping>()
251            .register_type::<Damping>()
252            .register_type::<Dominance>()
253            .register_type::<Ccd>()
254            .register_type::<SoftCcd>()
255            .register_type::<GravityScale>()
256            .register_type::<CollidingEntities>()
257            .register_type::<Sensor>()
258            .register_type::<Friction>()
259            .register_type::<Restitution>()
260            .register_type::<CollisionGroups>()
261            .register_type::<SolverGroups>()
262            .register_type::<ContactForceEventThreshold>()
263            .register_type::<ContactSkin>()
264            .register_type::<Group>()
265            .register_type::<RapierContextEntityLink>()
266            .register_type::<RapierConfiguration>()
267            .register_type::<SimulationToRenderTime>()
268            .register_type::<DefaultRapierContext>()
269            .register_type::<RapierContextInitialization>()
270            .register_type::<SpringCoefficientsWrapper>();
271
272        app.insert_resource(Messages::<CollisionEvent>::default())
273            .insert_resource(Messages::<ContactForceEvent>::default())
274            .insert_resource(Messages::<MassModifiedEvent>::default());
275        let default_world_init = app.world().get_resource::<RapierContextInitialization>();
276        if let Some(world_init) = default_world_init {
277            log::warn!("RapierPhysicsPlugin added but a `RapierContextInitialization` resource was already existing.\
278            This might overwrite previous configuration made via `RapierPhysicsPlugin::with_custom_initialization`\
279            or `RapierPhysicsPlugin::with_length_unit`.
280            The following resource will be used: {world_init:?}");
281        } else {
282            app.insert_resource(self.default_world_setup.clone());
283        }
284
285        app.add_systems(
286            PreStartup,
287            (insert_default_context, setup_rapier_configuration).chain(),
288        );
289
290        // These *must* be in the main schedule currently so that they do not miss events.
291        // See test `test_sync_removal` for an example of this.
292        if self.schedule != PostUpdate.intern() {
293            app.add_systems(
294                PostUpdate,
295                (systems::sync_removals,).before(TransformSystems::Propagate),
296            );
297        }
298
299        // Add each set as necessary
300        if self.default_system_setup {
301            app.configure_sets(
302                self.schedule,
303                (
304                    PhysicsSet::SyncBackend,
305                    PhysicsSet::StepSimulation,
306                    PhysicsSet::Writeback,
307                )
308                    .chain()
309                    .before(TransformSystems::Propagate),
310            );
311            app.configure_sets(
312                self.schedule,
313                RapierTransformPropagateSet.in_set(PhysicsSet::SyncBackend),
314            );
315            app.configure_sets(
316                self.schedule,
317                RapierBevyComponentApply.in_set(PhysicsSet::SyncBackend),
318            );
319            let mut add_systems_if_enabled = |physics_set: PhysicsSet| {
320                if self.enabled_physics_schedules.contains(&physics_set) {
321                    app.add_systems(self.schedule, Self::get_systems(physics_set));
322                }
323            };
324            add_systems_if_enabled(PhysicsSet::SyncBackend);
325            add_systems_if_enabled(PhysicsSet::Writeback);
326            add_systems_if_enabled(PhysicsSet::StepSimulation);
327
328            app.init_resource::<TimestepMode>();
329
330            // Warn user if the timestep mode isn't in Fixed
331            if self.schedule.dyn_eq(&FixedUpdate as &dyn DynEq) {
332                let config = app.world_mut().resource::<TimestepMode>();
333                match config {
334                    TimestepMode::Fixed { .. } => {}
335                    mode => {
336                        log::warn!("TimestepMode is set to `{mode:?}`, it is recommended to use `TimestepMode::Fixed` if you have the physics in `FixedUpdate`");
337                    }
338                }
339            }
340        }
341    }
342
343    fn finish(&self, _app: &mut App) {
344        #[cfg(all(feature = "dim3", feature = "async-collider"))]
345        {
346            use bevy::{
347                asset::AssetPlugin, mesh::MeshPlugin, world_serialization::WorldSerializationPlugin,
348            };
349            if !_app.is_plugin_added::<AssetPlugin>() {
350                _app.add_plugins(AssetPlugin::default());
351            }
352            if !_app.is_plugin_added::<MeshPlugin>() {
353                _app.add_plugins(MeshPlugin);
354            }
355            if !_app.is_plugin_added::<WorldSerializationPlugin>() {
356                _app.add_plugins(WorldSerializationPlugin);
357            }
358        }
359    }
360}
361
362/// Specifies a default configuration for the default [`RapierContext`]
363///
364/// Designed to be passed as parameter to [`RapierPhysicsPlugin::with_custom_initialization`].
365#[derive(Resource, Debug, Reflect, Clone)]
366pub enum RapierContextInitialization {
367    /// [`RapierPhysicsPlugin`] will not spawn any entity containing [`RapierContextSimulation`] automatically.
368    ///
369    /// You are responsible for creating a [`RapierContextSimulation`],
370    /// before spawning any rapier entities (rigidbodies, colliders, joints).
371    ///
372    /// You might be interested in adding [`DefaultRapierContext`] to the created physics context.
373    NoAutomaticRapierContext,
374    /// [`RapierPhysicsPlugin`] will spawn an entity containing a [`RapierContextSimulation`]
375    /// automatically during [`PreStartup`], with the [`DefaultRapierContext`] marker component.
376    InitializeDefaultRapierContext {
377        /// Integration parameters component which will be added to the default rapier context.
378        #[reflect(remote = IntegrationParametersWrapper)]
379        integration_parameters: IntegrationParameters,
380        /// Rapier configuration component which will be added to the default rapier context.
381        rapier_configuration: RapierConfiguration,
382    },
383}
384
385impl Default for RapierContextInitialization {
386    fn default() -> Self {
387        Self::default_with_length_unit(1f32)
388    }
389}
390
391impl RapierContextInitialization {
392    /// Configures rapier with the specified length unit.
393    ///
394    /// See the documentation of [`IntegrationParameters::length_unit`] for additional details
395    /// on that argument.
396    ///
397    /// The default gravity is automatically scaled by that length unit.
398    pub fn default_with_length_unit(length_unit: f32) -> Self {
399        let integration_parameters = IntegrationParameters {
400            length_unit,
401            ..default()
402        };
403
404        RapierContextInitialization::InitializeDefaultRapierContext {
405            integration_parameters,
406            rapier_configuration: RapierConfiguration::new(length_unit),
407        }
408    }
409}
410
411pub fn insert_default_context(
412    mut commands: Commands,
413    initialization_data: Res<RapierContextInitialization>,
414) {
415    match initialization_data.as_ref() {
416        RapierContextInitialization::NoAutomaticRapierContext => {}
417        RapierContextInitialization::InitializeDefaultRapierContext {
418            integration_parameters,
419            rapier_configuration,
420        } => {
421            commands.spawn((
422                Name::new("Rapier Context"),
423                RapierContextSimulation {
424                    integration_parameters: *integration_parameters,
425                    ..RapierContextSimulation::default()
426                },
427                *rapier_configuration,
428                DefaultRapierContext,
429            ));
430        }
431    }
432}
433
434pub fn setup_rapier_configuration(
435    mut commands: Commands,
436    rapier_context: Query<(Entity, &RapierContextSimulation), Without<RapierConfiguration>>,
437) {
438    for (e, rapier_context) in rapier_context.iter() {
439        commands.entity(e).insert(RapierConfiguration::new(
440            rapier_context.integration_parameters.length_unit,
441        ));
442    }
443}
444
445#[cfg(test)]
446mod test {
447
448    use bevy::{
449        ecs::schedule::{NodeId, Stepping},
450        prelude::Component,
451        time::{TimePlugin, TimeUpdateStrategy},
452    };
453    use rapier::{data::Index, dynamics::RigidBodyHandle};
454
455    use crate::{plugin::context::*, plugin::*, prelude::*};
456
457    #[cfg(feature = "dim3")]
458    fn cuboid(hx: Real, hy: Real, hz: Real) -> Collider {
459        Collider::cuboid(hx, hy, hz)
460    }
461    #[cfg(feature = "dim2")]
462    fn cuboid(hx: Real, hy: Real, _hz: Real) -> Collider {
463        Collider::cuboid(hx, hy)
464    }
465
466    #[derive(Component)]
467    pub struct TestMarker;
468
469    #[test]
470    pub fn hierarchy_link_propagation() {
471        return main();
472
473        use bevy::prelude::*;
474
475        fn run_test(app: &mut App) {
476            app.insert_resource(TimeUpdateStrategy::ManualDuration(
477                std::time::Duration::from_secs_f32(1f32 / 60f32),
478            ));
479
480            app.add_systems(Update, setup_physics);
481
482            app.finish();
483
484            let mut stepping = Stepping::new();
485
486            app.update();
487
488            stepping
489                .add_schedule(PostUpdate)
490                .add_schedule(Update)
491                .enable()
492                .set_breakpoint(PostUpdate, systems::on_add_entity_with_parent)
493                .set_breakpoint(PostUpdate, systems::init_rigid_bodies)
494                .set_breakpoint(PostUpdate, systems::on_change_context)
495                .set_breakpoint(PostUpdate, systems::sync_removals)
496                .set_breakpoint(Update, setup_physics);
497
498            app.insert_resource(stepping);
499
500            let mut stepping = app.world_mut().resource_mut::<Stepping>();
501            // Advancing once to get the context.
502            stepping.continue_frame();
503            app.update();
504            // arbitrary hardcoded amount to run the simulation for a few frames.
505            // This test uses stepping so the actual amount of frames is this `number / breakpoints`
506            for _ in 0..20 {
507                let world = app.world_mut();
508                let stepping = world.resource_mut::<Stepping>();
509                if let Some(cursor) = &stepping.cursor() {
510                    let system = world
511                        .resource::<Schedules>()
512                        .get(cursor.0)
513                        .unwrap()
514                        .systems()
515                        .unwrap()
516                        .find(|s| NodeId::System(s.0) == cursor.1)
517                        .unwrap();
518                    println!(
519                        "next system: {}",
520                        system
521                            .1
522                            .name()
523                            .to_string()
524                            .split_terminator("::")
525                            .last()
526                            .unwrap()
527                    );
528                } else {
529                    println!("no cursor, new frame!");
530                }
531                let mut stepping = world.resource_mut::<Stepping>();
532                stepping.continue_frame();
533                app.update();
534
535                let rigidbody_set = app
536                    .world_mut()
537                    .query::<&RapierRigidBodySet>()
538                    .single(app.world())
539                    .unwrap();
540
541                println!("{:?}", &rigidbody_set.entity2body);
542            }
543            let rigidbody_set = app
544                .world_mut()
545                .query::<&RapierRigidBodySet>()
546                .single(app.world())
547                .unwrap();
548
549            assert_eq!(
550                rigidbody_set.entity2body.iter().next().unwrap().1,
551                // assert the generation is 0, that means we didn't modify it twice (due to change world detection)
552                &RigidBodyHandle(Index::from_raw_parts(0, 0))
553            );
554        }
555
556        fn main() {
557            let mut app = App::new();
558            app.add_plugins((
559                TransformPlugin,
560                TimePlugin,
561                RapierPhysicsPlugin::<NoUserData>::default(),
562            ));
563            run_test(&mut app);
564        }
565
566        pub fn setup_physics(mut commands: Commands, mut counter: Local<i32>) {
567            // run on the 3rd iteration: I believe current logic is:
568            // - 1st is to setup bevy internals
569            // - 2nd is to wait for having stepping enabled, as I couldnt get it to work for the first update.
570            // - 3rd, we're looking to test adding a rapier entity while playing, opposed to a Startup function,
571            //   which most examples are focused on.
572            let run_at = 3;
573            if *counter == run_at {
574                return;
575            }
576            *counter += 1;
577            if *counter < run_at {
578                return;
579            }
580
581            commands.spawn((
582                Transform::from_xyz(0.0, 13.0, 0.0),
583                RigidBody::Dynamic,
584                cuboid(0.5, 0.5, 0.5),
585                TestMarker,
586            ));
587        }
588    }
589
590    #[test]
591    pub fn test_sync_removal() {
592        return main();
593
594        use bevy::prelude::*;
595
596        fn run_test(app: &mut App) {
597            app.insert_resource(TimeUpdateStrategy::ManualDuration(
598                std::time::Duration::from_secs_f32(1f32 / 60f32),
599            ));
600            app.insert_resource(Time::<Fixed>::from_hz(20.0));
601
602            app.add_systems(Startup, setup_physics);
603            app.add_systems(Update, remove_rapier_entity);
604            app.add_systems(FixedUpdate, || println!("Fixed Update"));
605            app.add_systems(Update, || println!("Update"));
606            app.finish();
607            // startup
608            app.update();
609            // normal updates starting
610            // render only
611            app.update();
612            app.update();
613            // render + physics
614            app.update();
615
616            let mut context_query = app.world_mut().query::<RapierContext>();
617            let context = context_query.single(app.world()).unwrap();
618            assert_eq!(context.rigidbody_set.entity2body.len(), 1);
619
620            // render only + remove entities
621            app.update();
622            // Fixed Update hasn“t run yet, so it's a risk of not having caught the bevy removed event, which will be cleaned next frame.
623
624            let mut context_query = app.world_mut().query::<RapierContext>();
625            let context = context_query.single(app.world()).unwrap();
626
627            println!("{:?}", &context.rigidbody_set.entity2body);
628            assert_eq!(context.rigidbody_set.entity2body.len(), 0);
629        }
630
631        fn main() {
632            let mut app = App::new();
633            app.add_plugins((
634                TransformPlugin,
635                TimePlugin,
636                RapierPhysicsPlugin::<NoUserData>::default().in_fixed_schedule(),
637            ));
638            run_test(&mut app);
639        }
640
641        pub fn setup_physics(mut commands: Commands) {
642            commands.spawn((
643                Transform::from_xyz(0.0, 13.0, 0.0),
644                RigidBody::Dynamic,
645                cuboid(0.5, 0.5, 0.5),
646                TestMarker,
647            ));
648            println!("spawned rapier entity");
649        }
650        pub fn remove_rapier_entity(
651            mut commands: Commands,
652            to_remove: Query<Entity, With<TestMarker>>,
653            mut counter: Local<i32>,
654        ) {
655            *counter += 1;
656            if *counter != 5 {
657                return;
658            }
659            println!("removing rapier entity");
660            for e in &to_remove {
661                commands.entity(e).despawn();
662            }
663        }
664    }
665
666    #[test]
667    fn parent_child() {
668        return main();
669
670        use bevy::prelude::*;
671
672        fn main() {
673            let mut app = App::new();
674            app.add_plugins((
675                TransformPlugin,
676                TimePlugin,
677                RapierPhysicsPlugin::<NoUserData>::default().in_fixed_schedule(),
678            ));
679            run_test(&mut app);
680        }
681
682        fn run_test(app: &mut App) {
683            app.insert_resource(TimeUpdateStrategy::ManualDuration(
684                std::time::Duration::from_secs_f32(1f32 / 60f32),
685            ));
686            app.add_systems(Startup, init_rapier_configuration);
687            app.add_systems(Startup, setup_physics);
688
689            app.finish();
690            for _ in 0..100 {
691                app.update();
692            }
693
694            let mut context_query = app.world_mut().query::<RapierContext>();
695            let context = context_query.single(app.world()).unwrap();
696
697            println!("{:#?}", &context.rigidbody_set.bodies);
698        }
699
700        pub fn init_rapier_configuration(
701            mut config: Query<&mut RapierConfiguration, With<DefaultRapierContext>>,
702        ) {
703            let mut config = config.single_mut().unwrap();
704            *config = RapierConfiguration {
705                force_update_from_transform_changes: true,
706                ..RapierConfiguration::new(1f32)
707            };
708        }
709
710        pub fn setup_physics(mut commands: Commands) {
711            let parent = commands
712                .spawn(Transform::from_scale(Vec3::splat(5f32)))
713                .id();
714            let mut entity_commands = commands.spawn((
715                Collider::ball(1f32),
716                Transform::from_translation(Vec3::new(200f32, 100f32, 3f32)),
717                RigidBody::Fixed,
718            ));
719            entity_commands.insert(ChildOf(parent));
720        }
721    }
722
723    #[test]
724    fn initial_scale() {
725        return main();
726
727        use bevy::prelude::*;
728
729        fn main() {
730            let mut app = App::new();
731            app.add_plugins((
732                TransformPlugin,
733                TimePlugin,
734                RapierPhysicsPlugin::<NoUserData>::default().in_fixed_schedule(),
735            ));
736            run_test(&mut app);
737        }
738
739        fn run_test(app: &mut App) {
740            app.insert_resource(TimeUpdateStrategy::ManualDuration(
741                std::time::Duration::from_secs_f32(1f32 / 60f32),
742            ));
743            app.add_systems(Update, setup_physics.run_if(run_once));
744
745            app.finish();
746
747            // Running startup
748            app.update();
749            // Running first physics setup, initializes colliders and scaling.
750            app.update();
751
752            let collider = app
753                .world_mut()
754                .query::<&Collider>()
755                .single(app.world())
756                .unwrap();
757            approx::assert_relative_eq!(collider.scale, Vect::splat(0.1), epsilon = 1.0e-5);
758
759            let mut context_query = app.world_mut().query::<RapierContext>();
760            let context = context_query.single(app.world()).unwrap();
761            let physics_ball_radius = context
762                .colliders
763                .colliders
764                .iter()
765                .next()
766                .unwrap()
767                .1
768                .shape()
769                .as_ball()
770                .unwrap()
771                .radius;
772            approx::assert_relative_eq!(physics_ball_radius, 0.1, epsilon = 0.1);
773        }
774
775        pub fn setup_physics(mut commands: Commands) {
776            commands.spawn((
777                Transform::from_translation(Vec3::new(-0.2, 0.0, 0.0)).with_scale(Vec3::splat(0.1)),
778                Collider::ball(1.0),
779            ));
780        }
781    }
782}