😇

Unityで安全にGameObjectをDestroyする

2024/08/28に公開

忙しい人のための結論コード

以下のコードはpublic domainとします。
自由にコピペ・改変可能です。

using UnityEngine;

public static class GameObjectExtensions
{
    /// <summary>
    /// GameObjectを例外を出さずに安全に破棄する
    /// unity fake nullに対しても安全に破棄する
    /// EditoModeテストやprefabモード、エディタ拡張での編集中でも実行してよい
    /// </summary>
    public static void SafeDestroy(this GameObject go)
    {
        if (!go)
        {
            return;
        }

#if UNITY_EDITOR
        if (!Application.isPlaying)
        {
            Object.DestroyImmediate(go);
            return;
        }
#endif
        Object.Destroy(go);
    }

    /// <summary>
    /// nullクリア付きSafeDestroy
    /// </summary>
    public static void SafeDestroy(ref GameObject go)
    {
        go.SafeDestroy();
        go = null;
    }
}

背景

悪名高き Unity の fake null問題があるため、 GameObjectを破棄するときは気を付ける必要があります。各MonoBehaviourが OnDisable や finally句の中で this.gameObjectを破棄しようとするとき、すでに別のMonoBehaviourから破棄済みである可能性があります。
また、シーンアンロード時やApplicationQuit時はすべて不定順でDestroyされるため、破棄可能なのか気を付けないと破棄シーケンス・終了シーケンスで例外が発生します。

最悪なのは try-catch-finally句の中で 例外を出すことです。
例外が発生したあとにfinally句の中で例外を出すと前の例外は隠蔽されてその例外がthrowされます。
真の例外が隠蔽されてしまうため、原因解明がとんでもなく難しくなります。

他にも、Unity Test Framework を用いて EditModeテスト, PlayModeテスト内でGameObjectやコンポーネントを作成するとTearDownで破棄したくなります。このときテスト用一時オブジェクトを破棄するときに文脈に応じて Destroyと DestroyImmediateを使い分ける必要があり大変面倒です。
また、製品コードに対してテスト実行したときに テスト環境においてDestroyが正しく実行されないという問題を孕みます。そのためすべての明示的なDestroyは SafeDestroyに置き換える必要があります。そうでもしないとテスト実行が困難になります。

解説

UnityEngine.Object には operator== が実装されていますが、同時に
operator bool も実装されています。null 比較することと (bool)に暗黙的・明示的にキャストすることはほぼ同じ処理ですが、中身を見てみると微妙に呼び出す関数が異なるようです。
operator bool の方がほんの少し?処理が軽そうなのでそちらを使用しています。

Managed Leaked Shell対策

DestroyしたGameObject なメンバフィールドには明示的にnull代入したいところです。
参照型のため ref this 拡張メソッドが使用できませんでした。
そのため、泣く泣く SafeDestroy(ref this._gameObject); と呼び出すことで _gameObjectフィールドへnull代入できるようにしています。

ちなみにMonoBehaviourが直接 SafeDestroy(ref this.gameObject) することはできません。 this.gameObjectは get only propertyなので一時オブジェクトを返します。一時オブジェクトに対するsetは行えませんし意味がありません。

余談

SafeDestroy という名前は 古き良きC++で使用されていた SAFE_DELETE マクロになぞらえています。古きC++ 言語では nullptr に対する delete は未定義の動作となっていました。
現在のC++ ではnullptrに対して delete しても何もしないというように仕様変更がなされ安全になりました。そもそもスマートポインタ使え、生ポはやめとけ、という先人からの助言もあり、SAFE_DELETEは姿を消しましたとさ。

// nullptrもなかった古のC++
#define SAFE_DELETE(x) if(x){ delete x; x = NULL;}
#define SAFE_DELETE_ARRAY(x) if(x){ delete [] x; x = NULL;}

int main(void)
{
    int* p = new int();
    SAFE_DELETE(p);

    int* pp = new int[5];
    SAFE_DELETE_ARRAY(pp);

    return 0;
}

Discussion