【Unity検証】StartとAwakeとOnEnableの呼ばれ方を調べてみた

<イベント関数が呼ばれるタイミングについての疑問>

さて、多くの方がオブジェクトを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を検証しよう>

ゲームオブジェクトが非アクティブの場合

とりあえず、ゲームオブジェクトを非アクティブにして再生してみます。

disable game object console

まぁ、何も表示されませんね。これは予想通りです。あと最初から非アクティブだとOnDisableは呼ばれないみたいです。

Discovery

ゲームオブジェクトが非アクティブ状態だと何も呼ばれない

ゲームオブジェクトはアクティブだがスクリプトは無効の場合

さて、あんまり意味は無さそうですが一応検証してみます。

game object active script disable

この状態で再生すると

awake called script disable

ふぁ!?

スクリプトは無効にされているのにAwakeが呼ばれました。

公式によると

Awake: この関数は常に Start 関数の前およびプレハブのインスタンス化直後に呼び出されます。(もしゲームオブジェクトがスタートアップ時に無効である場合、有効になるまで Awake は呼び出されません。)

スクリプトが無効だろうがなんだろうが、インスタンス化されると呼ばれるみたいです。

そして、ゲームオブジェクトが初期状態非アクティブの場合はアクティブになった瞬間に呼ばれるようです。

これが、Awakeがいつの間にか呼ばれたりしていた原因のようです。

Discovery

・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>");
    }
}

インスタンシエイトする対象のゲームオブジェクトのログが流れるのがいやなので非アクティブでインスタンシエイトし、インスタンシエイトし終わったらアクティブにしています。

アクティブな状態でインスタンシエイトしても、非アクティブな状態でインスタンシエイトした後アクティブにしても結果は変わらなかったので大丈夫です。

instantiate awake

インスタンシエイト後のログより手前にAwakeとEnableが呼ばれている事がわかります。

つまり、AwakeとOnEnableは関数の途中であろうと、インスタンス化された瞬間に呼ばれる事がわかります。

Discovery

・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>");

結果はこのようになりました。

active and disable

関数の途中でもあるにも関わらず非アクティブになった瞬間OnDisableが呼ばれているのがわかります。

そして、Startが呼ばれる前に非アクティブにするとStartは呼ばれていない事がわかります。

Discovery

・OnDisableは関数の途中であろうと、非アクティブになった瞬間に呼ばれる
・Startが呼ばれる前に非アクティブにするとStartは呼ばれない

Awakeで非アクティブにするとどうなるか

スクリプトを2つ用意し、片方のAwakeで非アクティブにした場合、もう片方のAwakeは呼ばれるのでしょうか?

もう片方のスクリプトをScript Execution Orderで必ず後に呼ばれるように設定します。

script execution order

2つ目のスクリプトは同じように各種イベント関数で「テスト用」というログを出します。

最初に処理されるスクリプトの方のAwakeを↓のようにします。

     private void Awake()
     {
         Debug.Log("Awake");
         Debug.Log("非アクティブ前");
         gameObject.SetActive(false);
         Debug.Log("非アクティブ後");
     }

結果は↓のようになりました。

two script awake disable

2つ目のスクリプトが一切呼ばれていません。

また、途中で非アクティブにしましたが、関数は最後まで処理されている事がわかります。

Discovery

・Awakeで非アクティブにすると他の未実行のAwakeは呼ばれない
・関数の途中で非アクティブにしても関数は最後まで処理される

AwakeでDestroyするとどうなるか

↑のgameObject.SetActive(false)をDestroy(this.gameObject)に変えるとどうなるかやってみます。

結果は↓のようになりました。

awake destroy

Destroyした場合、Awakeが呼ばれている事がわかります。

また、OnEnableとStartは呼ばれていません。

ところでOnDestroyが呼ばれるタイミングが謎なのでOnDestroyはOnDestroyで検証記事を作成したいと思います。

ちなみに、OnEnableでデストロイした場合、もう一個のスクリプトのOnEnableは実行されませんでした。

Discovery

・Awakeでデストロイされても、他の未実行のAwakeも呼ばれる
・関数の途中でデストロイしても関数は最後まで処理される
・デストロイされると未実行のOnEnableとStartは呼ばれない




2つ以上のAwakeとOnEnableの順番

さて、今度は単純に2つのスクリプトを用意して

Awake→Awake→…..→OnEnable→OnEnable→…..となるのか

Awake→OnEnable→Awake→OnEnable→……となるのかを検証してみたいと思います。

awake onEnable order

どうやら、Awake→OnEnable→Awake→OnEnable→……で実行されるようです。

Discovery

・1つのスクリプトのAwakeとOnEnableが終わってから次のスクリプトのAwakeへ行く

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>");
    }
}

結果はこんな感じになりました。

instantiate update

先ほどのAwakeとOnEnableとは違い、Startは関数が終了した後に呼ばれている事がわかります。

また公式はUpdateの前に呼ばれると言っていますが、

Start: スクリプトのインスタンスが有効な場合にのみ、最初のフレームのアップデート前に Start が呼び出されます。

