【Unity】MonoBehaviour

はじめに

こんにちは カヤックのソーシャルゲーム事業部で働いているUnityエンジニアのmadaです。
今日はMonoBehaviourについて紹介します。
この記事はカヤックUnityアドベントカレンダー2016の4日目の記事です。

MonoBehaviourについて理解するにはUnityのマニュアルをよく読むことが一番大切ですが、
重要な項目についての補足記事を書きました。

SerializeFieldについて

MonoBehaviourを継承したObjectはメンバー変数にSerializeField属性を設定すると、Inspector上で値を設定できるようになります。
設定した値はシーンやPrefabの中に保存され、初期化時にその値が設定されます。

SerializeFieldの使用例

using UnityEngine;
using System.Collections.Generic;

public class NewBehaviourScript : MonoBehaviour 
{
    [SerializeField]
    private int _intValue;
    [SerializeField]
    private string _stringValue;
    [SerializeField]
    private List<GameObject> _gameObjects;
}

f:id:kayac-mada:20161201184321p:plain
Inspectorで値を設定できるようになる例

シリアライズするための条件

シリアライズするためには以下の条件を満たす必要があります。

  • public変数か[SerializeField]属性を持つこと
  • static変数ではないこと
  • constではないこと
  • readonlyではないこと
  • プロパティではないこと

シリアライズ可能な型について

以下のいずれかの条件を満たしている型がシリアライズ可能です。

  • UnityEngine.Objectを継承した型(GameObject, Component, MonoBehaviour, Texture2D など)
  • 基本的なクラス(int, string, float, bool など)
  • Enum
  • Unityの基本的なクラス(Vector2, Vector3, Vector4, Quaternion, Matrix4x4, Color, Rect, LayerMask など)
  • シリアライズ可能な型のArray
  • シリアライズ可能な型のList
  • [Serializable] 属性を持つカスタム非抽象クラス
  • [Serializable] 属性を持つカスタム構造体(Unity4.5から)

public変数のシリアライズ属性の小話

public変数が多いと仕様変更で定義を変えたいときに影響範囲の調査が大変になるため、 現在のプロジェクトではpublic変数のシリアライズを避けています。 インスペクタのみから値を設定する場合は、[SerializeField] private で宣言し、 他のクラスから値を取得したい場合は、getterのみのpropertyを別途定義して使用しています。

using UnityEngine;

public class NewBehaviourScript : MonoBehaviour 
{
    // メンテナンスしづらくなるので、下のようなコードは避ける
    // public GameObject _player

    // メンテナンスしやすいように以下のコードに書き換える
    [SerializeField]
    private GameObject _player;

    public GameObject player { get { return _player; } }
}

シリアライズされた変数名のリネームの小話

シリアライズされた変数名をリネームすると、Inspectorで設定した値がリセットされます。 実行時にNullPointerExceptionにならないよう注意が必要です。

f:id:kayac-mada:20161201185258p:plainf:id:kayac-mada:20161201185300p:plain
変数名のリネームで、Inspectorで設定した値がリセットされる例

[FormerlySerializedAsAttribute]属性を用いると、リネーム時にInspectorで設定した値がリセットされるのを防ぎます。 使い方はマニュアルを参考にしてください。

FormerlySerializedAsAttribute

NonSerialized

publicなメンバーをシリアライズしたくないときは、[NonSerialized]属性を利用します。

HideInInspector

public や [SerializeField] 属性の変数をInspectorに表示したくないときに利用します。 Inspectorに表示されなくなるのはNonSerializedと同様ですが、値はシリアライズされて保存されます。 シリアライズされていることを知らずに[HideInInspector]を利用すると、意図しない初期値が設定されてバグのもとになるので注意が必要です。

ライフサイクル

Unity上でゲーム開発するときにライフサイクルを理解するのは重要です。 主なライフサイクル関数についてはこの記事でも扱いますが、マニュアルにはより詳しいことが書いてあります。

イベント関数の実行順

Awake

Awakeはスクリプトのインスタンスが生存期間中に、 オブジェクトが最初にActiveになったタイミングで一度だけ呼び出されます。 Startと異なり、コンポーネントがdisableな場合も呼び出されます。 Awakeの呼び出される順番は不定なので他のObjectの情報を取得してはいけません。 (後述する方法で実行順を設定することもできます。)

OnEnable

