UnityのECSを理解する その2(初心者向け)

2019/12/06 追記
本記事で使用しているサンプルは、ECSのバージョンアップにより2019/11/05にGitHubから削除されました。

お久しぶりです。EFです。
今回も Unity の ECS をサンプルを使って解説したいと思います。

注意点

この記事は2019/11/13時点のものになります。
また、本記事は間違った情報を載せている可能性が大いにあることに注意してください。



環境

Windows 10
Unity 2019.1.0f1 以上(本記事は 2019.3.0b3 )

Package
Entities : preview - 0.1.1
Burst : 1.1.2
Jobs : peview - 0.1.1

ECSの導入方法や解説は過去の記事を参考にしてください。
UnityのECS導入方法 - EF Blog
UnityのECSを理解する その1(初心者向け) - EF Blog


サンプル

GitHubからUnity公式のサンプルを落としてきます。
github.com
UnityHub でリストに追加し、2019.1.0f1以上のバージョンで開いてください。

開くシーンは「Asset > HelloCube > 1b. ForEachWithEntityChanges > ForEachWithEntityChanges.scene」です。


理解する

このシーンは4つのスクリプト、1つのゲームオブジェクトで構成されています。
スクリプト中のコメントは翻訳しています(合ってるかわからない)
意訳も含まれるのでご了承ください。

まずはデータから見ていきましょう。

MoveUp_ForEachWithEntityChanges

using System;
using Unity.Entities;

namespace Samples.HelloCube_1b
{
    // Serializable 属性はエディタサポート用です。
    [Serializable]
    public struct MoveUp_ForEachWithEntityChanges : IComponentData
    {
        // MoveUpは「タグ」コンポーネントであり、データを含みません。
        // タグコンポーネントを使用して、システムが処理するエンティティをマークできます。
    }
}
タグコンポーネント

タグコンポーネントとは、エンティティにとあるコンポーネントが含まれるアーキタイプだった場合、処理を分けるためのコンポーネントのことです。
f:id:EorF:20191030184740j:plain:w600


MovingCube_ForEachWithEntityChanges.cs

using System;
using Unity.Entities;

namespace Samples.HelloCube_1b
{
    // Serializable 属性はエディタサポート用です。
    [Serializable]
    public struct MovingCube_ForEachWithEntityChanges : IComponentData
    {
        // MovingCube_ForEachWithEntityChangesは「タグ」コンポーネントであり、データを含みません。
        // タグコンポーネントを使用して、システムが処理するエンティティをマークできます。
    }
}

上記同様こちらもタグコンポーネントになります。


MovementSystem_ForEachWithEntityChanges.cs

using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

namespace Samples.HelloCube_1b
{
    // シーン内の全てのエンティティをTranslationコンポーネントで更新します。
    // エンティティにMoveUpコンポーネントがあるかどうかで処理方法を変えてます。
    public class MovementSystem_ForEachWithEntityChanges : ComponentSystem
    {
        protected override void OnUpdate()
        {
            // MoveUpコンポーネントが存在する場合は、エンティティが上昇するように直接Translationコンポーネントが更新されます。
            // 図形が一定の高さに達すると、MoveUpコンポーネントが削除されます。
            Entities.WithAllReadOnly<MovingCube_ForEachWithEntityChanges, MoveUp_ForEachWithEntityChanges>().ForEach(
                (Entity id, ref Translation translation) =>
                {
                    var deltaTime = Time.deltaTime;
                    translation = new Translation()
                    {
                        Value = new float3(translation.Value.x, translation.Value.y + deltaTime, translation.Value.z)
                    };

                    if (translation.Value.y > 10.0f)
                        EntityManager.RemoveComponent<MoveUp_ForEachWithEntityChanges>(id);
                }
            );

            // エンティティはMoveUpコンポーネントがない場合(Translationコンポーネントは存在します)、
            // エンティティは開始点まで下に移動されMoveUpコンポーネントが追加されます。
            Entities.WithAllReadOnly<MovingCube_ForEachWithEntityChanges>().WithNone<MoveUp_ForEachWithEntityChanges>().ForEach(
                (Entity id, ref Translation translation) =>
                {
                    translation = new Translation()
                    {
                        Value = new float3(translation.Value.x, -10.0f, translation.Value.z)
                    };

                    EntityManager.AddComponentData(id, new MoveUp_ForEachWithEntityChanges());
                }
            );
        }
    }
}

今回のポイントとなるスクリプトです。
タグコンポーネントを追加・削除し、オブジェクトの判別を行い、処理を分けています。
このスクリプトでは以下の処理を行っています。

・エンティティにMoveUpコンポーネントがあるなら、オブジェクトを上昇させ、一定以上になるとMoveUpコンポーネントを削除。
・エンティティにMoveUpコンポーネントがないなら、座標をリセットし、エンティティにMoveUpコンポーネントを追加。


このコードではエンティティの取得に Entities.WithAllReadOnly を使用していますが、他にも取得する関数があるので紹介します。

Entities.ForEach

ForEach はラムダにコンポーネントを引数を渡すと、自動で必要なコンポーネントを推論し、そのコンポーネントをすべて含むエンティティを取得します。
推論は
Entities.ForEach( (T t, U u) => {});
と書くと
Entities.ForEach<T, U>( (T t, U u) => {});
のように展開されます。

