OculusQuestでゲームをするためのジョイスティックを作る

# はじめに
OculusQuestでゲームをするためにジョイスティックが欲しくなったので作ってみました。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">以前作ったゲームをジョイスティックで動かせるようになった <a href="https://t.co/z48IKOzdG7">pic.twitter.com/z48IKOzdG7</a></p>&mdash; 高浜 (@SatoshiTakahama) <a href="https://twitter.com/SatoshiTakahama/status/1255279568304451585?ref_src=twsrc%5Etfw">April 28, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

XR Interaction Toolkitを使っているのでOculusQuest以外でも動きそうですが未検証です。

#開発環境
Unity 2019.3.10f1
XR Interaction Toolkit 0.9.4
Oculus XR Plugin 1.3.3

#作成方法
##構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215278/e5cc4165-1028-819e-43b3-b0d1ddbf5181.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215278/bdef5bae-96b1-09cd-0599-8d68486fdd9c.png)


##土台(Base)
・ジョイスティック自体をつかんで操作するために、XR Grab Interactableスクリプトをつけます。
・XR Grab Interactableで必要なのでRigidBodyをつけます。
・つかんでいないときに動かないように、RigidBodyのIsKinematic=trueにしておきます。

##スティックの下側のボール(Ball1)
・このオブジェクトを回転させることで、スティックを動かします。
・XR Stick Interactableスクリプト(スティックを操作するための自作スクリプト、後で説明します)で必要なのでRigidBodyをつけます。
・スティックを操作するためのスクリプトはこのオブジェクトにつけてしまうと認識されないため(上位の階層にXRBaseInteractable派生のスクリプトがあるとダメみたいです)、別の階層につけます。

##スティック(Cylinder)
・スティックをつかむ判定に必要なのでColliderをつけます。

##制御(handle)
・XR Stick Interactableスクリプト(スティックを操作するための自作スクリプト)をつけます。
・XR Stick InteractableスクリプトのCollidersには操作対象のスティックのColliderを、MovingRigidBodyには動かすボールのオブジェクトを、OnStickChangeには動きを通知する関数を登録します。
・SetTextは動きの確認表示用、GetInputActionは動きをゲームに伝えるためにつけているスクリプトです。

```C#:XRStickInteractable.cs
using System;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.XR.Interaction.Toolkit;

public class XRStickInteractable : XRBaseInteractable
{
    [Serializable]
    public class StickChangeEvent : UnityEvent<float, float> {}

    public Rigidbody MovingRigidbody;
    public StickChangeEvent OnStickChange;

    private XRBaseInteractor m_GrabbingInteractor;
    private Quaternion m_StartRotation;
    private Vector3 m_CacheTarget;
    private Quaternion m_CacheRotation;


    private Quaternion CheckStickValue(Vector3 target, Vector3 center)
    {
        Quaternion rotation = m_CacheRotation;

        if (m_CacheTarget != target)
        {
            m_CacheTarget = target;

            Vector3 relativePos = target - center;
            if (relativePos.y < 0)
            {
                relativePos.y = 0;
            }
            rotation = Quaternion.LookRotation(relativePos);
            m_CacheRotation = rotation;

            if (OnStickChange != null)
            {
                try
                {
                    float axis_h = Vector3.Angle(transform.right, relativePos);
                    float axis_v = Vector3.Angle(transform.forward, relativePos);
                    float horizontal = (90 - axis_h) / 90;
                    float vertical = (90 - axis_v) / 90;
                    OnStickChange.Invoke(horizontal, vertical);
                }
                catch
                {
                    Debug.LogError("A delegate failed to execute for OnStickChange in XRStickInteractable");
                }
            }
        }
        return rotation;
    }

    // Start is called before the first frame update
    void Start()
    {
        if (MovingRigidbody == null)
        {
            MovingRigidbody = GetComponentInChildren<Rigidbody>();
        }
        if (MovingRigidbody != null)
        {
            m_StartRotation = MovingRigidbody.rotation;
        }
    }

    public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase)
    {
        if (isSelected)
        {
            if (updatePhase == XRInteractionUpdateOrder.UpdatePhase.Fixed)
            {
                if (MovingRigidbody != null)
                {
                    Quaternion rotation = CheckStickValue(m_GrabbingInteractor.transform.position, MovingRigidbody.transform.position);
                    MovingRigidbody.MoveRotation(rotation);
                }
            }
        }
    }

    protected override void OnSelectEnter(XRBaseInteractor interactor)
    {
        base.OnSelectEnter(interactor);

        m_GrabbingInteractor = interactor;
    }

    protected override void OnSelectExit(XRBaseInteractor interactor)
    {
        base.OnSelectExit(interactor);

        if (MovingRigidbody != null)
        {
            MovingRigidbody.MoveRotation(m_StartRotation);
        }
    }
}
```

GetInputAction.csは操作対象のゲームにより書き換える必要があります。
水平方向の動きがhorizontalにより-1～1の範囲で、垂直方向の動きがverticalにより-1～1の範囲で通知されてくるので、それをゲームにあった動きに変換します。GetInputは操作対象のゲームから呼ばれる処理です。

```C#:GetInputAction.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GetInputAction : MonoBehaviour
{
    private float m_CacheHorizontal = 0;
    private float m_CacheVertical = 0;

    public void GetStickValue(float horizontal, float vertical)
    {
        m_CacheHorizontal = horizontal;
        m_CacheVertical = vertical;
    }
    public float[] GetInput()
    {
        var action = new float[2];
        if (m_CacheHorizontal > 0.5f)
        {
            action[1] = 1f; //move right
        }
        else if (m_CacheHorizontal < -0.5f)
        {
            action[1] = 2f; //move left
        }
        if (m_CacheVertical > 0.5f)
        {
            action[0] = 1f; //move up
        }
        if (m_CacheVertical < -0.5f)
        {
            action[0] = 2f; //move down
        }
        return action;
    }
}
```