以前の記事で現行環境でとりあえずエラーは出ない状態にはなっていましたが、空間分割近傍探索ライブラリのテストアプリとしていじくりまわしているうちにだいぶ面影が変わってしまったので、ECSに関連する部分の設計と実装について簡単にまとめます。
本題の空間分割近傍探索ライブラリのテスト結果はこちらをどうぞ。
[Unity] DOTSを用いた空間分割アルゴリズムの比較
環境
- Unity 2020.3.33f
- Hybrid Renderer 0.50-preview.24
- Entities 0.50.0-preview.24
- Collections 1.2.3
- Burst 1.6.5
当方の実装結果
アプリの設計
アルゴリズム別に性能を評価するため、いちいち再コンパイルするのは地獄なのでランタイムでGUIからパラメータを与えてECS側が動作する必要がありました。またECSから外部のデータへのアクセスはstaticなメンバを通して行うのが楽だったので、MonoBehaviourとECSの間でパラメータとトリガの仲介を行う ***_Bootstrap
クラスを作り、これに対する入出力を通してMonoBehaviourによるGUI部分とECSが連携します。
▼ Boids の生成、破棄
Bootstrapからトリガ送信としているのはBoidsの生成、破棄の指示で、MonoBehaviourとECSのシステム間では実行順序を制御する手段がないため、ECS側のSystemGroupの定義で一番最初に実行されるように指定したManagerSystemGroup
に属する生成、破棄用システムSpawnAndRemoveSystem
でトリガを受け取り、Boidsの数を増減させます。また、Boidsの全体数を示す変数 static int Bootstrap.BoidsCount
は SpawnAndRemoveSystem
での生成または破棄が完了した後にBootstrapへ通知します。これは空間分割などを行うシステムでコンテナサイズとしてBoidsの全体数が必要なため、実行順序によっては
Bootstrap(削除を指示) -> 空間分割(削除後の数で初期化) -> まだ削除されていないEntityのため容量オーバーでエラー
のように異常終了の原因になるため、このような構成としています。
ソースコード(折り畳み)
public class Bootstrap : MonoBehaviour
{
public static Bootstrap Instance { get; private set; }
[SerializeField] GameObject prefab_obj;
private Entity prefab_entity;
// UI interface
[SerializeField]
private UI_controller ui_input;
private int n_boid;
private EntityManager entity_manager;
private Entity _triggerForBoidsSpawner;
void Awake()
{
Instance = this;
}
public void Start()
{
//--- setup managers
var world = World.DefaultGameObjectInjectionWorld;
entity_manager = world.EntityManager;
// convert prefab_obj -> prefab_entity
prefab_entity = GameObjectConversionUtility.ConvertGameObjectHierarchy(
prefab_obj,
GameObjectConversionSettings.FromWorld(world, null)
);
// add user defined component
entity_manager.AddComponent<Prefab>(prefab_entity);
entity_manager.AddComponent<BoidType>(prefab_entity);
entity_manager.AddComponent<Scale>(prefab_entity);
entity_manager.AddComponent<Velocity>(prefab_entity);
entity_manager.AddComponent<Acceleration>(prefab_entity);
entity_manager.AddComponent<NeighborsEntityBuffer>(prefab_entity);
entity_manager.AddComponent<Tag_UpdateInteraction>(prefab_entity);
entity_manager.AddComponent<Tag_ComputeNeighbors_Direct>(prefab_entity);
n_boid = 0;
// initialize trigger prefab
_triggerForBoidsSpawner
= entity_manager.CreateEntity(typeof(Prefab), typeof(BoidsSpawner));
}
public static int BoidsCount { get { return Instance.n_boid; } }
void UpdateBoidNum(int n_tgt)
{
if (n_tgt < 0) return;
int n_diff = n_tgt - n_boid;
if (n_diff == 0) return;
var trigger = entity_manager.Instantiate(_triggerForBoidsSpawner);
var spawner = new BoidsSpawner
{
Prefab = prefab_entity,
n = n_diff,
scale = boidScale,
initSpeed = BoidParams_Bootstrap.Param.initSpeed
};
entity_manager.SetComponentData(trigger, spawner);
}
public static void UpdateBoidNumComplete(BoidsSpawner spawner)
{
Instance.n_boid += spawner.n;
}
void Update()
{
UpdateBoidNum(ui_input.boidCount);
}
}
[UpdateInGroup(typeof(ManagerSystemGroup))]
public partial class SpawnerAndRemoveSystem : SystemBase
{
private EntityQuery _boids_query;
private Random _random;
protected override void OnCreate()
{
base.OnCreate();
_boids_query = EntityManager.CreateEntityQuery(new EntityQueryDesc
{
All = new[]
{
ComponentType.ReadOnly<BoidType>()
},
None = new[]
{
// わざわざ指定しなくてもいいかも
ComponentType.ReadOnly<Prefab>()
}
});
_random = new Random(853);
}
protected override void OnUpdate()
{
Dependency.Complete();
Entities.
WithStructuralChanges(). // メインスレッド実行を強制、チャンク構造の変更(EntityManagerの呼び出し)が可能
WithoutBurst(). // 当然 Burst も使用不可
ForEach(
(Entity trigger, in BoidsSpawner spawner) =>
{
Dependency.Complete();
if(spawner.n > 0)
{
UnityEngine.Debug.Log($"update boids num: add {spawner.n} boids.");
var spawnedEntities = new NativeArray<Entity>(spawner.n, Allocator.Temp);
EntityManager.Instantiate(spawner.Prefab, spawnedEntities);
float spawn_area = Bootstrap.WallScale * 0.4f;
for (int i = 0; i < spawner.n; i++)
{
var entity = spawnedEntities[i];
EntityManager.SetComponentData(entity, new Translation { Value = _random.NextFloat3(-spawn_area, spawn_area) });
EntityManager.SetComponentData(entity, new Rotation { Value = quaternion.identity });
EntityManager.SetComponentData(entity, new Scale { Value = spawner.scale });
EntityManager.SetComponentData(entity, new Velocity { Value = _random.NextFloat3Direction() * spawner.initSpeed });
EntityManager.SetComponentData(entity, new Acceleration { Value = float3.zero });
}
spawnedEntities.Dispose();
}
else if(spawner.n < 0)
{
int n_delete = -spawner.n;
UnityEngine.Debug.Log($"update boids num: remove {n_delete} boids.");
var entities = _boids_query.ToEntityArray(Allocator.Temp);
EntityManager.DestroyEntity(new NativeSlice<Entity>(entities, 0, math.abs(n_delete)));
entities.Dispose();
}
//--- 処理結果を通知
Bootstrap.UpdateBoidNumComplete(spawner);
//--- 完了したトリガを消去(忘れると無限に繰り返し実行される)
EntityManager.DestroyEntity(trigger);
}).Run();
}
}
public struct BoidsSpawner : IComponentData
{
public Entity Prefab;
public int n;
public float scale;
public float initSpeed;
}
Prefab Entity には、以前は自分で定義した判別用のタグコンポーネントを付加していましたが、現在は公式が用意している Entities.Prefab コンポーネントをタグとして使用しています。
このコンポーネントがついているEntityはEntityQueryの検索対象から除外され、かつEntityManager.Instanciate(Entity)
に引数として与えると、Entities.Prefab
が自動的に取り除かれたものが生成されます。
自作タグの場合はタグコンポーネントのつけ外しも自分で管理する必要があり、またそれに伴うアーキタイプの変更はチャンク間のエンティティの移動を引き起こす可能性があり、パフォーマンスへの悪影響も懸念されます。ここは公式のツールを活用しましょう。
生成自体もひとつひとつ Instanciate()
するのではなく、必要個数分の NativeArray<Entity>
を作ってまとめて生成するのが高速です。
ECSとのトリガの送受信にもEntityを利用します。
対象と合致するEntityQueryがあれば全てのEntityを取得できるので、今回は Mono -> ECS の向きにしか送信していませんが、ECSのシステムでトリガEntityを生成 -> Mono 側で当該 query.ToEntityArray().Length
が0でなければ何か処理、という形で送受信に利用できます。お互いのメンバを直接参照するわけではないのでECSのシステムの実行順序と分離して管理できるところもメリットです。
システムの実行順のデザインについて、今回は乱数を使用するためEntityCommandBufferを利用した並列化はしませんでしたが、デフォルトで用意されているECBのアップデートシステムで毎ループ実行されるものはSimulationSystemGroup
の Before / End のどちらかです。Entityの生成、破棄が中途半端なタイミングで実行されると事故の元なので、まず初めにEntityの生成、破棄のタイミングを決めて、そこからほかの処理の順番を並べていく形で ComponentSystemGroup
の実行順を指定するのがよいでしょう。
▼ Tagコンポーネントによるシステムの動作切り替え
本アプリではNeighborListの探索について、複数の実装を切り替えてそれらの性能の評価、およびデバッグを行うことを目的としています。この探索アルゴリズムの切り替えについて、一つのアルゴリズムにつき一つのシステムを実装し、それらの間をタグコンポーネントの付け替えによって切り替える形で実装しました。システムの実行順は次のようになっています。
Entityの生成、破棄と同様、変なタイミング(例えばNeighborDetectionSystemGroupのあと)でTagが変更されると後続のシステムが正しく動かなくなってしまうので、GUIからの指示はBootstrapからトリガとして送信され、ManagerSystemGroup
のReplaceTagComponentSystem
が処理を実行します。
ソースコード(折り畳み)
public struct Tag_UpdateInteraction : IComponentData { }
public struct Tag_ComputeNeighbors_Direct : IComponentData { }
public struct Tag_ComputeNeighbors_CellIndex_Entity_NeighborList : IComponentData { }
public struct Tag_ComputeNeighbors_CellIndex_Cell_NeighborList : IComponentData { }
public struct Tag_ComputeNeighbors_CellIndex_Cell_Cell : IComponentData { }
public struct Tag_ComputeNeighbors_CellIndex_Combined_CNL : IComponentData { }
public struct Tag_ComputeNeighbors_CellIndex_Combined_CC : IComponentData { }
public struct Tag_ComputeNeighbors_CellIndex_MergedCell_NL : IComponentData { }
public enum ComputeNeighborsPlan
{
Direct,
CellIndex_Entity_NeighborList,
CellIndex_Cell_NeighborList,
CellIndex_Cell_Cell,
CellIndex_Combined_CNL,
CellIndex_Combined_CC,
CellIndex_MergedCell_NL,
}
public struct ComputePlanSwicher : IComponentData
{
public Entity Prefab;
public EntityQuery Query;
public ComputeNeighborsPlan RemoveTarget, AddTarget;
}
public class Bootstrap : MonoBehaviour
{
private EntityManager entity_manager;
private Entity _triggerForComputePlanSwitcher;
public void Start()
{
//--- setup managers
var world = World.DefaultGameObjectInjectionWorld;
entity_manager = world.EntityManager;
// initialize trigger prefab
_triggerForComputePlanSwitcher = entity_manager.CreateEntity(typeof(Prefab), typeof(ComputePlanSwicher));
}
public void SwitchComputeNeighborsPlan(ComputeNeighborsPlan old_plan, ComputeNeighborsPlan new_plan)
{
var trigger = entity_manager.Instantiate(_triggerForComputePlanSwitcher);
var swapper = new ComputePlanSwicher
{
Prefab = prefab_entity,
Query = boids_query,
RemoveTarget = old_plan,
AddTarget = new_plan,
};
entity_manager.SetComponentData(trigger, swapper);
}
}
[UpdateInGroup(typeof(ManagerSystemGroup))]
public partial class ReplaceTagComponentSystem : SystemBase
{
protected override void OnUpdate()
{
Entities.
WithStructuralChanges().
ForEach(
(Entity trigger, in ComputePlanSwicher swicher) =>
{
Dependency.Complete();
SwitchComputeNeighborsPlan(EntityManager, swicher);
EntityManager.DestroyEntity(trigger);
}).Run();
}
public static void SwitchComputeNeighborsPlan(EntityManager manager, ComputePlanSwicher swicher)
{
var prefab_entity = swicher.Prefab;
var boids_query = swicher.Query;
var old_plan = swicher.RemoveTarget;
var new_plan = swicher.AddTarget;
switch (old_plan)
{
case ComputeNeighborsPlan.Direct:
RemoveComponentFromBoids<Tag_ComputeNeighbors_Direct>(manager, boids_query, prefab_entity);
break;
case ComputeNeighborsPlan.CellIndex_Entity_NeighborList:
RemoveComponentFromBoids<Tag_ComputeNeighbors_CellIndex_Entity_NeighborList>(manager, boids_query, prefab_entity);
break;
case ComputeNeighborsPlan.CellIndex_Cell_NeighborList:
RemoveComponentFromBoids<Tag_ComputeNeighbors_CellIndex_Cell_NeighborList>(manager, boids_query, prefab_entity);
break;
case ComputeNeighborsPlan.CellIndex_Cell_Cell:
RemoveComponentFromBoids<Tag_ComputeNeighbors_CellIndex_Cell_Cell>(manager, boids_query, prefab_entity);
break;
case ComputeNeighborsPlan.CellIndex_Combined_CNL:
RemoveComponentFromBoids<Tag_ComputeNeighbors_CellIndex_Combined_CNL>(manager, boids_query, prefab_entity);
break;
case ComputeNeighborsPlan.CellIndex_Combined_CC:
RemoveComponentFromBoids<Tag_ComputeNeighbors_CellIndex_Combined_CC>(manager, boids_query, prefab_entity);
break;
case ComputeNeighborsPlan.CellIndex_MergedCell_NL:
RemoveComponentFromBoids<Tag_ComputeNeighbors_CellIndex_MergedCell_NL>(manager, boids_query, prefab_entity);
break;
default: throw new ArgumentOutOfRangeException(nameof(old_plan));
}
switch (old_plan)
{
case ComputeNeighborsPlan.CellIndex_Combined_CNL:
case ComputeNeighborsPlan.CellIndex_Combined_CC:
AddComponentToBoids<Tag_UpdateInteraction>(manager, boids_query, prefab_entity);
AddBufferToBoids<NeighborsEntityBuffer>(manager, boids_query, prefab_entity);
break;
default:
break;
}
switch (new_plan)
{
case ComputeNeighborsPlan.Direct:
AddComponentToBoids<Tag_ComputeNeighbors_Direct>(manager, boids_query, prefab_entity);
break;
case ComputeNeighborsPlan.CellIndex_Entity_NeighborList:
AddComponentToBoids<Tag_ComputeNeighbors_CellIndex_Entity_NeighborList>(manager, boids_query, prefab_entity);
break;
case ComputeNeighborsPlan.CellIndex_Cell_NeighborList:
AddComponentToBoids<Tag_ComputeNeighbors_CellIndex_Cell_NeighborList>(manager, boids_query, prefab_entity);
break;
case ComputeNeighborsPlan.CellIndex_Cell_Cell:
AddComponentToBoids<Tag_ComputeNeighbors_CellIndex_Cell_Cell>(manager, boids_query, prefab_entity);
break;
case ComputeNeighborsPlan.CellIndex_Combined_CNL:
AddComponentToBoids<Tag_ComputeNeighbors_CellIndex_Combined_CNL>(manager, boids_query, prefab_entity);
break;
case ComputeNeighborsPlan.CellIndex_Combined_CC:
AddComponentToBoids<Tag_ComputeNeighbors_CellIndex_Combined_CC>(manager, boids_query, prefab_entity);
break;
case ComputeNeighborsPlan.CellIndex_MergedCell_NL:
AddComponentToBoids<Tag_ComputeNeighbors_CellIndex_MergedCell_NL>(manager, boids_query, prefab_entity);
break;
default: throw new ArgumentOutOfRangeException(nameof(new_plan));
}
switch (new_plan)
{
case ComputeNeighborsPlan.CellIndex_Combined_CNL:
case ComputeNeighborsPlan.CellIndex_Combined_CC:
RemoveComponentFromBoids<Tag_UpdateInteraction>(manager, boids_query, prefab_entity);
RemoveBufferFromBoids<NeighborsEntityBuffer>(manager, boids_query, prefab_entity);
break;
default:
break;
}
}
private static void RemoveComponentFromBoids<T>(EntityManager manager, EntityQuery boids_query, Entity prefab)
where T : IComponentData
{
manager.RemoveComponent<T>(prefab);
manager.RemoveComponent<T>(boids_query);
}
private static void AddComponentToBoids<T>(EntityManager manager, EntityQuery boids_query, Entity prefab)
where T : IComponentData
{
manager.AddComponent<T>(prefab);
manager.AddComponent<T>(boids_query);
}
private static void RemoveBufferFromBoids<T>(EntityManager manager, EntityQuery boids_query, Entity prefab)
where T : IBufferElementData
{
manager.RemoveComponent<T>(prefab);
manager.RemoveComponent<T>(boids_query);
}
private static void AddBufferToBoids<T>(EntityManager manager, EntityQuery boids_query, Entity prefab)
where T : IBufferElementData
{
manager.AddComponent<T>(prefab);
manager.AddComponent<T>(boids_query);
}
}
NeighborDetectionSystemGroup
内で具体的にどのシステムが実行されるかは Tag_ComputeNeighbors_***
で指定しています。また、ネイバーリストの構築後に相互作用を計算するか否かは Tag_UpdateInteraction
で指定しています。
Tagのつけ外しのほか、Combined な Plan ではネイバーリストの構築ではなくその場で相互作用を計算するためにネイバーリストの保存先である DynamicBuffer
も不要なので外しています。
▼ Systemが要求するEntityQueryの定義方法
ECS向けとして設計された SystemBase.Entities.ForEach()
や IJobEntityBatch
は Schedule()
時にEntityQuery
を渡すことでSystemのEntityQueryも自動的に設定されるようになっていましたが、これらは Entity でループを回すインターフェイスになっており、それ以外のイテレーションを行いたい場合-例えばセルごとに情報を持つテーブルを使ってセルでイテレーションをしたい場合など-には使えません。ループの長さをプログラマが指定する形、ということで今回は馴染み深い IJobParallelFor
を使いますが、このときSystemに明示的にEntityQueryを伝える必要があります。
public partial class ParallelForSystem : SystemBase
{
protected override void OnCreate()
{
base.OnCreate();
RequireForUpdate(
GetEntityQuery(ComponentType.ReadOnly<SomethingTag>(),
ComponentType.ReadOnly<SomethingType>(),
ComponentType.ReadWrite<SomethingResult>()));
}
private ParallelJob : IJobParallelFor
{
[ReadOnly]
public ComponentDataFromEntity<SomethingType> dataFromEntity;
[NativeDisableContainerSafetyRestriction]
public ComponentDataFromEntity<SomethingResult> resultFromEntity;
public void Execute(int i){ /* compute */ }
};
protected override OnUpdate()
{
int work_length = /* number of task */;
int batch_size = /* batch size of parallel job */;
var job = new ParallelJob
{
// RequireForUpdate() に登録したのでシステムが実行され、コンポーネントをとってこれる
dataFromEntity = GetComponentDataFromEntity<SomethingType>(true); // read only
resultFromEntity = GetComponentDataFromEntity<SomethingResult>();
};
Dependency = job.Schedule(work_length, batch_size, Dependency);
}
}
Systemの生成時に SystemBase.RequireForUpdate()
に EntityQuery
を渡すことで必要なEntityQueryを定義できます。ここで、SystemBase.RequireForUpdate()
と SystemBase.Entities.ForEach()
を両方定義すると両方共が内部配列に保存され、 EntityQuery[] SystemBase.EntityQueries
で参照可能です。
▼ GlobalなNativeContainerの置き場所
空間分割マップとして NativeMultiHashMap
を元に HashCellIndex
を作りましたが、これを複数のシステムから参照可能にするため HashCellIndex
自体はMonoBehaviour側の CellIndex_Bootstrap
に置き、System側からはstaticメンバを参照する形にしました。この際、システムの実行順序に絡んで2つの留意点があります。
〇 SystemによるMonoBehaviourのNativeContainerへの参照
ECSのSystemの処理は SystemBase.Dependency
経由で次々に放り込んでいるようで、staticな場所にNativeContainerを置いて参照する前に SystemBase.Dependency.Complete()
で先に走っている System を完了させておかないとNativeContainerの安全システムから「今Jobで使用中」と怒られます。
[UpdateInGroup(typeof(BuildCellIndexSystemGroup))]
public partial class BuildCellIndexSystetm : SystemBase
{
protected override void OnUpdate()
{
// UnityEngine にアクセスする前に Complete() する必要がある。
Dependency.Complete();
// 参照を取得
var cellIndex = CellIndex_Bootstrap.HashCellIndex;
CellIndex_Bootstrap.InitDomain(Bootstrap.BoidsCount);
// Jobで利用
var cellIndexWriter = cellIndex.AsParallelWriter();
Dependency = Entities.
WithName("UpdateCellIndexJob").
WithAll<BoidType>().
WithNone<Tag_ComputeNeighbors_Direct>().
WithBurst().
ForEach(
(Entity entity, in Translation pos) =>
{
cellIndexWriter.TryAdd(pos.Value, entity);
}).ScheduleParallel(Dependency);
// 終了処理用 (次節を参照)
CellIndex_Bootstrap.SetJobHandle(this.GetType().Name, Dependency);
// 次のシステム (NeighborDetectionSystemGroup) も使うのでここで Complete() してしまう
// (各システムで初めにComplete()してもよい)
Dependency.Complete();
}
}
〇 Systemに利用されるNattiveContainerの破棄
前節に絡んで、アプリの終了時にMonoBehaviourの破棄とSystemの完了、破棄の順番は不定なので、MonoBehaviourにECS Systemから参照するNativeContainerを置く場合にはコンテナの破棄前に関連するJobをすべて完了させておく必要があります。
なので、コンテナを利用する各 System から JobHandle
を受け取り、 Dispose()
前にすべて Complete()
させることで終了処理の順番を制御します。
各 System は class として定義されるので、 System ごとの区別は string class.GetType.Name
を JobHandle
の識別子として Dictionary
に登録します。
public class CellIndex_Bootstrap : MonoBehaviour
{
private static CellIndex_Bootstrap Instance;
private void Awake()
{
Instance = this;
}
public static HashCellIndex<Entity> HashCellIndex { get { return Instance._cellIndex; } }
private HashCellIndex<Entity> _cellIndex;
private Dictionary<string, JobHandle> _handles;
private bool _allocated;
void Start()
{
_cellIndex = new HashCellIndex<Entity>(Allocator.Persistent);
_handles = new Dictionary<string, JobHandle>();
_allocated = true;
}
private void OnDestroy()
{
Dispose();
}
~CellIndex_Bootstrap()
{
Dispose();
}
public void Dispose()
{
if (_allocated)
{
// コンテナの Dispose() 前に関連する Job をすべて完了させる
foreach(var handle in _handles.Values) handle.Complete();
_cellIndex.Dispose();
_allocated = false;
}
}
// 終了処理用の JobHandle を受け取る
public static void SetJobHandle(string job_identifier, JobHandle handle) => Instance.SetJobHandleImpl(job_identifier, handle);
private void SetJobHandleImpl(string job_identifier, JobHandle handle)
{
if (_handles.ContainsKey(job_identifier))
{
_handles[job_identifier] = handle;
}
else
{
_handles.Add(job_identifier, handle);
}
}
}
▼ Systemでprivateに利用するバッファ
Systemの外部とコンテナを共有せず、 private に使うだけなら SystemBase.OnCreate()
と SystemBase.OnDestroy()
が使えるので普通に Allocate して Dispose() します。
public partial class ComputeSystem : SystemBase
{
private NativeList<Data> localBuffer;
protected override void OnCreate()
{
base.OnCreate();
localBuffer = new NativeList<Data>(Allocator.Persistent);
}
protected override void OnDestroy()
{
base.OnDestroy();
localBuffer.Dispose();
}
protected override void OnUpdate()
{
base.OnUpdate();
/* use localBuffer in OnUpdate() */
}
}
その他
▽ SystemBase でできること
公式のSystemBaseのAPIを見ると結構いろいろな継承メンバを持っているので System で何ができるのかを考えるときには各継承メンバの仕様も併せて確認するといいと思います。いろいろできるのに Properties
や Methods
には書かれていないので当初ほしいAPI (RequireForUpdate
) が見つからず、途方にくれていました……
▽ DOTS向けのUnityEditor機能
Entities 0.50 で Editor 拡張も大幅に改良され、Inspectorがだいぶ見やすくなりました。
-
Window->DOTS->System
NeighborValidateSystem
(デバッグ用に作ったもの)はSystemBase.RequireForUpdate()
とSystemBase.Entities.ForEach()
の両方が定義されているため、EntityQueryが2つ表示されています。このとき生成されている Boids 100個はどちらにも該当するので、 Query1 と Query2 それぞれの EntityCount=100 が合算され Systems Window では200個のEntityが処理されていると解釈されています。 -
Window->DOTS->Hierarchy
GameObjectのようにDOTS Hierarchyで値を確認できるようになりました。Entities.Prefab
タグコンポーネントがついているかどうかもGameObjectのPrefab同様色分けされます。
また、値のフィールドを持たないComponentType
は自動的にタグと認識され、Tags
タブに表示されます。
参考
Original Implementation:
Unity で Boids シミュレーションを作成して Entity Component System (ECS) を学んでみた
Previous version:
[Unity] 現在(Entities 0.50)のECS環境でシンプルな Boids Simulation を書く
Qiita:
UnityのEntitiesプロジェクトをビルドする方法
【Unity】DOTS(ECS)を導入したゲームを作る際に設計してみた話
Official
Entities