ぐーるらいふ

底辺。

【Unity】Unity2019.1で初級者でも本当に簡単にECSを実現する

ECSって難しそう

  • ECSってのがあるらしい。
  • Unityで大量のオブジェクトを表示しても軽いらしい。
  • だがまだpreview版なので、仕様変更が激しいらしい。

という噂だけ聞いていて、

「ほーんでもまだゲームに使うには早そうだぬ」

と煎餅をバリバリ食べて放置していたんですが、
重い腰を上げてやってみたら凄い簡単に出来ちゃいました。

なので、記事にして記憶を残しておこうと思います。

ECSの解説…はちょっと置いといて

ECSってなに?ComponentSystemって?といった解説記事は
沢山の先駆者が残してくれていて、自分も存分に参考にさせて頂いてます。
ちゃんと知りたい!といった方は是非読んでみてください。

tsubakit1.hateblo.jp

qiita.com

www.f-sp.com


などなど、本当にいつもお世話になっております。

まず動かしてみよう

自分は、

まず動かしてみてから、動作を見つつ用語を紐づけて理解していく

スタイルでやってみようと思いました。

まず、単純に一つのcubeをECSを利用して表示するコードを紹介します。

導入

Unity 2019.1.1f1 を使用。
このサンプルでは新規プロジェクトから行ってますが、
途中のプロジェクトでも必要なpackageをインストールすれば問題なくECS使えます。

unityが開いたらPackageManagerを開きます。

f:id:ghoul_life:20190511140847p:plain

Advancedからpreview packageを表示させます。

f:id:ghoul_life:20190511141550p:plain

以下のpackageをinstallします。

  • Burst
  • Entities
  • Hybrid Renderer
  • Mathematics
  • Jobs (後々必要になると思いますが、本当に最小なら無くても大丈夫です)

f:id:ghoul_life:20190511141453p:plain

これで事前準備は完了です。

実装

早速コードを書いていきます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Entities;
using Unity.Transforms;
using Unity.Rendering;
using Unity.Collections;

public class ECSBootstrap : MonoBehaviour
{
    [SerializeField] private Mesh _mesh;
    [SerializeField] private Material _material;

    private const int ECS_CREATE_COUNT = 1;

    // Start is called before the first frame update
    void Start()
    {
        InitializeECS();
    }

    private void InitializeECS()
    {
        var entityManager = World.Active.EntityManager;

        var entityArchetype = entityManager.CreateArchetype(
            // renderer
             typeof(RenderMesh)
            ,typeof(LocalToWorld)

            // Transform
            ,typeof(Translation)
        );

        NativeArray<Entity> entityArray = new NativeArray<Entity>(ECS_CREATE_COUNT, Allocator.Temp);

        entityManager.CreateEntity(entityArchetype, entityArray);
        for (var i = 0; i < entityArray.Length; i++)
        {
            Entity entity = entityArray[i];

            entityManager.SetSharedComponentData(entity, new RenderMesh
            {
                mesh = _mesh,
                material = _material,
            });
        }
        entityArray.Dispose();
    }
}

コードこれだけでOKです。

meshは標準で入っているcubeを、materialは適当にcreateして使用します。shaderはstandardで大丈夫です。

f:id:ghoul_life:20190511144744p:plain

実行してみましょう。

f:id:ghoul_life:20190511144932p:plain

はい、出ました。ECS簡単ですね。
画像では、Entity Debuggerというものも一緒に載せています。
これはECSで作られたEntityを確認することが出来るwindowsです。
Window > Analysisの所にあります。

f:id:ghoul_life:20190511145233p:plain

ECSを利用してオブジェクトを生成するとInspectorに表示されないため、
どんな状態になっているか把握できないことがあります。
そういった時にこれを見ると、Entityが実際に作られているか確認することが出来ます。
この例で行くと、「Entity 0」が作られているのが確認できます。

Entityって?

EntityとはGameObjectに相当する構造体の事です。
ECSでは1Entityを1つの実体として処理します。

簡単に解説

Managerを取得

