2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

KLab EngineerAdvent Calendar 2024

Day 11

Unity ECSを学ぶ!

Last updated at Posted at 2024-12-10

この記事は、KLab Engineer Advent Calendar 2024 の11日目の記事です。
こんにちは。KLabでエンジニアをしている @tsune2ne です。

去年Unity ECSが正式リリースされました。
これを機に学習していきます。

え?DOTSのリリースから何年経ってると思ってるんだ遅すぎるって?
いいんです。始めるのに遅すぎるということはないんです。

Unity ECSの経緯

課題

今まではGameObjectごとにMonoBehaviourクラスをAddComponentしてプログラミングすることが多いと思います。
これはオブジェクト指向プログラミングに近く、GameObjectのふるまいをC#クラスで表現できるので非常に理解しやすいです。

しかし、これをデータフローで見ると実は効率が悪いです。
データと処理をまとめて管理しいるため、横断的な操作をすると歯抜けになってしまいアクセス効率が落ちます。

ecs_youtube13.png

解決策

オブジェクト指向ではデータとふるまいの紐づきが強すぎてメモリ配置を最適化できないと考え
データ(Entity)とふるまい(System)の設計を分離しようというのがECSの考えです。
これによりメモリアクセス速度の向上、マルチコアCPUの効率利用、シリアライズ・デシリアライズの高速化などのメリットがあります。

ecs_youtube24.png

上図で緑のデータにアクセスするのにECSのほうが有利なことがわかるでしょう。
もちろん状況によりけりですが右のレイアウトにしやすいのがECSのメリットです。

はじめての Unity ECS - Entity Component System を使ってみよう!

ECSは以下の3つの要素に分割されます。

  • Entity : データ、情報
  • Component : 物体にデータを紐づける仕組み
  • System : ふるまい、

UnityEditorでの見た目

UnityEditorのGUIでは現状のGameObjectにC#クラスをコンポーネントとして付属するのは直観的でわかりやすいです

ただデータ指向で効率化すると人間の感覚では直感的ではなくなります
Hierarchy上のGameObjectにC#クラスをAddComponentするというのは非常にわかりやすいです
そのため、ECSでもUnityEditorでは同じ感覚でAddComponentするようにできています

しかし実行するときにECS空間?変換する処理(Bake)を行います

サンプル実装

実行サンプルを作って比較してみましょう
雑に5000個のCubeを上下運動させます
Sample.gif

実装したコードは下記です
https://github.com/tsune2ne/UnityECSSample

ECSのデータ定義

ECSコンポーネントごとに持つデータを定義します
データを持つだけで処理やふるまいは持ちません

public struct MoveRotate : IComponentData
{
    public float Angle;
    public float Speed;
    public float3 BasePosition;
    public bool IsInitialized;
}

GameObjectコンポーネントをEntityに変換

ECS空間?で持つためのEntityおよびEntityへの変換処理を記述します
起動時に自動で変換されます

public class MoveRotateAuthoring : MonoBehaviour
{
    public float Angle;
    public float Speed;

    class RotateBaker : Baker<MoveRotateAuthoring>
    {
        public override void Bake(MoveRotateAuthoring authoring)
        {
            // Entityを生成
            var entity = GetEntity(TransformUsageFlags.Dynamic); // LocalTransformアリ
            AddComponent(entity, new MoveRotate
            {
                Angle = authoring.Angle,
                Speed = authoring.Speed,
            });
        }
    }
}

Baker処理とは?

UnityEditorで設定した値をECSコンポーネントに変換する処理のこと

ふるまいの定義

データのふるまいを記述します
これはHierarchy上にAddComponentしなくても実行されます
partialなのはC#のSource Generatorで補助的なコードを自動生成しているためです。

