エフアンダーバー

個人開発の記録

【Unity】 ECS『を』実装しよう

ECSの記事は徐々に増加しており盛り上がりを見せていますが、 それでもやはりECSとは何かよくわからないという声は多く、 なればその仕組みを紐解くべく自前で実装してみよう……という建前の思いつき記事です。

これを読んでUnity ECSが使いこなせるようになるかは知りませんが、 250行程度のソースコードで原始的なECSをUnityライクに実装してみたので、 ECSの発想自体は極めて簡単なんだな、と感じてもらえれば幸いです。

以前書いたECS関連の記事は硬めの内容になりがちだったので、今回はもう少し緩い感じでいこうと思います。

【2020/05/31】 0.11.0に対応。

はじめに

世の中にECSの紹介記事は多々ありますが、それを読んだ人たちの感想はこんな感じみたいです(私調べ)。

  • CPUを使い切るらしい
  • オブジェクト指向は捨てたらしい
  • MonoBehaviourも捨てたらしい
  • 昨日書いたコードは明日には動かないらしい

そんなこんなで、一部では「Unityは速さのために悪魔に魂を売ったのだ」と囁かれているとかいないとか。

しかし、もちろん実際にはそんなわけもなく、 単にプログラミング業界全体に押し寄せるシンプル化の波がUnityにもやってきただけだと思っています。 詳細は過去記事を参照して欲しいのですが、 ざっくりと言えば「オブジェクト指向って人類には早すぎたよね」ってことです。 「継承?コンポジションでいいんじゃない?」「データと処理を一体化?なんか意味あった?」 みたいな疑問から構造をシンプルにしていく動きが現在そこかしこで起きていて、 ECSもその流れのひとつのように思われます。 ちなみに、ECSが速いのも構造がシンプルだからCPUの得意な形に持っていきやすいというだけだったりします。

で、何が言いたいかと言えば、ECSはオブジェクト指向よりも簡単なわけで、 『ECSがそんなに簡単なら自分でも実装できるはずだ』ということです。

……以上が書きたかっただけな強引な導入になりますが、 このまま終わるとさらに誤解を生んで色々と怒られそうなので以下にちょっとだけ補足を。

  • MonoBehaviourはなくなりません(ただ旧機能として扱われます)
  • MonoBehaviourからの移行支援はあります
  • MonoBehaviourとの共存もできます
  • まだ正式リリース前なのでAPIがよく変わります

詳細はググってください。

ECSとは

こういう記事を読む人はすでに知っているような気もしますが、一応「ECSってなんだ」という話を簡単に。

現在、"ECS"と聞くとUnityかAWSかという雰囲気がありますが、 ECSという言葉自体はゲーム全体の設計パターンを指すもので、特にUnityが発明したものではありません。 Unityはこのパターンに従って設計・実装した新しいゲーム開発の基盤を"ECS"と呼ぶため非常に紛らわしいですが、 パターンとしての"ECS"と実装としての"Unity ECS"が存在することになります。 例えるなら、MVCフレームワークに"MVC"という名前が付いている状態です。

パターンとしてのECS(Entity Component System)はその名の通り、 エンティティ・コンポーネント・システムの三つの要素からなります。 日本語に直すとそれぞれ『存在』・『部品』・『法則』といった感じでしょうか。 ゲーム世界は『部品』によって装飾された多くの『存在』が『法則』に従って動いているというイメージです。

エンティティはそれぞれが別の存在だと認識するためのいわば空のオブジェクトで、 それにコンポーネントをプロパティとしてくっつけていくことで、ゲーム世界の一場面を表します。 このままでは、時が止まった世界となってしまうため、 システムによってその場面から次の場面を計算することで、時を1フレーム分進めます。 あとはそれを繰り返せば、生き生きとした時の流れる世界となるという仕組みです。

// 当たらずとも遠からずなECSイメージコード
// 実際はもう少し制約があるけどだいたいこんな感じ

var entities = new Set<Entity>();
var components = new Map<Entity, Set<Component>>();
var systems = new List<System>();

