エフアンダーバー

個人開発の記録

【Unity】 EditorWindowのライフサイクルの謎

EditorWindowが他のGUIアプリケーションと比べてかなり特殊な挙動をするのでその辺について。

前回の記事前々回の記事は このあたりを調べている過程で学習したことです。

はじめに

EditorWindowクラスを継承してカスタムウィンドウをつくっていると、 思わぬタイミングで変数の値が変わったりなど、 通常のGUIアプリケーションではありえないような挙動をすることがあります。 そこで、EditorWindowクラスのイベント関数が呼び出されるタイミングについて調べてみました。

EditorWindowクラスのイベント関数にもいろいろありますが、 今回はライフサイクルに大きくかかわる次の三つについて調べます。

  • OnEnable
  • OnDisable
  • OnDestroy

調査用スクリプト

EditorWindowを継承したカスタムウィンドウクラスを定義し、デバッグ出力を見ます。

コードは次の通り。

using UnityEditor;
using UnityEngine;

public class WindowLifecycle : EditorWindow
{
    [MenuItem("WindowLifecycle/Open")]
    private static void Open()
    {
        Debug.Log("BeforeGetWindow");

        GetWindow<WindowLifecycle>();

        Debug.Log("AfterGetWindow");
    }

    private void OnEnable()
    {
        Debug.Log("OnEnable");
    }

    private void OnDisable()
    {
        Debug.Log("OnDisable");
    }

    private void OnDestroy()
    {
        Debug.Log("OnDestroy");
    }
}

ウィンドウを開いたとき

メニューからウィンドウを開いたとき(Openメソッドが呼び出されたとき)の出力は次の通り。

BeforeGetWindow
OnEnable
AfterGetWindow

当然、すでにウィンドウが存在していた場合にはOnEnableは呼び出されません。

BeforeGetWindow
AfterGetWindow

これは直感通り。

ウィンドウを閉じたとき

ウィンドウを閉じたときの出力は次の通り。

OnDisable
OnDestroy

これも直感通り。

ゲームの再生時

ウィンドウを開いた状態でゲームを再生したときの出力は次の通り。

OnDisable
OnEnable

何故かOnDisableで無効化されたあとにOnEnableで再度有効化されています。

原因は何か?

原因を調べるために次のスクリプトを追加してみます。

using System;
using UnityEditor.Callbacks;
using UnityEngine;

public class AssemblyEventMonitor
{
    [DidReloadScripts]
    private static void OnAssemblyLoad()
    {
        Debug.Log("OnAssemblyLoad");

        AppDomain.CurrentDomain.DomainUnload += (sender, e) => OnAssemblyUnload();
    }

    private static void OnAssemblyUnload()
    {
        Debug.Log("OnAssemblyUnload");
    }
}

簡単に説明すると、スクリプトを含むアセンブリがロード・アンロードされたときにログを出力するものです。 DidReloadScripts属性をつけておくと、Unityがアセンブリのリロードを行ったあとにそのメソッドを呼び出してくれます。

再度ログを出力すると、結果は次のようになります。

OnDisable
OnAssemblyUnload
OnEnable
OnAssemblyLoad

アセンブリをリロードするために一度EditorWindowを破棄し、 アセンブリのリロード後に再度EditorWindowを生成しているようです。

この際、データの保存・復元にはシリアライズが使用されます。 そのため、EditorWindowのフィールドにうまくシリアライズできないものが含まれると、 それに関する情報はゲームの再生の度に失われることになります。

もう少し詳しく

ゲーム再生時の動作についてもう少し詳細に調べるため、 次のスクリプトを編集中のシーン内の適当なゲームオブジェクトにアタッチします。

using UnityEngine;

public class ScriptEventMonitor : MonoBehaviour, ISerializationCallbackReceiver
{
    public void OnAfterDeserialize()
    {
        Debug.Log("OnAfterDeserialize");
    }

    public void OnBeforeSerialize()
    {
        Debug.Log("OnBeforeSerialize");
    }
}

さらに、次のスクリプトで事前に不要なオブジェクトを生成しておきます。

using UnityEditor;
using UnityEngine;

public static class SceneClearEventMonitor
{
    [MenuItem("WindowLifecycle/New Garbage")]
    private static void NewGarbage()
    {
        ScriptableObject.CreateInstance<Garbage>();
    }

    private class Garbage : ScriptableObject
    {
        private void OnDestroy()
        {
            Debug.Log("OnSceneClear");
        }
    }
}

すると、出力は次のようになります。

OnBeforeSerialize
OnBeforeSerialize
OnDisable
OnBeforeSerialize
OnAssemblyUnload
OnAfterDeserialize
OnEnable
OnAssemblyLoad
OnSceneClear
OnAfterDeserialize

