【Unity】GameObjectがDestroyされたら一緒にオブジェクトを削除する

ある GameObject が削除されたときに関連付けた Object が一緒に削除される仕組みを紹介したいと思います。

特に、Unity の Material はプロパティに一度でも触ると勝手にコピーされて開放は自分でやってね、という動作のため開放処理を OnDestory にいちいち書くのが面倒です。なので何もせずとも同時に破棄されるようにしたいと思います。

確認環境

  • Unity 2022.3.5f1
  • Windows11 + VisualStudio 2022

エディター上のみで動作確認

問題の確認

問題の確認です。以下のコメントのようにマテリアルのプロパティに触った瞬間マテリアルが複製されて自分で開放しないとリソースリーク(メモリリーク)が発生します。

public class MaterialLeak : MonoBehaviour
{
    [SerializeField] Color _color;

    private void Awake()
    {
        var renderer = GetComponent<Renderer>();
        // ここでmaterialは手動で開放しないといけないコピーが作成される
        renderer.material.color = _color; 
    }

    // 本来こうやって書かないといけない
    Material _mat;
    private void Awake()
    {
        var renderer = GetComponent<Renderer>();
        _mat = renderer.material;
        _mat.color = _color;
    }

    private void OnDestroy()
    {
        Destroy(_mat);
    }
}

使い方

先に使い方の紹介です。

以下のようにGameObject に対して BindTo(this) と記述すれば Destroy したときに一生に削除できるようになります。

private void Awake()
{
    var renderer = GetComponent<Renderer>();

    // こうやって自分の所属するゲームオブジェクトに関連付ける
    var mat = renderer.material;
    mat.BindTo(this);
    mat.color = _color;

    // もう参照しないなら1行で読み捨てもOK
    renderer.material.BindTo(this).color = _color;
}

何回も BindTo で登録すると開放処理も何度も呼ばれます。多重呼び出しは無害ですが 1度だけにしたほうがゲームに優しいです。

実装方法

実装方法とか言っていますが、自分も以下のようなテクニックを知ったときは結構驚いた覚えがあります。

まず、OnDestory されたことをイベント通知するためのコンポーネントを準備して、BindTo したときにイベントを登録しておきます。コンポーネント側で OnDestory されたときに登録された削除用のイベントを呼び出します。

UnityEngineObjectExtensions.cs

using System;
using UnityEngine;

public static class UnityEngineObjectExtensions
{
    public static T BindTo<T>(this T self, Component component) 
        where T : UnityEngine.Object
    {
        return BindTo(self, component.gameObject);
    }

    public static T BindTo<T>(this T self, GameObject gameObject) 
        where T : UnityEngine.Object
    {
        if (gameObject.Equals(null))
        {
            ObjectUtil.SafeDestroyWithEditor(self);
            throw new ArgumentNullException(nameof(gameObject));
        }

        if (!gameObject.TryGetComponent(out DestroyedEventNotifer eventDispatcher))
        {
            eventDispatcher = gameObject.AddComponent<DestroyedEventNotifer>();
        }

        return BindTo(self, (IEventDispatcher)eventDispatcher);
    }

    internal static T BindTo<T>(this T self, IEventDispatcher eventDispatcher) 
        where T : UnityEngine.Object
    {
        if (eventDispatcher == null)
        {
            UnityEngine.Object.Destroy(self);
            throw new ArgumentNullException(nameof(eventDispatcher));
        }

        void OnDispatch()
        {
            UnityEngine.Object.Destroy(self);
            eventDispatcher.OnDispatch -= OnDispatch;
        }

        eventDispatcher.OnDispatch += OnDispatch;
        return self;
    }
}

// DestroyedEventNotifer.cs
public class DestroyedEventNotifer : MonoBehaviour, IEventDispatcher
{
    public event Action OnDispatch;
    private void OnDestroy()
    {
        OnDispatch?.Invoke();
        OnDispatch = null;
    }
}

UniRx を使うと多重登録禁止などが簡単にできるようになると思いますが、汎用性重視のため外部ライブラリを使用しない実装にました。