OculusGoでVR空間上にメニューを表示してクリックする(uGUI)

OculusGoでBackボタンを押したとき、VR空間上にメニューを表示する方法です。
実現方法Backボタンを押したときにカメラの前にメニューを移動してきて表示するというだけで、OculusGo固有の注意点というものありません。

こんな感じのメニューを表示します。
![menu.png](https://qiita-image-store.s3.amazonaws.com/0/92354/9245f17d-10d2-011b-4f06-972f96fb448f.png)

環境: Unity 2018.1.5f1 (64-bit) / OculusUtilities(1.26.0 2018/06/20)

## 作成方法

uGUIをOculusGoコントローラーで指し示すことができるように、いくつか通常のUnityでuGUIを作成するのと異なる手順が必要です。

OculusGoでEventSystemを使ってオブジェクトをクリックする方法(not uGUI)と共通するところがあるので、細かい手順はそちらも参照のこと。
https://qiita.com/culage/items/c7e27d72025276ddf557


### ヒエラルキー

以下のような構成で作成します。

![hierarchy.png](https://qiita-image-store.s3.amazonaws.com/0/92354/e4a13367-fe6d-1937-f893-4ca0a85da43f.png)

`Menu`はCanvasを非表示にしたときにも操作できるようにするためのオブジェクトです。空オブジェクト`Menu`を作成して、その下にCanvasを配置してください。
`Canvas`、`EventSystem`はUI→Button等を配置すると自動的に作成されます。
`OVRGazePointer`は、Oculus Utilities をインポートして/Oculus/VR/Scenes/UIシーン内にあるものをPrefab化したものです。


### コンポーネントやプロパティの設定

- EventSystem

StandaloneInputModuleを削除して、OVRInputModuleを追加します。
RayTransformにRightHandAnchorを設定します。

- Canvas

CanvasのRenderModeをWorldSpaceに設定します。
EventCameraをCenterEyeAnchorに設定します。

GraphicRaycasterを削除して、OVRRaycasterを追加します。
(Pointerの設定はnullのままでも動作するようです)

RectTransform で、PosX、PosY、PosZは(0,0,0)に設定します。
また、Sacelはそのままだと大きすぎるので全て0.001くらいに設定します。

- OVRGazePointer

RayTransformにRightHandAnchorを設定します。

- Menu

Transform で、PosX、PosY、PosZは(0,0,0)に設定します。

また以下のコードを作成してアタッチします。


## コード

```C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class MenuScript : MonoBehaviour {

	void Start () {
		AddButtonHandler("Button1", () => {
			GameObject.Find("Button1").GetComponentInChildren<Text>().text = "clicked.";
		});
		AddButtonHandler("Button2", () => {
			GameObject.Find("Button2").GetComponentInChildren<Text>().text = "clicked.";
		});

		ShowMenu(false);
	}

	private void AddButtonHandler(string buttonName, UnityEngine.Events.UnityAction func) {
		Button button = GameObject.Find(buttonName).GetComponent<Button>();
		button.onClick.AddListener(func);
	}

	bool _isShowMenu;
	void Update() {
		if (Input.GetKeyDown(KeyCode.Escape)) { // OculusコントローラーのBackボタンはEscapeキー扱い
			_isShowMenu = !_isShowMenu;
			ShowMenu(_isShowMenu);
		}
	}
	
	private void ShowMenu(bool isShow) {
		_isShowMenu = isShow;

		var canvas = transform.Find("Canvas");
		if (isShow) {
			// 自分の目の前に持ってくる(角度はすこしずらす)
			Vector3 menuLocalPos = Quaternion.Euler(0f, -30f, 0f) * Vector3.forward * 0.2f;
			Vector3 menuWorldPos = Camera.main.transform.TransformPoint( menuLocalPos ); 
			transform.position = menuWorldPos;
			// こっちを向かせる(角度調整あり)
			//transform.LookAt(Camera.main.transform.position); // ……だと何故かイベントが効かなくなる
			Vector3 menuAngle = Camera.main.transform.eulerAngles;
			transform.eulerAngles = new Vector3(menuAngle.x, menuAngle.y - 30f, menuAngle.z);
			canvas.gameObject.SetActive(true);
		} else {
			canvas.gameObject.SetActive(false);
		}
		
	}

}
```

## 説明

### 各ボタン動作と、メニュー初期状態を非表示にする処理

`Start()`で、各ボタン動作と、メニュー初期非表示を設定をしています。
これについてはVR空間上にメニューを表示する主題にあまり関係ないので、説明を割愛します。

### ShowMenu()で表示/非表示を切り替える

SetActive()でメニューの有効無効を切り替えることで、メニューを非表示にしています。
SetActive()で無効にするGameObject自体にこのスクリプトを組み込むと無効状態時にスクリプト自体が動作しなくなってしまうので、Canvasの親である`Menu`にスクリプトをアタッチしています。

```C#
	var canvas = transform.Find("Canvas");
	if (isShow) {
		// (略)
		canvas.gameObject.SetActive(true);
	} else {
		canvas.gameObject.SetActive(false);
	}
```

### カメラの前にメニューを持ってくる

カメラのローカル座標で「眼の前」の座標は`Vector3.forward`で求められるので、それを`TransformPoint()`でワールド座標に変換して、そこにメニューを移動しています。
このコードではカメラの真正面にメニューを持ってくるのではなく、30度左にメニューが表示されるように調整しています。

また角度を調整しないと向いている方向によってメニュー角度がずれるので、こちらを向くように角度を調整しています。
「こちらを向く」という操作のために`LookAt()`というメソッドが存在するのですが、なぜかそれを使うとイベントが取れなくなるので算出した角度を設定しています。
(Oculusに限らず普通のunityのuGUIでも同じ現象が出ていたので仕様なんだと思う)

```C#
	// 自分の目の前に持ってくる(角度はすこしずらす)
	Vector3 menuLocalPos = Quaternion.Euler(0f, -30f, 0f) * Vector3.forward * 0.2f;
	Vector3 menuWorldPos = Camera.main.transform.TransformPoint( menuLocalPos ); 
	transform.position = menuWorldPos;
	// こっちを向かせる(角度調整あり)
	//transform.LookAt(Camera.main.transform.position); // ……だと何故かイベントが効かなくなる
	Vector3 menuAngle = Camera.main.transform.eulerAngles;
	transform.eulerAngles = new Vector3(menuAngle.x, menuAngle.y - 30f, menuAngle.z);
```

## コントローラーからレーザーを表示した場合にメニューを突き抜けてしまう問題の対策

uGUIは特になにもしないとPhysics.Raycastで突き抜けてしまうので、コントローラーからレーザーを表示する場合などはレーザーが貫通してしまう。
対策としては、単純にメニューにBoxColliderコンポーネントなどを追加してPhysics.Raycastで取得できる当たり判定を追加してやればよい。