EntityとEntityの接触判定

EntityとEntityの接触判定の取り方について備忘録です。
基本的にはunity physicsの[サンプルプロジェクト](https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/PhysicsSamples/README.md)の6. Events /Collisionsを参考にしていますが、他のプロジェクトに適用しようとすると結構面倒なので、メモを残します。

オブジェクト指向でいうOn Trigger Enter,On Trigger Stayに相当する機能を再現します。

https://x.com/khbowling/status/1769191391039799321?s=20



## OnTriggerStay的な機能（TriggerEvent）を再現
必要なscriptの一覧は以下
DisplayCollisionTextAuthoring.cs　※空のtextオブジェクトにアタッチするスクリプト
DisplayCollisionTextSystem.cs　※Authoringと対
StatefulCollisionEventAuthoring.cs　※地面側にアタッチするスクリプト
StatefulCollisionEventSystem.cs　※Authoringと対
EntitySender.cs　※キャラクター側、地面側、双方にアタッチするスクリプト
IStatefulSimulationEvent.cs
StatefulEventCollectionJobs.cs　
StatefulSimulationEventBuffers.cs　
StatefulTriggerEvent.cs　

詳細は公式サンプルを参照。
Physics Shape > Collision ResponseをCollide Raise Collision Eventsに変更するの忘れないように注意。

#### 【説明】
DisplayCollisionTextAuthoring.csをアタッチしたTextオブジェクトが、キャラクターの上に移動して接触中の時間経過を表示します。


## OnTriggerEnter的な機能（CollisionEvent）を再現
TriggerVolumePortalAuthoring.cs　※静的Entity側にアタッチするスクリプト
TriggerVolumePortalSystem.cs　※Authoringと対
TriggerVolumeChangeMaterialAuthoring　※静的Entity側にアタッチするスクリプト
TriggerVolumeChangeMaterialSystem　※Authoringと対
StatefulTriggerEventAuthoring　※静的Entity側にアタッチするスクリプト
StatefulTriggerEventSystem　※Authoringと対
IStatefulSimulationEvent.cs
StatefulEventCollectionJobs.cs　
StatefulSimulationEventBuffers.cs　
StatefulTriggerEvent.cs　

#### 【説明】
TriggerVolumeChangeMaterialAuthoringにより、オブジェクトの色や移動方向を変更します


以下３つのスクリプトは、OnTriggerStay的な機能とOnTriggerEnter的な機能の両方に必要です。
IStatefulSimulationEvent.cs
StatefulEventCollectionJobs.cs　
StatefulSimulationEventBuffers.cs　

IStatefulSimulationEvent.csでは、以下4つの状態を作成します。
・Undefined,
・Enter,
・Stay,
・Exit

StatefulEventCollectionJobs.csには、[BurstCompile]で実行するJobが羅列されている。
TriggerEventとCollisionEvent、どちらも定義されていて、それぞれ関連する〇〇Eventを実行する流れに繋がる。

