Jondolf / avian

ECS-driven 2D and 3D physics engine for the Bevy game engine.
https://crates.io/crates/avian3d
Apache License 2.0
1.55k stars 120 forks source link

Children entity collision handling is *weird*. #261

Open ashirviskas opened 1 year ago

ashirviskas commented 1 year ago

I've uncountered some interesting collision handling behaviour and I am not sure if it is an error on my part, or a bug in bevy_xpbd.

I have entities that have Vision. I implement Vision, by spawning a circle with radius X collider as a child of an Entity (craber), in the middle of the Entity (craber).

I want to catch when other entities (Food) get into the vision (anywhere in the radius X) and I found a way of doing that by using xpbd's SpatialQuery.shape_intersections. However, it was very costly, so I implemented it in such a way, that would only check for intersections if a Food entity has entered the Vision via Collision. However, it seems, that the Collision event between Vision and Food only happens at a craber spawning time. It does not happen at any other times.

Here is the relevant code (excuse how messy it is):

  1. Craber with Vision spawning link
  2. Food spawning link
  3. Checking for collisions link
TeamDman commented 10 months ago

I have made a nice reproduction of the weirdness

In my example, the character is the parent of both the pressure plate and the hand. As the character moves, its position and transform are both updated, but on the children only the transform is updated. This results in the children visually following the parent while the physics position not following the parent.

If the child has a velocity, the physics logic takes priority and overwrites the transform, resulting in the child moving with a velocity independent of the parent's velocity.

In the example, I have added ghosts to show the difference between the transform and the position

https://github.com/Jondolf/bevy_xpbd/assets/9356891/8fac3ecc-7886-42e0-a2df-2b96c7cdd005

This is all related to the decision to use components instead of Transform to handle the physics (https://github.com/Jondolf/bevy_xpbd/issues/16 and https://github.com/Jondolf/bevy_xpbd/pull/96)

/// When synchronizing changes in [`Position`] or [`Rotation`] to `Transform`,
/// the engine treats nested [rigid bodies](RigidBody) as a flat structure. This means that
/// the bodies move independently of the parents, and moving the parent will not affect the child.
///
/// If you would like a child entity to be rigidly attached to its parent, you could use a [`FixedJoint`]
/// or write your own system to handle hierarchies differently.

ref

Here's a naïve attempt at inheriting the parent velocity. I changed the pressure plate from static to dynamic so that it also receives a velocity component that we can update.

fn inherit_velocity(
    mut kids: Query<(&mut LinearVelocity, &mut AngularVelocity, &Parent), Without<Character>>,
    parents: Query<(&LinearVelocity, &AngularVelocity), With<Character>>,
) {
    for (mut kid_linear_velocity, mut kid_angular_velocity, parent) in &mut kids.iter_mut() {
        if let Ok((parent_linear_velocity, parent_angular_velocity)) = parents.get(parent.get()) {
            kid_linear_velocity.x = parent_linear_velocity.x;
            kid_linear_velocity.y = parent_linear_velocity.y;
            kid_angular_velocity.0 = parent_angular_velocity.0;
        }
    }
}

https://github.com/Jondolf/bevy_xpbd/assets/9356891/80b17322-8c05-4259-9fd8-d192f26e5d92

This breaks the hand movement logic because the acceleration from its inputs gets clobbered when inheriting the parent velocity. Additionally, this would cause the children to rotate in place rather than pivoting around the parent.

Overall, the decision to have separate components instead of using Transform is because the physics system is inherently flat rather than hierarchical. The ECS itself is also flat, with Parent and Children being conveniences. Using a joint, as recommended by the code comment, is probably the best way to solve this issue.

If you're going to manually adjust transforms to have entities follow another, watch out for: Jitter when making the camera follow a physics object

ashirviskas commented 2 weeks ago

@TeamDman thank you for your response, adding a simple FixedJoint between parent and child solved the issue.

However, it seems to be degrading the performance by a lot (per my testing, roughly between 30% and 50%) when I have a ton of entities. Is there a way to have something more solid than a FixedJoint, so it would not try to calculate all the dampenings, but just treat two entities as one regarding the physics?

I've also tried the method of applying the velocities as per the example provided, but it leads to the entities being out of sync.

ashirviskas commented 2 weeks ago

@Jondolf maybe I could get a bit of your input. How to connect two entities properly, where one is physical and the other is just a sensor without incuring physics calculation costs? As FixedJoint seems to have a big performance impact compared to naive solutions (but they have other costs). Any other way to basically glue an entity and a sensor together?

Jondolf commented 2 weeks ago

Doesn't spawning the sensor collider as a child work? Made a quick demo (on main branch, using Bevy 0.15 RC), seems to work fine

Code here ```rust use avian2d::{math::*, prelude::*}; use bevy::prelude::*; fn main() { App::new() .add_plugins((DefaultPlugins, PhysicsPlugins::default())) .add_systems(Startup, setup) .add_systems(Update, (movement, apply_sensor_color).chain()) .run(); } #[derive(Component)] struct Character; #[derive(Component)] struct Vision; fn setup( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { commands .spawn(( Name::new("Player"), Character, RigidBody::Kinematic, Collider::capsule(12.5, 20.0), Mesh2d(meshes.add(Capsule2d::new(12.5, 20.0))), MeshMaterial2d(materials.add(Color::srgb(0.9, 0.9, 0.9))), Transform::default(), )) .with_child(( Name::new("Vision Sensor"), Vision, Sensor, Collider::circle(100.0), Mesh2d(meshes.add(Circle::new(100.0))), MeshMaterial2d(materials.add(Color::srgb(0.2, 0.7, 0.9))), Transform::from_xyz(0.0, 0.0, -1.0), // Read entities colliding with this entity. CollidingEntities::default(), )); commands.spawn(( Name::new("Food"), Collider::circle(10.0), Mesh2d(meshes.add(Circle::new(10.0))), MeshMaterial2d(materials.add(Color::srgb(1.0, 0.3, 0.3))), Transform::from_xyz(0.0, 150.0, 1.0), )); commands.spawn(Camera2d); } fn movement( mut lin_vel: Single<&mut LinearVelocity, With>, keyboard_input: Res>, ) { let left = keyboard_input.pressed(KeyCode::KeyA); let right = keyboard_input.pressed(KeyCode::KeyD); let up = keyboard_input.pressed(KeyCode::KeyW); let down = keyboard_input.pressed(KeyCode::KeyS); let horizontal = right as i8 - left as i8; let vertical = up as i8 - down as i8; let direction = Vec2::new(horizontal as Scalar, vertical as Scalar); if direction != Vec2::ZERO { lin_vel.0 = direction * 50.0; } else { lin_vel.0 = Vec2::ZERO; } } fn apply_sensor_color( mut query: Query<(&mut MeshMaterial2d, &CollidingEntities), With>, mut materials: ResMut>, ) { for (mut material, colliding_entities) in &mut query { if colliding_entities.0.is_empty() { material.0 = materials.add(Color::srgb(0.2, 0.7, 0.9)); } else { material.0 = materials.add(Color::srgb(0.9, 0.7, 0.2)); } } } ```

https://github.com/user-attachments/assets/6d241ce8-73cf-4b7d-9c8e-0fdb92ea340c

Jondolf commented 2 weeks ago

I noticed in your original code the child collider is also a RigidBody (link). You generally shouldn't nest rigid bodies like this, especially dynamic ones. Instead, just spawn the child with a collider, but only have RigidBody on the parent. Not sure if this is related to your actual issue though.