Setup(ref systems);
Initialize(ref entities, ref components);

while (true)
{
    foreach (var system in systems)
    {
        system.Update(ref entities, ref components);
    }
}

ECSの特徴として、エンティティとコンポーネントがデータ、システムが処理、と、 データと処理が完全に分離していることがあります。 これがオブジェクト指向ではない根拠ですね。

また、コンポーネントの記憶方法やシステムの実装方法なんかにも特徴がありますが、 これらは追々実装とともに説明していくことにします。

実装としてのUnity ECSでは、これらをベースに、より使いやすくするための様々な概念が登場しますが、 この記事で実装するのはこれらだけです。

いざ、実装

というわけで、さっそく実装といきたいとこですが、その前に誤解のないように注意事項だけ。

この記事の実装はUnity ECSのガワを真似ていますが、Unity ECSの実装とはまったくの別物です。 最後に違いについていくつか書きますが、Unity ECSではかなりいろいろな最適化がされています。 ECSの仕組みをつくるだけなら簡単でも、実用的なレベルにもっていくにはかなりの工夫が必要なのです。

では、実装について見ていきましょう。

エンティティを表現しよう

エンティティはコンポーネントを持っているイメージがあるため、 メンバとしてコンポーネントのリストを持ちたくなりますが、 ECSにおいてはエンティティは単なるIDとして扱うことになっています。 というのも、エンティティはコンポーネントをくっつける以外の機能を一切持たないため、 エンティティとコンポーネントの対応関係さえどこかに記憶しておけば十分で、 エンティティ内部に構造を持つ必要がないのです。

というわけで、エンティティの実装は本当にIDを持っているだけの構造体です。

public readonly struct Entity
{
    public readonly int ID;

    public Entity(int id) => this.ID = id;
}

極論すればusing Entity = System.Int32;でもいいのですが、 型を付けておいたほうが後々面倒がないので構造体でラップしておきます。

コンポーネントを表現しよう

コンポーネントはデータそのものなため、 データの内容ごとにそれ用のコンポーネント型をつくることになります。

C#のECSにおいて、コンポーネントは基本的にアンマネージド型である必要があります。 アンマネージド型というのはざっくりと参照型を持たない構造体のことで、 型の表すデータがすべて連続したメモリ上に並んでいるという特徴があります。 メモリ的に構造がシンプルな型というわけです。 こういう型にはメモリ的に等値比較ができたり複製が簡単だったりという利点があります。

基本的に必要なのはそれだけなのですが、 Unity ECSではコンポーネントに対して、 「これはコンポーネントだよ」と印をつける作法になっているのでそれに倣います。 印にはマーカーインターフェース(空のインターフェース)を使います。

public interface IComponentData { }

これを使って次のように必要なコンポーネントを定義します。

public struct MyComponent : IComponentData
{
    public int Value;
}

ちなみに、コンポーネントとして何かを参照するデータを持ちたい場合には、 参照型の代わりにエンティティやリソースハンドルを持ちます。

システムを表現しよう

システムは毎フレーム更新処理を行うので、それ用のメソッドを持ったクラスで表現します。 処理の種類ごとにシステムを用意することになるため、 抽象クラスを定義して、これを継承する形で個々のシステムを実装します。

Unity ECSにガワだけ合わせているため、少し無意味なことをしていますがこんな感じです。

public abstract partial class SystemBase
{
    public void Update() => OnUpdate();

    protected abstract void OnUpdate();
}

更新処理のためにいろいろ参照する必要があるのであとでいろいろ足しますが、 メイン部分はこれだけです。

エンティティを管理しよう

エンティティを単なるIDとして定義したため、 異なるエンティティに同じIDが割り当てられないように管理するためのクラスをつくります。 このクラスを介して、エンティティの生成・破棄をすることになります。

IDは単純に0から順番に割り当てて、二値配列で破棄されていないかどうかだけ管理します。 適宜配列の容量を拡張するだけで特に難しいことは何もありません。

