Unity
EntityComponentSystem

UnityのECS(Entity Component System)で雪の結晶を降らせる

はじめに

UniteTokyo2018でECSに関する講演がありました。

この中でECSを使って雪を大量に降らせるというデモが行われていたので、今回自分でも実装に挑戦してみました。

ezgif.com-optimize.gif

ECSは今までのGameObject&Monobehaviorとは異なるデータ指向型によるアプローチです。
ECSってなに?という解説はこちらの記事が勉強になります。

準備

ECSを使うための準備です。

Unityの準備

Unity2018.1をインストールします。
(今回はUnity2018.1.0f2で検証しています)

ECSのインストール

次にプロジェクトを作るとAssetsと同じ階層にPackagesというフォルダがありその中のmanifest.jsonを書換えます。

manifest.json

manifest.json
{
    "dependencies":{
            "com.unity.incrementalcompiler": "0.0.38", 
        "com.unity.entities": "0.0.12-preview.1"
    },
    "testables": [
        "com.unity.collections",
        "com.unity.entities",
        "com.unity.jobs"
    ],
    "registry": "https://staging-packages.unity.com"
}

ECSとは関係ないことですが、このPacakagesManagerの仕組みが入って、ExperimentalやBetaなどのお試し機能の導入が随分楽になりました。

.NETレベル

さらにPlayerSettingsから.NETのバージョンを4.xにします。
スクリーンショット 2018-05-14 3.33.05.png

雪の結晶モデル

今回はこちらのAssetに含まれる雪の結晶を使いました。

実装

以下の2つのスクリプトを用意します。

SnowScrap.cs

SnowScrap.cs
using Unity.Entities;
using Unity.Mathematics;
using Unity.Rendering;
using Unity.Transforms;
using UnityEngine;
using UnityEngine.UI;

public class SnowStrap : MonoBehaviour 
{
    public Mesh snowMesh;
    public Material snowMaterial;
    public Text countText;
    public Text fpsText;

    public int step = 10; // 1フレームあたりに作る数

    // FPS
    private float m_updateInterval = 0.5f;
    private float m_accum;
    private int m_frames;
    private float m_timeleft;
    private float m_fps;

    // 雪結晶の数
    private int count = 0;

    private EntityManager entityManager;
    private EntityArchetype snowArchetype;

    // ECSで雪の結晶を作るエントリーポイント
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    private void Start()
    {
        entityManager = World.Active.GetOrCreateManager<EntityManager>();

        snowArchetype = entityManager.CreateArchetype(
            typeof(TransformMatrix),
            typeof(Position),
            typeof(Rotation),
            typeof(MeshInstanceRenderer)
        );
    }

    private void Update()
    {
        if (Input.GetKey(KeyCode.Space))
        {
            for (int i = 0; i < step; i++)
            {
                var snow = entityManager.CreateEntity(snowArchetype);

                entityManager.SetSharedComponentData(snow, new MeshInstanceRenderer
                {
                    mesh = snowMesh,
                    material = snowMaterial,
                });

                entityManager.SetComponentData(snow, new Position
                {
                    Value = new float3(Random.Range(-20.0f, 20.0f), 20, Random.Range(-20.0f, 20.0f))
                });
                entityManager.SetComponentData(snow, new Rotation
                {
                    Value = Quaternion.Euler(0f, Random.Range(0.0f, 180.0f), 90f)
                });

                count++;

            }

            countText.text = count.ToString();

            // FPS
            m_timeleft -= Time.deltaTime;
            m_accum += Time.timeScale / Time.deltaTime;
            m_frames++;

            if ( 0 < m_timeleft ) return;

            m_fps = m_accum / m_frames;
            m_timeleft = m_updateInterval;
            m_accum = 0;
            m_frames = 0;

            fpsText.text = "FPS: " + m_fps.ToString("f2");
        }
    }
}

SnowMovementSystem.cs

SnowMovementSystem.cs
using Unity.Entities;
using Unity.Transforms;

public class SnowMovementSystem : ComponentSystem {

    // 落下速度
    private float delta = 0.1f;

    public struct SnowGroup
    {
        public ComponentDataArray<Position> snowPostion;
        public int Length;
    }

    [Inject] private SnowGroup _snowGroup;

    protected override void OnUpdate()
    {
        for (int i = 0; i < _snowGroup.Length; i++)
        {
            var newPos = _snowGroup.snowPostion[i];
            newPos.Value.y -= delta;
            _snowGroup.snowPostion[i] = newPos;
        }
    }
}

本来は雪の結晶をレンダリングするシステムと雪の結晶を動かすシステムの2つのシステムを作る必要がありますが、レンダリングに関するシステムはUnity側で用意してくれている MeshInstanceRenderSystemというものがあるのでそれを使っています。

ComponetDataもPositionなどあらかじめUnity側で用意してくれているので自分では作っていません。

20180513012513.png (174.4 kB)

空のゲームオブジェクトを作成してSnowScrap.csをコンポーネントとして貼り付けます。
publicフィールドのプロパティを割り当てます。
ここで気をつけなければいけないのがMaterialの設定です。

20180513012942.png

Enable GPU Instancingにチェックを入れなければいけません。
MeshInstanceRenderSystemの内部でGPU Instancing使っているようです。

最後に確認です。
EditorのRunボタンを押してシーンをスタートさせます。
Unity -> Window -> EntityDebuggerを開きます。
ECSのEntityはヒエラルキーには表示されないのでこちらを使って確認します。

20180513014716.png (67.7 kB)

SnowMovementSystemとMeshInstanceRenderSystemがあるのが分かります。
システムをどうさせるのに必要なComponetDataやEntityの詳細(transformなど)を確認することができます。

サンプルプロジェクト

サンプルプロジェクトGithubに上げました。

まとめ

ECS楽しい、凄い!

これくらい簡単なものであればシステムやデータコンポーネントの設計でそれほど迷うことはありませんが、もう少し大きめのことをやろうとすると設計の難易度は格段に上がりそうで慣れが必要になってきそうです。
レンダリングのようにUnity側で用意してくれるシステムが充実してくれることにも期待です。

参考

英語のブログ記事ですが役に立ちました。

本家のECSサンプルです。こちらも参考になります。

FPSの求め方はこちらを参考にしています。