OculusGoコントローラでオブジェクトを掴んで投げてみる

![oculusgo.jpg](https://qiita-image-store.s3.amazonaws.com/0/101808/db348e46-dffa-3a29-c24c-f52ba61e15fd.jpeg)

この記事はOculusRiftアドベントカレンダー10日目の記事です。

#はじめに

Oculus GoでVirtual Virtual Realityというゲームを遊んでみたのですがVR空間で「物を掴んで投げる」だけで結構楽しそうだなあと思ったので、勉強がてら実装してみることにしました。ちなみに筆者はUnity,C#ともに初心者です。

#成果物
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/oculusgo?src=hash&amp;ref_src=twsrc%5Etfw">#oculusgo</a> で物を掴んで投げる機能実装できた。 <a href="https://t.co/iqvMLS4ifn">pic.twitter.com/iqvMLS4ifn</a></p>&mdash; やぶからぼう (@_5ENA) <a href="https://twitter.com/_5ENA/status/1072168201520242688?ref_src=twsrc%5Etfw">2018年12月10日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

こんな感じでオブジェクトを掴んで移動させたり、投げ飛ばすことができるようにしてみました。
Virtual Virtual Realityとは少し操作方法が異なります。トリガーを押し込むとオブジェクトを選択。トリガーを離すと選択解除。オブジェクト選択中にタッチパッドで選択オブジェクトとの距離を長くしたり、短くしたりできます。

#実装

以下のスクリプトを作成し、OculusGoのコントローラにでもアタッチするだけでいけるはずです。

``` HandController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class HandController : MonoBehaviour {

    struct TransformInfo
    {
        public Vector3 position;
        public Quaternion rotation;
        public TransformInfo(Transform transform)
        {
            position = transform.position;
            rotation = transform.rotation;
        }
    }

    private GameObject selectedObject;                       // 選択中のオブジェクト
    private Vector2 touchDeltaPosition;                      // タッチパッドの移動量
    private TransformInfo selectedObjectOriginTransformInfo; // 選択した瞬間のオブジェクトの情報
    private TransformInfo originTransformInfo;               // 選択した瞬間のコントローラーの情報
    private float distance;                                  // コントローラとオブジェクトの距離

	// Update is called once per frame
	void Update () {

        if (OVRInput.GetDown(OVRInput.Button.PrimaryIndexTrigger))
        {
            onTriggerDown();
        }

        if (OVRInput.GetUp(OVRInput.Button.PrimaryIndexTrigger))
        {
            onTriggerUp();
        }

        if (OVRInput.GetDown(OVRInput.Touch.One))
        {
            this.onTouchBegan();
        }

        if (OVRInput.Get(OVRInput.Touch.One))
        {
            this.onTouching();
        }

        if (OVRInput.GetUp(OVRInput.Touch.One))
        {
            this.onTouchEnded();
        }

        this.updateSelectedObjectTransform();
    }

    void onTriggerDown()
    {
        GameObject selectedObject = getSelectedObject();
        if (!selectedObject) return;

        this.selectedObjectOriginTransformInfo = new TransformInfo(selectedObject.transform);
        this.originTransformInfo = new TransformInfo(this.transform);
        this.distance = Mathf.Abs(Vector3.Distance(this.selectedObjectOriginTransformInfo.position,this.originTransformInfo.position));

        activateObject(selectedObject, false);

        this.selectedObject = selectedObject;
    }

    void onTriggerUp()
    {
        activateObject(selectedObject, true);
        selectedObject = null;
    }

    // タッチパッドを触った瞬間
    void onTouchBegan()
    {
        this.touchDeltaPosition = OVRInput.Get(OVRInput.Axis2D.PrimaryTouchpad);
    }

    // タッチパッドを触っているとき
    void onTouching()
    {
        float deltaMoveY = OVRInput.Get(OVRInput.Axis2D.PrimaryTouchpad).y - this.touchDeltaPosition.y;
        float sensitivey = 3.0f;
        this.distance = this.distance + deltaMoveY * sensitivey;

        this.touchDeltaPosition = OVRInput.Get(OVRInput.Axis2D.PrimaryTouchpad);
    }

    // タッチパッドを離した瞬間
    void onTouchEnded()
    {

    }

    // Rayを飛ばして選択すべきオブジェクトを返す
    GameObject getSelectedObject()
    {
        Ray ray = new Ray(this.transform.position, this.transform.forward);
        RaycastHit hitInfo;
        float maxDistance =400.0f;

        if (Physics.Raycast(ray, out hitInfo, maxDistance))
        {
            return hitInfo.collider.gameObject;
        }

        return null;
    }

    // オブジェクトの有効無効設定を切り替える
    void activateObject(GameObject gameObject, bool activate)
    {
        if (gameObject == null) return;

        BoxCollider collider = gameObject.GetComponent<BoxCollider>();

        Rigidbody rb = gameObject.GetComponent<Rigidbody>();
        rb.useGravity = activate;

        float drag = 2.0f;
        if (activate) drag = 0.0f;
        rb.drag = drag;

        Color color = Color.blue;
        if (activate) color = Color.red;

        gameObject.GetComponent<MeshRenderer>().material.color = color;
    }

    //　選択オブジェクトの位置と角度を更新
    void updateSelectedObjectTransform()
    {
        if (this.selectedObject == null) return;

        Quaternion deltaRotation = this.transform.rotation* Quaternion.Inverse(this.originTransformInfo.rotation);

        Vector3 targetPos = (this.transform.position - deltaRotation * Vector3.Normalize(this.originTransformInfo.position - this.selectedObjectOriginTransformInfo.position) * this.distance);

        Vector3 delta = targetPos - this.selectedObject.transform.position;

        Rigidbody rb = this.selectedObject.GetComponent<Rigidbody>();
        rb.AddForce(Vector3.Normalize(delta)*delta.sqrMagnitude*10); // 適当に２乗して１０倍してる

        this.selectedObject.transform.rotation = deltaRotation * this.selectedObjectOriginTransformInfo.rotation;
    }
}

```

#あとがき
座標系の変換を何度か行っていたり、クォータニオンがでてきたり、計算部分が少し複雑でとっつきにくい感じになってしまいました。自分もちゃんと理解しているわけではないので当然なんですが、座標系の計算を読みやすく記述するのってなかなか難しいですね。