見出し画像

Unity DOTS 入門 (13) - SystemBase

以下の記事を参考にして書いてます。

Class SystemBase | Entities | 0.7.0-preview.19

1. SystemBase

SystemBase」は、「ECS」の主要3要素の1つ「システム」(操作)を実装する基底クラスです。

Object
 ・ ComponentSystemBase
  ・ SystemBase

「システム」は、任意のコンポーネント種別セットを持つエンティティを検索して操作します。

以下は、Entities.ForEach() を使用してエンティティを検索して操作する、基本的な処理の流れを示しています。PositionコンポーネントとVelocityコンポーネントを持つエンティティを検索し、デルタ時間に基づいてPositionコンポーネントのデータを更新しています。

public struct Position : IComponentData
{
    public float3 Value;
}

public struct Velocity : IComponentData
{
    public float3 Value;
}

public class ECSSystem : SystemBase
{
    protected override void OnUpdate()
    {
        float dT = Time.DeltaTime; 

        Entities
            .WithName("Update_Displacement")
            .ForEach(
                (ref Position position, in Velocity velocity) =>
                {
                    position = new Position()
                    {
                        Value = position.Value + velocity.Value * dT
                    };
                }
            )
            .ScheduleParallel();
    }
}

2. システムのオーバーライドメソッド

「システム」実装時に、以下の「オーバーライドメソッド」を実装することができます。

・OnCreate() : システム作成時に呼ばれる。
・OnStartRunning() : 最初にOnUpdate()の前、システム実行再開時に呼ばれる。
・OnUpdate() : システム有効時にフレーム毎に呼ばれる。
・OnStopRunning() : システム実行停止時(クエリに一致するエンティティが見つからないなど)、OnDestroy()の前に呼ばれる。
・OnDestroy() : システム破棄時に呼ばれる。

これらの関数は全てメインスレッドで実行されます。 バックグラウンドスレッドで実行するには、OnUpdate()からJobをスケジュールします。

3. システムの実行順序

システムの実行順序は、ComponentSystemGroup によってグループ化したツリー構造で定義します。システムの属するグループと、グループ内の順序は、属性で定義します。

[UpdateInGroupAttribute] : システムが属するグループを指定。
[UpdateBeforeAttribute] ・ [UpdateAfterAttribute]  : システムのグループ内の順序を指定。

システムのグループを指定しない場合、デフォルトで「World SimulationSystemGroup」に配置されます。 デフォルトでは、全てシステムが検出、インスタンス化され、デフォルトワールドに追加されます。[DisableAutoCreationAttribute] を使用して、システムが自動的に作成されないようにすることもできます。

4. エンティティクエリ

「システム」は、作成した全てのクエリをキャッシュします。

デフォルトでは、クエリがエンティティを見つけた時、ランタイムはシステムのOnUpdate()のみを呼び出します。AlwaysUpdateSystemAttributeを使用して、システムを常に更新することができます。クエリのないシステムもフレーム毎に更新されることに注意してください。

5. Entities.ForEach()およびJob.WithCode()

Entitiesは、エンティティを反復するための便利なメカニズムを提供します。 Entities.ForEach() を使用すると、エンティティクエリを定義し、各エンティティに対して実行するラムダ関数を指定して、バックグラウンドスレッドで実行するように作業をスケジュールするか、メインスレッドですぐに実行することができます。

Entities.ForEach()は、C#コンパイラ拡張を使用して、意図を説明するデータクエリ構文を取得し、それを効率的な(オプションで)Jobベースのコードに変換します。

Jobプロパティは、C# Jobを定義するための同様のメカニズムを提供します。 Schedule()を使用して実行できるJob.WithCode() は、ラムダ関数を単一のJobとして実行します。

6. システム属性

SystemBaseの実装で、属性を使用して、いつ更新するかを制御できます。

・UpdateInGroupAttribute : システムをComponentSystemGroupに配置。
・UpdateBeforeAttribute : 常に同じグループ内の別のシステムの前にシステムを更新。
・UpdateAfterAttribute : 常に同じグループ内の別のシステムの後にシステムを更新。
・AlwaysUpdateSystemAttribute : フレーム毎にOnUpdateを呼び出す。
・DisableAutoCreationAttribute : システムを自動的に作成しない。
・AlwaysSynchronizeSystemAttribute : OnUpdateを呼び出す前に同期点を強制。

7. プロパティ

・Dependency : システムのECS関連データの依存関係を示すJobハンドルを取得。
・Entities : エンティティクエリで選択したエンティティに対してラムダ関数を実行する機能を提供。
・Job : [IJob]を定義および実行する機能を提供。

8. Dependency

システムのECS関連データの依存関係を示す「Jobハンドル」を取得します。

