長ったらしいタイトルですが、その通り「エディタ拡張で、staticなフィールドに置いたインスタンスがコンパイル/再生時に初期化される問題の対策について」です。
初期化されるパラメータ
Unityのエディタ拡張は、ホットリロードの要領でソースコードを書き換えると即座にコンパイル・修正したコードを反映させます。
正確には、「シリアライズ」→「コンパイル」→「シリアライズしたデータ戻し」としているみたいです。
これよく見ると、シリアライズしたデータを一時退避し書き出したデータを再設定する事で、コンパイル前に設定したパラメータを保持しています。
逆を言えば、シリアライズ出来なかったパラメータが破棄されます。
例:シリアライズしないデータがある場合
例えば下のようなコードを用意します。
ウィンドウを表示後、Pushボタンを押すと#が増えるという簡単な物ですが、ソースコードのコンパイルやゲームの再生を行うとパラメータが破棄されてしまいます。
解決方法
この問題は、要するに「シリアライズ出来ないパラメータがある」事が問題となります。なので、シリアライズ出来る項目で固めておけば、良いのです。
シリアライズ出来る項目
シリアライズ出来る項目は、以下の通り。
static
ではないことconst
ではないことreadonly
ではないこと[Serializable]
の属性を持つクラスまたは構造体- UnityEngine.Objectから派生したクラス
(MonobehaviourやScriptableObject) - プリミティブな型(int, float, double, bool, string, 等々)
- シリアライズ出来る型の配列もしくはList
この項目から外れている物は、シリアライズ出来ないのでホットリロード時に破棄されます。
上のサンプルコードでは、stringはシリアライズ出来ますがstaticに格納していたためシリアライズ出来ず、リロード時に値が破棄されていた訳です。
つまり、破棄しないように、もしくは破棄しても流し込むようにすれば、値を保持し続けてくれる訳です。
解決法1:シリアライズ出来る場所に移動
手っ取り早い解決方法は、シリアライズ出来る場所にフィールドを持っていく事です。上のコードの場合、staticではなくフィールドに配置すればシリアライズ出来ます。
解決法2:ScriptableSingletonを使用する
どうしてもstatic値が欲しい場合があります。例えば一つしかウィンドウを出したくないといった場合、staticに自分のインスタンスを登録して有無をチェックするコードが使用されますが、staticがデシリアライズ時に破棄されるため十分ではありません。
そんなときはScriptableSingletonを使用します。
ScriptableSingletonはシングルトンのような動作をしつつ、シリアライズ可能な値をシリアライズして保持してくれるので、staticのように全体で共有して扱う値も保持してくれます。
エディタの情報やアセット、GameObjectのインスタンスを保持する場合、これが何かと便利です。
但し、シングルトンのような挙動をおこなうせいで、初期化コードを書いた方が良いケースが多々あります。
(これのランタイム版があれば色々と楽なのですが…)
解決法3:シリアライズするタイミングで、IOに書き出してしまう
微妙な方法ではありますが、シリアライズするタイミングのコールバックを取得して、IOに書き込んでしまうというアプローチです。
ISerializationCallbackReceiverを実装している場合、シリアライズ前とデシリアライズ後にコールバックが呼ばれるので、何らかの処理を挟む事が出来ます。
この方法なら、ディクショナリだろうが何だろうが、自分でシリアライズしてデシリアライズ出来るなら、値を保持出来ます。
なお、このコールバック中にUnityAPIは呼べません。つまりEditorPrefsは使えません。まぁ、UnityのAPIが必要な物は大体シリアライズ出来るので大した問題では。
関連
monkey coders' - 実行中にスクリプトを編集したときのNullReferenceExceptionを回避する