diff --git a/src/collision/collider/backend.rs b/src/collision/collider/backend.rs index bcab08df..f3d36642 100644 --- a/src/collision/collider/backend.rs +++ b/src/collision/collider/backend.rs @@ -91,6 +91,9 @@ impl Plugin for ColliderBackendPlugin { app.insert_resource(ColliderRemovalSystem(collider_removed_id)); } + // Initialize missing components for colliders. + app.observe(on_add_collider::); + // Register a component hook that updates mass properties of rigid bodies // when the colliders attached to them are removed. // Also removes `ColliderMarker` components. @@ -197,7 +200,6 @@ impl Plugin for ColliderBackendPlugin { app.add_systems( self.schedule, ( - init_colliders::.in_set(PrepareSet::InitColliders), init_transforms:: .in_set(PrepareSet::InitTransforms) .after(init_transforms::), @@ -214,9 +216,7 @@ impl Plugin for ColliderBackendPlugin { // Update collider parents for colliders that are on the same entity as the rigid body. app.add_systems( self.schedule, - update_root_collider_parents:: - .after(PrepareSet::InitColliders) - .before(PrepareSet::Finalize), + update_root_collider_parents::.before(PrepareSet::Finalize), ); let physics_schedule = app @@ -251,12 +251,11 @@ impl Plugin for ColliderBackendPlugin { pub struct ColliderMarker; /// Initializes missing components for [colliders](Collider). -#[allow(clippy::type_complexity)] -pub(crate) fn init_colliders( +pub(crate) fn on_add_collider( + trigger: Trigger, mut commands: Commands, - mut colliders: Query< + colliders: Query< ( - Entity, &C, Option<&ColliderAabb>, Option<&ColliderDensity>, @@ -265,28 +264,29 @@ pub(crate) fn init_colliders( Added, >, ) { - for (entity, collider, aabb, density, is_sensor) in &mut colliders { - let density = *density.unwrap_or(&ColliderDensity::default()); - let mass_properties = if is_sensor { - ColliderMassProperties::ZERO - } else { - collider.mass_properties(density.0) - }; - - commands.entity(entity).try_insert(( - *aabb.unwrap_or(&collider.aabb(Vector::ZERO, Rotation::default())), - density, - mass_properties, - CollidingEntities::default(), - ColliderMarker, - )); - } + let Ok((collider, aabb, density, is_sensor)) = colliders.get(trigger.entity()) else { + return; + }; + + let density = *density.unwrap_or(&ColliderDensity::default()); + let mass_properties = if is_sensor { + ColliderMassProperties::ZERO + } else { + collider.mass_properties(density.0) + }; + + commands.entity(trigger.entity()).try_insert(( + *aabb.unwrap_or(&collider.aabb(Vector::ZERO, Rotation::default())), + density, + mass_properties, + CollidingEntities::default(), + ColliderMarker, + )); } /// Updates [`ColliderParent`] for colliders that are on the same entity as the [`RigidBody`]. /// /// The [`ColliderHierarchyPlugin`] should be used to handle hierarchies. -#[allow(clippy::type_complexity)] fn update_root_collider_parents( mut commands: Commands, mut bodies: Query< diff --git a/src/collision/collider/hierarchy.rs b/src/collision/collider/hierarchy.rs index ee6e331d..5eb58ae2 100644 --- a/src/collision/collider/hierarchy.rs +++ b/src/collision/collider/hierarchy.rs @@ -58,17 +58,13 @@ impl Plugin for ColliderHierarchyPlugin { app.configure_sets( self.schedule, - MarkColliderAncestors - .after(PrepareSet::InitColliders) - .before(PrepareSet::PropagateTransforms), + MarkColliderAncestors.before(PrepareSet::PropagateTransforms), ); // Update collider parents. app.add_systems( self.schedule, - update_collider_parents - .after(PrepareSet::InitColliders) - .before(PrepareSet::Finalize), + update_collider_parents.before(PrepareSet::Finalize), ); // Run transform propagation if new colliders without rigid bodies have been added. diff --git a/src/dynamics/ccd/mod.rs b/src/dynamics/ccd/mod.rs index c64c681a..db10aadc 100644 --- a/src/dynamics/ccd/mod.rs +++ b/src/dynamics/ccd/mod.rs @@ -226,11 +226,11 @@ //! Finally, making the [physics timestep](Physics) smaller can also help. //! However, this comes at the cost of worse performance for the entire simulation. -use crate::{collision::broad_phase::AabbIntersections, prelude::*, prepare::PrepareSet}; +use crate::{collision::broad_phase::AabbIntersections, prelude::*}; #[cfg(any(feature = "parry-f32", feature = "parry-f64"))] use bevy::ecs::query::QueryData; use bevy::{ - ecs::{intern::Interned, schedule::ScheduleLabel}, + ecs::component::{ComponentHooks, StorageType}, prelude::*, }; use derive_more::From; @@ -240,36 +240,12 @@ use parry::query::{ }; /// A plugin for [Continuous Collision Detection](self). -pub struct CcdPlugin { - schedule: Interned, -} - -impl CcdPlugin { - /// Creates a [`CcdPlugin`] with the schedule that is used for running the [`PhysicsSchedule`]. - /// - /// The default schedule is `PostUpdate`. - pub fn new(schedule: impl ScheduleLabel) -> Self { - Self { - schedule: schedule.intern(), - } - } -} - -impl Default for CcdPlugin { - fn default() -> Self { - Self::new(PostUpdate) - } -} +pub struct CcdPlugin; impl Plugin for CcdPlugin { fn build(&self, app: &mut App) { app.register_type::().register_type::(); - app.add_systems( - self.schedule, - init_ccd_aabb_intersections.in_set(PrepareSet::InitColliders), - ); - // Get the `PhysicsSchedule`, and panic if it doesn't exist. let physics = app .get_schedule_mut(PhysicsSchedule) @@ -397,7 +373,7 @@ impl SpeculativeMargin { /// )); /// } /// ``` -#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)] +#[derive(Clone, Copy, Debug, PartialEq, Reflect)] #[reflect(Component)] pub struct SweptCcd { /// The type of sweep used for swept CCD. @@ -481,6 +457,19 @@ impl SweptCcd { } } +impl Component for SweptCcd { + const STORAGE_TYPE: StorageType = StorageType::Table; + + fn register_component_hooks(hooks: &mut ComponentHooks) { + hooks.on_add(|mut world, entity, _| { + world + .commands() + .entity(entity) + .insert(AabbIntersections::default()); + }); + } +} + /// The algorithm used for [Swept Continuous Collision Detection](self#swept-ccd). /// /// If two entities with different sweep modes collide, [`SweepMode::NonLinear`] @@ -510,12 +499,6 @@ pub enum SweepMode { NonLinear, } -fn init_ccd_aabb_intersections(mut commands: Commands, query: Query>) { - for entity in &query { - commands.entity(entity).insert(AabbIntersections::default()); - } -} - #[cfg(any(feature = "parry-f32", feature = "parry-f64"))] #[derive(QueryData)] #[query_data(mutable)] diff --git a/src/lib.rs b/src/lib.rs index 35930e63..17f8cc57 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -735,9 +735,9 @@ impl PluginGroup for PhysicsPlugins { .add(ContactReportingPlugin) .add(IntegratorPlugin::default()) .add(SolverPlugin::new_with_length_unit(self.length_unit)) - .add(CcdPlugin::new(self.schedule)) + .add(CcdPlugin) .add(SleepingPlugin) - .add(SpatialQueryPlugin::new(self.schedule)) + .add(SpatialQueryPlugin) .add(SyncPlugin::new(self.schedule)) } } diff --git a/src/prepare.rs b/src/prepare.rs index 8e2d73b6..a0190a66 100644 --- a/src/prepare.rs +++ b/src/prepare.rs @@ -6,7 +6,11 @@ use crate::prelude::*; use bevy::{ - ecs::{intern::Interned, query::QueryFilter, schedule::ScheduleLabel}, + ecs::{ + intern::Interned, + query::{QueryData, QueryFilter}, + schedule::ScheduleLabel, + }, prelude::*, }; @@ -51,26 +55,17 @@ impl Default for PreparePlugin { /// You can use these to schedule your own initialization systems /// without having to worry about implementation details. /// -/// 1. `PreInit`: Used for systems that must run before initialization. -/// 2. `InitRigidBodies`: Responsible for initializing missing [`RigidBody`] components. -/// 3. `InitColliders`: Responsible for initializing missing [`Collider`] components. -/// 4. `PropagateTransforms`: Responsible for propagating transforms. -/// 5. `InitMassProperties`: Responsible for initializing missing mass properties for [`RigidBody`] components. -/// 6. `InitTransforms`: Responsible for initializing [`Transform`] based on [`Position`] and [`Rotation`] +/// 1. `First`: Runs at the start of the preparation step. +/// 2. `PropagateTransforms`: Responsible for propagating transforms. +/// 3. `InitTransforms`: Responsible for initializing [`Transform`] based on [`Position`] and [`Rotation`] /// or vice versa. -/// 7. `Finalize`: Responsible for performing final updates after everything is initialized. +/// 4. `Finalize`: Responsible for performing final updates after everything is initialized and updated. #[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum PrepareSet { - /// Used for systems that must run before initialization. - PreInit, - /// Responsible for initializing missing [`RigidBody`] components. - InitRigidBodies, - /// Responsible for initializing missing [`Collider`] components. - InitColliders, + /// Runs at the start of the preparation step. + First, /// Responsible for propagating transforms. PropagateTransforms, - /// Responsible for initializing missing mass properties for [`RigidBody`] components. - InitMassProperties, /// Responsible for initializing [`Transform`] based on [`Position`] and [`Rotation`] /// or vice versa. Parts of this system can be disabled with [`PrepareConfig`]. /// Schedule your system with this to implement custom behavior for initializing transforms. @@ -85,10 +80,7 @@ impl Plugin for PreparePlugin { app.configure_sets( self.schedule, ( - PrepareSet::PreInit, - PrepareSet::InitRigidBodies, - PrepareSet::InitColliders, - PrepareSet::InitMassProperties, + PrepareSet::First, PrepareSet::PropagateTransforms, PrepareSet::InitTransforms, PrepareSet::Finalize, @@ -100,6 +92,9 @@ impl Plugin for PreparePlugin { app.init_resource::() .register_type::(); + // Initialize missing components for rigid bodies. + app.observe(on_add_rigid_body); + // Note: Collider logic is handled by the `ColliderBackendPlugin` app.add_systems( self.schedule, @@ -112,14 +107,6 @@ impl Plugin for PreparePlugin { .run_if(match_any::>) .in_set(PrepareSet::PropagateTransforms), ) - .add_systems( - self.schedule, - init_rigid_bodies.in_set(PrepareSet::InitRigidBodies), - ) - .add_systems( - self.schedule, - init_mass_properties.in_set(PrepareSet::InitMassProperties), - ) .add_systems( self.schedule, init_transforms::.in_set(PrepareSet::InitTransforms), @@ -371,84 +358,71 @@ pub fn init_transforms( } } -/// Initializes missing components for [rigid bodies](RigidBody). -fn init_rigid_bodies( - mut commands: Commands, - mut bodies: Query< - ( - Entity, - Option<&LinearVelocity>, - Option<&AngularVelocity>, - Option<&ExternalForce>, - Option<&ExternalTorque>, - Option<&ExternalImpulse>, - Option<&ExternalAngularImpulse>, - Option<&Restitution>, - Option<&Friction>, - Option<&TimeSleeping>, - ), - Added, - >, -) { - for ( - entity, - lin_vel, - ang_vel, - force, - torque, - impulse, - angular_impulse, - restitution, - friction, - time_sleeping, - ) in &mut bodies - { - commands.entity(entity).try_insert(( - AccumulatedTranslation(Vector::ZERO), - *lin_vel.unwrap_or(&LinearVelocity::default()), - *ang_vel.unwrap_or(&AngularVelocity::default()), - PreSolveLinearVelocity::default(), - PreSolveAngularVelocity::default(), - *force.unwrap_or(&ExternalForce::default()), - *torque.unwrap_or(&ExternalTorque::default()), - *impulse.unwrap_or(&ExternalImpulse::default()), - *angular_impulse.unwrap_or(&ExternalAngularImpulse::default()), - *restitution.unwrap_or(&Restitution::default()), - *friction.unwrap_or(&Friction::default()), - *time_sleeping.unwrap_or(&TimeSleeping::default()), - )); - } +#[derive(QueryData)] +struct RigidBodyInitializationQuery { + lin_vel: Option<&'static LinearVelocity>, + ang_vel: Option<&'static AngularVelocity>, + force: Option<&'static ExternalForce>, + torque: Option<&'static ExternalTorque>, + impulse: Option<&'static ExternalImpulse>, + angular_impulse: Option<&'static ExternalAngularImpulse>, + restitution: Option<&'static Restitution>, + friction: Option<&'static Friction>, + time_sleeping: Option<&'static TimeSleeping>, + mass: Option<&'static Mass>, + inverse_mass: Option<&'static InverseMass>, + inertia: Option<&'static Inertia>, + inverse_inertia: Option<&'static InverseInertia>, + center_of_mass: Option<&'static CenterOfMass>, } -/// Initializes missing mass properties for [rigid bodies](RigidBody). -fn init_mass_properties( +// NOTE: This will be redundant once we have required components. +/// Initializes missing components for [rigid bodies](RigidBody). +fn on_add_rigid_body( + trigger: Trigger, mut commands: Commands, - mass_properties: Query< - ( - Entity, - Option<&Mass>, - Option<&InverseMass>, - Option<&Inertia>, - Option<&InverseInertia>, - Option<&CenterOfMass>, - ), - Added, - >, + bodies: Query>, ) { - for (entity, mass, inverse_mass, inertia, inverse_inertia, center_of_mass) in &mass_properties { - commands.entity(entity).try_insert(( - *mass.unwrap_or(&Mass( - inverse_mass.map_or(0.0, |inverse_mass| 1.0 / inverse_mass.0), - )), - *inverse_mass.unwrap_or(&InverseMass(mass.map_or(0.0, |mass| 1.0 / mass.0))), - *inertia.unwrap_or( - &inverse_inertia.map_or(Inertia::ZERO, |inverse_inertia| inverse_inertia.inverse()), - ), - *inverse_inertia - .unwrap_or(&inertia.map_or(InverseInertia::ZERO, |inertia| inertia.inverse())), - *center_of_mass.unwrap_or(&CenterOfMass::default()), - )); - } + let Ok(rb) = bodies.get(trigger.entity()) else { + return; + }; + + // Two separate inserts needed because of tuple size limit + let mut entity_commands = commands.entity(trigger.entity()); + + entity_commands.try_insert(( + AccumulatedTranslation(Vector::ZERO), + *rb.lin_vel.unwrap_or(&LinearVelocity::default()), + *rb.ang_vel.unwrap_or(&AngularVelocity::default()), + PreSolveLinearVelocity::default(), + PreSolveAngularVelocity::default(), + *rb.force.unwrap_or(&ExternalForce::default()), + *rb.torque.unwrap_or(&ExternalTorque::default()), + *rb.impulse.unwrap_or(&ExternalImpulse::default()), + *rb.angular_impulse + .unwrap_or(&ExternalAngularImpulse::default()), + *rb.restitution.unwrap_or(&Restitution::default()), + *rb.friction.unwrap_or(&Friction::default()), + *rb.time_sleeping.unwrap_or(&TimeSleeping::default()), + )); + + entity_commands.try_insert(( + *rb.mass.unwrap_or(&Mass( + rb.inverse_mass + .map_or(0.0, |inverse_mass| 1.0 / inverse_mass.0), + )), + *rb.inverse_mass + .unwrap_or(&InverseMass(rb.mass.map_or(0.0, |mass| 1.0 / mass.0))), + *rb.inertia.unwrap_or( + &rb.inverse_inertia + .map_or(Inertia::ZERO, |inverse_inertia| inverse_inertia.inverse()), + ), + *rb.inverse_inertia.unwrap_or( + &rb.inertia + .map_or(InverseInertia::ZERO, |inertia| inertia.inverse()), + ), + *rb.center_of_mass.unwrap_or(&CenterOfMass::default()), + )); } /// Updates each body's [`InverseMass`] and [`InverseInertia`] whenever [`Mass`] or [`Inertia`] are changed. diff --git a/src/spatial_query/mod.rs b/src/spatial_query/mod.rs index 25e55760..88eb5b0d 100644 --- a/src/spatial_query/mod.rs +++ b/src/spatial_query/mod.rs @@ -170,35 +170,13 @@ pub use shape_caster::*; #[cfg(any(feature = "parry-f32", feature = "parry-f64"))] pub use system_param::*; -use crate::{prelude::*, prepare::PrepareSet}; -use bevy::{ - ecs::{intern::Interned, schedule::ScheduleLabel}, - prelude::*, -}; +use crate::prelude::*; +use bevy::prelude::*; /// Initializes the [`SpatialQueryPipeline`] resource and handles component-based [spatial queries](spatial_query) /// like [raycasting](spatial_query#raycasting) and [shapecasting](spatial_query#shapecasting) with /// [`RayCaster`] and [`ShapeCaster`]. -pub struct SpatialQueryPlugin { - schedule: Interned, -} - -impl SpatialQueryPlugin { - /// Creates a [`SpatialQueryPlugin`] with the schedule that is used for running the [`PhysicsSchedule`]. - /// - /// The default schedule is `PostUpdate`. - pub fn new(schedule: impl ScheduleLabel) -> Self { - Self { - schedule: schedule.intern(), - } - } -} - -impl Default for SpatialQueryPlugin { - fn default() -> Self { - Self::new(PostUpdate) - } -} +pub struct SpatialQueryPlugin; impl Plugin for SpatialQueryPlugin { fn build(&self, app: &mut App) { @@ -208,14 +186,6 @@ impl Plugin for SpatialQueryPlugin { ))] app.init_resource::(); - app.add_systems(self.schedule, init_ray_hits.in_set(PrepareSet::PreInit)); - - #[cfg(all( - feature = "default-collider", - any(feature = "parry-f32", feature = "parry-f64") - ))] - app.add_systems(self.schedule, init_shape_hits.in_set(PrepareSet::PreInit)); - let physics_schedule = app .get_schedule_mut(PhysicsSchedule) .expect("add PhysicsSchedule first"); @@ -241,33 +211,6 @@ impl Plugin for SpatialQueryPlugin { } } -fn init_ray_hits(mut commands: Commands, rays: Query<(Entity, &RayCaster), Added>) { - for (entity, ray) in &rays { - let max_hits = if ray.max_hits == u32::MAX { - 10 - } else { - ray.max_hits as usize - }; - commands.entity(entity).try_insert(RayHits { - vector: Vec::with_capacity(max_hits), - count: 0, - }); - } -} - -#[cfg(any(feature = "parry-f32", feature = "parry-f64"))] -fn init_shape_hits( - mut commands: Commands, - shape_casters: Query<(Entity, &ShapeCaster), Added>, -) { - for (entity, shape_caster) in &shape_casters { - commands.entity(entity).try_insert(ShapeHits { - vector: Vec::with_capacity(shape_caster.max_hits.min(100_000) as usize), - count: 0, - }); - } -} - type RayCasterPositionQueryComponents = ( &'static mut RayCaster, Option<&'static Position>, diff --git a/src/spatial_query/ray_caster.rs b/src/spatial_query/ray_caster.rs index 2db86a46..42df506a 100644 --- a/src/spatial_query/ray_caster.rs +++ b/src/spatial_query/ray_caster.rs @@ -1,6 +1,9 @@ use crate::prelude::*; use bevy::{ - ecs::entity::{EntityMapper, MapEntities}, + ecs::{ + component::{ComponentHooks, StorageType}, + entity::{EntityMapper, MapEntities}, + }, prelude::*, }; #[cfg(all( @@ -67,7 +70,7 @@ use parry::query::{ /// } /// } /// ``` -#[derive(Component, Clone, Debug, PartialEq, Reflect)] +#[derive(Clone, Debug, PartialEq, Reflect)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))] #[reflect(Debug, Component, PartialEq)] @@ -318,6 +321,26 @@ impl RayCaster { } } +impl Component for RayCaster { + const STORAGE_TYPE: StorageType = StorageType::Table; + + fn register_component_hooks(hooks: &mut ComponentHooks) { + hooks.on_add(|mut world, entity, _| { + let ray_caster = world.get::(entity).unwrap(); + let max_hits = if ray_caster.max_hits == u32::MAX { + 10 + } else { + ray_caster.max_hits as usize + }; + + world.commands().entity(entity).try_insert(RayHits { + vector: Vec::with_capacity(max_hits), + count: 0, + }); + }); + } +} + /// Contains the hits of a ray cast by a [`RayCaster`]. /// /// The maximum number of hits depends on the value of `max_hits` in [`RayCaster`]. diff --git a/src/spatial_query/shape_caster.rs b/src/spatial_query/shape_caster.rs index 467c52c7..3ada5f65 100644 --- a/src/spatial_query/shape_caster.rs +++ b/src/spatial_query/shape_caster.rs @@ -1,6 +1,9 @@ use crate::prelude::*; use bevy::{ - ecs::entity::{EntityMapper, MapEntities}, + ecs::{ + component::{ComponentHooks, StorageType}, + entity::{EntityMapper, MapEntities}, + }, prelude::*, }; use parry::query::{details::TOICompositeShapeShapeBestFirstVisitor, ShapeCastOptions}; @@ -50,7 +53,7 @@ use parry::query::{details::TOICompositeShapeShapeBestFirstVisitor, ShapeCastOpt /// } /// } /// ``` -#[derive(Component, Clone, Debug, Reflect)] +#[derive(Clone, Debug, Reflect)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))] #[reflect(Debug, Component)] @@ -342,6 +345,26 @@ impl ShapeCaster { } } +impl Component for ShapeCaster { + const STORAGE_TYPE: StorageType = StorageType::Table; + + fn register_component_hooks(hooks: &mut ComponentHooks) { + hooks.on_add(|mut world, entity, _| { + let shape_caster = world.get::(entity).unwrap(); + let max_hits = if shape_caster.max_hits == u32::MAX { + 10 + } else { + shape_caster.max_hits as usize + }; + + world.commands().entity(entity).try_insert(ShapeHits { + vector: Vec::with_capacity(max_hits), + count: 0, + }); + }); + } +} + /// Contains the hits of a shape cast by a [`ShapeCaster`]. The hits are in the order of time of impact. /// /// The maximum number of hits depends on the value of `max_hits` in [`ShapeCaster`]. By default only diff --git a/src/sync/mod.rs b/src/sync/mod.rs index c0ee89e7..ca5579e2 100644 --- a/src/sync/mod.rs +++ b/src/sync/mod.rs @@ -85,7 +85,7 @@ impl Plugin for SyncPlugin { app.configure_sets( self.schedule, - MarkRigidBodyAncestors.in_set(PrepareSet::PreInit), + MarkRigidBodyAncestors.in_set(PrepareSet::First), ); // Initialize `PreviousGlobalTransform` and apply `Transform` changes that happened