StatefulSimulationEventBuffers.csについては、[バッファーについての記事](https://note.com/npaka/n/nb551f56b03e5)が参考になりそうです。


試しにTriggerVolumePortalSystem.csの中身を見てみます。
```
using Unity.Assertions;
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Physics;
using Unity.Physics.GraphicsIntegration;
using Unity.Physics.Stateful;
using Unity.Physics.Systems;
using Unity.Transforms;

namespace Events
{
    [RequireMatchingQueriesForUpdate]
    [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    [UpdateAfter(typeof(PhysicsSystemGroup))]
    public partial struct TriggerVolumePortalSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<TriggerVolumePortal>();
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            var hierarchyChildQuery = SystemAPI.QueryBuilder().WithAll<Parent, LocalToWorld>().Build();
            Assert.IsFalse(hierarchyChildQuery.HasFilter(),
                "The use of EntityQueryMask in this system will not respect the query's active filter settings.");

            var nonTriggerDynamicBodyQuery = SystemAPI.QueryBuilder()
                .WithAll<LocalTransform, PhysicsVelocity>()
                .WithNone<StatefulTriggerEvent>().Build();
            Assert.IsFalse(nonTriggerDynamicBodyQuery.HasFilter(),
                "The use of EntityQueryMask in this system will not respect the query's active filter settings.");

            state.Dependency = new TriggerVolumePortalJob()
            {
                LocalToWorldLookup = SystemAPI.GetComponentLookup<LocalToWorld>(),
                LocalTransformLookup = SystemAPI.GetComponentLookup<LocalTransform>(),
                TriggerVolumePortalLookup = SystemAPI.GetComponentLookup<TriggerVolumePortal>(),
                PhysicsVelocityLookup = SystemAPI.GetComponentLookup<PhysicsVelocity>(),
                PhysicsGraphicalSmoothingLookup = SystemAPI.GetComponentLookup<PhysicsGraphicalSmoothing>(),
                HierarchyChildMask = hierarchyChildQuery.GetEntityQueryMask(),
                NonTriggerDynamicBodyMask = nonTriggerDynamicBodyQuery.GetEntityQueryMask()
            }.Schedule(state.Dependency);
        }

        [BurstCompile]
        partial struct TriggerVolumePortalJob : IJobEntity
        {
            public EntityQueryMask HierarchyChildMask;
            public EntityQueryMask NonTriggerDynamicBodyMask;
            public ComponentLookup<LocalTransform> LocalTransformLookup;
            public ComponentLookup<LocalToWorld> LocalToWorldLookup;
            public ComponentLookup<TriggerVolumePortal> TriggerVolumePortalLookup;
            public ComponentLookup<PhysicsVelocity> PhysicsVelocityLookup;
            public ComponentLookup<PhysicsGraphicalSmoothing> PhysicsGraphicalSmoothingLookup;

            public void Execute(Entity portalEntity, ref DynamicBuffer<StatefulTriggerEvent> triggerBuffer) //
            {
                if (!TriggerVolumePortalLookup.HasComponent(portalEntity))
                {
                    return;
                }

                var triggerVolumePortal = TriggerVolumePortalLookup[portalEntity];
                var companionEntity = triggerVolumePortal.Companion; //もう一つのポータルのこと？
                var companionTriggerVolumePortal = TriggerVolumePortalLookup[companionEntity];

                for (int i = 0; i < triggerBuffer.Length; i++)
                {
                    var triggerEvent = triggerBuffer[i];
                    var otherEntity = triggerEvent.GetOtherEntity(portalEntity); //otheretityも、portalentityも、接触して移動する側の動的entytyを指す。

                    // exclude other triggers, static bodies and processed events
                    if (triggerEvent.State != StatefulEventState.Enter ||
                        !NonTriggerDynamicBodyMask.MatchesIgnoreFilter(otherEntity))
                    {
                        continue;
                    }

                    // Check if entity just teleported to this portal,
                    // and if it did, decrement TransferCount
                    if (triggerVolumePortal.TransferCount != 0)
                    {
                        triggerVolumePortal.TransferCount--;
                        continue;
                    }

                    // a static body may be in a hierarchy, in which case Translation and Rotation may not be in world space
                    var portalTransform = HierarchyChildMask.MatchesIgnoreFilter(portalEntity)
                        ? Math.DecomposeRigidBodyTransform(LocalToWorldLookup[portalEntity].Value)
                        : new RigidTransform(LocalTransformLookup[portalEntity].Rotation,
                        LocalTransformLookup[portalEntity].Position);

                    var companionTransform = HierarchyChildMask.MatchesIgnoreFilter(companionEntity)
                        ? Math.DecomposeRigidBodyTransform(LocalToWorldLookup[companionEntity].Value)
                        : new RigidTransform(LocalTransformLookup[companionEntity].Rotation,
                        LocalTransformLookup[companionEntity].Position);


                    var portalPositionOffset = companionTransform.pos - portalTransform.pos;
                    var portalRotationOffset = math.mul(companionTransform.rot, math.inverse(portalTransform.rot));

                    var entityLocalTransformComponent = LocalTransformLookup[otherEntity];
                    var entityVelocityComponent = PhysicsVelocityLookup[otherEntity];

                    entityVelocityComponent.Linear = math.rotate(portalRotationOffset, entityVelocityComponent.Linear);
                    entityLocalTransformComponent.Position += portalPositionOffset;
                    entityLocalTransformComponent.Rotation =
                        math.mul(entityLocalTransformComponent.Rotation, portalRotationOffset);

                    LocalTransformLookup[otherEntity] = entityLocalTransformComponent;
                    PhysicsVelocityLookup[otherEntity] = entityVelocityComponent;

                    if (PhysicsGraphicalSmoothingLookup.HasComponent(otherEntity))
                    {
                        var entitySmoothingComponent = PhysicsGraphicalSmoothingLookup[otherEntity];
                        entitySmoothingComponent.ApplySmoothing = 0;
                        PhysicsGraphicalSmoothingLookup[otherEntity] = entitySmoothingComponent;
                    }

                    companionTriggerVolumePortal.TransferCount++;
                }

                TriggerVolumePortalLookup[portalEntity] = triggerVolumePortal;
                TriggerVolumePortalLookup[companionEntity] = companionTriggerVolumePortal;
            }
        }
    }
}

```

60行目の
public void Execute(Entity portalEntity, ref DynamicBuffer<StatefulTriggerEvent> triggerBuffer) 
では、Buffernに格納された動的Entityの情報（接触時にBufferに格納される）を参照し、以後portalEntityとして取り扱っています。
そして、
entityLocalTransformComponent.Position += portalPositionOffset;
の部分で、球に対して移動したい二つのポータルボックス間のワールド座標の差分を与えて、位置情報を変化させています。

この辺の記述を色々書き換えれば、色々な接触時のアクションを再現できそうです。

## Entityを接触時に消滅させる
例えばEntity×Entityの接触で片方を消滅させるには、
OnUpdateの中に、
```
state.Dependency = new TriggerVanishJob()
{
    _ecb = state.World.GetOrCreateSystemManaged<BeginSimulationEntityCommandBufferSystem>().CreateCommandBuffer()
}.Schedule(state.Dependency);
```
を追記して、partial struct TriggerVanishJob : IJobEntity内に
```
{
    public EntityCommandBuffer _ecb;
    public void Execute(Entity portalEntity, ref DynamicBuffer<StatefulTriggerPortalEvent> triggerBuffer)
    {
        for (int i = 0; i < triggerBuffer.Length; i++)
        {
            var triggerEvent = triggerBuffer[i];
            _ecb.DestroyEntity(otherEntity);
```
のように追記してあげれば、以下動画のように再現できます。

https://x.com/khbowling/status/1770361481382793525?s=20

ただこのままだと、玉の移動と、消滅を両方同時に再現できません。
本当は右の玉だけ消滅させたかったのですが、上記動画でも3つとも消滅してしまいました。
理由は、衝突した玉の情報を全て同じBufferに格納しているためです。

そこで、衝突と消滅のBufferを別々に作ってあげれば解決します。

https://twitter.com/i/status/1770438670073659676

玉の消滅を別のBufferに格納するために、StatefulTriggerVanishEventという別のイベントを作成します。
この例の場合、機能別にわかりやすく管理するために、
・StatefulTriggerEvent
・TriggerVolumePortalSystem
・StatefulTriggerEventAuthoring
・StatefulTriggerEventSystem
・StatefulEventCollectionJobs
の中を、
StatefulTriggerEvent　→　StatefulTriggerVanishEvent

TriggerVolumePortalAuthoring
TriggerVolumePortalSystem
の中を、
Events →　EventVanish

とかに変更するのが良さそうです。

以下についても変更が必要です。

・TriggerVolumePortalAuthoringの中に、AddBuffer<StatefulTriggerVanishEvent>(entity);を追加
・StatefulTriggerEventSystemの中で、各job名を変更
　例：TriggerVanishEventsなど、StatefulEventCollectionJobsで自分で定義した名前に合わせる。

沢山の接触Triggerイベントを作るには、同じことを繰り返せばOKです。

Entityベースの機能追加は、通常のオブジェクト指向で作っていく何倍も難しいですね。
コードが複雑だし読みづらい...