Unity ECSではEntityManagerという構造体がエンティティを管理します。 構造体として実装されていますが機能的にはクラスっぽいので、ここではクラスとして書くことにします。

public sealed partial class EntityManager
{
    private const int InitialCapacity = 8;

    private int nextID;

    private bool[] entities = new bool[InitialCapacity];

    public int EntityCapacity => this.entities.Length;

    public Entity CreateEntity()
    {
        var entity = new Entity(this.nextID++);

        this.entities = EnsureCapacity(this.entities, entity.ID);
        this.entities[entity.ID] = true;

        return entity;
    }

    public void DestroyEntity(Entity entity)
    {
        this.entities[entity.ID] = false;
    }

    public bool Exists(Entity entity) => this.entities[entity.ID];

    private static T[] EnsureCapacity<T>(T[] array, int index)
    {
        if (index < array.Length) return array;

        var capacity = array.Length * 2;
        while (index >= capacity) capacity *= 2;

        var newArray = new T[capacity];
        Array.Copy(array, newArray, array.Length);
        return newArray;
    }
}

「あれ、この実装ちょっとやばくない?」という疑念を抱いた人もいるかもしれませんが、 とりあえず先に進みましょう。

コンポーネントを管理しよう

エンティティの次はコンポーネントを管理します。

ECSではコンポーネントは型ごとに配列で管理するという決まりがあります。 理由は後で説明しますが、とにかくコンポーネントのデータは配列に記憶することにします。 すると、後は前述した通りエンティティとコンポーネントの対応関係、 つまり、【エンティティのID→コンポーネントのインデックス】な辞書をなんらかの形で持てばよいことになります。

「辞書」と聞くとDictionaryが思い浮かびますが、今回は恒等関数にしてしまいます。 つまり、エンティティのIDをそのままコンポーネントのインデックスとして使います。 しかし、それだけだとエンティティがコンポーネントを保持しているかどうかわからないため、 それを判断するための二値配列も用意します。

Unity ECSではEntityManagerがコンポーネントも管理します。 そのため、コンポーネントの生成・破棄はこのクラスを介して行うことになります。

public sealed partial class EntityManager
{
    internal class ComponentArray<T>
    {
        public bool[] Used;
        public T[] Data;

        public ComponentArray(int capacity)
        {
            this.Used = new bool[capacity];
            this.Data = new T[capacity];
        }
    }

    private readonly Dictionary<Type, object> components = new Dictionary<Type, object>();

    public void AddComponent<T>(Entity entity) where T : unmanaged, IComponentData
    {
        VerifyEntityExists(entity);

        var array = GetComponentArray<T>();
        if (EntityHasComponent(array, entity))
        {
            throw new InvalidOperationException("component already exists");
        }

        array.Used = EnsureCapacity(array.Used, entity.ID);
        array.Data = EnsureCapacity(array.Data, entity.ID);
        array.Used[entity.ID] = true;
        array.Data[entity.ID] = default;
    }

    public void RemoveComponent<T>(Entity entity) where T : unmanaged, IComponentData
    {
        VerifyEntityExists(entity);

        var array = GetComponentArray<T>();
        if (!EntityHasComponent(array, entity))
        {
            throw new InvalidOperationException("component not exist");
        }

        array.Used[entity.ID] = false;
    }

    public bool HasComponent<T>(Entity entity) where T : unmanaged, IComponentData
    {
        VerifyEntityExists(entity);

        return EntityHasComponent(GetComponentArray<T>(), entity);
    }

    private void VerifyEntityExists(Entity entity)
    {
        if (!Exists(entity))
        {
            throw new ArgumentException("entity already destroyed", nameof(entity));
        }
    }

    internal ComponentArray<T> GetComponentArray<T>()
    {
        if (this.components.TryGetValue(typeof(T), out var result)) return (ComponentArray<T>)result;

        var array = new ComponentArray<T>(this.EntityCapacity);
        this.components.Add(typeof(T), array);

        return array;
    }

