前置き
業務でUnityを主に使用しているのですが、なかなかモダンな技術に触れる機会もなく、ECSを触ってみたい!と思っているまま数年…。このままじゃだめだ!と思い、せっかくなので次の個人的な制作に使ってみたいと思い、ECSに触れてみました。
ECS is 何
まず、ECSとは何なのかを調べてみました。
ECSとは、Entity Component Systemを略したもので、データ指向の考えを取り入れた新たなコンポーネントシステムです。
元々、Unityのコンポーネントシステムは、オブジェクト指向の考え方で設計されておりメモリ内のデータの配置やアクセス速度に関してはデータ指向に劣っているようです。
ECSにおいては、メモリがアクセスしやすい、データレイアウト(メモリへのデータの配置)になっているため、メモリへのアクセス速度が向上するようです。
また、並列処理とも相性が良いとのこと。
ここらへんはComputeShaderにおける考え方と少し似ている部分があるかもしれません。
https://blog.unity.com/ja/engine-platform/on-dots-entity-component-system
さっそく使ってみる
今回はECSを使って、大量にオブジェクトを生成するところまでをやってみました。
環境
- Unity 2022.3.33f1
- Burst 1.8.15
- Entities 1.2.3
- Entities Graphics 1.2.3
まずは、URPでプロジェクトを作成し、SubSceneを作成します。
そして、SubSceneに空のPrefabを作成し、Spawnerと命名。
データを定義する
まずは、ECSに重要なデータを定義します。
最初にIComponentDataを継承し、コンポーネントを定義します。
このSpawnerクラスはPrefabの情報をEntityとして持ちます。
using Unity.Entities; using Unity.Mathematics; /// <summary> /// 生成クラス /// </summary> public struct Spawner : IComponentData { public Entity Prefab; public float SpawnRadius; public int SpawnCount; public uint RandomSeed; }
次は、GameObjectをEntityに変換するための仕組みを作成します。
/// <summary> /// ゲームオブジェクトをEntityに変換する /// </summary> public class SpawnerAuthoring : MonoBehaviour
{ public GameObject prefab = null; public float spwanRate = 0.0f; public int spwanCount = 10; public uint RandomSeed = 100; } public class SpawnerBaker : Baker<SpawnerAuthoring> { public override void Bake(SpawnerAuthoring authoring) { var data = new Spawner() { Prefab = GetEntity(authoring.prefab, TransformUsageFlags.Dynamic), SpawnRadius = authoring.spwanRate, SpawnCount = authoring.spwanCount, RandomSeed = authoring.RandomSeed }; AddComponent(GetEntity(TransformUsageFlags.None), data); } }
ベイク(GameObjectをEntityに変換)するために必要な情報をSpawnerAuthoringで定義し
その情報を元にベイクするクラスを定義します。
Bake関数では生成したEntityに対してデータをもとに作成したコンポーネントを追加しています。
この時生成したEntityというのはこのスクリプトをアタッチしたGameObjectのことを指します。このスクリプトがアタッチされたGameObjectはEntityに変換されます。
今回のケースではSubSceneに配置したSpawnerオブジェクトにアタッチしています。
そのためSpawnerオブジェクトがBakeされEntity化、その後コンポーネントを追加するような流れになっています。
上記までの流れでGameObjectをEntity化する流れと、コンポーネントを追加する仕組みを作成しました。
しかし、この状態ではデータを定義しただけにすぎず、何も動作はしません。
そこで実際にこのEntityの動作を定義していきます。
データを元に動かす
public partial struct SpawnerSystem : ISystem { public void OnCreate(ref SystemState state) => state.RequireForUpdate<Spawner>(); public void OnDestroy(ref SystemState state) { } public void OnUpdate(ref SystemState state) { var config = SystemAPI.GetSingleton<Spawner>(); // Prefabのインスタンス化 var instances = state.EntityManager.Instantiate( config.Prefab, config.SpawnCount, Allocator.Temp); var rand = new Random(config.RandomSeed); foreach (var entity in instances) { // 各コンポーネントへのアクセサを得る var xform = SystemAPI.GetComponentRW<LocalTransform>(entity); xform.ValueRW = LocalTransform.FromPositionRotation (rand.NextFloat3() * config.SpawnRadius, rand.NextQuaternionRotation()); } state.Enabled = false; } }
ISystemを継承し、SpawnerSystemクラスを作成しました。
OnCreate関数ではSpawnerが存在しない場合にこのシステムが動かないようにしています。
そして、肝心のOnUpdate関数ではセットしたPrefabをEntityとしてインスタンス化し
そのEntityのコンポーネント<LocalTransform>を取得し、そのコンポーネントにランダムな値を設定しています。
この処理で興味深いのがValueRWに直接値を設定しているという点で、コンポーネントはあくまで定義されたデータなので、そのバッファに直接的に値を設定しているという点です。(あくまで予想)
そして、最後に state.Enabled を falseにすることで、一度しか呼ばれないようにしています。
実際に動かしてみる
これで動かすまでの準備はできたので、実際に再生してみます。
SubSceneに配置したSpawnerオブジェクトのSpawenerAuthoringのパラメータを調整。
![](https://www.bearbeatcat.jp/kumanekos/wp-content/uploads/2024/06/image-4.png)
最後に
今回は大量のキューブを生成しただけですが、データ指向では今までの方法とは異なり、結構手間がかかります。
考え方も異なるので習得には時間がかかりそうですが、しっかりと身につけておきたいですね…。
今回は以下の動画をベースに行いました、ECSとは何なのかが学べる思うのでぜひ見てみてください!
https://www.youtube.com/watch?v=zn3m6ZFppdQ