Unity + Oculus Integration環境で作るVRプログラムで困ったこと色々


※下書き中


OVR_EventSystemによるUIについて
[ドロップダウンの挙動がおかしい](#ドロップダウンの挙動がおかしい)
[プレイヤーサイズ(OVRPlayerControllerのlocalScale)変更時にuiポインタがずれる](#プレイヤーサイズ変更時のuiポインタ描写)
[VRコントローラによるVRメニューの操作をUnity Editor上でデバッグしたい](#メニューの操作をunity-editor上でデバッグ)

その他
[player(OVRPlayerController)のpositionを動かしてもワープしない](#playerのpositionを動かしてもワープしない)
[コントローラでオブジェクトを掴む](#コントローラでオブジェクトを掴む)


Unity + Oculus Integration環境で用意されているOVR_EventSystem、非VR環境用に作ったUIを、ほぼそのままVR環境にもっていける良いブレハブなのですが、いくつか躓いた事があるのでメモを残します。


#OVR_EventSystemによるUIについて
Unity + Oculus Integration環境で用意されているOVR_EventSystemは、非VR環境用に作ったUIをそのままVR環境にもっていける良いブレハブなのですが、いくつか躓いた事があります。

###ドロップダウンの挙動がおかしい
OVR_EventSystemプレハブで作ったUI上に置いたドロップダウンの挙動がおかしい問題

■事象
通常のcanvas上に置かれたUnityのドロップボックスは、以下のような挙動をします。
・ドロップダウンをクリックすると展開される
・展開されている間、ドロップダウン以外の画面のどこか(ボタンや他のドロップダウン等)をクリックしても反応せず、代わりに開いていたドロップダウンが閉じる　※つまり複数のドロップダウンが同時に開くことがない

しかしVR上のcanvas(Render Mode = Wold Space)に配置したドロップダウンでは、ドロップダウンが開いている間も他のUIが操作可能になる問題が発生しました。
開いたドロップダウンの裏にボタン等があると、ドロップダウンよりもそちらが優先されるため、まともに操作できません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421201/c9ab8cd4-9d64-7e91-cfb9-37992ed5dfa2.png)

■原因

通常(非VR)におけるドロップダウンの挙動は、以下の仕組みにより実装されています。
・開いたドロップダウンにはCanvasコンポーネントがOrder in Layer=30000で設定されている（他のUIより優先される）
　これにより、開いたドロップダウンを選択したのに、ドロップダウンの裏にあるボタンが反応することが無いようになる。
・ドロップダウンを開いている間、BlockerというオブジェクトがCanvasコンポーネント(Order in Layer=29999)をもって画面全体に生成される。
　通常のUIはOrder in Layer=0なので、開いているドロップダウン以外、画面のどこをクリックしてもUIは反応せず、「開いていたドロップダウンが閉じる」という操作になる。

OVR_EventSystemにおける当たり判定(ray判定優先度)は、CanvasのOrder in Layerではなく、OVR RaycasterのSort Order値に依存します。
このSort Order値が、通常のcanvasのOrder in Layerと同じように設定されてくれれば良いのですが、どうやらすべて0で設定される様子。

■対応
OVRRaycaster.csあたりを改変して、OVR Raycasterの付与時にCanvasと同じようなOrderを設定するようにするのが正攻法ですが、Oculus Integrationのアップデート時に問題が生じても嫌なのでオブジェクトの初期値を工夫して対応。

・ベースとなるCanvasについているOVR RaycasterのSort Orderをマイナスに設定する(-100など)
・ドロップダウンの子オブジェクトであるTemplateに予めOVR Raycasterをアタッチしておき、Sort Orderを30000に設定しておく。（展開されたドロップダウンもSort Order = 30000で生成されるようになる。）

これにより、
・開いたドロップダウン(Sort Order = 30000)が最優先で判定される
・ドロップダウンが開いている間に生成されるBlockerはデフォルト(Sort Order = 0)なのでその次に優先。
・それ以外のUIはSort Order = -100なので、ドロップダウンが開いている間はBlockerに遮られて反応しない。

というように、非VR環境での動作と同じように動いてくれます。


###プレイヤーサイズ変更時のuiポインタ描写
OVRPlayerControllerのlocalScaleを変えると、VR UI操作の位置がずれる

■事象
VR UIの操作を、目線ではなくコントローラで操作するようにしていました。
(これはOVR_EventSystemのRay Transformと、OVRGazePointerのRay Transformにコントローラを登録するだけで実装できます。)

そんな中、VRプレイヤーのサイズを変更して、小人や巨人になるような状況を試してみたところ、ポインタ(下図の青い丸)の表示位置がずれて見える問題が発生しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421201/f55a888a-8328-cad5-d4fb-cb31e1e014b4.png)

デフォルトでは視線で操作するため違和感が薄い(真正面なので)ですが、コントローラで操作する場合、HMDの位置とコントローラの位置が遠くなるほど違和感がひどくなります。

■原因
ポインタ(OVRGazePointer)の位置は間違っていませんでした。
このポインタ、右目用と左目用で別々のオブジェクトが用意されていて、右目用はRayが命中したcanvas座標より奥側、左目用は手前側に配置することで、立体的にそれっぽく見せている様子です。
プレイヤーのサイズが変わっても、この左右の視差のための補正値が変わらないため、立体視すると異常な位置に見えると思われます。


■対応
これを修正するのは非常に面倒なので、独自のポインタオブジェクトに変えました。

ポインタオブジェクトとして、あらかじめ適当に色つけてCollider消したSphereをHierarchy上に作っておきます。

修正するのはOVRInputModule.csのみ。変更箇所は以下だけ

[public class OVRInputModule内で変数宣言]
以下を追加

```c#
public Transform MyCursor;   //作ったSphereを登録する。
public Transform vrCamera;   //OVRPlayerControllerの中にあるCenterEyeAnchorを登録
```

[virtual protected MouseState GetGazePointerData()の中]
以下の箇所でポインタの位置を指定しているので、これをコメントアウト
>m_Cursor.SetCursorStartDest(rayTransform.position, worldPos, normal);

同じ場所に、代わりに以下を追加

```c#
MyCursor.transform.position = worldPos;
float curScale = Vector3.Distance(MyCursor.position, vrCamera.position) * 0.03f;
if (curScale > 0.03f) curScale = 0.03f; //遠くから見た時の最大サイズ
MyCursor.localScale = new Vector3(curScale, curScale, curScale);
```
以下のifで、rayがVR用canvasに当たっている時の処理をしている。
>if (ovrRaycaster)

そのifの最後にelseを追加。（VR用canvasに当たっていない時は、ポインタを遠くに逃がしておく）

```c#
else MyCursor.transform.position = new Vector3(0.0f, 1000.0f, 0.0f);
```
要は、操作するメニュー画面が大きくても小さくても、遠くても近くても操作できるように以下の仕様にしています。
・rayが当たった位置にポインタオブジェクトを配置
・rayがメニューにあたっていない間はポインタは表示しない。
・ポインタオブジェクトの大きさはカメラとの距離に依存させる(遠くても近くても同じ大きさに見えるように)
・ポインタオブジェクトの最大サイズは決めておく（非常に遠くからコントローラをメニューに向けた時のため）

同スクリプト内には、もう1箇所「m_Cursor.SetCursorStartDest()」を呼んでいるところがあるので、そっちも改変しても良いも？（しなくても支障ありませんでしたが）

###メニューの操作をunity editor上でデバッグ
VRコントローラによるVRメニューの操作をUnity Editor上でデバッグしたい。

■事象
ちょっとしたメニュー操作のデバッグのためにquestをかぶりたくない。
あるいは作業環境が悪くてoculus link使えない。（ビルドしないとデバッグできない）
そんな時に、キーボードからメニューを操作してデバッグする。


■対応
OVRInputModule.csの以下の箇所を、
>var pressed = Input.GetKeyDown(gazeClickKey) || OVRInput.GetDown(joyPadClickButton);
>var released = Input.GetKeyUp(gazeClickKey) || OVRInput.GetUp(joyPadClickButton);

以下のようにtキーでも反応するように変更するだけ。

```c#
var pressed = Input.GetKeyDown(gazeClickKey) || OVRInput.GetDown(joyPadClickButton) || Input.GetKeyDown("t");
var released = Input.GetKeyUp(gazeClickKey) || OVRInput.GetUp(joyPadClickButton) || Input.GetKeyUp("t");
```
Rayを発射するオブジェクト(コントローラ等)をSceneビューで動かしてtキーを押せば、VR環境での操作にかなり近い状況でのデバッグができます。


#その他
###playerのpositionを動かしてもワープしない
OVRPlayerControllerを座標指定で強制移動させる際の問題

■事象
VRプレイヤーが地面を抜けて落ちた時、yを指定して復帰されようとOVRPlayerControllerのpositionを動かしたが効果がない。

■原因/対応
VRに限らずありがちなもんだいですが、OVRPlayerControllerにはcharactor controllerが付与されているため、無効化しないと駄目です。
落下時から復帰する場合、デフォルトでは無制限に落下加速するので、ついでにFallSpeedも0.0fに戻しておきましょう。

```c#
OVRPlayerController.GetComponent<CharacterController>().enabled = false;
OVRPlayerController.transform.position = new Vector3(OVRPlayerController.transform.position.x, fukkipoint_y, OVRPlayerController.transform.position.z);
OVRPlayerController.GetComponent<CharacterController>().enabled = true;
OVRPlayerController.GetComponent<OVRPlayerController>().FallSpeed = 0.0f;
```


###コントローラでオブジェクトを掴む
VRコントローラでオブジェクトを使う方法は色々とありますが、実装方法によって色々と問題が生じます。

■方法1　掴んだオブジェクトを、コントローラの子供にする。

```c#
対象オブジェクト.transform.SetParent(コントローラ,false)
```
で、コントローラの子オブジェクトにしてしまえば、掴んだ瞬間の相対座標を維持できますので、好きな角度で手に貼り付けられます。
Rigidbodyで動いている物体でも、

```c#
対象オブジェクト.GetComponent<Rigidbody>().velocity = Vector3.zero;
対象オブジェクト.GetComponent<Rigidbody>().isKinematic = true;
```
で停止しておけば処理できます。

ただし、掴んだ相手の親子関係を破壊することになりますので、複数オブジェクトの親子関係で構成されているような物体を持つと壊れます。


■方法2　掴んだオブジェクトの座標を固定する。
掴んだオブジェクトの、コントローラからの相対position/rotationを変数に記録しておき、Update()内で毎フレームその値に固定し続けば、相手オブジェクトの親子関係を壊すことなく掴めます。
ただし、方法1よりもオブジェクトが荒ぶりやすいです。LateUpdateで補正すれば見た目はマシになりますが。
Rigidbodyが付与されている場合、たとえ座標を固定しても重力や衝突による加速がたまり続けるので、方法1と同様にRigidbodyは止めておくのが無難です。

■方法3　FixedJointを使う

方法1,2では、掴んだオブジェクトに無理な負荷がかかっても掴み続けます。
たとえば、短いチェーンの両端を右手と左手でもって引っ張ると、チェーンをつなぐjointが壊れるか、あるいは物理挙動がバグります。

FixedJointを使って掴む方法なら、掴んだ物体に無理な負荷がかかった際に、自動的に手放すことができます。
ただし掴めるオブジェクトはRigidbodyを付与されたものに限定されます。


あらかじめコントローラの配下にRigidbodyをもったオブジェクトを用意しておきます。
このRigidbodyはConstrainsで全Position/Rotationを固定しておき、IsKinematicも有効化しておきます。(手から離れて飛んでいっても困りますので)
Massが大きすぎると荒ぶるので1、Drag/AngularDragはInfinityで問題ありません。
このオブジェクトに対して、スクリプト内でFixedJointを付与します。

以下変数を定義しておきます。

```c#
private GameObject GrabbedObj; //手に持っているオブジェクトの格納先
private GameObject GrabJointObj; //先程作ったRigidbody付与のオブジェクトを登録
private Joint GrabJoint = null; //付与されたjointを格納

private bool isTriggerDown = false; //操作用。トリガが押されているかどうか
private OVRInput.Axis1D Trigger; //トリガの格納。いろんなコントローラに対応できるように変数化
```

Triggerには、Start()内で適当なものを入れるようにします。
たとえばquestの右手コントローラのTriggerであれば、以下のようにしておきます。

```c#
Trigger = OVRInput.Axis1D.SecondaryIndexTrigger;
```

オブジェクトを手放した際の処理を作っておきます。
オブジェクトを手放す=FixedJointから開放 ですが、FixedJointで繋がれた物体は自動的にRigidbodyが停止してしまい、Jointから開放されても動きません。
何らかのパラメータを変化させればRigidbodyが復活するので、手放した台ミンツでuseGravityを反転し、再びもとに戻します。

```c#
private void GrabbedObjRelease()
{
    GrabbedObj.GetComponent<Rigidbody>().useGravity = !GrabbedObj.GetComponent<Rigidbody>().useGravity;
    GrabbedObj.GetComponent<Rigidbody>().useGravity = !GrabbedObj.GetComponent<Rigidbody>().useGravity;
    GrabbedObj = null;
}
```


Update()内で、先のオブジェクトにFixedJointを付与します。
無理な持ち方をした際に壊れるように、braekForce/breakTorqueを調整しておきます。
作り直す = 壊れたということなので、現在掴んでいるオブジェクトを離します。

```c#
if (GrabJoint == null)
{
    //Debug.Log("joint Break");
    GrabJoint = GrabJointObj.AddComponent<FixedJoint>();
    GrabJoint.enablePreprocessing = false;
    GrabJoint.breakForce = 2000.0f;
    GrabJoint.breakTorque = 2000.0f;

    if (GrabbedObj != null)
        GrabbedObjRelease();
}
```

Triggerが押されている間、isTriggerDown=trueにします。
Triggerが押されていない時はisTriggerDown=falseにする他、
現在掴んでいるオブジェクトがある場合は離します。

```c#
if ((OVRInput.Get(Trigger)) > 0)
    isTriggerDown = true;
else 
{
    isTriggerDown = false;

    if (GrabbedObj != null)
    {
        GrabJoint.connectedBody = null; //jointから開放
        GrabbedObjRelease();
    }
}
```


最後に掴む処理です。
これの発動トリガはケースバイケースだと思いますが、自分は、isTriggerのColliderを付与しておき、これに別のオブジェクトが触れた際に、それを掴む仕組みにしています。

```c#
void OnTriggerStay(Collider col)
{
 //トリガが押されていて、現在何も掴んでおらず、
 //Jointも壊れていない場合に掴み処理が可能
 if (isTriggerDown && (GrabbedObj == null) && (GrabJoint != null) )
  if (col.gameObject.name == 対象オブジェクトが掴んで良い物かのフィルタ)
  {
    GrabbedObj = col.gameObject;
    GrabJoint.connectedBody = GrabbedObj.GetComponent<Rigidbody>();
  }
 }
}
```

この方法だと複数のオブジェクトに同時に触れた場合などに問題が生じますので、Project SettingsのPhysicsで、掴むことを許容しないオブジェクトとは、なるべく当たり判定が無いように調整しておきます。

実際には「そのオブジェクトが既にもう片方の手で掴まれている場合はどうするか？」や、「オブジェクトが特定の状態には掴めなくする」などの処理のため、色々な分岐を入れる必要がありますが、上記の実装だけでも最低限は動きます。