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

2019/12/06 追記
本記事で使用しているサンプルはECSのバージョンアップにより、2019/11/05にソースコードが変わっています。

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


注意点

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

環境

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の導入方法は過去の記事を参考にしてください。
eorf.hatenablog.com


サンプル

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

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


DOTSとは

DOTS は「Data-Oriented Technology Stack」の略で、マルチスレッドに対応している Unity のコアになる機能です。

DOTS には以下の機能が含まれます。

本記事では上記の1つである ECS を理解しようと思います。


ECSとは

エンティティ、コンポーネント、システムの3つからなるアーキテクチャパターンです。
データと処理を分離させ、並列化しやすい構造、効率の良いメモリアクセスなどを実現させるものです。

ほかの方が詳しく解説しているので、リンクを貼っておきます。

ECSの単語

とは言っても ECS には知っておかないといけない概念があるので書きます。
上記の記事に書かれているのでn番煎じですが・・・。

コンポーネント

ECS のコンポーネントはデータを指します。ある処理を行うために必要な変数の集まりのことです。イメージとしてはピュアな struct です。
データのみなので関数などの処理は持ってないです。


エンティティ (Entity)

エンティティはコンポーネントを管理するものです。GameObject に近いですが、コンポーネント同様処理はありません。
処理がデータを必要なときに、すでに存在していないコンポーネントをアクセスしないようにチェックしてくれます。


アーキタイプ (Archetype)

アーキタイプはエンティティ内のコンポーネントの組み合わせを指します。
使われてるコンポーネントがA,Bの場合、アーキタイプAB になります。
使われてるコンポーネントがA,B,Cの場合、アーキタイプ ABC となり、アーキタイプAB とは異なるタイプになります。


チャンク (Chunk)

チャンクはエンティティ、コンポーネントを含む箱(メモリの領域)で、必ず16KiBのサイズです。
また、コンポーネントを種類別に並び替えたものになります。
種類別に並び替えるというのは、アーキタイプABC(コンポーネントA,B,Cが含まれている)が3つあった場合、ABCABCABC を AAABBBCCC のように同じコンポーネント別に、塊にすることです。
システムは処理を行う際、チャンクから必要なコンポーネントのみを参照し、扱います。
この時、コンポーネントが連続的にメモリに並んでいるとキャッシュミスが少なくなり、処理速度が上がります。


システム (System)

システムは動作を指します。処理を行うために必要な関数のことです。
システムはデータを持たないので、ある入力があると毎回同じ出力をします。
処理を行う時、入出力に必要なコンポーネントを持っているチャンクを探し、塊になっているコンポーネント取得し、計算を行い、取得したコンポーネントに書き込みます。


エンティティマネージャ (Entity Manager)

エンティティマネージャとはエンティティを管理するものです。
エンティティの作成、読み取り、更新、破棄を行うことが可能です。


ワールド (World)

ワールドは EntityManager(エンティティ、コンポーネント)、ComponentSystems(システム)を包含しているものです。
ワールドは自体は複数あってもいいですが、ワールド同士の直接的な干渉は不可能です。


理解する

このシーンは3つのスクリプトと1つのゲームオブジェクト(エンティティ)で構成されています。
スクリプト中のコメントは翻訳しています(合ってるかわからない)

RotationSpeed_ForEach.cs

using Unity.Entities;
using System;

// Serializable属性はエディタサポート用です。
// 名前の一貫性が失われるとReSharperは無効になります。
[Serializable]
public struct RotationSpeed_ForEach : IComponentData
{
    public float RadianPerSecond;
}

まずはデータ定義です。
strcut で定義し IComponentData を継承します。
IComponentData を継承することでコンポーネントとして扱えます。

コンポーネントの種類は5つあり、ここでは簡単に紹介します。

  • IComponentData

 - 普通のコンポーネント
 - Unityにメモリ管理されるオブジェクトへの参照は入れられない。

  • ISharedComponentData

 - エンティティ間で共有されるコンポーネント。(ex:レンダラー)

  • ISystemStateComponentData

 - システム内部のリソースをコールバックに依存することなく作成・破棄が行えるコンポーネント

  • ISystemStateSharedComponentData

 - ほぼ ISystemStateComponentData で Shared になったもの。

  • IBufferElementData

 - 可変長のバッファをエンティティに関連付けられるコンポーネント


