当たり判定(Entity Component System)

当たり判定(Entity Component System)

PlayerとEnemyの衝突を検知し、Effectを再生します。

Script

Component

Player

Playerの移動速度を決めるComponentです。

using Unity.Entities;

namespace EmptyCan
{
    public struct Player : IComponentData
    {
        public float Speed;
    }
}

Playerがキー入力を受け取るためのComponentです。

using Unity.Entities;
using Unity.Mathematics;

namespace EmptyCan
{
    public struct InputsData : IComponentData
    {
        public float2 PlayerMove;
    }
}

以上のComponentを追加したEntityを作成します。

using Unity.Entities;
using UnityEngine;

namespace EmptyCan
{
    public class PlayerAuthoring : MonoBehaviour
    {
        public float Speed;

        class Baker : Baker<PlayerAuthoring>
        {
            public override void Bake(PlayerAuthoring authoring)
            {
                Player data = new Player() { Speed = authoring.Speed };
                var entity = GetEntity(TransformUsageFlags.Dynamic);
                AddComponent(entity, data);
                AddComponent(entity, new InputsData());
            }
        }
    }
}

Enemy

Enemyの名前と自身が破壊された際に再生するエフェクトをまとめたComponentです。

using Unity.Collections;
using Unity.Entities;

namespace EmptyCan
{
    public struct Enemy : IComponentData
    {
        public FixedString32Bytes Name;
        public Entity Effect;
    }
}

以上のComponentを追加したEntityを作成します。

using Unity.Entities;
using UnityEngine;

namespace EmptyCan
{
    public class EnemyAuthoring : MonoBehaviour
    {
        public string Name;
        public GameObject DestroyEffect;

        class EnemyAuthoringBaker : Baker<EnemyAuthoring>
        {   
            public override void Bake(EnemyAuthoring authoring)
            {
                var entity = GetEntity(TransformUsageFlags.Dynamic);
                AddComponent(entity, new Enemy {
                    Name = new Unity.Collections.FixedString32Bytes(authoring.name),
                    Effect = GetEntity(authoring.DestroyEffect, TransformUsageFlags.Dynamic)
                });
            }
        }
    }
}

Effect

Effectへ追加するデータを持たないComponentです。再生が終了した際に削除したいEffectとそうでないEffectを判別するために使用します。

using Unity.Entities;

public struct BurstEffect : IComponentData { }

System

キー入力

キー入力を受け取るためのSystemです。詳しくはInputSystemで入力を取得(Entity Component System)を参照してください。

using UnityEngine;
using Unity.Entities;

namespace EmptyCan
{
    [UpdateInGroup(typeof(InitializationSystemGroup), OrderLast = true)]
    public partial class PlayerInputSystem : SystemBase
    {
        InputControls _input_controls;

        protected override void OnCreate()
        {
            _input_controls = new InputControls();
        }

        protected override void OnStartRunning()
        {
            _input_controls.Enable();
        }

        protected override void OnUpdate()
        {
            foreach (RefRW<InputsData> data in SystemAPI.Query<RefRW<InputsData>>())
            {
                data.ValueRW.PlayerMove = _input_controls.PlayerMap.PlayerMove.ReadValue<Vector2>();
            }
        }

        protected override void OnStopRunning()
        {
            _input_controls.Disable();
        }

        protected override void OnDestroy()
        {
            _input_controls.Disable();
            _input_controls.Dispose();
        }
    }
}

当たり判定

ITriggerEventsJobでは衝突したEntityを取得することができます。これらのEntityが特定の組み合わせであるかを確認し、その後、衝突した際の処理を行います。

  • ComponentLookup
     指定したEntityからComponentを取得することができます。ComponentLookupはGetComponentLookup<T>(bool)で取得できます。boolは読み取り専用かを指定できます。読み取り専用にした場合、ITriggerEventsJob内のComponentLookupを受け取るフィールドはReadOnlyとする必要があります。また、書き込みを可能にした場合は他スレッドで書き込みが行われたかを確認する処理が入るため実行速度に影響があるそうです。
     ITriggerEventsJobでは衝突したEntity(evt.EntityA、evt.EntityB)が取得できるので、ComponentLookupから目的のComponentを取得できます。
  • PlayerHitJob
     ITriggerEventsJobを継承したstructで当たり判定を受け取ることができます。PlayerHitJobはOnUpdate内でスケジュールを行い、実行します。
  • EntityCommandBuffer
     Entityの作成等、Enitityの変更は非同期で実行するのは望ましくありません。EntityCommandBufferを使用することでジョブを完了した後にメインスレッドで実行することができます。よって、PlayerHitJob内ではEntityCommandBufferを用いてEntityの作成、Componentの追加及び破棄を行っています。
using Unity.Burst;
using Unity.Entities;
using Unity.Physics;
using Unity.Physics.Systems;
using Unity.Collections;
using Unity.Transforms;

