PONOS Advent Calendar 2020の20日目の記事です。
前日記事:@MilayYadokari [読みやすいソースコードの作り方]
翌日記事:@FW14B [【Unity】32bitアーキテクチャを切り捨てれば開発効率が上がる]
便利なイベント関数
UnityのMonoBehaviourに用意されているイベント関数。
記述しておけば特定のタイミングに自動で呼び出され、非常に便利です。
ところが、何も考えずに利用を続けると大きな落とし穴に成長してしまいます。
そこで今回は初期化に関する関数 Awake()と Start()について、一記事書いていきます。
記事投稿時点での使用 Unity 2019.4.8f1
Unityの気分次第
全てのオブジェクトをAwake()やStart()で初期化していると、
ディレクターやマネージャー等の管理者的な上位クラスよりも、大量に配置されている敵キャラ等の末端クラスの方が先に初期化される場合があります。結構順番がランダムです。
もし末端クラスで上位のクラスを参照していれば、ゲームを再生する度に nullが出たり出なかったりといった事態に陥ります。
それぞれのイベント順序は明確に決まっていますが、同じイベント関数はどのオブジェクトから呼び出されるのか保証されていません。
イベント順序は公式マニュアルで↓
https://docs.unity3d.com/ja/2019.4/Manual/ExecutionOrder.html
また、自動呼び出しなので任意のタイミングで初期化ができません。
実行順の指定はできるが…
Edit → ProjectSettings → Script Execution Order からスクリプト毎に実行順を
指定できます。
ですがこの方法では新規スクリプトを作成する度に設定する必要があり、数が増えてくるとこのウィンドウでは扱いきれなくなるのでオススメできません。
プロジェクト全体での順序より、とあるシーンのとあるオブジェクト群での順序を明確にできればそれで良いといった場合の方が多いと思います。
手動で行えばいい
対応策はごく単純です。
Unityに自動で呼び出させていたところを手動で呼び出すようにすれば良いだけです。
public class SceneInitializer : MonoBehaviour
{
[SerializeField] PlayerChara playerChara = default;
void Awake() // Start()でも良い
{
// オブジェクトを順に初期化(変数は一例)
masterData.Initialize();
playerChara.Initialize();
enemyManager.Initialize();
// 完了後は不要なので自壊
Destory(this);
}
}
public class PlayerChara : MonoBehaviour
{
// Awake()は書かずに自作関数を
public void Initialize() {}
}
具体的にはシーン初期化用のスクリプトを用意し、Awake()はそこだけに記述する。
そのAwake()内で各オブジェクト群の初期化処理を意図通りの順番で記述すればOK。
シーンの初期化が完了すれば初期化用スクリプトをDestroyすればスッキリ済みます。
ヒエラルキーの各所にバラバラと配置してあるオブジェクトも、ここで一元化することができます。
また動的にオブジェクトを生成する場合も、生成されたオブジェクトのAwake()は使わず、生成元で初期化処理を呼ぶようにします。
非同期処理に向けてのアレンジ
public class SceneInitializer : MonoBehaviour
{
[SerializeField] PlayerChara playerChara = default;
IEnumerator Start()
{
yield masterData.Initialize();
yield playerChara.Initialize();
yield enemyManager.Initialize();
Destory(this);
}
}
public class PlayerChara : MonoBehaviour
{
// 戻り値をIEnumerator型にしてコルーチンに対応
public IEnumerator Initialize()
{
yield break;
}
}
Start() は戻り値の型をIEnumeratorにすることでコルーチン化させることもできます。
各初期化処理の方も IEnumerator にすることで、アセットの非同期読み込みなどの完了を待たせられます。
AddressableAssetsは読み込みが非同期なので、待機させる処理がどこかしら必要になってくると思います。
後の禍根にならないよう
新機能の実装などでは都度テストしながらになるため、勝手に呼び出される Awake()で済ませがち。
意図した機能を作ることに注力するので、くっつけるだけで動作するAwake()はよく使います。
ですがそのまま忘れて見返すことなく開発が進み、同じ構造がどんどん増えてくると…
早めの見直し&対応を心がけたいところです。