EntityManagerはECSのEntityを管理するクラスです。
ECSを利用するためにまずそのManagerを取得します。

        // 現在有効なEntityManagerを取得
        var entityManager = World.Active.EntityManager;

どのようなEntityを生成するか指定

続いてどのようなEntityを生成するか?を指定します。
cubeを表示するだけならLocalToWorld , Translationは必要ないのでは?
と思うかもしれませんが、描画する位置情報を処理するarcheTypeも追加しないと
カメラ上には出てきませんので、追加しています。
(Entity的には無くてもちゃんと作られます)

        // どのようなEntityを生成するか指定
        var entityArchetype = entityManager.CreateArchetype(
            // renderer
             typeof(RenderMesh) // Meshを描画する
            ,typeof(LocalToWorld) // 座標をWorld座標に変換させ3D空間に描画する

            // Transform
            ,typeof(Translation) // 座標を指定してEntityに反映させる
        );

Entityをいくつ作るかを指定

次にそのEntityはいくつ作るか?を指定します。

        // いくつEntityを生成するかを指定。ここでは1つだが、この値を変えればその個数分作られる
        private const int ECS_CREATE_COUNT = 1;
        NativeArray<Entity> entityArray = new NativeArray<Entity>(ECS_CREATE_COUNT, Allocator.Temp);
        entityManager.CreateEntity(entityArchetype, entityArray);

Entityの初期設定

そして作ったEntityの初期設定を行います。

        // 各Entityに対する初期設定を行います。
        for (var i = 0; i < entityArray.Length; i++)
        {
            Entity entity = entityArray[i];
            
            // 各EntityのRenderMeshに使用したいmeshとmaterialを渡します。
            entityManager.SetSharedComponentData(entity, new RenderMesh
            {
                mesh = _mesh,
                material = _material,
            });
        }
        entityArray.Dispose(); // ちゃんと破棄する

こんな流れになります。

初期位置を変更してみよう

つまり、初期設定で位置を指定すれば位置を変えることが出来そうだ。
やってみましょう。

まず個数を10にします。

private const int ECS_CREATE_COUNT = 10;  // 10に変更

続いて、以下のように初期化処理にTranslationを操作する処理を追加します。

       using Unity.Mathematics; // float3はUnity.Mathematics.float3なので

        entityManager.CreateEntity(entityArchetype, entityArray);
        for (var i = 0; i < entityArray.Length; i++)
        {
            Entity entity = entityArray[i];
            entityManager.SetComponentData(entity, new Translation { Value = new float3(UnityEngine.Random.Range(-5f, 5f), UnityEngine.Random.Range(-5f, 5f), UnityEngine.Random.Range(-5f, 5f)) }); // 追加

            entityManager.SetSharedComponentData(entity, new RenderMesh
            {
                mesh = _mesh,
                material = _material,
            });
        }
        entityArray.Dispose();

実行してみましょう。

f:id:ghoul_life:20190512155018p:plain

Entityの個数が10に増え、位置がランダムになっています。
このようにentityManager.SetComponentDataで初期値を操作する事が可能です。

独自の処理を追加する

こっからです。
もちろん予め用意されたものだけではゲームなんて作れません。
独自の振る舞いを追加したいに決まっています。

初期値で定めた速度で上下に移動する処理

を追加したいと思います。

オリジナルのComponentDataを作成する

移動速度を持つComponentDataを作成します。

using Unity.Entities;

// classではなく、structなのに注意して下さい。
public struct MoveSpeedComponent : IComponentData
{
    public float _moveSpeed;
}

Entityを移動させる処理を作成する

Entityをリアルタイムで操作する処理はComponentSystemが行っています。
そのComponentSystemを独自に実装する事で振る舞いを追加することが出来ます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;

