Unityではイベント毎に各オブジェクトにメッセージが飛んで、そのメッセージを処理することでオブジェクトが動作するしくみになっている。なのでどういうイベントがどういう順番で起こるかを知っておくことはとても重要。
でもその辺のことはちゃんとマニュアルに書いてある。
ここで書いておきたいことは、スクリプトがビルドされた後にどんなイベントが起こるのかということ。というのも、スクリプトを編集した後にUnityに戻ると、シーンを実行中だったりExecuteInEditMode属性のついたスクリプトがあったりするとおかしなことになることが頻繁にあるからだ。
以下は調べた結果。
シーンを実行していないときにスクリプトがリビルトされた時(ExecuteInEditModeではない場合)
- Static Constructor
- Constructor
- OnValidate
シーンを実行中にスクリプトがリビルトされた時(ExecuteInEditModeではない場合)
- OnDisable
- Static Constructor
- Constructor
- OnValidate + OnEnable
- OnApplicationFocus
- FixedUpdate
- Update
- :
シーンを実行していないときにスクリプトがリビルトされた時(ExecuteInEditModeの場合)
- OnDisable
- Static Constructor
- Constructor
- OnValidate + OnEnable
- OnApplicationFocus
- Update
- :
参考までに、ExecuteInEditModeのスクリプトをオブジェクトに追加した時
- Static Constructor(はじめて追加した時のみ)
- Constructor
- Awake + OnEnable
- Start
- Update
- :
さて、以上が調べた結果なのだが、ここでのポイントは、スクリプトがビルドされた後は静的コンストラクタやコンストラクタがもう一度呼ばれるということ。それなのに、AwakeやStartは呼ばれない(まあ呼ばれても困るんだけどね)。なので、AwakeやStartで初期化をしている場合(普通はそうすると思うけど)、問題が起こる場合がある。
と言っても、大抵の場合は大丈夫。シリアライズの対象ではないメンバ変数なんかもきちんと復元されていたので、問題が起こるのは以下のような場合。
- 静的メンバ変数をAwakeやStartで初期化している場合
シングルトンの場合なんかはこれをやっていることが多いと思うけど、そうするとスクリプトがビルドされた後にインスタンスを参照してた静的メンバ変数がnullになっちゃう。
他には、僕の場合はシェーダーのプロパティIDを静的メンバ変数にしていて、AwakeのときにShader.PropertyToIDで静的メンバ変数に値を保存していたのだけど、これらが静的コンストラクタで全部0になっちゃうのでシェーダー定数のセットがちゃんとできなくなる。 - シリアライズ可能ではないメンバ変数を使っている場合
スクリプトがビルドされた後でも復元される変数というのはシリアライズ可能な変数に限られるようだ。実際にシリアライズの対象になっているかどうかは関係ない。シリアライズ可能でさえあればprivateなメンバ変数もちゃんと復元される。
例えば、Dictionary<T,T>なんかよく使うと思うのだけど、これはスクリプトがリビルドされると無惨にもクリアされてしまう。これがList<T>だと大丈夫。
自分で作ったObjectではないクラスなんかもSystem.Serializable属性をつけておかないと復元されない。
対策
Dictionaryとかがクリアされちゃうと辛いものがあるけど、とれる対策としてはOnValidateを使うこと。これはインスペクタなどでプロパティが編集されたときに呼ばれるもので、Editor上でしか呼ばれない。この関数で再初期化できるものはしてしまう。
他にOnEnableとOnDisableを使うという手もある。Awakeは呼ばれないけどこれらは呼ばれるので、再初期化をOnEnableでして、後始末をOnDisableでするようにすると、とても行儀の良いオブジェクトになると思う。
また、変数が参照されるときにnullだったら初期化するというようなコードにしておけば、こういう問題は起きなくなる。でも実行時にいちいちnullチェックをするのもなぁ、と思わなくもないので、そういう場合は #if UNITY_EDITOR
をつける。
また、シングルトンのポインタなんかはコンストラクタで初期化しても良い。