Activeなオブジェクトかつ、コンポーネントがenableになったときに呼び出されます。 初期化時はAwakeの後に、OnEnableが呼ばれます。

Start

OnEnableの後で、最初のUpdateが呼ばれる前にStartが呼ばれます。 Awake関数と同様に、Start 関数はスクリプトの生存期間中に一度だけ呼び出されます。 オブジェクトがActiveになったときに呼ばれるAwakeと異なり、 Startはコンポーネントがenableになったときに呼ばれます。 よってAwakeとStartは同じフレームで呼び出されない場合もあります。

FixedUpdate

Updateの実行タイミングは処理速度依存ですが、 FixUpdateは固定フレームレートで実行されます。 FixedUpdate内の処理は1フレームに複数回呼ばれることも有るので注意が必要です。

Update

Updateは毎フレーム実行されます。 FPSが落ちると1秒あたりの実行回数も少なくなります。 Object間でUpdateの処理される順番は保証されていないので注意が必要です。 (後述する方法で実行順を設定することもできます。) UpdateはFixedUpdateの後に呼ばれます。

LateUpdate

全てのUpdateが終了した後に実行されます。 LateUpdateはUpdateの後に呼ばれます。

OnDisable

コンポーネントがdisableになったときに呼び出されます。

OnDestroy

MonoBehaviourが破棄されるときに呼ばれます。

AwakeとStartのライフサイクルの検証

AwakeとStartのライフサイクルの検証をするため次のコードを用いました。 AwakeはGameObjectがActiveになったときに呼ばれ、 StartはComponentがenalbeになったときに呼ばれているか調べてみましょう。 検証に使用したUnityEditorはUnity5.3.6p7です。

using UnityEngine;
using System.Collections;

public class ParentObject : MonoBehaviour 
{
    [SerializeField]
    // このスクリプトと同じようにログ出力するだけのクラス
    // 初期化時は、_chieldObject.gameObject.activeSelf = false
    // 初期化時は、_chieldObject.enable = falseの状態
    private ChieldObject _chieldObject;

    void Awake()
    {
        Debug.Log("<color=red>ParentObject</color> Awake");
        StartCoroutine(CoEndOfFrame());
        StartCoroutine(CoWaitNextFrame());
    }

    void OnEnable()
    {
        Debug.Log("<color=red>ParentObject</color> OnEnable");
    }

    void Start()
    {
        Debug.Log("<color=red>ParentObject</color> Start");
    }

    void Update()
    {
        Debug.Log("<color=red>ParentObject</color> Update");
    }

    void LateUpdate()
    {
        Debug.Log("<color=red>ParentObject</color> LateUpdate");
    }

    IEnumerator CoEndOfFrame()
    {
        while (true)
        {
            yield return new WaitForEndOfFrame();
            Debug.Log("<color>EndOfFrame</color>");
        }
    }

    IEnumerator CoWaitNextFrame()
    {
        yield return null;

        Debug.Log("<color>_chieldObject.gameObject.SetActive(true);</color>");
        _chieldObject.gameObject.SetActive(true);

        yield return null;

        Debug.Log("<color>_chieldObject.enabled = true;</color>");
        _chieldObject.enabled = true;
    }
}

f:id:kayac-mada:20161204180323p:plain:h400

ChieldObjectのAwakeとStartが別フレームで実行されていることから、 実行タイミングが異なることがわかります。

スクリプトでInstatiateした場合のライフサイクルの検証

スクリプトからPrefabをInstantiateしたときのライフサイクルについて、 マニュアルだけだと理解しづらいので実際に検証してみましょう。 (Prefabについては明日の記事で詳しく解説されます。) InstatiateをAwake,Start,Update,LateUpdate内で実行したときに、どのような実行順になるでしょうか。

using UnityEngine;
using System.Collections;

public class ParentObject : MonoBehaviour 
{
    [SerializeField]
    // このスクリプトと同じようにログ出力するだけのクラス
    // 初期化時は、_chieldObject.gameObject.activeSelf = true
    // 初期化時は、_chieldObject.enable = trueの状態
    private ChieldObject _chieldObject;

    private bool _firstTimeUpdate = true;
    private bool _firstTimeLateUpdate = true;

    void Awake()
    {
        Debug.Log("<color=red>ParentObject</color> Awake");
        StartCoroutine(CoEndOfFrame());

        // 結果1は以下のコードを有効にします
        // Instantiate(_chieldObject);
    }

