【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について紹介します。

【Unity】カメラとレイヤー

はじめに

はじめまして。 カヤックのソーシャルゲーム事業部のUnityエンジニアのmadaです。
今日はUnityのカメラとレイヤーについて紹介します。
この記事はカヤックUnityアドベントカレンダー2016の3日目の記事です。

カメラについて

UnityのSceneに配置されたオブジェクトは、それを映すカメラによってGameViewに描写されます。 カメラがなければGameViewに表示されないので、シーンにカメラを配置しましょう。 (Unityで新規シーンを作成するとデフォルトでカメラも配置されます)

f:id:kayac-mada:20161202110704p:plain
上 Scene内にActiveなカメラがないので、GameViewで警告メッセージが表示されている例

f:id:kayac-mada:20161202110707p:plain
上 Scene内にActiveなカメラがあり、GameViewにオブジェクトが表示されている例

カメラは好きな数だけ追加できますが、追加すると処理が重くなります。 複数のカメラのレンダリングの順序や、スクリーン上の位置、一部を表示するなどの設定もできます。 この記事では触れていないことも詳しく書いてあるので、Unityのマニュアルも読んでみてください。

Unityマニュアル
Unityスクリプトリファレンス

f:id:kayac-mada:20161202123016p:plain:w300
上 CameraのInspectorのスクリーンショット

Projectionについて

3D空間のオブジェクトをどのように2Dのスクリーンにシミュレートするかの設定をすることができます。 UnityではPerspectiveとOrthographicの2種類が用意されています。

  • Perspective
    遠近法を用いた描写を行います。奥のオブジェクトは小さく表示されます。
    f:id:kayac-mada:20161202130432p:plain

  • Orthographic
    平行投射を用いた描写を行います。奥のオブジェクトは手前のオブジェクトと同じ大きさで表示されます。 f:id:kayac-mada:20161202130519p:plain

カメラの描写範囲の指定

カメラで描写する範囲を設定してみましょう。 3D空間を映すカメラなので、描写範囲も3D空間を表現するものです。 描写範囲をその形状から視錐台と呼ぶこともあります。 より詳しい解説は以下のマニュアルを参考にしてください。
視錐台を理解する

カメラの描写範囲の指定(XY方向)

XY方向の指定はProjectionにより名前が異なります。

  • Perspectiveの時
    Field of viewの値で垂直画角を設定できます。 Blenderの視野角設定は水平画角なのでカメラ設定を統一するときは注意が必要です。
    f:id:kayac-mada:20161202065918p:plain
    左:Field of view:60, 右:Field of view:110
  • Orthographicの時
    Sizeの値で縦に表示する視野量の半分の値を設定します。

カメラの描写範囲の指定(Z方向)

カメラの描写範囲のZ方向はnearClipPlaneまでの距離とfarClipPlaneまでの距離を設定できます。 この範囲内にあるオブジェクトのみがレンダリングの対象になります。

クリックした位置にオブジェクトを移動する例

クリックした位置にオブジェクトを移動してみましょう。 クリックした位置のWorldPositionを取得するのに Camera.ScreenToWorldPoint(Vector3 position) を利用してみます。 position.zはカメラと取得したい座標の距離を設定する必要があります。

using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
    [SerializeField]
    private Camera _camera;
    [SerializeField]
    private GameObject _targetObject;

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            // 第3引数は、カメラと取得したい座標の距離を指定します
            Vector3 mousePosition = new Vector3(Input.mousePosition.x, Input.mousePosition.y, 10);
            _targetObject.transform.position = _camera.ScreenToWorldPoint(mousePosition);
        }
    }
}

f:id:kayac-mada:20161202075056g:plain:w300
クリックした位置のWorldPositionを取得して、オブジェクトを移動する例
前半はカメラのProjectionをPerspective、後半はOrthographicの設定にしています。

クリックした位置のオブジェクトを取得する例

マウスでクリックした位置に描写されているオブジェクトを取得するには、
Camera.ScreenPointToRay(Vector3 position) を利用すると便利です。

using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
    [SerializeField]
    private Camera _camera;

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            RaycastHit raycastHit;
            Ray ray = _camera.ScreenPointToRay(Input.mousePosition);
            if (Physics.Raycast(ray, out raycastHit))
            {
                // 衝突したコライダーまたは Rigidbody の Transform
                Transform hitObject = raycastHit.transform;
                Debug.Log(hitObject.name);
            }
        }
    }
}

f:id:kayac-mada:20161202053457g:plain:w300
クリックしたオブジェクト名がデバッグログに表示される例

FrameDebugger

FrameDebuggerを利用すると、1オブジェクトづつ描写順を確認できるので便利です。 Unityメニューの【Window】->【FrameDebugger】をクリックし、表示されたWindow左上の【Enable】をクリックすると利用できます。

f:id:kayac-mada:20161202101835g:plain
FrameDebuggerの使用例

複数カメラの描写順

複数のカメラが設定されている場合は、低い値のdepthから高い値のdepthの順で描写されます。

レイヤー

GameObjectにはレイヤーを設定することができます。 レイヤーを分けることで、レンダリングするカメラを分けることができます。 まずは新規レイヤーを作成してみましょう。 Unityメニューの【Editor】->【Project Settings】->【Tags and Layers】をクリックし設定画面を表示します。 ここでは、RedLayerとBlueLayerを新規登録しました。

f:id:kayac-mada:20161202134151p:plain:w200

次にGameObjectにレイヤーを設定しましょう。 設定するGameObjectのInspectorの右上にLayerを設定できるメニューがあるのでクリックして設定します。

f:id:kayac-mada:20161202134932p:plain:w200

これでGameObjectにLayerを設定できました。

カリングマスク

カメラにカリングマスクを設定することで、カメラが描写するオブジェクトをレイヤー単位で指定することができます。 RedCameraは赤いオブジェクトのみ表示するように設定してみましょう。

f:id:kayac-mada:20161202194038p:plain:w250
上 カメラにカリングマスクを設定する例

実際に描写するオブジェクトをレイヤー単位で指定できているか確認してみます。

f:id:kayac-mada:20161202143805g:plain:w300
上 カメラにカリングマスクを設定した例

カリングマスクを用いて奥のオブジェクトを手前に表示する例

次の図に示した位置関係で、座標を動かさずに黒いキューブを一番手前に表示するにはどうしたら良いでしょうか?

f:id:kayac-mada:20161202145718p:plain:w300

レイヤー + カリングマスク + depthを用いることで解決できます。 複数カメラの描写順番は、カメラのdepthで設定できるので、BlackCameraのdepthを大きくします。

f:id:kayac-mada:20161202151038p:plain:w300

するとBlackCubeが一番手前に表示されるようになりました。 常に手前に表示する必要があるUIなども、この方法で設定することができます。

カメラエフェクトの紹介

Unityで用意されたカメラのエフェクトを利用すると、簡単にぼかし効果のある描写などに変更することができます。 用意されたエフェクトを利用するためには、StanderdAssetsのImportが必要で、Unityメニューの【Assets】->【ImportPackage】->【Effects】で行えます。

f:id:kayac-mada:20161202125042g:plain:w300
上 Bluerエフェクトのサンプル

おわりに

以上Unityのカメラとレイヤーについて紹介しました。 ゲーム中でどのように描写されるか描写順はどうなるか、 自分の環境でカメラの設定を変更して確かめてみると理解が深まると思います。 明日は、MonoBehaviourについての記事を書くので、引き続きよろしくお願いします。