avian2d/physics_transform/
mod.rs

1//! Manages physics transforms and synchronizes them with [`Transform`].
2//!
3//! See [`PhysicsTransformPlugin`].
4
5mod transform;
6pub use transform::{Position, PreSolveDeltaPosition, PreSolveDeltaRotation, Rotation};
7#[allow(unused_imports)]
8pub(crate) use transform::{RotationValue, init_physics_transform};
9
10mod helper;
11pub use helper::PhysicsTransformHelper;
12
13#[cfg(test)]
14mod tests;
15
16use crate::{
17    prelude::*,
18    schedule::{LastPhysicsTick, is_changed_after_tick},
19};
20use approx::AbsDiffEq;
21use bevy::{
22    ecs::{
23        change_detection::Tick, intern::Interned, schedule::ScheduleLabel, system::SystemChangeTick,
24    },
25    prelude::*,
26    transform::systems::{mark_dirty_trees, propagate_parent_transforms, sync_simple_transforms},
27};
28
29/// Manages physics transforms and synchronizes them with [`Transform`].
30///
31/// # Syncing Between [`Position`]/[`Rotation`] and [`Transform`]
32///
33/// By default, each body's `Transform` will be updated when [`Position`] or [`Rotation`]
34/// change, and vice versa. This means that you can use any of these components to move
35/// or position bodies, and the changes be reflected in the other components.
36///
37/// You can configure what data is synchronized and how it is synchronized
38/// using the [`PhysicsTransformConfig`] resource.
39///
40/// # `Transform` Hierarchies
41///
42/// When synchronizing changes in [`Position`] or [`Rotation`] to `Transform`,
43/// the engine treats nested [rigid bodies](RigidBody) as a flat structure. This means that
44/// the bodies move independently of the parents, and moving the parent will not affect the child.
45///
46/// If you would like a child entity to be rigidly attached to its parent, you could use a [`FixedJoint`]
47/// or write your own system to handle hierarchies differently.
48pub struct PhysicsTransformPlugin {
49    schedule: Interned<dyn ScheduleLabel>,
50}
51
52impl PhysicsTransformPlugin {
53    /// Creates a [`PhysicsTransformPlugin`] with the schedule that is used for running the [`PhysicsSchedule`].
54    ///
55    /// The default schedule is `FixedPostUpdate`.
56    pub fn new(schedule: impl ScheduleLabel) -> Self {
57        Self {
58            schedule: schedule.intern(),
59        }
60    }
61}
62
63impl Default for PhysicsTransformPlugin {
64    fn default() -> Self {
65        Self::new(FixedPostUpdate)
66    }
67}
68
69impl Plugin for PhysicsTransformPlugin {
70    fn build(&self, app: &mut App) {
71        app.init_resource::<PhysicsTransformConfig>();
72
73        // In case `TransformPlugin` is not added
74        app.init_resource::<StaticTransformOptimizations>();
75
76        if app
77            .world()
78            .resource::<PhysicsTransformConfig>()
79            .position_to_transform
80        {
81            app.register_required_components::<Position, Transform>();
82            app.register_required_components::<Rotation, Transform>();
83        }
84
85        // Run transform propagation and transform-to-position synchronization before physics.
86        app.configure_sets(
87            self.schedule,
88            (
89                PhysicsTransformSystems::Propagate,
90                PhysicsTransformSystems::TransformToPosition,
91            )
92                .chain()
93                .in_set(PhysicsSystems::Prepare),
94        );
95        app.add_systems(
96            self.schedule,
97            (
98                mark_dirty_trees,
99                propagate_parent_transforms,
100                sync_simple_transforms,
101            )
102                .chain()
103                .in_set(PhysicsTransformSystems::Propagate)
104                .run_if(|config: Res<PhysicsTransformConfig>| config.propagate_before_physics),
105        );
106        app.add_systems(
107            self.schedule,
108            transform_to_position
109                .in_set(PhysicsTransformSystems::TransformToPosition)
110                .run_if(|config: Res<PhysicsTransformConfig>| config.transform_to_position),
111        );
112
113        // Run position-to-transform synchronization after physics.
114        app.configure_sets(
115            self.schedule,
116            PhysicsTransformSystems::PositionToTransform.in_set(PhysicsSystems::Writeback),
117        );
118        app.add_systems(
119            self.schedule,
120            position_to_transform
121                .in_set(PhysicsTransformSystems::PositionToTransform)
122                .run_if(|config: Res<PhysicsTransformConfig>| config.position_to_transform),
123        );
124    }
125}
126
127/// Configures how physics transforms are managed and synchronized with [`Transform`].
128#[derive(Resource, Reflect, Clone, Debug, PartialEq, Eq)]
129#[reflect(Resource)]
130pub struct PhysicsTransformConfig {
131    /// If true, [`Transform`] is propagated before stepping physics to ensure that
132    /// [`GlobalTransform`] is up-to-date.
133    ///
134    /// Default: `true`
135    pub propagate_before_physics: bool,
136    /// Updates [`Position`] and [`Rotation`] based on [`Transform`] changes
137    /// in [`PhysicsTransformSystems::TransformToPosition`],
138    ///
139    /// This allows using transforms for moving and positioning bodies,
140    ///
141    /// Default: `true`
142    pub transform_to_position: bool,
143    /// Updates [`Transform`] based on [`Position`] and [`Rotation`] changes
144    /// in [`PhysicsTransformSystems::PositionToTransform`],
145    ///
146    /// Default: `true`
147    pub position_to_transform: bool,
148    /// Updates [`Collider::scale()`] based on transform changes.
149    ///
150    /// This allows using transforms for scaling colliders.
151    ///
152    /// Default: `true`
153    pub transform_to_collider_scale: bool,
154}
155
156impl Default for PhysicsTransformConfig {
157    fn default() -> Self {
158        PhysicsTransformConfig {
159            propagate_before_physics: true,
160            position_to_transform: true,
161            transform_to_position: true,
162            transform_to_collider_scale: true,
163        }
164    }
165}
166
167/// System sets for managing physics transforms.
168#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
169pub enum PhysicsTransformSystems {
170    /// Propagates [`Transform`] before physics simulation.
171    Propagate,
172    /// Updates [`Position`] and [`Rotation`] based on [`Transform`] changes before physics simulation.
173    TransformToPosition,
174    /// Updates [`Transform`] based on [`Position`] and [`Rotation`] changes after physics simulation.
175    PositionToTransform,
176}
177
178/// A deprecated alias for [`PhysicsTransformSystems`].
179#[deprecated(since = "0.4.0", note = "Renamed to `PhysicsTransformSystems`")]
180pub type PhysicsTransformSet = PhysicsTransformSystems;
181
182/// Copies [`GlobalTransform`] changes to [`Position`] and [`Rotation`].
183/// This allows users to use transforms for moving and positioning bodies and colliders.
184///
185/// To account for hierarchies, transform propagation should be run before this system.
186#[allow(clippy::type_complexity)]
187pub fn transform_to_position(
188    mut query: Query<(&GlobalTransform, &mut Position, &mut Rotation)>,
189    length_unit: Res<PhysicsLengthUnit>,
190    last_physics_tick: Res<LastPhysicsTick>,
191    system_tick: SystemChangeTick,
192) {
193    // On the first tick, the last physics tick and system tick are both defaulted to 0,
194    // but to handle change detection correctly, the system tick should always be larger.
195    // So we use a minimum system tick of 1 here.
196    let this_run = if last_physics_tick.0.get() == 0 {
197        Tick::new(1)
198    } else {
199        system_tick.this_run()
200    };
201
202    // If the `GlobalTransform` translation and `Position` differ by less than 0.01 mm, we ignore the change.
203    let distance_tolerance = length_unit.0 * 1e-5;
204    // If the `GlobalTransform` rotation and `Rotation` differ by less than 0.1 degrees, we ignore the change.
205    let rotation_tolerance = (0.1 as Scalar).to_radians();
206
207    for (global_transform, mut position, mut rotation) in &mut query {
208        let global_transform = global_transform.compute_transform();
209        #[cfg(feature = "2d")]
210        let transform_translation = global_transform.translation.truncate().adjust_precision();
211        #[cfg(feature = "3d")]
212        let transform_translation = global_transform.translation.adjust_precision();
213        let transform_rotation = Rotation::from(global_transform.rotation.adjust_precision());
214
215        let position_changed = !position.is_added()
216            && is_changed_after_tick(
217                Ref::from(position.reborrow()),
218                last_physics_tick.0,
219                this_run,
220            );
221        if !position_changed && position.abs_diff_ne(&transform_translation, distance_tolerance) {
222            position.0 = transform_translation;
223        }
224
225        let rotation_changed = !rotation.is_added()
226            && is_changed_after_tick(
227                Ref::from(rotation.reborrow()),
228                last_physics_tick.0,
229                this_run,
230            );
231        if !rotation_changed
232            && rotation.angle_between(transform_rotation).abs() > rotation_tolerance
233        {
234            *rotation = transform_rotation;
235        }
236    }
237}
238
239/// Marker component indicating that the `position_to_transform` system should be applied
240/// to this entity.
241///
242/// By default, the `position_to_transform` system only runs for entities that have a
243/// [`RigidBody`] component
244#[derive(Component, Default)]
245pub struct ApplyPosToTransform;
246
247type PosToTransformComponents = (
248    &'static mut Transform,
249    &'static Position,
250    &'static Rotation,
251    Option<&'static ChildOf>,
252);
253
254type PosToTransformFilter = (
255    Or<(With<RigidBody>, With<ApplyPosToTransform>)>,
256    Or<(Changed<Position>, Changed<Rotation>)>,
257);
258
259type ParentComponents = (
260    &'static GlobalTransform,
261    Option<&'static Position>,
262    Option<&'static Rotation>,
263);
264
265/// Copies [`Position`] and [`Rotation`] changes to [`Transform`].
266/// This allows users and the engine to use these components for moving and positioning bodies.
267///
268/// Nested rigid bodies move independently of each other, so the [`Transform`]s of child entities are updated
269/// based on their own and their parent's [`Position`] and [`Rotation`].
270#[cfg(feature = "2d")]
271pub fn position_to_transform(
272    mut query: Query<PosToTransformComponents, PosToTransformFilter>,
273    parents: Query<ParentComponents, With<Children>>,
274) {
275    for (mut transform, pos, rot, parent) in &mut query {
276        if let Some(&ChildOf(parent)) = parent {
277            if let Ok((parent_transform, parent_pos, parent_rot)) = parents.get(parent) {
278                // Compute the global transform of the parent using its Position and Rotation
279                let parent_transform = parent_transform.compute_transform();
280                let parent_pos = parent_pos.map_or(parent_transform.translation, |pos| {
281                    pos.f32().extend(parent_transform.translation.z)
282                });
283                let parent_rot = parent_rot.map_or(parent_transform.rotation, |rot| {
284                    Quaternion::from(*rot).f32()
285                });
286                let parent_scale = parent_transform.scale;
287                let parent_transform = Transform::from_translation(parent_pos)
288                    .with_rotation(parent_rot)
289                    .with_scale(parent_scale);
290
291                // The new local transform of the child body,
292                // computed from the its global transform and its parents global transform
293                let new_transform = GlobalTransform::from(
294                    Transform::from_translation(
295                        pos.f32()
296                            .extend(parent_pos.z + transform.translation.z * parent_scale.z),
297                    )
298                    .with_rotation(Quaternion::from(*rot).f32()),
299                )
300                .reparented_to(&GlobalTransform::from(parent_transform));
301
302                transform.translation = new_transform.translation;
303                transform.rotation = new_transform.rotation;
304            }
305        } else {
306            transform.translation = pos.f32().extend(transform.translation.z);
307            transform.rotation = Quaternion::from(*rot).f32();
308        }
309    }
310}
311
312/// Copies [`Position`] and [`Rotation`] changes to [`Transform`].
313/// This allows users and the engine to use these components for moving and positioning bodies.
314///
315/// Nested rigid bodies move independently of each other, so the [`Transform`]s of child entities are updated
316/// based on their own and their parent's [`Position`] and [`Rotation`].
317#[cfg(feature = "3d")]
318pub fn position_to_transform(
319    mut query: Query<PosToTransformComponents, PosToTransformFilter>,
320    parents: Query<ParentComponents, With<Children>>,
321) {
322    for (mut transform, pos, rot, parent) in &mut query {
323        if let Some(&ChildOf(parent)) = parent {
324            if let Ok((parent_transform, parent_pos, parent_rot)) = parents.get(parent) {
325                // Compute the global transform of the parent using its Position and Rotation
326                let parent_transform = parent_transform.compute_transform();
327                let parent_pos = parent_pos.map_or(parent_transform.translation, |pos| pos.f32());
328                let parent_rot = parent_rot.map_or(parent_transform.rotation, |rot| rot.f32());
329                let parent_scale = parent_transform.scale;
330                let parent_transform = Transform::from_translation(parent_pos)
331                    .with_rotation(parent_rot)
332                    .with_scale(parent_scale);
333
334                // The new local transform of the child body,
335                // computed from the its global transform and its parents global transform
336                let new_transform = GlobalTransform::from(
337                    Transform::from_translation(pos.f32()).with_rotation(rot.f32()),
338                )
339                .reparented_to(&GlobalTransform::from(parent_transform));
340
341                transform.translation = new_transform.translation;
342                transform.rotation = new_transform.rotation;
343            }
344        } else {
345            transform.translation = pos.f32();
346            transform.rotation = rot.f32();
347        }
348    }
349}