はじめに
Unity 4.6 から導入された新 UI システムでは、マウスやキーボード、キーパッドの入力を担当する Standalone Input Module
と、タッチを担当する Touch Input Module
によって、入力イベントが扱われます。何かしらの UI 要素を追加すると、自動でこれらのモジュール及び EventSystem
のアタッチされたゲームオブジェクトが自動で生成されます。
そして新 UI システムでは、Canvas
のRender Mode
を World Space
にすることで、VR のシーンでも簡単に扱うことが出来ます。が、そのままではマウスやキーボードの操作になってしまい、色んなガジェットと組み合わせたりしながら VR 内で望ましい UI を色々と試行錯誤するにはカスタムしてあげる必要があります。
そこで本エントリでは、独自のイベントハンドリングを新 UI システムで行う方法について調べてみた内容を共有したいと思います。
参考
既に VR 用に実装されている方がいらっしゃいますので、これが何をしているか理解するところまでやります。
- Oculus Rift World Space Cursors for World Space Canvases in Unity 4.6 | Ralph Barbagallo's Self Indulgent Blog
- Custom Event System example? (Getting source to Input Modules) | Unity Community
あとはドキュメント、及び Gist に上がった各モジュールのコードを参考に追っていきます。
- Unity - Scripting API: BaseInputModule
- Unity - Scripting API: PointerInputModule
- Unity - Scripting API: TouchInputModule
- Unity - Scripting API: StandaloneInputModule
- stramit's Gists
イベント送信の仕組み概要
イベントを送信するのは各 Input Module の基底クラス BaseInputModule
が担当します。これは MonoBehaviour
を継承した UIBehaviour
を継承したクラスのため、GameObject にアタッチすることができます。カスタムしたモジュールのコードをドキュメントを参考にして書いてみます。
using UnityEngine; using UnityEngine.EventSystems; public class CustomInputModule : BaseInputModule { public GameObject[] targetObjects; public override void Process() { if (targetObjects == null || targetObjects.Length == 0) { return; } foreach (var target in targetObjects) { ExecuteEvents.Execute(target, new BaseEventData(eventSystem), ExecuteEvents.submitHandler); } } }
Process()
は Update()
のように毎フレーム呼ばれる関数です(BaseInputModule
内で呼ばれます)。ここで対象となる GUI の GameObject を判断し、ExecuteEvents.Execute()
を通じて対象の GameObject に登録されたハンドラを呼び出します。引数は順番に、対象の GameObejct、引数、ハンドラとなっています。Inspector から適当なボタンを targetObjects
に登録しておくと、毎フレームクリックされる形になりますので試してみてください。
おまけ
更に進んでカスタムイベントを作成するには以下の Gist がとても参考になります。
StandaloneInputModule を見てみる
概要が掴めたので StandaloneInputModule
を見てみると何となく概要が掴めると思います。
namespace UnityEngine.EventSystems { [AddComponentMenu("Event/Standalone Input Module")] public class StandaloneInputModule : PointerInputModule { // ...(略) public override void Process() { bool usedEvent = SendUpdateEventToSelectedObject (); if (!usedEvent) usedEvent |= SendMoveEventToSelectedObject (); if (!usedEvent) SendSubmitEventToSelectedObject (); ProcessMouseEvent (); } // ...(略) private void ProcessMouseEvent() { bool pressed = Input.GetMouseButtonDown (0); bool released = Input.GetMouseButtonUp (0); var pointerData = GetMousePointerEventData (); // Take care of the scroll wheel float scroll = Input.GetAxis ("Mouse ScrollWheel"); pointerData.scrollDelta.x = 0f; pointerData.scrollDelta.y = scroll; if (!UseMouse (pressed, released, pointerData)) return; // Process the first mouse button fully ProcessMousePress (pointerData, pressed, released); ProcessMove (pointerData); if (!Mathf.Approximately (scroll, 0.0f)) { var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler> (pointerData.pointerCurrentRaycast.go); ExecuteEvents.ExecuteHierarchy (scrollHandler, pointerData, ExecuteEvents.scrollHandler); } } // ...(略) } }
Process()
を見てみると ProcessMouseEvent()
で Input.GetMouse*()
をしているのが分かります。ここで得られたデータを元に色々なイベントに変換して、対象の GameObject に対して ExecuteEvents.Execute()
してイベントハンドラを呼び出しています。では対象の GameObject はどうやって取得しているか見てみます。
ProcessMouseEvent()
の中で GetMousePointerEventData()
を呼び出しているのが見えると思います。これは基底クラスの PointerInputModule
で定義されている関数で中を見てみしょう。
using System.Collections.Generic; using System.Text; namespace UnityEngine.EventSystems { public abstract class PointerInputModule : BaseInputModule { // ...(略) protected virtual PointerEventData GetMousePointerEventData() { PointerEventData pointerData; var created = GetPointerData (kMouseId, out pointerData, true); pointerData.Reset (); if (created) pointerData.position = Input.mousePosition; Vector2 pos = Input.mousePosition; pointerData.delta = pos - pointerData.position; pointerData.position = pos; pointerData.scrollDelta = Input.mouseScrollDelta; eventSystem.RaycastAll (pointerData, m_RaycastResultCache); var raycast = FindFirstRaycast (m_RaycastResultCache); pointerData.pointerCurrentRaycast = raycast; m_RaycastResultCache.Clear (); return pointerData; } // ...(略) } }
中を見てみると、なにやら eventSystem.RaycastAll()
なるものが見えます。実は Canvas オブジェクトについていた Graphic Raycaster
が、この Raycast を行っているコンポーネントで、Graphic Raycaster
はコライダのついていない GUI のオブジェクトを判定するためのものです。
つまりこいつが、マウスカーソルがどの GUI の上に乗っているか判断してくれているものになります。他にもコライダのついたオブジェクトに対して Physics Raycaster
があったり、独自に Raycaster を作成することも出来ます。
まとめると、Raycaster を通じて得られたマウスカーソルが乗った(or タッチした等)オブジェクトに対して、Input Module が解釈したイベントを、イベントハンドラを通じて呼び出している形になっています。つまり、イベントを送信する対象をマウスカーソルでなく別の方法で決定したかったらカスタム Raycaster を、独自のルールでクリックやホバーなどを行いたかったらカスタム Input Module を作成する、ということを行えばよい形になります。
追記
次のエントリで図解しました。
VRInputModule を見てみる
概要が掴めたところで、先ほどの VRInputModule.cs
を見てみます。
ここでは、カスタム Raycaster を作らずに、SetTargetObject()
という static なメンバを通じてどのオブジェクトにイベントを送信するか、というのを決定しています。別途 InputManager
というクラスで独自のボタン押下を検出し、SetTargetObject()
を通じて指定された GameObject に対して Submit
イベントを通知しています。また SetTargetObject()
内で、以前指定されていたオブジェクトは PointerExit
イベントを、新しく指定されたオブジェクトに対しては PointerEnter
イベントを送信することにより、フォーカス表現を再現しています。
元エントリにもある通りちょっと dirty な感じがしますが、実装はとても簡単なので良いですね。真面目にやるのであればカスタム Raycaster を作成すれば良いと思います。
おわりに
次回は、これを元に VR 用の GUI モジュールを作成してみます。