前置き

業務で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のパラメータを調整。

そして再生してみるとこのように設定したPrefabが大量に生成されました。

最後に

今回は大量のキューブを生成しただけですが、データ指向では今までの方法とは異なり、結構手間がかかります。

考え方も異なるので習得には時間がかかりそうですが、しっかりと身につけておきたいですね…。

今回は以下の動画をベースに行いました、ECSとは何なのかが学べる思うのでぜひ見てみてください!
https://www.youtube.com/watch?v=zn3m6ZFppdQ