読者です 読者をやめる 読者になる 読者になる

エフアンダーバー

個人でのゲーム開発

Unityのリソース管理

Unity

前回の記事や次回の記事を書くにあたってUnityのリソース管理についていろいろ調べたのでメモ。

前回の記事を書いたときも思ったのだけど、「リソース」ってどの範囲を指す言葉だろう。 「オブジェクト」とか「メモリ」とか他の言葉を使うことも考えたのですが、どれが正解かわからず、とりあえず「リソース」を使ってます。 まあ表現はともかく今回は、Unity内部でのUnityEngine.Objectの管理の話です。 表現がおかしかったら適宜読み替えてください。

はじめに

Unityのエディタ拡張を書いていると、変数の値が突然nullになるなど、おかしなバグに悩まされることがあります。 HideFlagsなどこれらの動作を制御できるものが存在することは以前から知っていましたが、 スクリプトリファレンスを読んでもいまいち理解できなかったので、 実際にいろいろと設定しながら調べてみました。

あらかじめ断っておくと、 この記事にはUnityのプロファイラなどを使って実際に確認した動作から推測した内容が含まれています。 以降の情報が正確とは限らないことに留意してお読みください。

Unityのリソース管理区分

Unity標準のプロファイラを開き、メモリの項目でスナップショットをとると次の五つの項目に分類されて表示されます (説明はマニュアルより)。

分類 説明
Scene Memory GameObject and attached components
Not Saved GameObjects marked as DontSave
Assets Asset referenced from user or native code
Builtin Resources Unity Editor resources or Unity default resources
Other GameObjects not marked in the above categories

正直、マニュアルの説明があまり正確とは思えないのですが、大体の雰囲気はわかると思います。 おそらくUnityはこのような区分のもとリソースを管理していると思われます。

"Builtin Resources"と"Other"については基本的にUnityエディタやUnityエンジンの利用しているメモリのようですので、 今回はこの部分には触れません。

というわけで、残りの三つについてリソースがどのように区分されるのかを見ていきます。

リソースの生成

まずはScriptableObjectを継承したクラスFooを生成してどこに記録されるかを見てみます。

var foo = CreateInstance<Foo>();

"Scene Memory" > "MonoBehaviour" とみていくと"Foo"の項目が追加されているのがわかります。 つまり、普通にインスタンスを生成した場合にはすべて"Scene Memory"に記録されるようです。

今回、EditorWindow継承クラスのOnGUIからScriptableObject継承クラスを生成したにも関わらず、 "Scene Memory"に記録されたということはあまり"Scene"という言葉に惑わされないほうがよさそうです。 もちろんシーンのヒエラルキーの情報も記録されていますが、それ以外もまずはここに記録されます (むしろ シーン=ヒエラルキー という発想を捨てた方がいいのかもしれません、LightmapSettingsなんかもシーンの一部なので)。

DontUnloadUnusedAssetの設定

次にFooに対してHideFlags.DontUnloadUnusedAssetを設定してみます。

var foo = CreateInstance<Foo>();

foo.hideFlags |= HideFlags.DontUnloadUnusedAsset;

今度は"Not Saved" > "MonoBehaviour" に"Foo"の項目が追加されているのがわかります。 つまり、HideFlags.DontUnloadUnusedAssetを設定したリソースは"Not Saved"に記録されます。 ちなみに、HideFlags.DontUnloadUnusedAssetをクリアすると"Scene Memory"に戻ります。

余談ですが、EditorWindowなんかはHideFlags.DontSaveの設定されたScriptableObjectの継承クラスなので、 カスタムエディタウィンドウを作成すると"Not Saved"に値が記録されます *1

AssetDatabaseに登録

次はFooをアセットとして保存してみます。 アセットとして保存するためにはAssetDatabaseクラスのメソッドを使用します。

var foo = CreateInstance<Foo>();

AssetDatabase.CreateAsset(foo, "Assets/foo.asset");
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();

今度は"Assets" > "MonoBehaviour" に"Foo"の項目が追加されます。 つまり、アセットとして保存したリソースは"Assets"に記録されます。

アセットを削除すると"Assets"から削除され、リソースは解放されます。 "Not Saved"とは異なり、"Scene Memory"には戻りません。

AssetDatabase.DeleteAsset("Assets/foo.asset");
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();

両方やってみる

最後にHideFlags.DontUnloadUnusedAssetの設定とAssetDatabaseへの登録をどちらもやってみます。

var foo = CreateInstance<Foo>();

foo.hideFlags |= HideFlags.DontUnloadUnusedAsset;

AssetDatabase.CreateAsset(foo, "Assets/foo.asset");
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();

結果は"Not Saved"でした。 ただし、HideFlags.DontUnloadUnusedAssetをクリアすると"Assets"に移動します。

まとめ

  1. HideFlags.DontUnloadUnusedAssetが設定されているなら"Not Saved"
  2. アセットとして登録されているなら"Assets"
  3. どちらでもないなら"Scene Memory"

リソースの回収

Unityエディタではいくつかのタイミングでリソースが自動回収されます。 これには大きく次の二種類があるようです。

  • UnloadUnusedAssets による不要なリソースの解放
  • ゲームの開始時および終了時のシーンのクリア

UnloadUnusedAssets による不要なリソースの解放

UnloadUnusedAssetsはGCのようにどこからも参照されていない不要なリソースを探して解放します。

その方法はルートとなるオブジェクト群から順に参照を辿って到達できるリソースをマークしていき、 すべてを終わったときにマークされていなかったリソースを不要と判断して解放するという方法です。 ここで、ルートとなるオブジェクト群とは何かというのが問題となってきます。

