はじめに
Unity の ECS で Entity が破棄された際にクリーンアップを行いたいと思ったことはありますか?
ICleanupComponentData
を使うと実現することができます!
- Entity が破棄された際に、その Entity から
ICleanupComponentData
以外のコンポーネントが削除される - Entity が破棄されているかどうかを判別するために、
ICleanupComponentData
以外の通常のタグコンポーネントなどを追加しておく - 通常通りにクリーンアップコンポーネントを利用する System では、タグコンポーネントがあることを条件に加える
- クリーンアップシステムでは、タグコンポーネントがないことを条件に加える
- クリーンアップシステムでは、必要であれば
OnDestroy
でもクリーンアップを行う
実行環境
- Unity6000.0.23f1
- Unity.Entities 1.3.5
Unity や ECS などの説明はありません。
必要であれば、公式のマニュアルやその他記事などをご覧ください。
※ECS の記事やマニュアルはバージョンが古い情報も多いので、ご注意ください
流れ
CleanupSampleAuthoring
でクリーンアップコンポーネントを追加するための印を追加AddCleanupComponentSampleSystem
でクリーンアップコンポーネントを追加SampleDestroySystem
で通常通りにコンポーネントを利用や破棄するCleanupSampleSystem
で Entity をクリーンアップする
コンポーネントデータ追加
StillNoCleanupComponent
クリーンアップコンポーネントを持つ Entity が「まだクリーンアップする必要がない(Entity が破棄されていない)」かどうかを判別するために使います。
StillNoCleanupComponent
が追加されている場合は、Entity が破棄されていないと判断できます。
StillNoCleanupComponent
が追加されていない場合は、Entity が破棄されていると判断できます。
using Unity.Entities;
/// <summary>
/// クリーンアップが必要な状態かどうかを判別するためのコンポーネント
/// </summary>
/// <remarks>
/// この通常のコンポーネントを一緒に追加しておき、これがなくなっていたらEntityが破棄されたと判断する
/// </remarks>
public struct StillNoCleanupComponent : IComponentData
{
}
AddCleanupSampleComponent
このコンポーネントを使ってクリーンアップコンポーネントを Runtime で追加します。
Baker ではクリーンアップコンポーネントを追加することができないため、Runtime で追加するための印として使用します。
クリーンアップコンポーネント追加後は不要なので Remove します。
Make sure to add it to entities at runtime, because cleanup components cannot be baked.
(DeepL 翻訳:クリーンアップ・コンポーネントはベイクできないので、必ず実行時にエンティティに追加すること。)
using Unity.Entities;
/// <summary>
/// <see cref="CleanupSampleComponent"/> をRuntimeで付与するためのコンポーネント
/// </summary>
/// <remarks>
/// <see cref="ICleanupComponentData"/>を実装しているコンポーネントはBakerで付与できないのでこのコンポーネントを印として使う
/// </remarks>
public struct AddCleanupSampleComponent : IComponentData
{
}
CleanupSampleComponent
今回の目玉のクリーンアップコンポーネントです。
破棄していない間は通常通りに値を利用したり、変更したりします。
クリーンアップする際に必要なデータを持っておきます。
using Unity.Entities;
using UnityEngine;
/// <summary>
/// クリーンアップコンポーネント
/// </summary>
public struct CleanupSampleComponent : ICleanupComponentData
{
/// <summary>
/// クリーンアップに使用するデータや、その他必要なデータ
/// </summary>
[field: SerializeField]
public int SampleValue { get; set; }
}
クリーンアップコンポーネントを追加するための印を Baker で追加
CleanupSampleAuthoring
コンポーネントでも書きましたが、Baker ではクリーンアップコンポーネントを追加できないため、追加するためのマーク用コンポーネントだけ追加しておきます。
Make sure to add it to entities at runtime, because cleanup components cannot be baked.
(DeepL 翻訳:クリーンアップ・コンポーネントはベイクできないので、必ず実行時にエンティティに追加すること。)
using Unity.Entities;
using UnityEngine;
public class CleanupSampleAuthoring : MonoBehaviour
{
private class Baker : Baker<CleanupSampleAuthoring>
{
public override void Bake(CleanupSampleAuthoring authoring)
{
var entity = this.GetEntity(TransformUsageFlags.None);
// クリーンアップコンポーネントはBakerで追加できないので、
// 「Runtimeでクリーンアップコンポーネントを追加するための印のコンポーネント」を追加
this.AddComponent<AddCleanupSampleComponent>(entity);
}
}
}
クリーンアップコンポーネントを追加するシステム
AddCleanupComponentSampleSystem
ここでクリーンアップコンポーネントを追加します。
通常通りにコンポーネントデータを利用する場合はここで値を追加することもできます。
クリーンアップコンポーネントを追加するためのマーク用コンポーネントは、もう不要なので Remove します。
using Unity.Burst;
using Unity.Entities;
/// <summary>
/// クリーンアップコンポーネントを付与するシステム
/// </summary>
[RequireMatchingQueriesForUpdate]
partial struct AddCleanupComponentSampleSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var commandBufferSystem = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = commandBufferSystem.CreateCommandBuffer(state.WorldUnmanaged);
foreach (var (_, entity) in SystemAPI.Query<AddCleanupSampleComponent>().WithEntityAccess())
{
// クリーンアップコンポーネントの追加: お試しで数字を入れておく
ecb.AddComponent(entity, new CleanupSampleComponent() { SampleValue = 12345 });
// 一緒に通常のコンポーネントを追加しておく
// 通常のコンポーネントなので、Entity破棄時に消えるので、それを利用して破棄されているかを判別する
ecb.AddComponent<StillNoCleanupComponent>(entity);
// クリーンアップコンポーネントを追加するための印だっただけなのでRemove
ecb.RemoveComponent<AddCleanupSampleComponent>(entity);
}
}
}
コンポーネントを利用や破棄するシステム
SampleDestroySystem
この時点では、Entity は以下のようになっています。
▼ コンポーネントを追加し終えて処理中の Entity の Inspector
StillNoCleanupComponent
があることを条件に加えることで、
クリーンアップコンポーネントを通常のコンポーネントとして扱うことができます。
using Unity.Entities;
using UnityEngine;
/// <summary>
/// お試しでEntityを破棄するシステム
/// </summary>
[RequireMatchingQueriesForUpdate]
partial struct SampleDestroySystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
var commandBufferSystem = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = commandBufferSystem.CreateCommandBuffer(state.WorldUnmanaged);
// StillNoCleanupComponent もまだ一緒に追加されていれば、破棄された状態ではないと判断する
foreach (var (cleanup, _, entity) in SystemAPI.Query<RefRW<CleanupSampleComponent>, RefRO<StillNoCleanupComponent>>().WithEntityAccess())
{
// お試しの値が0以下になったらEntityを破棄
if (cleanup.ValueRO.SampleValue <= 0)
{
ecb.DestroyEntity(entity);
Debug.unityLogger.Log($"destroy {entity}");
}
else
{
// デクリメントしておく
--cleanup.ValueRW.SampleValue;
Debug.unityLogger.Log($"decrement {entity}, {cleanup.ValueRO.SampleValue}");
}
}
}
}
破棄されたクリーンアップコンポーネントをクリーンアップするシステム
CleanupSampleSystem
クリーンアップを行うシステムです。
StillNoCleanupComponent
が存在しないことを条件に加えることで、
クリーンアップが必要な状態(Entity が破棄されている状態)であることを確認します。
using Unity.Entities;
using UnityEngine;
/// <summary>
/// クリーンアップシステム
/// </summary>
[RequireMatchingQueriesForUpdate]
partial struct CleanupSampleSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
// Runtimeで必要なクリーンアップ
var commandBufferSystem = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = commandBufferSystem.CreateCommandBuffer(state.WorldUnmanaged);
// CleanupSampleComponentがあって、StillNoCleanupComponentがないものは破棄されたEntityと判断し、クリーンアップする
foreach (var (cleanup, entity) in SystemAPI.Query<RefRO<CleanupSampleComponent>>().WithAbsent<StillNoCleanupComponent>().WithEntityAccess())
{
this.Cleanup(ref state, ref ecb, cleanup, entity);
}
}
public void OnDestroy(ref SystemState state)
{
// システム終了時にも必要であればクリーンアップ
var commandBufferSystem = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = commandBufferSystem.CreateCommandBuffer(state.WorldUnmanaged);
// 必要に応じて、システム破棄時はクリーンアップコンポーネントを「Entityが破棄されているか」に関係なくクリーンアップ
foreach (var (cleanup, entity) in SystemAPI.Query<RefRO<CleanupSampleComponent>>().WithEntityAccess())
{
this.Cleanup(ref state, ref ecb, cleanup, entity);
}
}
private void Cleanup(ref SystemState state, ref EntityCommandBuffer ecb, RefRO<CleanupSampleComponent> cleanup, Entity entity)
{
// クリーンアップを行う:今回はお試しでログを出すだけ
Debug.unityLogger.Log($"cleanup on destroy {cleanup.ValueRO.SampleValue}");
// クリーンアップコンポーネントをRemoveすることで、Entityが破棄される
ecb.RemoveComponent<CleanupSampleComponent>(entity);
}
}
つぶやき
CleanupEntity
というタグがついているのでこれを使えるとStillNoCleanupComponent
がいらないのになあ、と思いました。
internal なので残念ながら使用できません。
▼ Entity が破棄されクリーンアップコンポーネントだけが残っている状態