e.blog

主にUnity/UE周りのことについてまとめていきます

uGUIのEventSystemを理解する

概要

Unity4.6から導入されたEventSystem。 調べてみると色々と学びがあるのでメモとして残しておきます。 (若干、今さら感がありますが)

まず最初に大事な概念として、このシステムはいくつかのクラスが密接に連携しながらイベントを処理していく形になっています。アーキテクチャ的にも学びがある実装になっています。

登場人物

このシステムを構成する、必要な登場人物は以下にあげるクラスたちです。

  • BaseInputModule
  • BaseRaycaster
  • ExecuteEvents
  • PointerEventData
  • RaycastResult

すごくおおざっぱに言うと。

BaseInputModuleが全体を把握し、適切にイベントを起こす、まさに「インプット」を管理するクラスとなります。

まず、BaseRaycasterによって対象オブジェクトを収集します。(複数のRaycasterクラスがある場合はすべて実行して、対象となるオブジェクトをかき集めるイメージ) そして得られたオブジェクトに対してどういう状態なのかを判断します。例えば、ホバーされているのか、ホバーが解除されたのか、あるいはクリックされたのか、などなど。

そしてその判断された状態に応じて、ExecuteEventsを利用してイベントを送出する、というのが全体的な流れになります。

カスタムする

Baseと名前がついていることから分かる通り、これらを適切に継承・使用することで、独自の仕組みで対象オブジェクトを決定し、独自のイベントを伝播させる、ということも可能になります。

BaseInputModuleを継承したカスタムクラス

BaseInputModule を継承したカスタムクラスを作成することで、独自のイベントを定義することができます。そもそも冒頭で書いたように、このクラス内でオブジェクトの収集を行い、イベントの状態管理をするのが目的なのでそれを行うためのクラスとなります。

なお、 BaseInputModuleUIBehaviour -> MonoBehaviour を継承しているクラスのため、GameObjectにアタッチすることができるようになっています。

※ ちなみに、シーン内でアクティブなInputModuleはひとつだけと想定されています。

Processメソッドのオーバーライド必須

BaseInputModule には Process メソッドがabstractで定義されており、これのオーバーライドは必須となっています。この Process メソッドは、BaseInputModule 内で自動的に呼ばれ、Updateと似たような形で毎フレームごとに呼ばれるメソッドとなっています。

なので、作成したカスタムクラスを適当なGameObjectにアタッチし、Process メソッド内に処理を書くと毎フレーム呼ばれるのが確認できると思います。

BaseRaycasterを継承したカスタムクラス

BaseRaycasterを継承したカスタムクラスを作成することで、収集の対象とするオブジェクトを独自で定義することができます。このベースクラスのRaycastメソッドとeventCameraプロパティはabstractで宣言されており、派生クラスではoverride必須となっています。

Raycastメソッドのオーバーライド

BaseRaycasterRaycastメソッドをオーバーライドすることで、EventSystem.RaycastAllメソッド実行時に収集対象が収集されます。

RaycastAllメソッドのシグネチャは以下のようになっています。

public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults);

引数として、ポインターイベントのデータオブジェクトと、Raycast結果を格納するList<RaycastResult>型のオブジェクトを渡します。このRaycastAllメソッド内で、BaseRaycasterを継承したクラスのRaycastが順次呼び出されるので、その処理の中でヒットしたオブジェクトをリストに追加します。

最後に、全RaycasterのRaycast結果を元にして、対象オブジェクトにイベントを送出します。

Raycastメソッドで対象オブジェクトを収集する

Raycastと名がついていますが、別にRaycastを必ず実行しないとならないわけではありません。
あくまで、Raycastが判断として使われているためについている名前でしょう。つまり、このメソッド内で適当なオブジェクトを結果リストに入れてあげれば、Raycastをしていなくともそれが「ヒット候補」として送られることになります。

RaycastResult

