LoginSignup
9
11

More than 5 years have passed since last update.

OculusTouch対応のゲームを作る手順:オブジェクトを掴む、運ぶ、投げるを実装する

Posted at

OculusTouch対応のゲームを作る手順目次はこちら

概要

VRFunHouseのオブジェクトをVR空間内で掴む機能の実装を紹介します。
掴むというのは、レーザー光線のようなものを飛ばして衝突した物を引き寄せて掴むことを言っています。遠くのものを掴めるのは座りながらVRを遊ぶときとか、落としたオブジェクトを拾うときにあると便利な機能になります。
考え方としては以下のようになります。

  1. OculusTouchの位置からRayを飛ばして遠くにあるオブジェクトを捕捉
  2. OculusTouchのトリガーの操作で手元にオブジェクトを引き寄せキャッチする
  3. キャッチしたオブジェクトをリリースすると、コントローラーの速度をオブジェクトに与えて投げる

完成図

OculusTouch.gif

クラス

今回紹介するクラスの内容を説明します。

全体のクラス図

image.png

HandRaycaster

HandRaycasterはObjectを捕捉を検知するクラスです。
左右のOculus Touchコントローラーの位置からRayを飛ばして、Rayが衝突したObject(EventBase)に対してExecuteEventsでメッセージを送信します。
ExecuteEventsについてはこのリンク先を参照
ExecuteEventsを実行するタイミングは以下の3つです。
1. Rayが衝突し始めた時 (OnStartTouchEvent)
2. Rayが衝突している最中 (OnInTouchEvent)
3. Rayが衝突し終わった時 (OnEndTouchEvent)

EventBase

このクラスを継承するとHandRaycasterからRay衝突時のメッセージを受信する事ができます。

TouchObject

引き寄せる、掴む、運ぶ、投げる対象のオブジェクトにアタッチして利用します。また、それらの処理は全てこのクラス内のOnStartTouchEvent、OnInTouchEvent、OnEndTouchEvent内に書かれることになります。