    void OnEnable()
    {
        Debug.Log("<color=red>ParentObject</color> OnEnable");
    }

    void Start()
    {
        Debug.Log("<color=red>ParentObject</color> Start");

        // 結果2は以下のコードを有効にします
        // Instantiate(_chieldObject);
    }

    void Update()
    {
        Debug.Log("<color=red>ParentObject</color> Update");
        if (_firstTimeUpdate)
        {
            _firstTimeUpdate = false;

            // 結果3は以下のコードを有効にします
            // Instantiate(_chieldObject);
        }
    }

    void LateUpdate()
    {
        Debug.Log("<color=red>ParentObject</color> LateUpdate");
        if (_firstTimeLateUpdate)
        {
            _firstTimeLateUpdate = false;

            // 結果4は以下のコードを有効にします
            // Instantiate(_chieldObject);
        }
    }

    IEnumerator CoEndOfFrame()
    {
        while (true)
        {
            yield return new WaitForEndOfFrame();
            Debug.Log("<color>EndOfFrame</color>");
        }
    }
} 

f:id:kayac-mada:20161204171350p:plain

左から結果1(Awake内で生成), 結果2(Start内で生成), 結果3(Update内で生成), 結果4(LateUpdate内で生成)です。 結果3, 結果4を見ると、UpdateやLateUpdateの中でPrefabをInstatiateしたとき、次フレームからUpdateが呼ばれていることがわかります。

スクリプトの実行順設定

通常では、異なるスクリプトのAwake,Updateなどの実行順は不定です。 明示的に実行順を指定したい場合は、Script Execution Orderを設定します。 詳しくはドキュメントを参照してください。

スクリプトの実行順設定

コルーチン

任意の時間後に処理を続行したい,1フレーム後に処理を続行したい,通信完了後に処理を続行したい場合、コルーチンを用いると便利です。 コルーチンに引数を渡して実行でき、コルーチンの中から別のコルーチンを呼び出すことができます。 使い方は後術する使用例とマニュアルを参考にしてください。

Coroutine

コルーチンの指定方法と実行タイミング

  • yield return null: 次のフレームですべてのUpdate関数が呼び出された後に処理を続行します。
  • yield return new WaitForSeconds: フレームに対してすべてのUpdate関数呼び出し後から、指定された時間経過後に処理を続行します。
  • yield return new WaitForFixedUpdate: すべてのFixedUpdate関数呼び出し後に処理を続行します。
  • yield return new WWW: 通信完了後に処理を続行します。
  • yield return StartCoroutine(CoFunc()): コルーチンの中からコルーチンを呼び出すことができます。この例ではCoFuncコルーチンが最初に完了するのを待ちます。

コルーチンの簡単な使用例

1秒ごとにログを出力します。

using UnityEngine;
using System.Collections;

public class NewBehaviourScript : MonoBehaviour 
{
    private Coroutine _coroutine;
    private WaitForSeconds _waitForSecond = new WaitForSeconds(1);

    public void Start()
    {
        _coroutine = StartCoroutine(CoCountUp(10));
    }

    public void StopCountUp()
    {
        if (_coroutine != null)
        {
            // 明示的に止める
            StopCoroutine(_coroutine);
        }
    }

    // カウントアップコルーチン
    // 引数も渡せます
    private IEnumerator CoCountUp(int maxCount)
    {
        var count = 0;
        while (true)
        {
            if (count > maxCount)
            {
                // Coroutineの中から止める例
                yield break;
            }

            Debug.Log("count: " +count.ToString());
            count++;

            // 1秒待機
            yield return _waitForSecond;
        }
    }
}

Invokeメソッド

指定した秒数後に処理を実行するために、Invokeメソッドが用意されいます。
MonoBehaviour.Invoke(string methodName, float time);
メソッド名を文字列で指定する必要があるため、メソッド名の変更に弱いコードになるのが難点です。

おわりに

MonoBehaviourについて簡単に紹介してみましたが、いかがだったでしょうか。 Unity上でコードを書くときはMonoBehaviourのライフサイクルを理解していないと、 予測できない動きをしたり、バグの原因になったりします。 この記事が少しでも理解に役立てば幸いです。

明日はダークネスがPrefabについて紹介します。