RaycastResultオブジェクトは、Raycastした結果を保持するオブジェクトです。Raycasterによって収集されたオブジェクトの結果を保持するもの、と考えるといいと思います。

BaseRaycasterクラスを継承したサブクラスのRaycast実行時に、結果を保持する際に使用します。

ライフサイクル

全体の簡単なライフサイクルを見てみましょう。

  • BaseInputModule.Process
  • EventSystem.RaycastAll
  • 各BaseRaycasterを継承したクラスのRaycast
  • ExecuteEventsを利用してイベントを伝達

大まかに言えば、上のようなライフサイクルでイベントを実行していきます。以下に、動作原理を理解するためだけの、とてもシンプルなコード例を載せておきます。

InputModuleサンプル

最初はInputModuleのサンプルです。
Processメソッド内で対象となるオブジェクトを集めます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class InputModule : BaseInputModule
{
    private List<RaycastResult> _result = new List<RaycastResult>();
    private PointerEventData _eventData;

    private void Awake()
    {
        _eventData = new PointerEventData(eventSystem);
    }

    public override void Process()
    {
        _result.Clear();
        eventSystem.RaycastAll(_eventData, _result);
        Debug.Log(_result[0].gameObject);
    }
}

PointerEventDataは、UIを操作するための「ポインタ」の位置や状態などを保持するデータです。
このデータを元に、Raycasterは対象オブジェクトがどれかを識別します。

なので、実際にはRaycastAllメソッドに渡す前に、適切に「現在の」ポインタの状態にアップデートする必要があります。が、今回はサンプルなので生成するだけに留めています。

Raycasterサンプル

次に、InputModule内で実行されるRaycastです。実際にはeventSystem.RaycastAllを通じて間接的に実行されます。

このメソッドの第二引数に渡しているList<RaycastResult>型のリストが、結果を保持するために使用されていることに注目してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class Raycaster : BaseRaycaster
{
    [SerializeField]
    private GameObject _obj1;

    [SerializeField]
    private GameObject _obj2;

    public override Camera eventCamera
    {
        get
        {
            return Camera.main;
        }
    }

    public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
    {
        RaycastResult result1 = new RaycastResult()
        {
            gameObject = _obj1,
            module = this,
            worldPosition = _obj1.transform.position,
        };

        RaycastResult result2 = new RaycastResult()
        {
            gameObject = _obj2,
            module = this,
            worldPosition = _obj2.transform.position,
        };

        eventData.pointerCurrentRaycast = result2;

        resultAppendList.Add(result1);
        resultAppendList.Add(result2);
    }
}

Raycastメソッドには、ポインタの状態としてPointerEventDataが、また結果を保持するためのリストとしてList<RaycastResult>型のリストが渡ってきます。

この第二引数のリストが、上のInputModule内で渡したリストになります。これでなんとなく関係が見えてきたのではないでしょうか。

あとは、レイキャストを実行するなりして「対象オブジェクト」を識別します。今回は動作原理をわかりやすくするため、レイキャストなどは行わず、予め登録されたオブジェクトをそのままリストに追加するだけの処理にしています。

実際にはここで、レイキャストや、その他必要なチェックを経て、実際のオブジェクトを選出することになります。

まとめ

InputModuleが全体のインプットを管理し、イベントシステムに対して適切にRaycastを実行させ、収集したオブジェクト(RaycastResult)を利用して各イベントの伝搬を行う過程がなんとなくわかったかと思います。

InputModuleは、RaycastResultのデータから各オブジェクトの状態(距離など)を使って、そのオブジェクトがホバー状態なのか、それが解除されたのか、などなどを判断し、またそう判断されたオブジェクトに対してはExecuteEventsを使ってイベントを伝達します。

オブジェクトを集める、判断する、イベントを伝える、というのが、いくつかのクラスが密接に連携しながら、かつ拡張性高く実装されているのが分かってもらえたかと思います。

VR向けにuGUIを拡張したりしているので、機会があったらそれも書きたいと思います。