初期化をするのにStartとAwakeってどう使い分ければいいのかわからない!そんな人の為に、Unityのイベント関数を細かく検証し、何をどのように使えばいいのかを調べてみました。
<Awakeメソッドとは>
まず、それぞれのメソッドについてざっくりとみて見ましょう。
Awakeメソッドというのは、スクリプトのインスタンスがロードされたときに呼び出されます。呼ばれるのはインスタンス化されてから1回のみです。
この時ゲームオブジェクトが有効である必要があります。
MonoBehaviourを継承したクラスを書いたスクリプトをゲームオブジェクトに貼り付けた場合、ゲームオブジェクトが無効であってもスクリプトのインスタンスはロードされます。
このメソッドはインスタンスがロードされてもゲームオブジェクトが有効になるまで待つ性質があるので、ゲームオブジェクトが有効になった瞬間に呼ばれるメソッドということになります。
<Startメソッドとは>
Startはスクリプトが有効で、Updateメソッドが最初に呼び出される前のフレームで呼び出されます。
Startもインスタンス化されてから1回だけ呼び出されます。
またこのメソッドはAwakeやOnEnableより後に呼ばれます。
<OnEnableメソッドとは>
OnEnableはオブジェクトが有効になったときに呼び出されます。
AwakeやStartと違って無効になってから、もう一度有効になっても呼ばれます。
有効になったら呼び出されるので、何回でも呼び出し可能です。
しかしながら、初回有効時にも呼ばれるのでタイミングとしてはAwakeやStartなどと似通ったタイミングでも呼ばれます。
このメソッドはAwakeより後、Startより先に呼ばれます。
<イベント関数が呼ばれるタイミングについての疑問>
さて、↑で解説した通り、ゲームオブジェクトをInstantiate(インスタンシエイト)したり初回アクティブにしたりすると
Awake
↓
OnEnable
↓
Start
という順で呼ばれます。
ですが、これらについて曖昧な覚え方をしている方も多いのでは無いのでしょうか?なんとなくこういう順で呼ばれるみたいな。
例えば、間に別のスクリプトが介在するとどうなるのでしょう?
StartはUpdateの前に呼ばれますが、別のスクリプトからUpdate内でインスタンシエイトした場合はどうなるのでしょう?また、LateUpdate内やFixedUpdate内でインスタンシエイトした場合はどうなるのでしょう?
あと、経験ある人もいるかもしれませんが、Awakeが呼ばれたり、呼ばれなかったりよくわからない感じになった人もいるかもしれません。
こういった疑問を解決するために色々検証していこうと思います。
ログを出力するだけのスクリプトを書きました。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class test : MonoBehaviour
{
private void Awake()
{
Debug.Log("Awake");
}
private void Start()
{
Debug.Log("Start");
}
private void OnEnable()
{
Debug.Log("Enable");
}
private void OnDisable()
{
Debug.Log("Disable");
}
private void Update()
{
Debug.Log("Update");
}
private void LateUpdate()
{
Debug.Log("Late");
}
private void FixedUpdate()
{
Debug.Log("FixedUpdate");
}
}
まずはこれをカラのゲームオブジェクトにくっつけて検証します。
<AwakeとStartとOnEnableを検証しよう>
ゲームオブジェクトが非アクティブの場合
とりあえず、ゲームオブジェクトを非アクティブにして再生してみます。
まぁ、何も表示されませんね。これは予想通りです。あと最初から非アクティブだとOnDisableは呼ばれないみたいです。
ゲームオブジェクトはアクティブだがスクリプトは無効の場合
さて、あんまり意味は無さそうですが一応検証してみます。
この状態で再生すると
ふぁ!?
スクリプトは無効にされているのにAwakeが呼ばれました。
公式によると
Awake: この関数は常に Start 関数の前およびプレハブのインスタンス化直後に呼び出されます。(もしゲームオブジェクトがスタートアップ時に無効である場合、有効になるまで Awake は呼び出されません。)
スクリプトが無効だろうがなんだろうが、インスタンス化されると呼ばれるみたいです。
そして、ゲームオブジェクトが初期状態非アクティブの場合はアクティブになった瞬間に呼ばれるようです。
これが、Awakeがいつの間にか呼ばれたりしていた原因のようです。
Instantiateされた瞬間はどうか
次はインスタンシエイトされた瞬間はどのように処理されるのか検証します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class test2 : MonoBehaviour
{
public GameObject obj;
private void Awake()
{
Debug.Log("<color=red>インスタンシエイト前</color>");
GameObject g = Instantiate(obj);
g.SetActive(true);
Debug.Log("<color=green>インスタンシエイト後</color>");
}
}
インスタンシエイトする対象のゲームオブジェクトのログが流れるのがいやなので非アクティブでインスタンシエイトし、インスタンシエイトし終わったらアクティブにしています。
アクティブな状態でインスタンシエイトしても、非アクティブな状態でインスタンシエイトした後アクティブにしても結果は変わらなかったので大丈夫です。
インスタンシエイト後のログより手前にAwakeとEnableが呼ばれている事がわかります。
つまり、AwakeとOnEnableは関数の途中であろうと、インスタンス化された瞬間に呼ばれる事がわかります。
アクティブ→非アクティブにするとどうなるか
次はアクティブにした瞬間、非アクティブにしてみたらどうなるか検証してみます。
Debug.Log("<color=red>インスタンシエイト前</color>"); GameObject g = Instantiate(obj); Debug.Log("<color=brown>アクティブ</color>"); g.SetActive(true); Debug.Log("<color=brown>非アクティブ</color>"); g.SetActive(false); Debug.Log("<color=green>インスタンシエイト後</color>");
結果はこのようになりました。
関数の途中でもあるにも関わらず非アクティブになった瞬間OnDisableが呼ばれているのがわかります。
そして、Startが呼ばれる前に非アクティブにするとStartは呼ばれていない事がわかります。
Awakeで非アクティブにするとどうなるか
スクリプトを2つ用意し、片方のAwakeで非アクティブにした場合、もう片方のAwakeは呼ばれるのでしょうか?
もう片方のスクリプトをScript Execution Orderで必ず後に呼ばれるように設定します。
2つ目のスクリプトは同じように各種イベント関数で「テスト用」というログを出します。
最初に処理されるスクリプトの方のAwakeを↓のようにします。
private void Awake() { Debug.Log("Awake"); Debug.Log("非アクティブ前"); gameObject.SetActive(false); Debug.Log("非アクティブ後"); }
結果は↓のようになりました。
2つ目のスクリプトが一切呼ばれていません。
また、途中で非アクティブにしましたが、関数は最後まで処理されている事がわかります。
AwakeでDestroyするとどうなるか
↑のgameObject.SetActive(false)をDestroy(this.gameObject)に変えるとどうなるかやってみます。
結果は↓のようになりました。
Destroyした場合、Awakeが呼ばれている事がわかります。
また、OnEnableとStartは呼ばれていません。
ところでOnDestroyが呼ばれるタイミングが謎なのでOnDestroyはOnDestroyで検証記事を作成したいと思います。
ちなみに、OnEnableでデストロイした場合、もう一個のスクリプトのOnEnableは実行されませんでした。
2つ以上のAwakeとOnEnableの順番
さて、今度は単純に2つのスクリプトを用意して
Awake→Awake→…..→OnEnable→OnEnable→…..となるのか
Awake→OnEnable→Awake→OnEnable→……となるのかを検証してみたいと思います。
どうやら、Awake→OnEnable→Awake→OnEnable→……で実行されるようです。
InstantiateするタイミングをUpdateにしたらどうか
さて、先ほどまではAwakeでインスタンシエイトしていました。
StartはUpdateより前に呼ばれますが、Update中にインスタンシエイトした場合どうなるのでしょうか?
呼び出し側をちょっと変えて検証してみます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class test2 : MonoBehaviour
{
public GameObject obj;
private bool ins = false;
private void Update()
{
if (!ins)
{
Debug.Log("<color=red>インスタンシエイト前</color>");
GameObject g = Instantiate(obj);
g.SetActive(true);
Debug.Log("<color=green>インスタンシエイト後</color>");
ins = true;
}
Debug.Log("<color=blue>生成側のUpdate</color>");
}
private void FixedUpdate()
{
Debug.Log("<color=blue>生成側のFixedUpdate</color>");
}
private void LateUpdate()
{
Debug.Log("<color=blue>生成側のLateUpdate</color>");
}
}
結果はこんな感じになりました。
先ほどのAwakeとOnEnableとは違い、Startは関数が終了した後に呼ばれている事がわかります。
また公式はUpdateの前に呼ばれると言っていますが、
Start: スクリプトのインスタンスが有効な場合にのみ、最初のフレームのアップデート前に Start が呼び出されます。
Updateが終わった後、LateUpdateが呼ばれる前に呼び出されている事がわかります。
どうやら、↑でいうアップデート前というのはUpdate関数の事ではなく「更新」の意味だと思います。まぎらわしい
そして、Awakeでインスタンシエイトしたフレームでは生成された側のUpdateも呼ばれていましたが、Update内でインスタンシエイトした場合、そのフレームでは生成された側のUpdateは呼ばれませんでした。
そして、Updateを飛ばしたのにも関わらず、LateUpdateは呼ばれている事がわかります。
では、Updateではなく、LateUpdateでインスタンシエイトしたらどうなるのでしょう?
こちらも、呼び出した関数が終わった後にStartが呼ばれ、同じLateUpdateが飛ばされているのがわかります。
FixedUpdateも同じ結果でした。
Startはインスタンス化された直後に呼ばれるのか
では今度はスクリプトを2つ用意して、インスタンシエイトされた関数の”直後”に呼ばれているのかテストしてみます。
もう片方のスクリプトをScript Execution Orderで必ず後に呼ばれるように設定します。
2つ目のスクリプトは同じように各種イベント関数で「テスト用」というログを出します。
そして、Update内でインスタンシエイトするようにします。
この状態で再生してみると
生成側のUpdate呼ばれた後、2つ目のスクリプトのUpdateが呼ばれてStartが呼ばれている事がわかります。
この事から、どうやらStartは呼ばれたイベント関数が全て終わってから次のイベント関数へ行く間で呼ばれている事がわかります。
イベント関数で非アクティブにするとレンダリングされるか
これらはまぁ、予想通りレンダリングされませんでした。
Cubeにスクリプトを貼っつけ、OnWillRenderObjectにDebug.Logを仕込みましたが呼ばれませんでした。
FixedUpdateはフレームとのズレにより不安定になるので例外となります。
↑の事実から、初期化だけして非アクティブにするのは有効かと思います。
<初期化にコンストラクタは使えるのか>
MonoBehaviourを継承していなければ使えるかもしれませんが、MonoBehaviourを継承しているクラスはコンストラクタでの初期化はやめておいた方がいいかもしれません。
理由はゲームプレイ時以外にもインスタンス化されているからです。
オブジェクトをシーン上にドラッグ&ドロップしたり、AddComponentしたりした時にインスタンス化してしまうので、コンストラクタが何度も呼ばれることになってしまいます。
また、コンストラクタで他のインスタンスにアクセスしようとすると、果たして対象がインスタンス化されているかどうかが不明でNullアクセス起こしまくるのでやめましょう。
数値の初期化などならいいかもしれませんが、それならAwakeやStartでもいいかなと思います。
<まとめ>
さて、色々検証してみましたが、いかがだったでしょうか?
AwakeとStartをどのように使えばいいか迷っていた人は一つの参考になったかと思います。
特に、他のスクリプトにアクセスしたら非アクティブになっていて初期化されてなかったーっていう事があるので、ちょっとしたヒントになりそうです。
AwakeとStartをどのように使うかはみなさん次第ですが、筆者は今回の検証を経て、次のような使い方をしようと思います。
- インスタンス化時、非アクティブなゲームオブジェクトを置かない生成しない。
- Awakeで自身のスクリプト内の初期化を行う
- Startで他スクリプトへのアクセスが必要な初期化を行う
- 初期非アクティブにしたいゲームオブジェクトはScript Execution Orderで必ず後に呼ばれるようなスクリプトを別で作り、Startで非アクティブにする。(初期非アクティブにする為だけのスクリプトを作ってアタッチする)
- もし初期非アクティブにしたいオブジェクトをInstantiateしたい場合、4のスクリプトで非アクティブにはせず、Instantiateを行う側でアクティブを制御する
- LateUpdateに「必ずUpdateが通っていないといけない処理」は書かない
このようにすることによって、「うぁぁぁぁ初期化されてないいぃぃいぃ」という事態を避ける事ができ、いちいちpublicな関数ごとに「初期化されてなかったら初期化する」みたいなコードを書く必要もなくなるかと思います。
どのような設計にするかは皆さん次第ですが、何か参考になったなら幸いです。