// どのエンティティにも Entity は含まれてるため、全てのエンティティを取得。
Entities.ForEach( (Entity e) =>
{
    // do stuff
});
// TとUが含まれたエンティティを取得。
Entities.ForEach( (T t, U u)=>
{
    // do stuff
});
Entities.WithAll

WithAll は指定したコンポーネントを全て含むエンティティを取得します。

// Tが含まれる全てのエンティティを取得。
Entities.WithAll<T>().ForEach( (T t) => 
{
    // do stuff
});
// TとUが含まれるエンティティを取得し、コンポーネントVを使って処理。
Entities.WithAll<T, U>().ForEach( (V v) =>
{
    // do stuff
});
Entities.WithAny

WithAny は指定したコンポーネントがどれか1つでも含まれるエンティティを取得。

// TもしくはUが含まれるエンティティを取得し、コンポーネントVを使って処理。
Entities.WithAny<T, U>().ForEach( (V v) =>
{
    // do stuff
});
// Tが含まれており、UもしくはVが含まれているエンティティを取得し、コンポーネントWを使って処理。
Entities.WithAll<T>().WithAny<U, V>().ForEach( (W w)=>
{
    // do stuff
});
Entities.WithNone

WithNone は指定したコンポーネントがどれか1つでも含まれていると除外して、エンティティを取得。

// TもしくはUが含まれるエンティティを除外・取得し、コンポーネントVを使って処理。
Entities.WithNone<T, U>().ForEach( (V v) =>
{
    // do stuff
});
// Tが含まれて、Uが含まれていないエンティティを取得し、コンポーネントVを使って処理。
Entities.WithAll<T>().WithNone<U>().ForEach( (V v) =>
{
    // do sfuff
});
Entities.WithAllReadOnly

WithAllReadOnly は WithAll にコンポーネント読み取り専用であることを示したものです。
読み取り専用ですが、書き込んでもエラーは出ません。

// TはタグコンポーネントなのでReadOnlyで取得し、コンポーネントUを使って処理。
Entities.WithAllReadOnly<T>().ForEach( (U u)=>
{
    // do stuff
});
Entities.WithAnyReadOnly

WithAnyReadOnly は WithAny にコンポーネント読み取り専用であることを示したものです。
読み取り専用ですが、書き込んでもエラーは出ません。

// T,UはタグコンポーネントなのでReadOnlyで取得し、コンポーネントVを使って処理。
Entities.WithAnyReadOnly<T, U>().ForEach( (V V)=>
{
    // do stuff
});



Spawner_ForEachWithEntityChanges.cs

using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

namespace Samples.HelloCube_1b
{
    public class Spawner_ForEachWithEntityChanges : MonoBehaviour
    {
        public GameObject Prefab;
        public int CountX = 100;
        public int CountY = 100;

        void Start()
        {
            // Entity Prefab(複製するための元となるエンティティ)を1度だけゲームオブジェクトから作ります。
            Entity prefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(Prefab, World.Active);
            var entityManager = World.Active.EntityManager;

            for (int x = 0; x < CountX; x++)
            {
                for (int y = 0; y < CountX; y++)
                {
                    // すでに変換されている Entity Prefab から一連のエンティティを効率よくインスタンス化します。
                    var instance = entityManager.Instantiate(prefab);

                    // インスタンス化されたエンティティをノイズのあるグリッドに配置します。
                    var position = transform.TransformPoint(new float3(x - CountX/2, noise.cnoise(new float2(x, y) * 0.21F) * 10, y - CountY/2));
                    entityManager.SetComponentData(instance, new Translation(){ Value = position });
                    entityManager.AddComponentData(instance, new MoveUp_ForEachWithEntityChanges());
                    entityManager.AddComponentData(instance, new MovingCube_ForEachWithEntityChanges());
                }
            }
        }
    }
}

このスクリプトではエンティティの初期化を行っています。

やっていることが多いので分割して説明します。

Entity prefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(Prefab, World.Active);
これはプレハブをエンティティに変換する処理です。
World.Active は GameObjectConversionSettings クラスになり、内部の設定をもとにプレハブをエンティティ変換しています。


var instance = entityManager.Instantiate(prefab);
プレハブから実際にインスタンスを生成している処理です。


var position = transform.TransformPoint(new float3(x - CountX/2, noise.cnoise(new float2(x, y) * 0.21F) * 10, y - CountY/2));
entityManager.SetComponentData(instance, new Translation(){ Value = position });
entityManager.AddComponentData(instance, new MoveUp_ForEachWithEntityChanges());
entityManager.AddComponentData(instance, new MovingCube_ForEachWithEntityChanges());

エンティティは処理を持たないので EntityManager からデータをセット・追加しています。


最後に

今回はタグコンポーネントを使って処理を使い分けるサンプルでした。
同じエンティティで処理を分けたいときに必要なテクニックなので覚えておきたいですね。

あと、カメラを Forward レンダリングにすると Depth バッファが表示されるのなんでなんだ・・・。


Special Thanks

Unityの黒河さん
黒河優介(YusukeKurokawa) (@wotakuro) | Twitter