[BurstCompile]
public partial struct DancerSystem : ISystem
{
    public void OnUpdate(ref SystemState state)
    {
        // MoveRotateとLocalTransformが付与されてるEntityを検索して処理
        foreach (var (rotate, xform) in SystemAPI.Query<
                RefRW<MoveRotate>,  // MoveRotateを書き込み権限で取得
                RefRW<LocalTransform>>()) // LocalTransformを書き込み権限で取得
        {
            // 初期化処理
            if (!rotate.ValueRW.IsInitialized)
            {
                rotate.ValueRW.Speed = Random.Range(0.1f, 10f);
                rotate.ValueRW.BasePosition = xform.ValueRO.Position;
                rotate.ValueRW.IsInitialized = true;
            }

            // 移動処理
            rotate.ValueRW.Angle += Time.deltaTime * rotate.ValueRW.Speed;
            xform.ValueRW.Position.y = Mathf.Cos(rotate.ValueRW.Angle) + rotate.ValueRW.BasePosition.y;
        }
    }
}

上記はJobSystem対応されておらずECSの効果が発揮されないので対応します

[BurstCompile]
public partial struct MoveRotateSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        // 初期化処理
        foreach (var (rotate, xform) in SystemAPI.Query<
            RefRW<MoveRotate>,
            RefRW<LocalTransform>>())
        {
            if (!rotate.ValueRW.IsInitialized)
            {
                // UnityEngine.Randomはシングルスレッド限定なので先に決めてしまう
                rotate.ValueRW.Speed = Random.Range(0.1f, 10f);
                rotate.ValueRW.BasePosition = xform.ValueRO.Position;
                rotate.ValueRW.IsInitialized = true;
            }
        }

        // ジョブインスタンスを生成して並列実行予約
        var ecb = GetEntityCommandBuffer(ref state);
        new ProcessRotateMoveJob
        {
            DeltaTime = Time.deltaTime,
            Ecb = ecb
        }.ScheduleParallel();
    }

    EntityCommandBuffer.ParallelWriter GetEntityCommandBuffer(ref SystemState state)
    {
        var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
        var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
        return ecb.AsParallelWriter();
    }

    [BurstCompile]
    public partial struct ProcessRotateMoveJob : IJobEntity
    {
        public float DeltaTime;
        public EntityCommandBuffer.ParallelWriter Ecb;

        // 引数のComponentを持つEntityを検索して実行される
        void Execute([ChunkIndexInQuery] int chunkIndex, ref MoveRotate rotate, ref LocalTransform xform)
        {
            // 移動処理
            rotate.Angle += DeltaTime * rotate.Speed;
            xform.Position.y = Mathf.Cos(rotate.Angle) + rotate.BasePosition.y;
        }
    }
}

性能評価

Profilerを眺めて比較してみましょう

↓普通の実装で動かした場合のPlayerLoop
oldのplayerloop.png

↓ECS+BurstCompiler+JobSystemで動かした場合のPlayerLoop
ecsのplayerloop.png

かなり減らせてますね!
※ただサンプル数1000個だと有意差は見られませんでした(なんで?)
※Windows + Unity2022.3.51f1 + Entities1.3.5で検証

↓Job項目で並列チェック
ecs+jobでJobをちゃんと使ってるっぽい?2.png

緑色が今回作った移動処理です。それぞれのジョブで実行されてますね。

データ指向設計(DOD)の話

このオブジェクト指向ではなくデータ中心の考え方をデータ指向設計(DOD)と呼ばれています。
既存とは全く違う設計が必要になるため、公式で学習コースが用意されています。
気になる人はぜひ受講してみてください。

DOTS ベストプラクティス

まとめ

雑なサンプルを作ってもプロファイラーに変化があったので、武器として知っておく価値は十分あります。
処理がボトルになっていて並列化で改善できる個所だけECS化するのが既存PJへの最適解です。

PJ全体でECS化すると性能面で大きなアドバンテージを得られますが
既存のオブジェクト指向からデータ指向へパラダイムシフトが必要になります。
開発メンバー全員への教育コストが必要です。

またオブジェクト指向設計は人数のスケールアップがしやすいのが利点であるため
データ指向設計で多人数開発をすると可読性やパッケージ化に新しいノウハウが必要になります。

そのコストを払ってまでECS化するかは難しい判断になりそうです。

参考資料

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?