0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-12-22

ECSでの衝突判定

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

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

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

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

ECSのコライダーの制限

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

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

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

CharacterColliderComponentの作成

コンポーネントは以下のように作成しました。

public struct CharacterColliderComponent : IComponentData
{
    // CharacteControllerのついているEntityを保持
    public Entity ColliderEntity;
    // Colliderが作られたかどうかの判定(最初の1回のみプログラムで動的に作成するため)
    public bool IsCollide;
    // ボーンによって作られたコライダーの総数
    public int ColliderCount;
}

今回はGameObjectのボーンに追従したいということで、GameObjectの頭、両手、両足の5つのポイントのボーンに追従させたいと思います。

GameObjectにアタッチするスクリプトは以下で、ここにボーンの位置情報を入れ、ECS側で参照する形をとります。

public class CopyColliderRigTransform : MonoBehaviour
{
    [SerializeField]
    public Transform[] myTransform;
}

GameObjectにアタッチして各ポイントを入れておいてください。
スクリーンショット 2024-12-22 114013.png

ECS側のAuthoringはこのような形となります。

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

システムは以下の通りです。

[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を保持するコンポーネントはこれです。

[ChunkSerializable]
public struct TAAComponent : IComponentData{
    
    public TransformAccessArray transformAccessArray;
}

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?