テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

【Unity】ISystemStateComponentDataという機能

今回は微妙に詳細な説明のないISystemStateComponentDataという機能を紹介します。

EntityがDestroyされても破棄されないコンポーネント

 ISystemStateComponentDataはすごく特殊なデータ構造です。具体的には、ISystemStateComponentDataが存在するEntityは、EntityをDestoryされても破棄されなくなるというルールを持っています。

 例えば簡単なEntityを作り、それを破棄してみます。通常であればそのままEntityのIDがリサイクルされるだけなのですが、Entityが破棄されずに残るのを確認出来ます。

// 適当なISystemStateComponentData 
public struct MyData : ISystemStateComponentData { }
    // Entityを作ってから破棄する処理
    IEnumerator Start()
    {
        var dst = World.Active.EntityManager;
        var entity = dst.CreateEntity(typeof(MyData), typeof(LocalToWorld), typeof(Translation));

        yield return new WaitUntil(() => Input.GetKeyDown(KeyCode.D)); // Dキーを押すまで待機

        // MyData以外のComponentDataが破棄される。Entityは破棄されない
        dst.DestroyEntity(entity); 
    }

f:id:tsubaki_t1:20191027124742j:plain
ISystemStateComponentDataがあるEntityは破棄されない

 ISystemStateComponentDataのあるEntityを消すには、ISystemStateComponentDataをRemoveする必要があります。全てのISystemStateComponentDataが破棄されるとEntitiyも破棄されリサイクルに回されます。

    IEnumerator Start()
    {
        var dst = World.Active.EntityManager;
        var entity = dst.CreateEntity(typeof(MyData), typeof(LocalToWorld), typeof(Translation));

        yield return new WaitUntil(() => Input.GetKeyDown(KeyCode.D));
        dst.DestroyEntity(entity); // MyData以外のComponentDataが破棄される

        yield return new WaitUntil(() => Input.GetKeyDown(KeyCode.C));

        dst.RemoveComponent<MyData>(entity); // 改めてEntityが破棄される
    }

Entity生成・破棄のタイミングで処理を一回実行する

 ISystemStateComponentDataの応用で、ComponentDataを生成(追加)したタイミングと破棄したタイミングに処理を実行する方法を考えてみます。本来ならコールバックでこういった事をやりたい所ですが、コールバックはECSの思想とは合ってないので、少し回りくどい方法になっています。使用するのは通常のComponentDataであるBehaviourコンポーネントと、ISystemStateComponentDataのBehaviourInitializedです。

public struct BehaviourInitialized : ISystemStateComponentData{}

public struct Behaviour : IComponentData {}

初期化時の処理(Awake的な)

 最初の状態はBehaviourのみEntityに存在する状態を作ります。これは単純にBehaviourコンポーネントのみをEntityに追加すれば良いです。

f:id:tsubaki_t1:20191027171846j:plain

 上の状態のEntityはEntityQuery的にはBehaviourがあるがBehaviourInitializedが無いという条件で取得することが可能です。ここで見つけたEntity一覧には初期化処理を行い、BehaviourInitializedを追加して初期化処理に呼ばれないようにします。(onStartQuery.CalculateChunkCount() > 0)は、このシステムが

 他のシステムで初期化完了までに処理を行いたい場合、Entityの生成から下のシステムが呼ばれる間にシステムを挿入します。

    protected override void OnCreate()
    {
         // BehaviourはあるがBehaviourInitializedが無いEntity
        onStartQuery = GetEntityQuery(ReadOnly<Behaviour>(), Exclude<BehaviourInitialized>());
    }

    protected override void OnUpdate()
    {
        Entities.With(onStartQuery).ForEach((Entity entity) =>
        {
            // 初期化処理を記述
            Debug.Log($"On Create Enemy {entity.Index}");
        });
        // BehaviourInitializedが無いEntityにBehaviourInitializedを一括追加
        EntityManager.AddComponent<BehaviourInitialized>(onStartQuery);
    }

初期化後の毎フレーム実行する処理(Update的な)

 初期化が完了すれば、下のようにBehaviourBehaviourInitializedが揃った状態になります。もしアップデート処理を行う場合、この2つが揃っていることを確認すれば安全に初期化済みのEntityを操作することが出来ます。

f:id:tsubaki_t1:20191027173108j:plain

protected override void OnUpdate()
{
    Entities.WithAnyReadOnly<Behaviour,BehaviourInitialized>().ForEach((Entity entity) =>
    {
        Debug.Log($"On Update Enemy {entity.Index}");
    });
}

破棄時の処理(OnDestroy的な)

 Entityが破棄された時の処理です。Entityが破棄されてもISystemStateComponentDataであるBehaviourInitializedがあるので破棄されません。それ以外の全てのコンポーネントが破棄された状態になります。つまり、BehaviourInitializedのみのEntityを検索すれば、破棄されてクリーンアップ待ちのEntityを見つけることが出来ます。後はBehaviourInitializedを破棄すれば、破棄処理を完了出来ます。

 こちらもクリーンアップする前に他の処理を挟めば、クリーンナップ処理を他のシステム側で行うことが出来ます。

f:id:tsubaki_t1:20191027174017j:plain

protected override void OnCreate()
{
    // BehaviourInitializedはあるがBehaviourが無いEntity
    onDestroyQuery = GetEntityQuery(Exclude<Behaviour>(), ReadOnly<BehaviourInitialized>());
}

protected override void OnUpdate()
{
    Entities.With(onDestroyQuery).ForEach((Entity entity) =>
    {
        Debug.Log($"On Destroy Enemy {entity.Index}");
    });
    // クェリーからBehaviourInitializedを一括削除
    EntityManager.RemoveComponent<BehaviourInitialized>(onDestroyQuery);
}

その他

  • この例ではISystemStateComponentDataは値を持っていませんが、普通に持てます。データを使用する場合はIComponentDataと同じように取得します。
  • ISystemStateComponentDataのアプローチはOnDestroyと異なり、破棄された後に破棄された座標を確認といった事が出来ません。正確にはISystemStateComponentDataに座標を突っ込んでおけば取得出来ますが、小頻度の更新はあまり推奨されません。
    キャラクターの死亡演出の出力やスコアの計上は破棄するシステム側が責任を持ってやるべきです(もしくは破棄されたフラグを付けて、他のシステムに任せる)
  • SubSceneのコンバージョンワークフローでISystemStateComponentDataを追加すると、追加されないです。コレはバグかもしれませんが。