    internal static bool EntityHasComponent<T>(ComponentArray<T> array, Entity entity)
        => entity.ID < array.Used.Length && array.Used[entity.ID];
}

エンティティに紐づけたコンポーネントのデータの取得・設定もEntityManagerを介して行います。

public sealed partial class EntityManager
{
    public T GetComponentData<T>(Entity entity) where T : unmanaged, IComponentData
    {
        VerifyEntityExists(entity);

        var array = GetComponentArray<T>();
        if (!EntityHasComponent(array, entity))
        {
            throw new InvalidOperationException("component not exist");
        }

        return array.Data[entity.ID];
    }

    public void SetComponentData<T>(Entity entity, T value) where T : unmanaged, IComponentData
    {
        VerifyEntityExists(entity);

        var array = GetComponentArray<T>();
        if (!EntityHasComponent(array, entity))
        {
            throw new InvalidOperationException("component not exist");
        }

        array.Data[entity.ID] = value;
    }
}

「あれ、この実装ちょっとやばくない?」という疑念が確信に変わった人もいるかもしれませんが、 とりあえず先に進みましょう。

世界を構築しよう

エンティティ、コンポーネントときたら最後はシステムですね。

システムの管理専用のクラスを作成してもいいのですが、システム管理は特に複雑なことはしません。 そこで、システム管理を担当するクラスにEntityManagerを放り込んでしまうと、 エンティティ・コンポーネント・システムを全て含んだゲーム世界を表すクラスが爆誕します。

ほとんどのことはEntityManagerがやってくれるため、 世界が直接やることはシステムの生成と順次更新くらいです。

Unity ECSではそのまんまWorldという名前のクラスが対応します。

public sealed class World
{
    private readonly List<SystemBase> systems = new List<SystemBase>();

    public EntityManager EntityManager { get; } = new EntityManager();

    public T CreateSystem<T>() where T : SystemBase, new()
    {
        var system = new T() { World = this };
        this.systems.Add(system);

        return system;
    }

    public void Update()
    {
        foreach (var system in systems) system.Update();
    }
}

システムでは、更新時に世界の情報を参照・変更できる必要があるため、所属している世界を記憶しておきます。

public abstract partial class SystemBase
{
    public World World { get; internal set; }

    public EntityManager EntityManager => this.World.EntityManager;
}

エンティティを列挙しよう

足りない最後のピースは「システムの更新処理ってどうやって書くの?」ということですね。

ECSにおけるシステムの基本動作というのは次のようになっています。

  1. すべてのエンティティを列挙する
  2. 特定のコンポーネントの組み合わせを持つものだけをフィルタリングする
  3. それらのコンポーネントのデータでエンティティごとにアレコレする

「アレコレ」はともかく、それ以外の部分は共通化できそうですね。 具体的には、特定のコンポーネントの組み合わせを持つエンティティをすべて列挙する機能になります。

これを行うForEachはUnity ECSにおいて、ForEachLambdaJobDescriptionという構造体に、 インターフェースに対する拡張メソッドという形で定義されています。 この構造体はコンパイラ拡張によってコンパイル時に別のコードに変換される前提のとてもややこしい型なのですが、 ここではその辺りは忘れて愚直にガワだけ真似ることにします。

ForEachは例のごとく各エンティティごとに実行するデリゲートを引数に受け取るのですが、 このデリゲートの型引数としてコンポーネント型を指定することで、 そのコンポーネントを持つエンティティだけを列挙する仕様になっています。

Entities
    .ForEach((Entity entity, ref MyComponent component) => {
        // MyComponent を持つエンティティだけが列挙される
    })
    .Run();

Unity ECSではエンティティ自体を受け取るものや受け取らないもの、 指定できるコンポーネントの数などによって多数のオーバーロードがありますが、 今回はそのうち二つだけを実装します。