RotationSpeedSystem_ForEach.cs

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

// このシステムはRotationSpeed_ForEachコンポーネントとRotationコンポーネントの両方を使用してシーン内のすべてのエンティティを更新します。

// 名前の一貫性が失われるとReSharperは無効になります。
public class RotationSpeedSystem_ForEach : ComponentSystem
{
    protected override void OnUpdate()
    {
        // Entities.ForEachはメイン・スレッドでComponentDataの各セットを処理します。
        // これは最高のパフォーマンスを得るための推奨方法ではありません。
        // しかし、ここではまずComponentSystem Update(論理)とComponentData(データ)のより明確な分離を説明します。
        // 個々のComponentDataには更新ロジックがありません。
        Entities.ForEach((ref RotationSpeed_ForEach rotationSpeed, ref Rotation rotation) =>
        {
            var deltaTime = Time.deltaTime;
            rotation.Value = math.mul(math.normalize(rotation.Value),
                quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * deltaTime));
        });
    }
}

システムは class で定義し、ComponentSystem を継承します。
OnUpdate は GameObject の Update のようなもので毎フレーム呼ばれます。また、ComponentSystem からオーバーライドしています。
サンプルでは RotationSpeed_Foreach と Rotation の両方を持つエンティティを回転させています。
Entities.ForEach は全てのエンティティから関連するコンポーネントを取得し、記述されているデリゲートを実行します。


RotationSpeedAuthoring_ForEach.cs

using System;
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;

// 名前の一貫性が失われるとReSharperは無効になります
[RequiresEntityConversion]
public class RotationSpeedAuthoring_ForEach : MonoBehaviour, IConvertGameObjectToEntity
{
    public float DegreesPerSecond;

    // MonoBehaviourはエンティティ上でComponentDataに変換されます
    // 具体的には、データのエディタの単位(度)からランタイムの単位(ラジアン)への変換を行っています。
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        var data = new RotationSpeed_ForEach { RadiansPerSecond = math.radians(DegreesPerSecond) };
        dstManager.AddComponentData(entity, data);
    }
}

このクラスはどのようにゲームオブジェクトからエンティティへ変換するかを定義したものです。
エンティティ自体は EntityManager でも作れますが、GameObject や Prefab から変換する方法もあります。今回は後者です。
IConvertGameObjectToEntity を継承するときは RequiresEntityConversion 属性をつける必要があります。
Convert関数は Unity がランタイム中に呼び出してエンティティに変換しています。


RotatingCube (GameObject)

RotatingCube はシーン内に置かれているゲームオブジェクトで、子に ChildCube にがあります。

RotatingCube には RotationSpeedAuthoring_ForEach.cs と ConvertToEntity.cs がアタッチされています。

Convert To Entity (Inspector上のコンポーネント)

これは、GameObject を Entity に変換の動作を設定するものです。
Conversion Mode パラメータが動作を決めます。
パラメータの種類は2つあります。

  • Convert And Destroy

 - エンティティに変換された後の GameObject を破棄する。(GameObjectを残さない)

  • Convert And Inject Game Object

 - エンティティに変換後、破棄された GameObject を生成する。(GameObjectを残す)

Rotation Speed Authoring_For Each (Inspector上のコンポーネント)

このコンポーネントには Degrees Per Second を設定するところがあります。
ここで設定した回転スピードは実行直後にラジアンに変換されます。
そして、RotationSpeed_ForEach (ECSのコンポーネント) が生成され、EntityManager に登録されます。
その後は ComponentSystem を継承してオーバーライドされた関数(OnUpdateなど)が適切なタイミングで呼ばれます。


最後に

以上がサンプルを解説です。
最後のほうは力尽きてざっくりした説明になってしまいました。
よくわからんとなった方はコメントで教えていただければと思います。