スタックトレースなども参考にしながら、このときのUnityの処理について推測すると次のようになります ("OnAssemblyLoad"はUnityがロードを完了したとするタイミングで呼び出されており、 実際のアセンブリのロードはもっと早いことに注意)。

  1. 編集中のシーンをシリアライズ (A)
    1. 各オブジェクトにファイルIDを割り当て
    2. 実際にシリアライズして保存
  2. すべてのオブジェクトをシリアライズ (B)
    • 編集中のシーン内のオブジェクトも含む
  3. アセンブリをアンロード
  4. アセンブリをロード
  5. すべてのオブジェクトをデシリアライズ (B)
  6. シーンの状態をクリア
  7. 編集中のシーンをデシリアライズ (A)

シーンの状態のクリアについては 前回の記事を参照してください。

ゲームの終了時

ウィンドウを開いた状態でゲームを終了したときの出力は次の通り。

特に何も呼ばれません。

もう少し詳しく

ゲームの再生時と同様に、スクリプトを用いて詳細な出力をすると、結果は次のようになります。

OnSceneClear
OnAfterDeserialize

ここからUnityの処理について推測すると次のようになります。

  1. シーンの状態をクリア
  2. 編集中のシーンをデシリアライズ (A)

Unityエディタを開いたとき

レイアウトの保存によってUnityエディタを開いたときに自動的にウィンドウが開かれたときの出力は次の通り。

OnEnable

これはこのときの出力云々というよりは、このようなウィンドウの開かれ方があるということを知っておくことが重要です。 つまり、GetWindowで取得したウィンドウに対して、外部からいろいろ操作する場合には注意が必要です。

Unityエディタを閉じたとき

ウィンドウを開いたままUnityエディタを閉じたときの出力は次の通り。

OnDisable
OnDestroy

特に問題なし。

ドッキング状態でUnityエディタを開いたとき

UnityではEditorWindowをドックに配置することができます(タブみたいな状態)。

この状態のままUnityエディタを開いたときの出力は次の通り。

OnEnable

これは特に問題なし。

ドッキング状態でUnityエディタを閉じたとき

EditorWindowをドッキングしたままUnityエディタを閉じたときの出力は次の通り。

何故か、何も呼ばれません。 バグっぽいような気もしますが、真相は不明。

このため、ドッキングしたEditorWindowがUnityエディタごと閉じられた場合に明示的なリソースの解放ができなくなります。 Unityを通して確保したリソースはUnityが解放してくれますが、 そうでない場合にはファイナライザの実装などで対応する必要があります。

おまけ: EditorWindowのシリアライズの謎

EditorWindowでゲームの再生時におけるシリアライズの実験をしているときに、 何故かprivateな変数がシリアライズされてしまうので調べたところ、 Unity公式ブログの記事にたどり着きました。 かなり古い記事なので、いくらか今のシリアライズ方法と違うのですが、その中に問題の一文が。

Some Serialization Rules

  • Avoid structs
  • Classes you want to be serializable need to be marked with [Serializable]
  • Public fields are serialized (so long as they reference a [Serializable] class)
  • Private fields are serialized under some circumstances (editor).
  • Mark private fields as [SerializeField] if you wish them to be serialized.
  • [NonSerialized] exists for fields that you do not want to serialize.

どうもエディタ中の特定の状況下ではprivate変数もシリアライズされる模様 *1。 シリアライズしたくなければ、NonSerialized属性を付ける必要があります。

また、自分も同じ失敗をしたのですが、 「ゲームの再生時にScriptableObjectの継承クラスがうまくシリアライズされない」という内容の質問をどこかで見かけました。 これ、どうもScriptableObjectがシリアライズされていないのではなく、 デシリアライズされた直後にシーンのクリアで回収されているようです。 ですので、HideFlags.DontSaveInEditorを設定しておくと、ゲームの再生後も値が失われません。 同じような失敗をしている方がいたら試してみてください。

おわりに

とりあえず、今回で何度か続いたエディタ拡張関連の話はおしまい・・・かな?
記事を書きながらも、書いた内容について検証する過程で、間違いを正したり新たな発見をしたりしているので、 もしかすると記事間に不整合があったりするかもしれません。 新しい記事ほど正しいはずなので適当に脳内修正してください。

「おわりに」って基本的には記事のまとめや結論を書くための場所だったはずなのだけど、 最近すっかり感想を書く場所になってしまった。 毎回、まとめが必要なほどの内容を書いているわけでもないのですが、 この節がないとHTML的に変な節にUnityのバージョン情報が入ってしまうんですよね。 まあ、そんなこんなであまり意味のない節なので読むときはてきとーに流してください。



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

*1:Unity公式ブログにはシリアライズに関するより新しい記事がありますが、そっちには書いてなかったような・・・