public readonly struct ForEachLambdaJobDescription
{
    private readonly SystemBase system;

    private EntityManager EntityManager => this.system.EntityManager;

    internal ForEachLambdaJobDescription(SystemBase system) => this.system = system;

    public delegate void VR<T0>(Entity entity, ref T0 component0);
    public ForEachLambdaJobDescription ForEach<T0>(VR<T0> action)
        where T0 : unmanaged, IComponentData
    {
        var manager = this.EntityManager;
        var capacity = manager.EntityCapacity;
        var components0 = manager.GetComponentArray<T0>();

        for (var i = 0; i < capacity; i++)
        {
            var entity = new Entity(i);
            if (manager.Exists(entity))
            {
                if (!EntityManager.EntityHasComponent(components0, entity)) continue;

                action(
                    entity,
                    ref components0.Data[entity.ID]
                );
            }
        }

        return this;
    }

    public delegate void VRR<T0, T1>(Entity entity, ref T0 component0, ref T1 component1);
    public ForEachLambdaJobDescription ForEach<T0, T1>(VRR<T0, T1> action)
        where T0 : unmanaged, IComponentData
        where T1 : unmanaged, IComponentData
    {
        var manager = this.EntityManager;
        var capacity = manager.EntityCapacity;
        var components0 = manager.GetComponentArray<T0>();
        var components1 = manager.GetComponentArray<T1>();

        for (var i = 0; i < capacity; i++)
        {
            var entity = new Entity(i);
            if (manager.Exists(entity))
            {
                if (!EntityManager.EntityHasComponent(components0, entity)) continue;
                if (!EntityManager.EntityHasComponent(components1, entity)) continue;

                action(
                    entity,
                    ref components0.Data[entity.ID],
                    ref components1.Data[entity.ID]
                );
            }
        }

        return this;
    }

    public void Run() { }
}

本当はRunで実行しないとおかしいのですが、面倒なのでForEachで実行しています。 APIとしてForEachRunが分かれているのはRun以外の実行方法があるためですが、 ここではRunしかないので。

あとはシステム内で簡単に使うためのプロパティを用意すれば完了です。

public abstract partial class SystemBase
{
    protected ForEachLambdaJobDescription Entities => new ForEachLambdaJobDescription(this);
}

実行してみる

ちゃんと動くかどうか確認してみます。

正方形の範囲内を多数の球が動き回るだけのサンプルです。

using UnityEngine;

public struct Position : IComponentData
{
    public float X;
    public float Y;
}

public struct Velocity : IComponentData
{
    public float X;
    public float Y;
}

public class MotionSystem : SystemBase
{
    protected override void OnUpdate()
    {
        Entities
            .ForEach((Entity entity, ref Position position, ref Velocity velocity) =>
            {
                position.X += velocity.X;
                position.Y += velocity.Y;

                if (position.X < -1f) velocity.X = Mathf.Abs(velocity.X);
                else if (position.X > 1f) velocity.X = -Mathf.Abs(velocity.X);
                if (position.Y < -1f) velocity.Y = Mathf.Abs(velocity.Y);
                else if (position.Y > 1f) velocity.Y = -Mathf.Abs(velocity.Y);
            })
            .Run();
    }
}

public class RenderSystem : SystemBase
{
    public Mesh Mesh { get; set; }

    public Material Material { get; set; }

    protected override void OnUpdate()
    {
        if (this.Mesh != null && this.Material != null)
        {
            Entities
                .ForEach((Entity entity, ref Position position) =>
                {
                    var matrix = Matrix4x4.TRS(
                        new Vector3(position.X, position.Y, 0f),
                        Quaternion.identity,
                        Vector3.one * 0.05f
                    );
                    Graphics.DrawMesh(this.Mesh, matrix, this.Material, 0);
                })
                .Run();
        }
    }
}

public class ECSTest : MonoBehaviour
{
    private World world = new World();

