概要
Unity4.6から導入されたEventSystem
。 調べてみると色々と学びがあるのでメモとして残しておきます。 (若干、今さら感がありますが)
まず最初に大事な概念として、このシステムはいくつかのクラスが密接に連携しながらイベントを処理していく形になっています。アーキテクチャ的にも学びがある実装になっています。
登場人物
このシステムを構成する、必要な登場人物は以下にあげるクラスたちです。
- BaseInputModule
- BaseRaycaster
- ExecuteEvents
- PointerEventData
- RaycastResult
すごくおおざっぱに言うと。
BaseInputModule
が全体を把握し、適切にイベントを起こす、まさに「インプット」を管理するクラスとなります。
まず、BaseRaycaster
によって対象オブジェクトを収集します。(複数のRaycasterクラスがある場合はすべて実行して、対象となるオブジェクトをかき集めるイメージ) そして得られたオブジェクトに対してどういう状態なのかを判断します。例えば、ホバーされているのか、ホバーが解除されたのか、あるいはクリックされたのか、などなど。
そしてその判断された状態に応じて、ExecuteEvents
を利用してイベントを送出する、というのが全体的な流れになります。
カスタムする
Baseと名前がついていることから分かる通り、これらを適切に継承・使用することで、独自の仕組みで対象オブジェクトを決定し、独自のイベントを伝播させる、ということも可能になります。
BaseInputModuleを継承したカスタムクラス
BaseInputModule
を継承したカスタムクラスを作成することで、独自のイベントを定義することができます。そもそも冒頭で書いたように、このクラス内でオブジェクトの収集を行い、イベントの状態管理をするのが目的なのでそれを行うためのクラスとなります。
なお、 BaseInputModule
は UIBehaviour -> MonoBehaviour
を継承しているクラスのため、GameObjectにアタッチすることができるようになっています。
※ ちなみに、シーン内でアクティブなInputModuleはひとつだけと想定されています。
Processメソッドのオーバーライド必須
BaseInputModule
には Process
メソッドがabstractで定義されており、これのオーバーライドは必須となっています。この Process
メソッドは、BaseInputModule
内で自動的に呼ばれ、Updateと似たような形で毎フレームごとに呼ばれるメソッドとなっています。
なので、作成したカスタムクラスを適当なGameObjectにアタッチし、Process
メソッド内に処理を書くと毎フレーム呼ばれるのが確認できると思います。
BaseRaycasterを継承したカスタムクラス
BaseRaycaster
を継承したカスタムクラスを作成することで、収集の対象とするオブジェクトを独自で定義することができます。このベースクラスのRaycast
メソッドとeventCamera
プロパティはabstract
で宣言されており、派生クラスではoverride
必須となっています。
Raycastメソッドのオーバーライド
BaseRaycaster
のRaycast
メソッドをオーバーライドすることで、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を拡張したりしているので、機会があったらそれも書きたいと思います。