ポイントとしては、TouchObjectにはRigitBodyがアタッチされているが掴んだ直後から掴んでいる最中は物理演算を切っておくこと。
m_rigit.isKinematic = true;
m_rigit.useGravity = false;
また、オブジェクトの掴みをリリースした後は物理演算を有効にするとともに、OculusTouchの速度と角速度をRigitBodyに与えると、いい感じに投げる事ができます。
m_rigit.velocity = OVRInput.GetLocalControllerVelocity (hitEvent.isLeftHand ? OVRInput.Controller.LTouch : OVRInput.Controller.RTouch);
m_rigit.angularVelocity = (OVRInput.GetLocalControllerAngularVelocity (hitEvent.isLeftHand

HandControllerManager

このスクリプトは適当なGameObjectにアタッチして、LineRenderのPrefabをセットして利用します。
機能としては主にOculusTouchのInputをラッパーする働きがあります。また、LocalAvatar内の右手、左手の位置にHandRaycasterをセットする機能もあります。このクラスは、Oculus Touchのセットアップが完了してOculus UtilitiesとOculus Avatarを導入した後に利用できます。

コード詳細

上記で紹介したコードの詳細を以下に示します。

HandRaycaster.cs
using UnityEngine;
using System.Collections;
using UnityEngine.EventSystems;
using System.Linq;
using UniRx;
using UniRx.Triggers;
using System;

/// <summary>
/// RayCastを発射
/// </summary>
public class HandRaycaster : MonoBehaviour
{

    /// <summary>
    /// どちらの手か
    /// </summary>
    public bool isLeftHand {
        get {
            return isLeft;
        }

        set {
            isLeft = value;
        }
    }

    /// <summary>
    /// レイキャストをロックする
    /// ロックすると、手近傍のTouchObjectを掴むモードになる
    /// </summary>
    /// <value><c>true</c> if is lock raycast; otherwise, <c>false</c>.</value>
    public bool isLockRaycast {
        get;
        set;
    }

    /// <summary>
    /// 掴んだものをキャッチする場所
    /// </summary>
    /// <value>The catch position.</value>
    public GameObject catchPosition {
        get;
        set;
    }


    public GameObject lineRenderPrefab {
        set {
            m_lineRenderPrefab = value;
        }
    }

    [SerializeField]
    bool isLeft = true;

    [SerializeField]
    GameObject m_lineRenderPrefab;

    GameObject m_lineRenderObj;

    GameObject m_hitObject;

    bool m_isCatchObject = false;

    Action m_task;
    float m_time;


    void Start ()
    {
        isLockRaycast = false;
        m_task = DispLineRenderTask;
        var racastHitStream = 
            this.UpdateAsObservable ()
                .Where (p => (HandControllerManager.Instance.IsGetHandTrigger (isLeft)))
                .Where (p => !m_isCatchObject)
                .Select (p => this.RaycastEventBase ())
                .Where (p => p != null);

        racastHitStream.Subscribe (p => ExcuteOnStartEvent (p)).AddTo (gameObject);

    }


    void Update ()
    {
        if (m_task != null) {
            m_task ();
        }
    }

    /// <summary>
    /// 掴んだ瞬間
    /// </summary>
    /// <param name="hitEvent">Hit event.</param>
    void ExcuteOnStartEvent (EventBase hitEvent)
    {
        m_isCatchObject = true;

        Debug.Log ("ExcuteOnStartEvent");
        m_hitObject = hitEvent.transform.gameObject;
        catchPosition.transform.position = m_hitObject.transform.position;
        hitEvent.time = 0f;

        // インターフェースを継承しているコンポーネントのメソッドを呼び出し
        ExecuteEvents.Execute<IEventReceiver> (
            target: m_hitObject, // 呼び出す対象のオブジェクト
            eventData: null,  // イベントデータ(モジュール等の情報)
            functor: (recieveTarget, y) => recieveTarget.OnStartTouchEvent (this)); // 操作

        m_time = 0f;
        m_task = WaitHandOutTask;

    }

    /// <summary>
    /// 掴んでいる最中
    /// </summary>
    /// <param name="time">Time.</param>
    void ExcuteOnInEvent (float time)
    {
        var eventBase = m_hitObject.GetComponent<EventBase> ();
        if (m_lineRenderObj != null) {
            m_lineRenderObj.GetComponent<LineRenderer> ().SetPositions (new Vector3[2] {
                transform.position,
                m_hitObject.transform.position
            });
        }

        eventBase.time = time;

        // インターフェースを継承しているコンポーネントのメソッドを呼び出し
        ExecuteEvents.Execute<IEventReceiver> (
            target: m_hitObject, // 呼び出す対象のオブジェクト
            eventData: null,  // イベントデータ(モジュール等の情報)
            functor: (recieveTarget, y) => recieveTarget.OnInTouchEvent (this)); // 操作
    }


    /// <summary>
    /// 掴んだものを離した時
    /// </summary>
    /// <param name="time">Time.</param>
    void ExcuteOnEndEvent (float time)
    {
        Debug.Log ("ExcuteOnEndEvent");
        m_isCatchObject = false;
        var eventBase = m_hitObject.GetComponent<EventBase> ();
        eventBase.time = time;

        if (m_lineRenderObj != null) {
            Destroy (m_lineRenderObj);
            m_lineRenderObj = null;
        }

        m_task = DispLineRenderTask;

        // インターフェースを継承しているコンポーネントのメソッドを呼び出し
        ExecuteEvents.Execute<IEventReceiver> (
            target: m_hitObject, // 呼び出す対象のオブジェクト
            eventData: null,  // イベントデータ(モジュール等の情報)
            functor: (recieveTarget, y) => recieveTarget.OnEndTouchEvent (this)); // 操作
    }


    /// <summary>
    /// EventBaseを継承したObjectにRayCastが衝突したかどうか
    /// </summary>
    /// <returns>The event base.</returns>
    EventBase RaycastEventBase ()
    {

        if (this.isLockRaycast) {
            Collider[] hitColliders = Physics.OverlapSphere (transform.position, 0.12f);
            return hitColliders.Where (p => p.gameObject.GetComponent<EventBase> () != null).Select (p => p.GetComponent<EventBase> ()).FirstOrDefault ();
        } else {
            RaycastHit hit;

            if (Physics.Raycast (this.transform.position, this.transform.forward * 10, out hit)) {
                var eventBase = hit.transform.gameObject.GetComponent<EventBase> ();
                if (eventBase != null) {
                    CreateLineRender (hit.transform.position);
                    return eventBase;
                } else {
                    return null;
                }
            }
            //Rayを画面に表示
            Debug.DrawRay (this.transform.position, this.transform.forward * 10, Color.red, 5, false);
            return null;
        }


    }


    /// <summary>
    /// ラインレンダーを作成
    /// </summary>
    /// <param name="targetpos">Targetpos.</param>
    void CreateLineRender (Vector3 targetpos)
    {
        if (m_lineRenderObj == null) {
            m_lineRenderObj = GameObject.Instantiate (m_lineRenderPrefab) as GameObject;
            var positions = new Vector3[2] { transform.position, targetpos };
            var lineRender = m_lineRenderObj.GetComponent<LineRenderer> ();
            lineRender.material = new Material (Shader.Find ("Particles/Multiply"));
            lineRender.widthMultiplier = 0.2f;
            lineRender.numPositions = positions.Length;
            lineRender.SetWidth (0.01f, 0.01f);
            lineRender.SetColors (Color.red, Color.blue);
            lineRender.SetPositions (positions);
        } else {
            m_lineRenderObj.GetComponent<LineRenderer> ().SetPositions (new Vector3[2] { transform.position, targetpos });
        }
    }


    /// <summary>
    /// ラインレンダーを作るタスク
    /// </summary>
    void DispLineRenderTask ()
    {
        if (isLockRaycast) {
            return;
        }
        if ((HandControllerManager.Instance.IsGetHandTrigger (isLeft))) {
            CreateLineRender (this.transform.position + transform.forward * 10);
        }
        if (HandControllerManager.Instance.IsGetUpHandTrigger (isLeft)) {
            if (m_lineRenderObj != null) {
                Destroy (m_lineRenderObj);
                m_lineRenderObj = null;
            }
        }

    }


    /// <summary>
    /// 掴んでいる最中、掴んだものを離すタイミングにEventを発行
    /// </summary>
    void WaitHandOutTask ()
    {
        if (HandControllerManager.Instance.IsGetUpHandTrigger (isLeft)) {
            m_task = null;
            ExcuteOnEndEvent (m_time);
            m_time = 0f;

        } else if (HandControllerManager.Instance.IsGetHandTrigger (isLeft)) {
            ExcuteOnInEvent (m_time);
        }
        m_time += Time.deltaTime;
    }
}
EventBase.cs
using UnityEngine;
using System.Collections;
using System;
using System.Linq;

/// <summary>
/// RayCastの衝突イベントを受け取る基底クラス
/// </summary>
public class EventBase : MonoBehaviour,IEventReceiver
{

    /// <summary>
    /// 衝突された時間
    /// </summary>
    public float time;

    void Awake ()
    {

    }


    /// <summary>
    /// Raises the start event event.
    /// </summary>
    public virtual void OnStartTouchEvent (HandRaycaster hitEvent)
    {
    }


    /// <summary>
    /// Raises the end gaze event event.
    /// </summary>
    /// <param name="time">Time.</param>
    public virtual void OnEndTouchEvent (HandRaycaster hitEvent)
    {

    }

    /// <summary>
    /// Raises the in gaze event event.
    /// </summary>
    /// <param name="time">Time.</param>
    public virtual void OnInTouchEvent (HandRaycaster hitEvent)
    {

    }



}

TouchObject.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using UniRx.Triggers;
using Unity.Linq;
using System.Linq;
using System;

/// <summary>
/// つかむことができるもの
/// </summary>
public class TouchObject : EventBase
{

    /// <summary>
    /// タッチした時
    /// </summary>
    public ReactiveProperty<bool> IsTouched = new ReactiveProperty<bool> (false);

    /// <summary>
    /// 掴んだ時
    /// </summary>
    public BoolReactiveProperty IsCatch = new BoolReactiveProperty (false);

    /// <summary>
    /// 掴んだものをリリースしたとき
    /// </summary>
    public BoolReactiveProperty IsReleased = new BoolReactiveProperty (false);

    public enum LockHand
    {
        None,
        Left,
        Right,
    }

    /// <summary>
    /// 操作をロックする手
    /// </summary>
    /// <value><c>true</c> if this instance is lock hand; otherwise, <c>false</c>.</value>
    public LockHand IsLockHand {
        get {
            return m_isLockHand;
        }
    }

    /// <summary>
    /// 操作している方の手が左手か
    /// </summary>
    /// <value><c>true</c> if this instance is left hand; otherwise, <c>false</c>.</value>
    public bool IsLeftHand {
        get {
            return m_isLockHand == LockHand.Right;
        }
    }

    [SerializeField] Transform m_glip;

    OVRHapticsClip m_hapticsClip;


    Rigidbody m_rigit;

    float m_moveToHandDistance;

    LockHand m_isLockHand;

    Renderer m_render;

    // Use this for initialization
    void Start ()
    {
        m_render = GetComponent<Renderer> ();
        if (m_render == null) {
            m_render = GetComponentInChildren<Renderer> ();

        }
        m_rigit = this.gameObject.AncestorsAndSelf ().Where (p => p.GetComponent<Rigidbody> () != null).Select (p => p.GetComponent<Rigidbody> ()).First ();
        m_isLockHand = LockHand.None;

        IsCatch.Value = false;

        IsTouched.Value = false;
        IsTouched.Subscribe (p => InTouchChanged ());
    }


    public override void OnStartTouchEvent (HandRaycaster hitEvent)
    {
        base.OnStartEvent (hitEvent);
        if (m_isLockHand == LockHand.None) {
            m_isLockHand = hitEvent.isLeftHand ? LockHand.Right : LockHand.Left;
        } else {
            return;
        }
        m_moveToHandDistance = 0f;

        IsTouched.Value = true;
        IsReleased.Value = false;
    }

    public override void OnInTouchEvent (HandRaycaster hitEvent)
    {
        base.OnInCatchEvent (hitEvent);
        if (hitEvent.isLeftHand && m_isLockHand == LockHand.Left) {
            return;
        } else if (!hitEvent.isLeftHand && m_isLockHand == LockHand.Right) {
            return;
        }
        MoveFollowHand (hitEvent);

        if (HandControllerManager.Instance.IsGetDownIndexTrigger (hitEvent.isLeftHand)) {
        } else if (HandControllerManager.Instance.IsGetIndexTrigger (hitEvent.isLeftHand)) {
            MoveDeltaToHand (hitEvent);
        }
    }


    public override void OnEndTouchEvent (HandRaycaster hitEvent)
    {
        base.OnEndTouchEvent (hitEvent);
        if (hitEvent.isLeftHand && m_isLockHand == LockHand.Left) {
            return;
        } else if (!hitEvent.isLeftHand && m_isLockHand == LockHand.Right) {
            return;
        }

        IsCatch.Value = false;
        m_isLockHand = LockHand.None;
        m_moveToHandDistance = 0f;
        IsTouched.Value = false;
        Debug.Log ("notTouched");
        m_rigit.isKinematic = false;
        m_rigit.useGravity = true;
        //投げるときにコントローラーの速度、各速度をRigitBodyに与える
        m_rigit.velocity = OVRInput.GetLocalControllerVelocity (hitEvent.isLeftHand ? OVRInput.Controller.LTouch : OVRInput.Controller.RTouch);
        m_rigit.angularVelocity = (OVRInput.GetLocalControllerAngularVelocity (hitEvent.isLeftHand ? OVRInput.Controller.LTouch : OVRInput.Controller.RTouch).eulerAngles * (Mathf.PI / 180f));
        IsReleased.Value = true;
    }


    void InTouchChanged ()
    {
        if (IsTouched.Value) {
            Debug.Log ("touched");
            m_rigit.isKinematic = true;
            m_rigit.useGravity = false;
        } 
    }


    /// <summary>
    /// 手のコントローラーの位置の動きと連動する
    /// </summary>
    /// <param name="hand">Hand.</param>
    void MoveFollowHand (HandRaycaster hand)
    {
        var vec = this.transform.position - hand.transform.position;
        var vece = vec.normalized;
        if (Vector3.Distance (this.transform.position, hand.transform.position) - m_moveToHandDistance > 0.05f) {
            this.transform.position = hand.catchPosition.transform.position - vece * m_moveToHandDistance;

        } else {
            IsCatch.Value = true;
            this.transform.position = hand.transform.position;
            if (m_glip != null) {
                this.transform.position = this.transform.position + this.transform.position - m_glip.transform.position;
            }
        }
        this.transform.localRotation = OVRInput.GetLocalControllerRotation (hand.isLeftHand ? OVRInput.Controller.LTouch : OVRInput.Controller.RTouch);
    }


    /// <summary>
    /// 手の位置に引き寄せる
    /// </summary>
    /// <param name="hand">Hand.</param>
    void MoveDeltaToHand (HandRaycaster hand)
    {
        if (Vector3.Distance (this.transform.position, hand.transform.position) - m_moveToHandDistance > 0.05f) {
            m_moveToHandDistance += Time.deltaTime * 5;
        }

    }

}
HandControllerManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

/// <summary>
/// ハンドコントローラー
/// </summary>
public class HandControllerManager : MonoBehaviour
{

    public static HandControllerManager Instance;

    /// <summary>
    /// 左手のTransform
    /// </summary>
    /// <value>The left hand.</value>
    public Transform leftHand {
        get;
        private set;
    }

    /// <summary>
    /// 右手のTransform
    /// </summary>
    /// <value>The right hand.</value>
    public Transform rightHand {
        get;
        private set;
    }

    [SerializeField] GameObject m_lineRenderPrefab;


    /// <summary>
    /// LocalAvatorの手にHandLaycasterを生成
    /// </summary>
    /// <returns>The hand raycaster.</returns>
    /// <param name="name">Name.</param>
    /// <param name="targetRoot">Target root.</param>
    /// <param name="isLeft">If set to <c>true</c> is left.</param>
    Transform CreateHandRaycaster (string name, Transform targetRoot, bool isLeft)
    {
        var handRoot = new GameObject (name);
        handRoot.transform.parent = targetRoot;
        handRoot.transform.localPosition = Vector3.zero;
        handRoot.transform.localRotation = Quaternion.Euler (new Vector3 (0, 0, 0));
        handRoot.transform.localScale = Vector3.one;
        var handr = handRoot.AddComponent<HandRaycaster> ();
        handr.lineRenderPrefab = m_lineRenderPrefab;
        var catchPos = new GameObject ("catchPosition");
        catchPos.transform.parent = handRoot.transform;
        catchPos.transform.localPosition = new Vector3 (0, 0.277f, 0);
        catchPos.transform.localRotation = Quaternion.identity;
        catchPos.transform.localScale = Vector3.one;
        handr.catchPosition = catchPos;
        handr.isLeftHand = isLeft;
        return handRoot.transform;
    }

    private void Awake ()
    {
        Instance = this;
        var localAvator = FindObjectsOfType<OvrAvatar> ().Where (p => p.ShowFirstPerson == true && p.ShowThirdPerson == false).First ();
        if (localAvator == null) {
            Debug.Log ("Not Found LocalAvator");
            return; 
        }
        leftHand = CreateHandRaycaster ("LeftHandRoot", localAvator.HandLeft.transform, true);
        rightHand = CreateHandRaycaster ("RightHandRoot", localAvator.HandRight.transform, false);
    }

    public bool IsGetDownHandTrigger (bool isLeft)
    {
        return (OVRInput.GetDown (OVRInput.RawButton.RHandTrigger) && !isLeft) || (OVRInput.GetDown (OVRInput.RawButton.LHandTrigger) && isLeft);
    }

    public bool IsGetHandTrigger (bool isLeft)
    {
        return (OVRInput.Get (OVRInput.RawButton.RHandTrigger) && !isLeft) || (OVRInput.Get (OVRInput.RawButton.LHandTrigger) && isLeft);
    }

    public bool IsGetUpHandTrigger (bool isLeft)
    {
        return (OVRInput.GetUp (OVRInput.RawButton.RHandTrigger) && !isLeft) || (OVRInput.GetUp (OVRInput.RawButton.LHandTrigger) && isLeft);
    }

    public bool IsGetDownIndexTrigger (bool isLeft)
    {
        return (OVRInput.GetDown (OVRInput.RawButton.RIndexTrigger) && !isLeft) || (OVRInput.GetDown (OVRInput.RawButton.LIndexTrigger) && isLeft);
    }

    public bool IsGetIndexTrigger (bool isLeft)
    {
        return (OVRInput.Get (OVRInput.RawButton.RIndexTrigger) && !isLeft) || (OVRInput.Get (OVRInput.RawButton.LIndexTrigger) && isLeft);
    }

    public bool IsGetUpIndexTrigger (bool isLeft)
    {
        return (OVRInput.GetUp (OVRInput.RawButton.RIndexTrigger) && !isLeft) || (OVRInput.GetUp (OVRInput.RawButton.LIndexTrigger) && isLeft);
    }

    public bool IsGetDownA ()
    {
        return OVRInput.GetDown (OVRInput.RawButton.A);
    }

    public bool IsGetDownButton (bool isLeft)
    {
        return (OVRInput.GetDown (OVRInput.RawButton.A) && !isLeft) || (OVRInput.GetDown (OVRInput.RawButton.X) && isLeft);
    }

    public void SetIsLockRaycaster (bool isLeftLock, bool isRightLock)
    {
        this.leftHand.GetComponent<HandRaycaster> ().isLockRaycast = isLeftLock;
        this.rightHand.GetComponent<HandRaycaster> ().isLockRaycast = isRightLock;
    }

    public Transform GetHand (bool isLeft)
    {
        if (isLeft) {
            return this.leftHand;
        }
        return this.rightHand;
    }
}

9
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
11