Dependencyプロパティは、現在のシステムが読み書きするコンポーネントの「Jobハンドル」を表します。Entities.ForEach() または Job.WithCode() を使用する場合、システムはDependencyプロパティを使用して、Jobの依存関係をスケジュールできます。

以下は、それぞれ前のJobに応じて、3つのJobをスケジュールします。

    protected override void OnUpdate()
    {
        Entities
            .WithName("ForEach_Job_One")
            .ForEach((ref AComponent c) =>
            {
                /*...*/
            })
            .ScheduleParallel();

        Entities
            .WithName("ForEach_Job_Two")
            .ForEach((ref AnotherComponent c) =>
            {
                /*...*/
            })
            .ScheduleParallel();

        Job
            .WithName("Job_Three")
            .WithCode(() =>
            {
                /*...*/
            })
            .Schedule();
    }

Entities.ForEach() または Job.WithCode() に「Jobハンドル」を明示的に渡すことにより、依存関係をスケジュールすることができます。「Jobハンドル」を渡すと、新しいJobと結合された入力依存関係を表す「Jobハンドル」を返します。

最後の「JobHandle」をDependencyに割り当て、ECS安全管理者が依存関係を後続のシステムに伝達できるようにします。

    protected override void OnUpdate()
    {
        JobHandle One = Entities
            .WithName("ForEach_Job_One")
            .ForEach((ref AComponent c) =>
            {
                /*...*/
            })
            .ScheduleParallel(this.Dependency);

        JobHandle Two = Entities
            .WithName("ForEach_Job_Two")
            .ForEach((ref AnotherComponent c) =>
            {
                /*...*/
            })
            .ScheduleParallel(this.Dependency);

        JobHandle intermediateDependencies =
            JobHandle.CombineDependencies(One, Two);

        JobHandle finalDependency = Job
            .WithName("Job_Three")
            .WithCode(() =>
            {
                /*...*/
            })
            .Schedule(intermediateDependencies);

        this.Dependency = finalDependency;
    }

9. Entities

「エンティティクエリ」で選択したエンティティに対してラムダ関数を実行する機能を提供します。

        Entities
            .WithName("Update_Position") // エラーメッセージとプロファイラに表示
            .WithAll<LocalToWorld>() // LocalToWorldコンポーネントが必要
            .ForEach(
                // Displacementの書き込み(参照)、Velocityの読み取り(in) 
                (ref Position position, in Velocity velocity) =>
                {
                    // 選択したエンティティ毎に実行
                    position = new Position()
                    {
                        // dTはキャプチャされた変数
                        Value = position.Value + velocity.Value * dT
                    };
                }
            )
            .ScheduleParallel(); // 並列Jobとしてスケジュール

◎ With
いくつかの「With」を追加して、処理するエンティティを識別できます。これらの句には次のものが含まれます。

・WithAll : 指定したコンポーネントを全て持つエンティティを選択。
・WithAny : 指定したコンポーネントを1つ以上持つエンティティを選択。
・WithNone : 指定したコンポーネントを持たないエンティティを選択。
・WithChangeFilter() : 指定したコンポーネントが変更された可能性があるエンティティのみ選択。
・WithSharedComponentFilter(ISharedComponentData) : 共有コンポーネントに指定された値を持つチャンクのみ選択。
・WithEntityQueryOptions(EntityQueryOptions) : EntityQueryOptionsオブジェクトで定義されている追加オプションを指定。
・WithStoreEntityQueryInField(EntityQuery) : Entities.ForEach()によって生成されたEntityQueryオブジェクトをシステムのEntityQueryフィールドに格納。

◎ ラムダ関数のパラメータ
ラムダ関数のパラメータの定義は、次のとおりです。

・値渡しのパラメータ(パラメータ修飾子なし)
・読み書きパラメータ(refパラメータ修飾子)
・読み専用パラメータ(inパラメーター修飾子)

最大8つのパラメータをラムダ関数に渡すことができます。 コンポーネントタイプに加えて、以下を使用することもできます。

・Entity entity : 現在のEntityのインスタンス。
・int entityInQueryIndex : クエリによって選択されたエンティティのリスト内のエンティティのインデックス。
・int nativeThreadIndex : ラムダ関数の反復を実行するスレッドの一意のインデックス。
                .ForEach((Entity entity,
                    int entityInQueryIndex,
                    ref WritableComponent aReadwriteComponent,
                    in ReadonlyComponent aReadonlyComponent) =>
                    {
                        /*..*/
                    })

◎ スケジュール
ForEach構築を実行するには、3つのオプションがあります。

・ScheduleParallel() : 並行なJobにスケジュール。
・Schedule() : 1つのJobにスケジュール。
・Run() : メインスレッドで即実行。

