今回は微妙に詳細な説明のない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); }
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に追加すれば良いです。
上の状態の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的な)
初期化が完了すれば、下のようにBehaviour
とBehaviourInitialized
が揃った状態になります。もしアップデート処理を行う場合、この2つが揃っていることを確認すれば安全に初期化済みのEntityを操作することが出来ます。
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を破棄すれば、破棄処理を完了出来ます。
こちらもクリーンアップする前に他の処理を挟めば、クリーンナップ処理を他のシステム側で行うことが出来ます。
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を追加すると、追加されないです。コレはバグかもしれませんが。