サイトロゴ
ほみのキャラ画像
ゲーム開発
当サイトは広告を含んでいます。おすすめの商品や、おいしかったものを紹介しています。
別途タイトルなどで【PR】のように明記されていない場合、記事の本題は広告とは関係ありません。
詳細は「プライバーポリシー#広告について」をご覧ください。

【Unity ECS】CleanupComponent を使ってEntityが破棄された時にクリーンアップ!

記事のヘッダー画像/>
                            </div>
                            
                        </section>
                        <section class=

目次

はじめに

Unity の ECS で Entity が破棄された際にクリーンアップを行いたいと思ったことはありますか?

ICleanupComponentData を使うと実現することができます!

  • Entity が破棄された際に、その Entity からICleanupComponentData以外のコンポーネントが削除される
  • Entity が破棄されているかどうかを判別するために、ICleanupComponentData以外の通常のタグコンポーネントなどを追加しておく
  • 通常通りにクリーンアップコンポーネントを利用する System では、タグコンポーネントがあることを条件に加える
  • クリーンアップシステムでは、タグコンポーネントがないことを条件に加える
  • クリーンアップシステムでは、必要であればOnDestroyでもクリーンアップを行う

実行環境

  • Unity6000.0.23f1
  • Unity.Entities 1.3.5

Unity や ECS などの説明はありません。
必要であれば、公式のマニュアルやその他記事などをご覧ください。
※ECS の記事やマニュアルはバージョンが古い情報も多いので、ご注意ください

流れ

  1. CleanupSampleAuthoringでクリーンアップコンポーネントを追加するための印を追加
  2. AddCleanupComponentSampleSystemでクリーンアップコンポーネントを追加
  3. SampleDestroySystemで通常通りにコンポーネントを利用や破棄する
  4. 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
コンポーネントを追加し終えて処理中の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 が破棄されクリーンアップコンポーネントだけが残っている状態
Entityが破棄されクリーンアップコンポーネントだけが残っている状態

参考


運営者

ほみのアイコン

ほみ

プログラミングとか
おえかきとか
いろいろするのがすき

サイトについての詳細

【PR】鮭ジャーキー

鮭とばとはまた違うおいしさで、柔らかくチーズの味が合っていておいしかったです!