Updateが終わった後、LateUpdateが呼ばれる前に呼び出されている事がわかります。

どうやら、↑でいうアップデート前というのはUpdate関数の事ではなく「更新」の意味だと思います。まぎらわしい

そして、Awakeでインスタンシエイトしたフレームでは生成された側のUpdateも呼ばれていましたが、Update内でインスタンシエイトした場合、そのフレームでは生成された側のUpdateは呼ばれませんでした。

そして、Updateを飛ばしたのにも関わらず、LateUpdateは呼ばれている事がわかります

では、Updateではなく、LateUpdateでインスタンシエイトしたらどうなるのでしょう?

instantiate late update

こちらも、呼び出した関数が終わった後にStartが呼ばれ、同じLateUpdateが飛ばされているのがわかります。

FixedUpdateも同じ結果でした。

Startはインスタンス化された直後に呼ばれるのか

では今度はスクリプトを2つ用意して、インスタンシエイトされた関数の”直後”に呼ばれているのかテストしてみます。

もう片方のスクリプトをScript Execution Orderで必ず後に呼ばれるように設定します。

2つ目のスクリプトは同じように各種イベント関数で「テスト用」というログを出します。

そして、Update内でインスタンシエイトするようにします。

この状態で再生してみると

instantiate two script

生成側のUpdate呼ばれた後、2つ目のスクリプトのUpdateが呼ばれてStartが呼ばれている事がわかります。

この事から、どうやらStartは呼ばれたイベント関数が全て終わってから次のイベント関数へ行く間で呼ばれている事がわかります。

Discovery

・Startはインスタンシエイトされたイベント関数が全て終わってから呼ばれる
・インスタンシエイトを行ったイベント関数と同じイベント関数は1回飛ばされる

Awake,OnEnable,Start,Update,LateUpdateで非アクティブにするとレンダリングされるか

これらはまぁ、予想通りレンダリングされませんでした。

Cubeにスクリプトを貼っつけ、OnWillRenderObjectにDebug.Logを仕込みましたが呼ばれませんでした。

FixedUpdateはフレームとのズレにより不安定になるので例外となります。

Discovery

・Awake,OnEnable,Start,Update,LateUpdateで非アクティブにしたフレームは描画されない

↑の事実から、初期化だけして非アクティブにするのは有効かと思います。

<初期化にコンストラクタは使えるのか>

MonoBehaviourを継承していなければ使えるかもしれませんが、MonoBehaviourを継承しているクラスはコンストラクタでの初期化はやめておいた方がいいかもしれません。

理由はゲームプレイ時以外にもインスタンス化されているからです。

オブジェクトをシーン上にドラッグ&ドロップしたり、AddComponentしたりした時にインスタンス化してしまうので、コンストラクタが何度も呼ばれることになってしまいます。

また、コンストラクタで他のインスタンスにアクセスしようとすると、果たして対象がインスタンス化されているかどうかが不明でNullアクセス起こしまくるのでやめましょう。

数値の初期化などならいいかもしれませんが、それならAwakeやStartでもいいかなと思います。

Point

MonoBehaviourを継承しているクラスはコンストラクタでの初期化はやめよう!

<まとめ>

さて、色々検証してみましたが、いかがだったでしょうか?

AwakeとStartをどのように使えばいいか迷っていた人は一つの参考になったかと思います。

特に、他のスクリプトにアクセスしたら非アクティブになっていて初期化されてなかったーっていう事があるので、ちょっとしたヒントになりそうです。

AwakeとStartをどのように使うかはみなさん次第ですが、筆者は今回の検証を経て、次のような使い方をしようと思います。

  1. インスタンス化時、非アクティブなゲームオブジェクトを置かない生成しない。
  2. Awakeで自身のスクリプト内の初期化を行う
  3. Startで他スクリプトへのアクセスが必要な初期化を行う
  4. 初期非アクティブにしたいゲームオブジェクトはScript Execution Orderで必ず後に呼ばれるようなスクリプトを別で作り、Startで非アクティブにする。(初期非アクティブにする為だけのスクリプトを作ってアタッチする)
  5. もし初期非アクティブにしたいオブジェクトをInstantiateしたい場合、4のスクリプトで非アクティブにはせず、Instantiateを行う側でアクティブを制御する
  6. LateUpdateに「必ずUpdateが通っていないといけない処理」は書かない

このようにすることによって、「うぁぁぁぁ初期化されてないいぃぃいぃ」という事態を避ける事ができ、いちいちpublicな関数ごとに「初期化されてなかったら初期化する」みたいなコードを書く必要もなくなるかと思います。

どのような設計にするかは皆さん次第ですが、何か参考になったなら幸いです。

<わからない事、質問等があれば>

このサイトの説明ではよくわからなかったとか、もっと知りたい事などがあれば

自分の Youtubeの動画にコメントで質問していただければ動画でお答えしようと思います。

文章同士のやり取りだと伝わりづらいし、ラリーに時間がかかりそうなので動画で回答します。

↓の動画が回答の一例になります。どの動画でもいいのでご遠慮なくコメントしてください

できたらチャンネル登録よろしくお願いします!



タイトルとURLをコピーしました