スクリプトリファレンスによると、 ヒエラルキー全体と静的変数と書いてあります。 しかし、これはゲーム内における場合です。 エディタ拡張も含めた場合には表示中のエディタもルートとなるようです。

また、当然ですが"Not Saved"に属するリソースはUnloadUnusedAssetsでは解放されません。

ちなみにUnloadUnusedAssetsには次の三種類があります。

  • Resources.UnloadUnusedAssets()
  • EditorUtility.UnloadUnusedAssetsImmediate(true)
  • EditorUtility.UnloadUnusedAssetsImmediate(false)

ひとつめはゲーム用で、ゲーム内でのみ動作します。 あとの二つはエディタ用で引数がtrueだとすべての参照を、引数がfalseだとシリアライズ対象の参照のみを辿るようです。

ゲームの開始時および終了時のシーンのクリア

Unityはゲームの開始時と終了時にシーンをまっさらな状態にするため、 DestroyWorldObjectsという何やらやばそうな関数を呼んで、 アセットでもエディタ要素でもないリソースを解放しようとします。

ここで問題なのはエディタ要素かどうかをどう判定するかです。 残念ながら、HideFlags.DontUnloadUnusedAssetはエディタとゲームを分けるようなフラグとはなっていません。 そこで、Unityは代わりにHideFlags.DontSaveInEditorを使用するようです。

HideFlags.DontSaveInEditorは対象オブジェクトをエディタ中のシーンに保存しないという意味なので、 これを設定していないからといって解放するのもどうかと思うのですが、 エディタ要素なら設定してしかるべきということなのかもしれません。

ちなみにHideFlags.DontSaveInEditorは単独で設定しても、"Not Saved"ではなく"Scene Memory"に記録されます。 なんか名前おかしくない・・・?

まとめ

  • HideFlags.DontUnloadUnusedAssetHideFlags.DontSaveInEditorを設定しておけば勝手に解放はされない。
  • 特に問題なければ、HideFlags.DontSaveHideFlags.HideAndDontSave

検証用ソースコード

using UnityEngine;

public class Foo : ScriptableObject { }
using UnityEngine;
using UnityEditor;

public class ResourceManagement : EditorWindow
{
    [SerializeField]
    private Foo foo;

    private Foo _foo;

    [MenuItem("ResourceManagement/Open")]
    private static void Open()
    {
        GetWindow<ResourceManagement>();
    }

    private void OnGUI()
    {
        if (GUILayout.Button("Print"))
        {
            Debug.LogFormat("foo: {0}({1}), _foo: {2}({3})",
                foo ? foo.GetInstanceID().ToString() : "null", foo ? foo.hideFlags.ToString() : "null",
                _foo ? _foo.GetInstanceID().ToString() : "null", _foo ? _foo.hideFlags.ToString() : "null");
        }

        if (GUILayout.Button("New"))
        {
            foo = CreateInstance<Foo>(); foo.name = "foo";
            _foo = CreateInstance<Foo>(); _foo.name = "_foo";
        }

        if (GUILayout.Button("Delete"))
        {
            DestroyImmediate(foo, true);
            DestroyImmediate(_foo, true);
        }

        if (GUILayout.Button("Set Null"))
        {
            foo = null;
            _foo = null;
        }

        if (GUILayout.Button("Clear HideFlags"))
        {
            foo.hideFlags = 0;
            _foo.hideFlags = 0;
        }

        if (GUILayout.Button("Set DontSaveInEditor"))
        {
            foo.hideFlags |= HideFlags.DontSaveInEditor;
            _foo.hideFlags |= HideFlags.DontSaveInEditor;
        }

        if (GUILayout.Button("Set DontUnloadUnusedAsset"))
        {
            foo.hideFlags |= HideFlags.DontUnloadUnusedAsset;
            _foo.hideFlags |= HideFlags.DontUnloadUnusedAsset;
        }

        if (GUILayout.Button("Save As Asset"))
        {
            AssetDatabase.CreateAsset(foo, "Assets/foo.asset");
            AssetDatabase.CreateAsset(_foo, "Assets/_foo.asset");
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
        }

        if (GUILayout.Button("Delete Asset"))
        {
            AssetDatabase.DeleteAsset("Assets/foo.asset");
            AssetDatabase.DeleteAsset("Assets/_foo.asset");
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
        }

        if (GUILayout.Button("UnloadUnusedAssets"))
        {
            Resources.UnloadUnusedAssets();
        }

        if (GUILayout.Button("UnloadUnusedAssetsImmediate(false)"))
        {
            EditorUtility.UnloadUnusedAssetsImmediate(false);
        }

        if (GUILayout.Button("UnloadUnusedAssetsImmediate(true)"))
        {
            EditorUtility.UnloadUnusedAssetsImmediate(true);
        }
    }
}

おわりに

Unityのエディタ拡張がうまく書けなくてつらい今日この頃。 おかげで、Unityのデバッグ技術だけは無駄に向上してます (プロファイラ眺めたり、エディタログ読んだり、フルのコールスタック表示したり、デバッガでメモリ書き換えたり・・・)。

最近の記事は内容的にちょっと硬いので、なんとなくキャッチーなタイトルつけて興味を引いてみたりしていたのですが、 ブクマとかつかないあたりみるとやっぱり需要ないんですかね・・・



執筆時のUnityのバージョン:5.4.0f3

*1:DontSave は DontSaveInBuild と DontSaveInEditor と DontUnloadUnusedAsset の複合フラグ