【Unity】UnityPhysicsで、オブジェクト同士が接触した判定を取得する
物理演算でオブジェクト同士が接触した時のイベントを発行する方法についてです。
なお、現段階では明らかに低レベルAPIのみしか提供されておらず、未完成です。
この記事は「今すぐ触ってみたい」という人向けで、正直あまり有益ではありません。
接触イベント
「榴弾の発射」や「玉を地面に落とした時」、「キャラクターが特定の範囲に到達した時」など、動く物体がオブジェクトに接触した時に何らかのイベントを発行したくなります。
UnityのPhysics(GameObjectがPhysXを利用)ではOnCollisionEnterやOnTriggerEnterを設定すれば良いのですが、UnityPhysicsは現時点では低レベルAPIしか提供していないため、割と面倒くさい処理が必要になります。
使用環境
- Unity 2019.1f2
- Unity Physics preview1 0.0.2
セットアップ
イベントを受け取るにはCollider
からの変換ではなくPhysicsShape
を使用する必要があります。その上で PhysicsShape > Advance > Raises Collision Event
にチェックを入れます。
(イベントを発行しない側はColliderにIsTrigerを設定しUnityPhysicsに変換するだけで良いです)
これでチェックを入れたEntityが他のオブジェクトに接触した時、イベントが発行されます。あとは発行されたイベントを総なめで内容を確認するシステムを用意します。
まずシステムの実行順ですが、StepPhysicsWorld
の後に実行します。
[UpdateAfter(typeof(StepPhysicsWorld))] public class ItemGetSystem: ComponentSystem {
次にイベントの取得と全Colliderの取得用に、BuildPhysicsWorld
とStepPhysicsWorld
を用意します。
private BuildPhysicsWorld buildPhysicsWorldSystem; private StepPhysicsWorld stepPhysicsWorldSystem; protected override void OnCreate() { buildPhysicsWorldSystem = World.GetOrCreateSystem<BuildPhysicsWorld>(); stepPhysicsWorldSystem = World.GetOrCreateSystem<StepPhysicsWorld>(); }
実際の中身を処理する部分です。
buildPhysicsWorldSystem
からPhysicsWorldを取得します。この中に現時点での全てのColliderの座標等が含まれます。
同様にstepPhysicsWorldSystem
からTriggerEvents
を取得します。この中にRaises Collision Eventが有効なオブジェクトが発行したイベントが格納されています。もしCollider同士の接触判定を取りたい場合はCollisionEvents
です。
あとはEventTriggerをキーに接触中のオブジェクトを取得して、イベント処理するだけです。
protected override void OnUpdate() { var physicsWorld = buildPhysicsWorldSystem.PhysicsWorld; var triggerEvents = stepPhysicsWorldSystem.Simulation.TriggerEvents; var items = GetComponentDataFromEntity<ItemTag>(true); foreach (var triggerEvent in triggerEvents) { // PhysicsWorldに全ての物理演算で動くEntityが含まれる。 // triggerEventsには、Raises Collision Eventが有効なオブジェクトが接触イベントを格納する // TriggerEventの中身を洗えば、接触判定が取れる var bodyA = physicsWorld.Bodies[triggerEvent.BodyIndices.BodyAIndex]; var bodyB = physicsWorld.Bodies[triggerEvent.BodyIndices.BodyBIndex]; Process(bodyA.Entity, items); Process(bodyB.Entity, items); } }
全文はこちら
なお、Unity Physicsはステートを持たないので、「接触開始」や「接触中」「接触終了」といったイベントは取れないみたいです。
特定のオブジェクト同士でのみ接触イベントを行いたい場合、上のコードのようにComponentDataFromEntityで特定のコンポーネントを持っていないかを判断するか、もしくはCustom Flag
を設定しておくとBodyA.CustomData
で判断が出来ます。
単純に接触したくない場合はCollision Filterを調整すると良いみたいです。この辺りのインターフェースはもう少し頑張ってほしい所です。
JobSystemで動かしたい
上の処理をJobSystemで動かしてみました。
ポイントは処理の依存関係を設定することです。UnityPhysicsはECSを使用していますがECS上で動作していないので、処理の依存関係を自動的に解決してくれません。システムから持ってくる必要があります。
inputDeps = JobHandle.CombineDependencies( inputDeps, buildPhysicsWorldSystem.FinalJobHandle, stepPhysicsWorldSystem.FinalSimulationJobHandle);
全文はこんな感じになります。
using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Jobs; using Unity.Mathematics; using Unity.Physics; using Unity.Physics.Systems; [UpdateAfter(typeof(StepPhysicsWorld)), UpdateBefore(typeof(EndFramePhysicsSystem))] public class ItemGetSystem : JobComponentSystem { BuildPhysicsWorld buildPhysicsWorldSystem; StepPhysicsWorld stepPhysicsWorldSystem; EntityCommandBufferSystem bufferSystem; protected override void OnCreate() { buildPhysicsWorldSystem = World.GetOrCreateSystem<BuildPhysicsWorld>(); stepPhysicsWorldSystem = World.GetOrCreateSystem<StepPhysicsWorld>(); bufferSystem = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>(); } protected override JobHandle OnUpdate(JobHandle inputDeps) { inputDeps = JobHandle.CombineDependencies( inputDeps, buildPhysicsWorldSystem.FinalJobHandle, stepPhysicsWorldSystem.FinalSimulationJobHandle); inputDeps = new HitAndDestroyJob { World = buildPhysicsWorldSystem.PhysicsWorld, TriggerEvents = stepPhysicsWorldSystem.Simulation.TriggerEvents, items = GetComponentDataFromEntity<ItemTag>(true), commandBuffer = bufferSystem.CreateCommandBuffer(), }.Schedule(1, 1, inputDeps); bufferSystem.AddJobHandleForProducer(inputDeps); return inputDeps; } struct HitAndDestroyJob : IJobParallelFor { [ReadOnly] public PhysicsWorld World; [ReadOnly] public TriggerEvents TriggerEvents; [ReadOnly] public ComponentDataFromEntity<ItemTag> items; [ReadOnly] public EntityCommandBuffer commandBuffer; public void Execute(int index) { foreach (var triggerEvent in TriggerEvents) { GetItem(World.Bodies[triggerEvent.BodyIndices.BodyAIndex].Entity); GetItem(World.Bodies[triggerEvent.BodyIndices.BodyBIndex].Entity); } } void GetItem(Entity entity) { if (items.Exists(entity) == false) return; commandBuffer.DestroyEntity(entity); UnityEngine.Debug.Log("you get item!"); } } }