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にアタッチして各ポイントを入れておいてください。
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から作ってもよいですが、コンポーネントが足りなくて画面表示されないとかなったりするので、、、
システムは以下の通りです。
[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のボーンの位置に合わせてコライダーも追従してくれます。