Skip to content

Commit

Permalink
Use hooks for component initialization (#483)
Browse files Browse the repository at this point in the history
# Objective

Fixes #475.

Missing components for rigid bodies, colliders, etc. are currently added at specific points in the physics shedule. This means that the world is momentarily in an invalid state before those systems are run, which can even lead to confusing crashes when using methods like `query.single()` for queries that expect those components to exist.

## Solution

Use component hooks for initializing missing components with minimal delay. This also meaningfully simplifies scheduling.

When Bevy gets required components (Soon™), those could be used instead.

### Performance

Using hooks appears to be as fast if not *faster* than the old initialization systems.

---

## Migration Guide

`PrepareSet::PreInit` has been renamed to `PrepareSet::First`, and `PrepareSet::InitRigidBodies`, `PrepareSet::InitColliders`, and `PrepareSet::InitMassProperties` have been removed. Most missing components are now initialized by component lifecycle hooks.

`CcdPlugin` and `SpatialQueryPipeline` no longer store a schedule and are now unit structs. Instead of `SpatialQueryPlugin::new(my_schedule)` or `SpatialQueryPlugin::default()`, just use `SpatialQueryPlugin`.
  • Loading branch information
Jondolf committed Aug 10, 2024
1 parent 73fe6c7 commit 5041bea
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 249 deletions.
70 changes: 32 additions & 38 deletions src/collision/collider/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,37 @@ impl<C: ScalableCollider> Plugin for ColliderBackendPlugin<C> {
app.insert_resource(ColliderRemovalSystem(collider_removed_id));
}

let hooks = app.world_mut().register_component_hooks::<C>();

// Initialize missing components for colliders.
hooks.on_add(|mut world, entity, _| {
let entity_ref = world.entity(entity);

let collider = entity_ref.get::<C>().unwrap();
let aabb = entity_ref
.get::<ColliderAabb>()
.copied()
.unwrap_or(collider.aabb(Vector::ZERO, Rotation::default()));
let density = entity_ref
.get::<ColliderDensity>()
.copied()
.unwrap_or_default();

let mass_properties = if entity_ref.get::<Sensor>().is_some() {
ColliderMassProperties::ZERO
} else {
collider.mass_properties(density.0)
};

world.commands().entity(entity).try_insert((
aabb,
density,
mass_properties,
CollidingEntities::default(),
ColliderMarker,
));
});

// Register a component hook that updates mass properties of rigid bodies
// when the colliders attached to them are removed.
// Also removes `ColliderMarker` components.
Expand Down Expand Up @@ -197,7 +228,6 @@ impl<C: ScalableCollider> Plugin for ColliderBackendPlugin<C> {
app.add_systems(
self.schedule,
(
init_colliders::<C>.in_set(PrepareSet::InitColliders),
init_transforms::<C>
.in_set(PrepareSet::InitTransforms)
.after(init_transforms::<RigidBody>),
Expand All @@ -214,9 +244,7 @@ impl<C: ScalableCollider> Plugin for ColliderBackendPlugin<C> {
// Update collider parents for colliders that are on the same entity as the rigid body.
app.add_systems(
self.schedule,
update_root_collider_parents::<C>
.after(PrepareSet::InitColliders)
.before(PrepareSet::Finalize),
update_root_collider_parents::<C>.before(PrepareSet::Finalize),
);

let physics_schedule = app
Expand Down Expand Up @@ -250,43 +278,9 @@ impl<C: ScalableCollider> Plugin for ColliderBackendPlugin<C> {
#[derive(Reflect, Component, Clone, Copy, Debug)]
pub struct ColliderMarker;

/// Initializes missing components for [colliders](Collider).
#[allow(clippy::type_complexity)]
pub(crate) fn init_colliders<C: AnyCollider>(
mut commands: Commands,
mut colliders: Query<
(
Entity,
&C,
Option<&ColliderAabb>,
Option<&ColliderDensity>,
Has<Sensor>,
),
Added<C>,
>,
) {
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,
));
}
}

/// 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<C: AnyCollider>(
mut commands: Commands,
mut bodies: Query<
Expand Down
6 changes: 2 additions & 4 deletions src/collision/collider/hierarchy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,14 @@ 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)
.after(PrepareSet::PropagateTransforms)
.before(PrepareSet::Finalize),
);

Expand Down
51 changes: 17 additions & 34 deletions src/dynamics/ccd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -240,36 +240,12 @@ use parry::query::{
};

/// A plugin for [Continuous Collision Detection](self).
pub struct CcdPlugin {
schedule: Interned<dyn ScheduleLabel>,
}

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::<SweptCcd>().register_type::<SweepMode>();

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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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`]
Expand Down Expand Up @@ -510,12 +499,6 @@ pub enum SweepMode {
NonLinear,
}

fn init_ccd_aabb_intersections(mut commands: Commands, query: Query<Entity, Added<SweptCcd>>) {
for entity in &query {
commands.entity(entity).insert(AabbIntersections::default());
}
}

#[cfg(any(feature = "parry-f32", feature = "parry-f64"))]
#[derive(QueryData)]
#[query_data(mutable)]
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Loading

0 comments on commit 5041bea

Please sign in to comment.