[AlwaysUpdateSystem]
public class MoveSystem : ComponentSystem
{
    protected override void OnUpdate()
    {

        Entities.ForEach((ref Translation translation, ref MoveSpeedComponent moveSpeedComponent) =>
        {
            translation.Value.y += moveSpeedComponent._moveSpeed;

            // 上下反転
            if (translation.Value.y > 5f)
            {
                moveSpeedComponent._moveSpeed = -math.abs(moveSpeedComponent._moveSpeed);
            }
            if (translation.Value.y < -5f)
            {
                moveSpeedComponent._moveSpeed = +math.abs(moveSpeedComponent._moveSpeed);
            }
        });
    }
}

生成した各EntityをForEachで回して、そのEntityが持っているComponentDataの参照を取得し処理を行うように記述します。

[AlwaysUpdateSystem]

これが無いと
f:id:ghoul_life:20190512161424p:plain
このようにComponentSystemが動いていない状態になります。
独自にWorldを作成している場合はこの限りでは無いと思うのですが、この例ではWorldは一旦置いています。

archeTypeにMoveSpeedComponentを追加

作成したMoveSpeedComponentをEntityに追加します。

        var entityArchetype = entityManager.CreateArchetype(
            // renderer
             typeof(RenderMesh) // Meshを描画する
            ,typeof(LocalToWorld) // 座標をWorld座標に変換させ3D空間に描画する

            // Transform
            ,typeof(Translation) // 座標を指定してEntityに反映させる

            // Custom
            ,typeof(MoveSpeedComponent) // Entityの移動速度を指定する
        );

Entityの初期化処理に移動速度を設定

移動速度をランダム値で初期化して、Entityごとに移動速度をバラつかせます。

        entityManager.CreateEntity(entityArchetype, entityArray);
        for (var i = 0; i < entityArray.Length; i++)
        {
            Entity entity = entityArray[i];
            entityManager.SetComponentData(entity, new Translation { Value = new float3(UnityEngine.Random.Range(-5f, 5f), UnityEngine.Random.Range(-5f, 5f), UnityEngine.Random.Range(-5f, 5f)) });
            entityManager.SetComponentData(entity, new MoveSpeedComponent { _moveSpeed = UnityEngine.Random.Range(-0.5f, 0.5f) }); // 追加

            entityManager.SetSharedComponentData(entity, new RenderMesh
            {
                mesh = _mesh,
                material = _material,
            });
        }
        entityArray.Dispose();

これで独自処理追加完了です。実行してみましょう。

f:id:ghoul_life:20190512162209g:plain

Entityがランダムな速度で上下移動しているのが確認できました。

プラスワン

個数を10000にしてみましょう。

f:id:ghoul_life:20190512164208p:plain

余りに大量なため、画面が埋まっていますが、
FPSに影響が無く動作する事が確認できます。

GameObjectがTransformで行っている処理であれば
Unity.Transforms.Scale , Unity.Transforms.Rotation
なども使えますし、
色を変化させたければ、RenderMeshのmaterialからcolorを変化させたりもできます。
(materialはSharedなので、全部一緒に変わりますが、shaderを使えばもっと複雑に出来そうではあります。)

        Entities.ForEach((Entity e) => {

            var renderMesh = EntityManager.GetSharedComponentData<RenderMesh>(e);
            var material = renderMesh.material;
            // ~~~~
            renderMesh.material = material;
       });

こんな感じでRenderMeshにアクセスすることはできます。

とっかかりとして

ECSをまず動かす。という観点でやってみました。

ここから

  • どんな仕組みで動いているんだろう
  • Worldってなんだろう
  • NativeArrayって何なんだろう
  • なんでComponentDataは構造体しか使えないんだろう
  • もっと複雑な処理をECSで実現したい

といったさらに深く学習していくとっかかりとなってくれたら幸いです。

反省

4月記事書いてない!最低でも一月一記事ぐらい投稿したかった。
次はもう少しゲームらしい記事を上げたい所です。

別件でストーリーのあるゲームを作成していまして

  • ストーリーのプロット完成
  • カットシーンのラフは完成(画像などは仮だが動作や文章は出来ている)
  • 立ち絵、顔画像 (いまここ作成中)

となっています。
エターなりそうですが、出来るだけ頑張りたいと思ってます…。