    private void Start()
    {
        var world = this.world;
        var manager = world.EntityManager;

        var motionSystem = world.CreateSystem<MotionSystem>();
        var renderSystem = world.CreateSystem<RenderSystem>();

        (var mesh, var material) = GetMeshAndMaterial();
        renderSystem.Mesh = mesh;
        renderSystem.Material = material;

        for (var i = 0; i < 1000; i++)
        {
            var entity = manager.CreateEntity();
            manager.AddComponent<Position>(entity);
            manager.AddComponent<Velocity>(entity);
            manager.SetComponentData<Position>(entity, new Position
            {
                X = Random.value * 2f - 1f,
                Y = Random.value * 2f - 1f,
            });
            manager.SetComponentData<Velocity>(entity, new Velocity
            {
                X = (Random.value * 5e-3f + 5e-3f) * (Random.Range(0, 2) * 2 - 1),
                Y = (Random.value * 5e-3f + 5e-3f) * (Random.Range(0, 2) * 2 - 1),
            });
        }
    }

    private void Update()
    {
        this.world.Update();
    }

    private static (Mesh, Material) GetMeshAndMaterial()
    {
        var go = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        var mesh = go.GetComponent<MeshFilter>().sharedMesh;
        var material = go.GetComponent<MeshRenderer>().sharedMaterial;
        Destroy(go);

        return (mesh, material);
    }
}

正しそうな実行結果のアニメGIF

大丈夫そうですね。

なんで速いの?

ECSが速い理由はコンポーネントの記憶方法とシステムの実装方法にあります。

これまで見てきた通り、コンポーネントは配列に格納され、システムでは列挙の際にそれに順次アクセスします。 配列の順次アクセスというのはプログラミングの基本中の基本の操作で、プログラム中に頻出する処理です。 当然CPUは頻出する処理が速くなるように設計されているので速くなります。以上。


……
………

具体的に説明するとCPUのキャッシュが云々という話になるのですが、 正直知ったところであまり訳に立たないような気がしていて、 それよりもECSの説明でまずこの話が出ることでハードルを上げてしまっている感が嫌なのですよね。 DOTSとしてC# Job SystemやBurstコンパイラと一緒に語られることが多いので高速化技術と勘違いされがちですが、 ECSは"A better approach to game design(ゲーム設計に関するより優れたアプローチ)"と紹介されている通り設計法であって、 速いのはおまけです。 もっと思想にフォーカスした方がいいんじゃないかと。

あともちろんキャッシュ云々でも速くなるのだろうけど、 どちらかといえばC# Job Systemと連携しやすいことの方がECSの速さに貢献しそうな気がしてます。

で、これ使えんの?

真面目にここまでのコードを読んだ人はわかると思いますが、この実装はあらゆる点で実用に耐えません。 まあこの程度で実装できるならUnityがここまで長い時間をかけて開発するはずもないわけで……。

しかし裏を返せば、Unity ECSというのはこの実装のダメな部分を徹底的に直したものであるはずで、 そう考えるとUnity ECSの様々な機能の意味も見えてくるんじゃないかと思います。 そこで、今回の実装の問題点とそれを改善するUnity ECSの実装について少しだけ触れることにします。

増え続ける配列容量

まず明らかにマズイのはエンティティのIDを単調に増やし続けていることですね。 エンティティの生成と破棄を繰り返して総数としてはそれほど多くなかったとしても、 エンティティのIDはどんどん大きな値になっていってしまいます。 エンティティのIDはそのまま配列のインデックスとして使用しているため、 これでは配列の容量もどんどん大きくなっていってしまいます。

Unity ECSではエンティティはIDの他に何代目かという情報を持っていて、IDは使いまわされるようになっています。 これにより無効となったエンティティ参照を検出しつつ、IDの増大を防いでいます。

不要なコンポーネントデータ

今回、コンポーネント配列のインデックスにはエンティティのIDを利用しました。 しかし、これだとエンティティがコンポーネントを持っていなかったとしても領域が確保されてしまいます。 コンポーネントの種類が増えてくるとこれは膨大なメモリを消費します。

これに対する改善策のひとつは、エンティティのIDから配列のインデックスを引く辞書をハッシュマップで持つことです。 これならば、コンポーネント配列の容量がエンティティのIDの範囲に依存することはありません。 しかし、いくらO(1)とはいえハッシュマップのアクセスはそれなりに時間のかかる処理です。 エンティティ列挙時に毎回ハッシュマップを引いていては速度がでません。

