Unity-Technologies / EntityComponentSystemSamples

Other
7.1k stars 1.6k forks source link

Physics stateful events inconsistency when both objects have physics body. #215

Open baznzab opened 2 years ago

baznzab commented 2 years ago

I found out that in case if both interacting objects have colliders and only one body has physics shape everything goes fine and trigger events are correct, but in case if both have rigidbodies at the same time instead the behaviour is chaotic.

Example

using Unity.Entities;
using Unity.Physics.Stateful;

[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
[UpdateAfter(typeof(StatefulTriggerEventBufferSystem))]
public partial class PickupRangeSystem : SystemBase {
  private BeginFixedStepSimulationEntityCommandBufferSystem _ecbBeginSystem;
  private EndFixedStepSimulationEntityCommandBufferSystem _ecbEndSystem;

  protected override void OnCreate() {
    base.OnCreate();
    _ecbBeginSystem = World.GetOrCreateSystem<BeginFixedStepSimulationEntityCommandBufferSystem>();
    _ecbEndSystem = World.GetOrCreateSystem<EndFixedStepSimulationEntityCommandBufferSystem>();
  }

  protected override void OnUpdate() {
    var ecbBegin = _ecbBeginSystem.CreateCommandBuffer().AsParallelWriter();
    var ecbEnd = _ecbEndSystem.CreateCommandBuffer().AsParallelWriter();

    Dependency = Entities
      .WithAll<Player>()
      .ForEach((Entity entity, int entityInQueryIndex, in DynamicBuffer<StatefulTriggerEvent> triggerEvents) => {
        foreach (var triggerEvent in triggerEvents) {
          if (!triggerEvent.TryGetOther(entity, out var other)) {
            continue;
          }
          if (triggerEvent.State == StatefulEventState.Enter) {
            ecbBegin.AddComponent(entityInQueryIndex, other.Entity, new PickupRangeEvent { State = PickupRangeState.Enter });
          } else if (triggerEvent.State == StatefulEventState.Exit) {
            ecbBegin.AddComponent(entityInQueryIndex, other.Entity, new PickupRangeEvent { State = PickupRangeState.Exit });
          }
        }
      }).ScheduleParallel(Dependency);

    Dependency = Entities
      .WithAll<PickupRangeEvent>()
      .ForEach((Entity entity, int entityInQueryIndex) => ecbEnd.RemoveComponent<PickupRangeEvent>(entityInQueryIndex, entity))
      .ScheduleParallel(Dependency);

    _ecbBeginSystem.AddJobHandleForProducer(Dependency);
    _ecbEndSystem.AddJobHandleForProducer(Dependency);
  }
}

In this example I'm using TryGetOther method i added to StatefulTriggerEvent for my convinience

public bool TryGetOther(in Entity entity, out (Entity Entity, int BodyIndex, ColliderKey ColliderKey) other) {
      if (EntityA.CompareTo(entity) != 0 && EntityB.CompareTo(entity) != 0) {
        other = (Entity.Null, -1, ColliderKey.Empty);
        return false;
      }
      other = EntityA.CompareTo(entity) == 0
        ? (EntityB, BodyIndexB, ColliderKeyB)
        : (EntityA, BodyIndexA, ColliderKeyA);
      return true;
    }

After running PickupViewSystem I'm continuously getting Enter and Exit events while bodies are colliding.

using Unity.Entities;

[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
public partial class PickupViewSystem : SystemBase {
  protected override void OnUpdate() {
    Entities
      .WithoutBurst()
      .WithAll<Pickup>()
      .ForEach((in PickupRangeEvent pickupRangeEvent) => {
        UnityEngine.Debug.Log(pickupRangeEvent.State);
      }).Run();
  }
}

Haven't tested for StatefulCollisionEvent but i assume and it's quite obvious the issue exists for both.

AlexMerzlikin commented 6 months ago

The issue is still relevant. I expect one trigger event that transitions from the 'Enter' to the 'Stay' state for all the subsequent frames. However, there are two trigger events with the following states instead: image

Furthermore, in the next frame, there are 2 new events with their states reversed: image

What is more interesting, this consistently occurs only in real-time. If you play your game frame by frame, then trigger events work as expected every time.

However, @baznzab mentioned that it happens when there are 2 rigid bodies, while the sample "Events - Collisions" has PhysicsBody added to all objects and works correctly as is, as well as when I replaced collisions with triggers. So the issue does not lie in rigid bodies or PhysicsBody components themselves but in the MotionType of PhysicsBody.

The sample uses 'Static' for the Event Surface and 'Dynamic' for balls. My setup used 'Kinematic' for both the object with the StatefulTriggerEvent buffer and the second object that is controlled by the character controller. I changed it to 'Static' and 'Kinematic' respectively, and now it works as expected. However, if anyone finds out how to make it work with multiple Kinematic physics bodies, that would be great.

Cheapetsgrow commented 6 months ago

kernel/config/build/android.bp/kernel_config.bp/r/android-4.14/android-base-conditionial.xml/androidbase.config/android-recommended-arm.config/androidrecommended-x86.config/non_debugable.config/android-4.19/android.bp/android-base-conditional.xml/android-base.config/android-recommended-arm.config/android-,recommended-,arm64.config/android-recommended-x86.confid/android-recommended.config/non_debugable.config/android-5.4/ run that in vs code man you will get 5,ooo+ solid lines of good code

On Fri, Mar 15, 2024, 11:19 AM Alex M. @.***> wrote:

The issue is still relevant. I expect one trigger event that transitions from the 'Enter' to the 'Stay' state for all the subsequent frames. However, there are two trigger events with the following states instead: image.png (view on web) https://github.com/Unity-Technologies/EntityComponentSystemSamples/assets/6983110/f30616ef-515f-423d-8e58-87947a3c5206

Furthermore, in the next frame, there are 2 new events with their states reversed: image.png (view on web) https://github.com/Unity-Technologies/EntityComponentSystemSamples/assets/6983110/4adc4a0e-ffaa-47c4-aa4e-71234b7d9308

What is more interesting, this consistently occurs only in real-time. If you play your game frame by frame, then trigger events work as expected every time.

However, @baznzab https://github.com/baznzab mentioned that it happens when there are 2 rigid bodies, while the sample "Events - Collisions" has PhysicsBody added to all objects and works correctly as is, as well as when I replaced collisions with triggers. So the issue does not lie in rigid bodies or PhysicsBody components themselves but in the MotionType of PhysicsBody.

The sample uses 'Static' for the Event Surface and 'Dynamic' for balls. My setup used 'Kinematic' for both the object with the StatefulTriggerEvent buffer and the second object that is controlled by the character controller. I changed it to 'Static' and 'Kinematic' respectively, and now it works as expected. However, if anyone finds out how to make it work with multiple Kinematic physics bodies, that would be great.

— Reply to this email directly, view it on GitHub https://github.com/Unity-Technologies/EntityComponentSystemSamples/issues/215#issuecomment-2000203399, or unsubscribe https://github.com/notifications/unsubscribe-auth/BGNX6FOROAT4U2IVAI2I6SLYYM3TFAVCNFSM54C3UNLKU5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TEMBQGAZDAMZTHE4Q . You are receiving this because you are subscribed to this thread.Message ID: <Unity-Technologies/EntityComponentSystemSamples/issues/215/2000203399 @github.com>

baznzab commented 6 months ago

@AlexMerzlikin I solved this issue myself after posting it here. It was long ago, so I'm not sure if my code still works, but here is what I did:

  1. Introduced custom comparer:
    
    using System;
    using System.Collections.Generic;
    using Unity.Entities;
    using Unity.Physics;

namespace Physics.Events { public struct SimulationEventComparer : IComparer where T : ISimulationEvent { public int Compare(T x, T y) { var xapair = new EntityColliderKeyPair(x.EntityA, x.ColliderKeyA); var xbpair = new EntityColliderKeyPair(x.EntityB, x.ColliderKeyB); var yapair = new EntityColliderKeyPair(y.EntityA, y.ColliderKeyA); var ybpair = new EntityColliderKeyPair(y.EntityB, y.ColliderKeyB);

  var xpairmax = xapair.CompareTo(xbpair) >= 0 ? xapair : xbpair;
  var xpairmin = xapair.CompareTo(xbpair) < 0 ? xapair : xbpair;
  var ypairmax = yapair.CompareTo(ybpair) >= 0 ? yapair : ybpair;
  var ypairmin = yapair.CompareTo(ybpair) < 0 ? yapair : ybpair;

  var result = xpairmax.CompareTo(ypairmax);
  if (result == 0) {
    result = xpairmin.CompareTo(ypairmin);
  }
  return result;
}

}

public struct EntityColliderKeyPair : IComparable { public Entity Entity; public ColliderKey ColliderKey;

public EntityColliderKeyPair(Entity entity, ColliderKey colliderKey) {
  Entity = entity;
  ColliderKey = colliderKey;
}

public int CompareTo(EntityColliderKeyPair other) {
  return GetHashCode().CompareTo(other.GetHashCode());
}

public override int GetHashCode() {
  var hash = new UnityEngine.Hash128();
  hash.Append(Entity.Index);
  hash.Append(ColliderKey.Value);
  return hash.GetHashCode();
}

} }


2. Inside SimulationEventBuffers:

using Unity.Collections; using Unity.Physics;

namespace Physics.Events { public class SimulationEventBuffers where T : unmanaged, ISimulationEvent { public NativeList Current { get; private set; } public NativeList Previous { get; private set; }

public SimulationEventBuffers() {
  Current = new NativeList<T>(Allocator.Persistent);
  Previous = new NativeList<T>(Allocator.Persistent);
}

public void Dispose() {
  Current.Dispose();
  Previous.Dispose();
}

public void Swap() {
  (Current, Previous) = (Previous, Current);
  Current.Clear();
}

public static void GetEvents(NativeList<T> current, NativeList<T> previous, NativeList<T> enter, NativeList<T> stay, NativeList<T> exit) {
  var comparer = new SimulationEventComparer<T>();
  current.Sort(comparer);
  previous.Sort(comparer);

  enter.Clear();
  stay.Clear();
  exit.Clear();

  int c = 0;
  int p = 0;
  while (c < current.Length && p < previous.Length) {
    int r = comparer.Compare(previous[p], current[c]);
    if (r == 0) {
      stay.Add(current[c]);
      c++;
      p++;
    } else if (r < 0) {
      exit.Add(previous[p]);
      p++;
    } else {
      enter.Add(current[c]);
      c++;
    }
  }
  if (c == current.Length) {
    while (p < previous.Length) {
      exit.Add(previous[p]);
      p++;
    }
  } else if (p == previous.Length) {
    while (c < current.Length) {
      enter.Add(current[c]);
      c++;
    }
  }
}

} }



Hope this helps.