UnityECS ECSのコライダーをGameObjectのボーンに追従させる

# ECSでの衝突判定
UnityECSにもコライダーは存在します。
しかし、GameObjectのコライダーとECSのコライダーは全く別物で、GameObjectのコライダーとECSのコライダーは衝突判定することはできません。

例えば既存のUnityで攻撃アニメーションで攻撃判定を行う場合、モデルの手や武器のボーンにコライダーを付けて、それをオン、オフして攻撃したい物体に衝突判定を行いダメージを与えるというものが一般的で、この時コライダーは手や武器のボーンに追従していないといけません。

ECSでHybridアプローチで行う場合も同様でレイヤーされているGameObjectモデルのボーンアニメーションにコライダーがついていかないといけません。

ということで、Hybridアプローチでアニメーションをする場合はそのアニメーションのボーンの動きをECSのコライダーに追従させていく方法を紹介します。

# ECSのコライダーの制限
UnityECSではAuthoringで階層で作ったコライダーはSceneCreateの時、一つのコライダーに一体化してしまいます。
これは一般のECSを使う場合は問題ないのですが、`CharacterControllerPackage`を使う場合は問題あります。

`CharacteControllerPackage`は基準のカプセルコライダーで物理挙動を計算しているので、アニメーション追従用のコライダーが合成されてしまったら、`LocalTransform`の位置がずれてしまって、よくないことが起こると思います。(未検証)

ということで今回はこの`CharacterController`の上にアニメーションのコライダーが入っているEntityを別で用意し、重ね合わせる方式をとります。
イメージとしては二人羽織です。

# CharacterColliderComponentの作成
コンポーネントは以下のように作成しました。
```csharp
public struct CharacterColliderComponent : IComponentData
{
    // CharacteControllerのついているEntityを保持
    public Entity ColliderEntity;
    // Colliderが作られたかどうかの判定(最初の1回のみプログラムで動的に作成するため)
    public bool IsCollide;
    // ボーンによって作られたコライダーの総数
    public int ColliderCount;
}
```
今回はGameObjectのボーンに追従したいということで、GameObjectの頭、両手、両足の5つのポイントのボーンに追従させたいと思います。

GameObjectにアタッチするスクリプトは以下で、ここにボーンの位置情報を入れ、ECS側で参照する形をとります。
```csharp
public class CopyColliderRigTransform : MonoBehaviour
{
    [SerializeField]
    public Transform[] myTransform;
}
```
GameObjectにアタッチして各ポイントを入れておいてください。
![スクリーンショット 2024-12-22 114013.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949075/1a5a19c2-e1b8-144d-3e69-985423b30ec1.png)

ECS側のAuthoringはこのような形となります。
```csharp
class CharacterColliderAuthoring : MonoBehaviour
{
    // ArchTypeとか用意しないといけないので、コライダーのテンプレートを保持するイメージ
    [SerializeField]
    private Collider ColliderEntity;
    class CharacterColliderAuthoringBaker : Baker<CharacterColliderAuthoring>
    {
        public override void Bake(CharacterColliderAuthoring authoring)
        {
            var entity = GetEntity(TransformUsageFlags.None);
            AddComponent(entity, new CharacterColliderComponent
            {
                IsCollide = false,
            });
        }
    }
}
```
コライダーしかついてないプレファブを作り`Collider Entity`にアタッチをしておきます。
これをすることで描画とか物理とかで必要なComponentを自動で作ってくれるので便利です。

自分でArcheTypeから作ってもよいですが、コンポーネントが足りなくて画面表示されないとかなったりするので、、、

![スクリーンショット 2024-12-22 121702.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949075/afff12e3-8e27-3116-e2da-20da5de8705b.png)