◎ 追加オプション
追加オプションは、次のとおり。

・WithName(string) : デバッグおよびプロファイリング時に関数識別に利用。
・WithStructuralChanges() : メインスレッドでラムダ関数を実行し、Burstを無効にして、関数内のエンティティデータに構造的な変更を加えられるようにする。パフォーマンス向上には、代わりにEntityCommandBufferを利用。
・WithoutBurst() : Burstコンパイルを無効。ラムダ関数にBurstサポート外のコードが含まれている場合に利用。
・WithBurst(FloatMode, FloatPrecision, bool) : Burstコンパイラオプションの指定。
 ・floatMode : 浮動小数点数学最適化モード
 ・floatPrecision : 浮動小数点演算の精度
 ・synchronousCompilation : 関数を即コンパイル。

10. Job

[IJob] を定義および実行するための機能を提供します。

Dependencyプロパティは、現在のシステムが読み書きするコンポーネントのJobハンドルを表します。Entities.ForEach() または Job.WithCode() を使用する場合、システムはDependencyプロパティを使用して、Jobの依存関係をスケジュールできます。

次の例は、暗黙的な依存関係管理に依存するOnUpdate()の実装を示しています。 この関数は、Jobに応じて、3つのJobをスケジュールします。

public class RandomSumJob : SystemBase
{
    private uint seed = 1;

    protected override void OnUpdate()
    {
        Random randomGen = new Random(seed++);
        NativeArray<float> randomNumbers
            = new NativeArray<float>(500, Allocator.TempJob);

        Job.WithCode(() =>
        {
            for (int i = 0; i < randomNumbers.Length; i++)
            {
                randomNumbers[i] = randomGen.NextFloat();
            }
        }).Schedule();

        // Jobからデータを取得するには、値が1つしかない場合でもNativeArrayを使用する必要がある
        NativeArray<float> result
            = new NativeArray<float>(1, Allocator.TempJob);

        Job.WithCode(() =>
        {
            for (int i = 0; i < randomNumbers.Length; i++)
            {
                result[0] += randomNumbers[i];
            }
        }).Schedule();

        // これにより、スケジュールされたJobが完了し、結果がすぐに得られるが、
        // 効率を高めるには、 1つのシステムのフレームの早い段階でJobをスケジュールし、
        // 別のシステムのフレームの後半で結果を取得する必要がある。
        this.CompleteDependency();
        UnityEngine.Debug.Log("The sum of "
            + randomNumbers.Length + " numbers is " + result[0]);

        randomNumbers.Dispose();
        result.Dispose();
    }
}

◎ ラムダ関数
Job.WithCode(lambda)
にラムダ関数を実装します。ラムダ関数はパラメーターを使用できません。ローカル変数をキャプチャできます。

・Schedule() : ラムダ関数を単一のJobとして実行。
・Run() : メインスレッドで即実行。

◎ ローカル変数のキャプチャ
ラムダ関数はローカル変数をキャプチャできます。Jobを使用して(Run()の代わりにSchedule()、ScheduleParallel()、ScheduleSingle()を呼び出すことによって)関数を実行する場合、キャプチャされた変数とその使用方法にいくつかの制限があります。

ネイティブコンテナblittable型のみをキャプチャできる。
・Jobは、ネイティブコンテナであるキャプチャされた変数にのみ書き込むことができる。 (単一の値を「返す」には、1つの要素を持つ[native array]を作成。)

次の関数を使用して、キャプチャされた[native container]変数([native arrays]を含む)に修飾子と属性を適用できます。

・WithReadOnly(myvar) : 変数へのアクセスを読み専用として制限。
・WithDeallocateOnJobCompletion(myvar) : Jobの完了後にネイティブコンテナーの割り当てを解除。
・WithNativeDisableParallelForRestriction(myvar) : 複数のスレッドが同じ書き込み可能なネイティブコンテナーにアクセスすることを許可。
・WithNativeDisableContainerSafetyRestriction(myvar) : ネイティブコンテナへの危険なアクセスを防ぐ通常の安全制限を無効。
・WithNativeDisableUnsafePtrRestrictionAttribute(myvar) : ネイティブコンテナーによって提供される安全でないポインタを使用できる。

11. メソッド

・CompleteDependency() : 完全な依存関係。
 GetComponent<T>(Entity) : エンティティのコンポーネントの値を取得。
HasComponent<T>(Entity) : エンティティに特定のタイプのコンポーネントがあるかどうかを確認。
SetComponent<T>(Entity, T) : エンティティのコンポーネントの値を設定。
OnUpdate() : OnUpdate()を実装して、このシステムの主要な作業を実行。
Update() : システムを手動で更新。


この記事が気に入ったらサポートをしてみませんか?