Unity ECSではこの問題をアーキタイプとチャンクという概念で解決しています。 アーキタイプというのはエンティティが持っているコンポーネントの組み合わせのことを指します。 エンティティを同じアーキタイプを持つもの同士でまとめて、 それらのコンポーネントデータとともにチャンクと呼ばれるメモリブロックにそれぞれ記憶しておきます。 列挙時にはエンティティの代わりにチャンクを列挙して、そのアーキタイプを調べれば、 少ない回数の比較で条件を満たすすべてのエンティティを探し出すことができます。

さらにチャンク内のコンポーネント配列はすべて対象であるかすべて対象でないかのどちらかになるため、 辞書を使った場合に発生してしまう歯抜け状態のアクセスになりません。 厳密な配列の順次アクセスとなるため、CPUの最適化が完全に機能します。

追加順に実行されるシステム

今回の実装ではシステムは追加された順番で実行していました。 しかし、実際にゲームをつくりはじめるとその実行順が問題になってきます。 数十、数百以上のシステムを依存関係を考慮しながら手動で並べ替えるのは正気の沙汰じゃありません。

Unity ECSではComponentSystemGroupによってシステムをグループ化し、実行順を木構造で管理することができます。 さらに半順序関係を定義しておくと、兄弟システム(グループ)同士を自動的に並べてくれます。

扱いづらい列挙

今回の実装ではForEachは列挙しながらコールバックを実行していました。 こういった場合にはコールバック内で列挙対象を変更してはいけないのがお約束です。 とはいえ、何のために列挙しているかといえば世界の状態を更新するため、 すなわちエンティティやコンポーネントを操作するためなわけで、 列挙中にエンティティやコンポーネントの生成・破棄ができないというのはつらすぎます。

Unity ECSのForEachはあらかじめすべての対象エンティティを特定してからコールバックを呼び出します。 そのため、コールバック内でエンティティやコンポーネントを増減したとしても、ForEach実行時の対象が列挙されます。

また、Unity ECSのForEachRun以外にも列挙の方法を提供していますが、 そういった場合にもうまく動作させるためにEntityCommandBufferという操作を遅延させる仕組みも提供しています。 エンティティに対するコマンドを(後で実行するために)記憶するバッファですね。

その他いろいろ

Unity ECSにはその他にも開発をサポートするいろいろな機能があります。

  • 並列化 → C# Job Systemとの連携 (JobComponentSystem)
  • エディタ連携 → サブシーン
  • 移行支援 → サブシーン、マネージドコンポーネント
  • 構造的アセット → BLOBアセット

詳細は過去記事を参照してください。

www.f-sp.com

www.f-sp.com

ソースコード全文

Gistにて公開中。

この記事のソースコードのライセンスはCC0とします。

おわりに

ECSが単純な仕組みだということを示すために自前で実装してみたという話でした。 個人的にはECSに期待しているんですが、ECSの印象って「速い」と「難しい」が支配的なようで、 このままだと大コケしそうで怖いんですよね……。

ECSの本質は単なるライン工場だと思っていて、 オブジェクト指向的なネットワークよりよっぽど簡単な気がしているんですが、 やはり慣れた思考から抜け出すというのはそれ自体がなかなか難しいもので。 あとはデザインパターンの知見が蓄積されていないのも原因のひとつですかね。 ちなみに、今回本当はECSをライン工場に見立てたムービーをつくる予定だったんですが、 今年中に終わりそうな気がしなかったので、急遽思いつきでこの内容になりました。

それにしても十二月は毎日Qiitaの通知が何十件も溜まってつらい……。 ECSのまとめ記事も更新しないといけないし、すべて読み終えるのはいったいいつになることやら。



執筆時のUnityのバージョン:2019.3.0f3
執筆時のECSのバージョン:0.4.0
更新時のUnityのバージョン:2019.3.15f1
更新時のECSのバージョン:0.11.0