namespace EmptyCan
{
    [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    [UpdateAfter(typeof(PhysicsSystemGroup))]
    partial struct PlayerCollisionSystem : ISystem
    {
        ComponentLookup<LocalTransform> _position_lookup;
        ComponentLookup<Player> _player_lookup;
        ComponentLookup<Enemy> _enemy_lookup;

        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<SimulationSingleton>();
            state.RequireForUpdate<BeginInitializationEntityCommandBufferSystem.Singleton>();
            state.RequireForUpdate<BeginSimulationEntityCommandBufferSystem.Singleton>();

            _position_lookup = SystemAPI.GetComponentLookup<LocalTransform>(true);
            _player_lookup = SystemAPI.GetComponentLookup<Player>(true);
            _enemy_lookup = SystemAPI.GetComponentLookup<Enemy>(true);
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            SimulationSingleton simulation = SystemAPI.GetSingleton<SimulationSingleton>();
            EntityCommandBuffer ecb_bis = SystemAPI.GetSingleton<BeginInitializationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);
            EntityCommandBuffer ecb_bss = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);

            //ComponentLookupの更新
            _position_lookup.Update(ref state);
            _player_lookup.Update(ref state);
            _enemy_lookup.Update(ref state);

            state.Dependency = new PlayerHitJob()
            {
                Positions = _position_lookup,
                PlayerLookup = _player_lookup,
                EnemyLookup = _enemy_lookup,
                EcbBis = ecb_bis,
                EcbBss = ecb_bss
            }.Schedule(simulation, state.Dependency);
        }

        [BurstCompile]
        private struct PlayerHitJob : ITriggerEventsJob
        {
            [ReadOnly] public ComponentLookup<LocalTransform> Positions;
            [ReadOnly] public ComponentLookup<Player> PlayerLookup;
            [ReadOnly] public ComponentLookup<Enemy> EnemyLookup;
            public EntityCommandBuffer EcbBis;
            public EntityCommandBuffer EcbBss;

            public void Execute(TriggerEvent evt)
            {
                var (player, enemy) = IdentifyEntityPair(evt);
                if (!Check(player, enemy)) return;

                Entity effect_entity = EcbBis.Instantiate(EnemyLookup[enemy].Effect);
                EcbBis.SetComponent(effect_entity, LocalTransform.FromPosition(Positions[enemy].Position));
                EcbBis.AddComponent<BurstEffect>(effect_entity);

                EcbBss.DestroyEntity(enemy);
            }

            //Player Component又はEnemy Componentを持っているEntityを取得
            (Entity, Entity) IdentifyEntityPair(TriggerEvent evt)
            {
                Entity player = Entity.Null;
                Entity enemy = Entity.Null;
                if (PlayerLookup.HasComponent(evt.EntityA)) player = evt.EntityA;
                if (PlayerLookup.HasComponent(evt.EntityB)) player = evt.EntityB;
                if (EnemyLookup.HasComponent(evt.EntityA)) enemy = evt.EntityA;
                if (EnemyLookup.HasComponent(evt.EntityB)) enemy = evt.EntityB;

                return (player, enemy);
            }

            //PlayerとEnemy Componentを持つEntityの組み合わせか
            bool Check(Entity player, Entity enemy)
            {
                return !Entity.Null.Equals(player) && !Entity.Null.Equals(enemy);
            }
        }
    }
}

Effectの削除

Effectが終了した際に残ったEntityを削除するためのSystemです。SystemAPI.QueryでVisualEffectを取得しています。VisualEffectはマネージドなComponentなので、このSystemではBurstCompileが使用できません。

using Unity.Entities;
using UnityEngine.VFX;
using UnityEngine;

[RequireMatchingQueriesForUpdate]
partial struct DestroyEffectSystem : ISystem
{
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<BeginSimulationEntityCommandBufferSystem.Singleton>();
    }

    public void OnUpdate(ref SystemState state)
    {
        var ecb_bss = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);

        foreach (var (effect, vfx, entity) in SystemAPI.Query<RefRO<BurstEffect>, SystemAPI.ManagedAPI.UnityEngineComponent<VisualEffect>>().WithEntityAccess())
        {
            //Debug.Log($"{vfx.Value.aliveParticleCount}");
            if (vfx.Value.aliveParticleCount != 0) return;
            //Debug.Log($"destroy");
            ecb_bss.DestroyEntity(entity);
        }
    }
}

設定と実行結果

PlayerとEnemyが衝突した際にすり抜けるように設定します。(Tirigger)

Player

Cubeを作成し、SubSceneへ移動させます。Box ColliderのIs Triggerにオンにします。また、Rigidbodyを追加し、Use Gravityをオフにします。そして、PlayerAuthoringを追加し、Speedを20へ変更します。

Enemy

Cubeを作成し、SubSceneへ移動させます。Box ColliderのIs Triggerにオンにします。DestroyEffectへEnemyがPlayerと接触した際に再生するVFXを選択します。

実行結果

実行すると以下の画像のようにPlayer(白いCube)がEnemy(灰色のCube)に衝突するとEffectが再生されます。

おまけ

ShaderGraph

Effectに使用したShaderは以下のように設定しています。Support VFX Graphにチェックを入れる必要があります。

VFX Graph

衝突した際に再生されるEffectのVFX Graphです。

参考ページ

[Unity DOTS]EntityComponentSystemSamplesを補足する

【Unity】ECSの並列処理(IJobParallelForやIJobForEach系)でEntityCommandBufferを使う

Unity ECS End Game – Implementing High-Performance Systems with Unity DOTS Job System, YouTube

[Unity] 衝突判定における、IsTriggerとRigidbodyとIsKinematicのパターンを実験