システムは以下の通りです。
```csharp
[UpdateInGroup(typeof(FixedStepSimulationSystemGroup), OrderFirst = true)]
partial class CharacterColliderSystem : SystemBase
{
    // 構造体の宣言
    struct TransformStash
    {
        public float3 position;
        public quaternion rotation;
    }
    protected override void OnUpdate()
    {
        EntityCommandBuffer ecb = SystemAPI.GetSingletonRW<EndSimulationEntityCommandBufferSystem.Singleton>().ValueRW.CreateCommandBuffer(World.Unmanaged);
        //Create
        // CharacterHybridLinkに保存してあるGameObjectが必要なのでとってくる
        foreach (var (hybridLink, characterCollider, taa, entity) in SystemAPI.Query<CharacterHybridLink, RefRW<CharacterColliderComponent>, RefRW<TAAComponent>>()
            .WithEntityAccess())
        {
            //Create
            if (!characterCollider.ValueRW.IsCollide)
            {
                // ゲームオブジェクトにアタッチしていたボーンのトランスフォーム情報を取得
                var transforms = hybridLink.Object.GetComponent<CopyColliderRigTransform>().myTransform;
                // TransformAccessArrayに変換
                // TransformをIJobParallelForTransformで並列処理したいので変換している
                var TAA = new TransformAccessArray(transforms);

                // TAAComponentに保存
                taa.ValueRW.transformAccessArray = TAA;

                // 1度呼び出されているので作成しましたよのフラグを立てる
                characterCollider.ValueRW.IsCollide = true;
                // 以下二人羽織用のコライダー作成
                characterCollider.ValueRW.ColliderCount = TAA.length;
                // とりあえず箱型のコライダーとして追加する。(Testなので)
                BoxGeometry box = new BoxGeometry
                {
                    Center = 0,
                    Size = 0.1f,
                    Orientation = quaternion.identity,
                };
                
                NativeList<ColliderBlobInstance> colliders = new NativeList<ColliderBlobInstance>(Allocator.Temp);
                // Boxコライダーをボーンのポイントの数だけ分作成
                for (int i = 0; i < TAA.length; i++)
                {
                    var collider = Unity.Physics.BoxCollider.Create(box, CollisionFilter.Default);
                    ColliderBlobInstance colliderBlob = new ColliderBlobInstance
                    {
                        Collider = collider,
                        Entity = entity,
                        CompoundFromChild = new Unity.Mathematics.RigidTransform
                        {
                            rot = transforms[i].rotation,
                            pos = transforms[i].position,
                        }
                    };
                    colliders.Add(colliderBlob);
                }
                // 上記で作成したコライダーでCompoundColliderに変更
                var tempCollArray = colliders.ToArray(Allocator.Temp);
                var blobAssetRef = CompoundCollider.Create(tempCollArray);

                // 作成したCompoundColliderでPhysicsColliderを作る
                PhysicsCollider physicsCollider = new PhysicsCollider
                {
                    Value = blobAssetRef
                };
                if (blobAssetRef.IsCreated)
                {
                    // Tempコライダーをインスタンシエイトする
                    var newEntity = this.EntityManager.Instantiate(characterCollider.ValueRO.ColliderEntity);

                    // 親に二人羽織のEntityを登録
                    characterCollider.ValueRW.ColliderEntity = newEntity;

                    // インスタンシエイトした二人羽織EntityのPhysicsColliderに先ほど作ったものをセットする
                    ecb.SetComponent(newEntity, physicsCollider);
                    ecb.AddComponent(newEntity, new ColliderParent
                    {
                        parent = entity,
                    });
                }
                // Dispose temp collider arrays        
                tempCollArray.Dispose();
                colliders.Dispose();
            }
        }
        // Update
        foreach (var (taaComponent, pCollider, cCollider, entity) in SystemAPI.Query<RefRO<TAAComponent>, PhysicsCollider, CharacterColliderComponent>().WithEntityAccess())
        {
            NativeArray<TransformStash> stashes = new NativeArray<TransformStash>(taaComponent.ValueRO.transformAccessArray.length, Allocator.TempJob);
            // 空の構造体を渡して、これにGameObjectのボーンのトランスフォームを入れてもらう
            var stashTransformJob = new StashTransformsJob
            {
                transformStashes = stashes,
            };
            var firstJobHandle = stashTransformJob.Schedule(taaComponent.ValueRO.transformAccessArray);
            firstJobHandle.Complete();

            if(stashes != null)
            {
                // ECSコライダーにGameObjectのボーンの情報を渡し、追従させる
                var copyTransformJob = new CopyPhysicsTransformJob
                {
                    transformStashes = stashes,
                    collider = SystemAPI.GetComponentLookup<PhysicsCollider>(),
                };
                var copyTransformjob = copyTransformJob.ScheduleParallel(Dependency);
                copyTransformjob.Complete();
            }

            stashes.Dispose();
        }
    }

    // GameObjectから取得したボーンのTransformAccessを取得するJob
    [BurstCompile]
    struct StashTransformsJob : IJobParallelForTransform
    {
        public NativeArray<TransformStash> transformStashes;
        
        // TransformAccsessで行うとforとかで回さなくてよくなるみたい
        // IJobParallelForTransform限定の処理でトランスフォームをポインタで取得できるらしい
        public void Execute(int index, TransformAccess transform)
        {
            // GameObjectで設定した各ポイントのトランスフォームを取得している
            
            transformStashes[index] = new TransformStash
            {
                rotation = transform.rotation,
                position = transform.position,
            };
        }
    }
    // ボーンの位置をECSのコライダーの位置に反映させるJob
    [BurstCompile]
    unsafe partial struct CopyPhysicsTransformJob : IJobEntity
    {
        // 受け取ったGameObject各ボーンの位置情報
        [ReadOnly] public NativeArray<TransformStash> transformStashes;
        // CompoenntLoockupでEntityからPhysicsColliderを抜き出す。
        // [NativeDisableParallelForRestriction]を指定しないとうまく動作しない
        [NativeDisableParallelForRestriction]
        public ComponentLookup<PhysicsCollider> collider;

        public void Execute(in CharacterColliderComponent cCollider, TAAComponent taa)
        {
            // アニメーション用のコライダーのエンティティのPhysicsコライダーを取得
            var pCollider = collider.GetRefRW(cCollider.ColliderEntity);
            
            if (pCollider.ValueRO.Value.Value.Type == ColliderType.Compound)
            {
                // CompoundColliderになっているため、その中の子を取得するのをポインターで行う
                var compoundPointer = (CompoundCollider*) pCollider.ValueRO.ColliderPtr;
                // CompoundColliderの子要素を取得
                var children = compoundPointer->Children;
                for (int i = 0; i < children.Length; i++)
                {
                    var transformStash = transformStashes[i];
                    // 子のRigidTransformをボーンの位置情報で書き換える
                    children[i].CompoundFromChild = new RigidTransform(transformStash.rotation, transformStash.position);
                }
                // 子のコライダーを先ほど設定した値で動かす
                // 新しいバージョンで追加されたAPIのため、Unity6LTSへのアップデートが必要
                compoundPointer->Update();
            }
        }
    }
}
```

TAAを保持するコンポーネントはこれです。
```csharp
[ChunkSerializable]
public struct TAAComponent : IComponentData{
    
    public TransformAccessArray transformAccessArray;
}
```

これで実行するとGameObjectのボーンの位置に合わせてコライダーも追従してくれます。