なおしのこれまで、これから

学んだこと・感じたこと・やりたいこと

VRでオブジェクトを掴む処理を自作する

f:id:vxd-naoshi-19961205-maro:20200712013321p:plain

始めに

過去記事です

shitakami.hatenablog.com


開発中にオブジェクトを掴むプログラムを探してた際にほとんどがAssetStoreのライブラリだったので、自分が使いやすいようにするため自作しました。

また、このプログラムを書くのが2回目だったのでブログにまとめようと思います。



仕様について

これから解説するプログラムの仕様は以下の通りです。

  • オブジェクトを掴むのは1つまで
  • 片方の手で持っているオブジェクトをもう片方が掴み直すことは不可
  • 当たり判定はTrigger
  • コントローラーではなくオブジェクトにRigidbodyを付ける



処理の流れ

簡単に処理の流れについて解説します。


コントローラーとオブジェクトに当たり判定を付ける

コントローラーとオブジェクトに当たり判定(Trigger)を付けます。図では緑色がコントローラー、灰色が掴むオブジェクトです。

f:id:vxd-naoshi-19961205-maro:20200712023618p:plain



触れたオブジェクトを登録する

コントローラーがオブジェクトに触れた際にリストに登録します。

f:id:vxd-naoshi-19961205-maro:20200712024825p:plain



リストからコントローラーに一番近いオブジェクトを選択する

リストに登録されているオブジェクトとコントローラーとの距離を求め、一番距離が短いオブジェクトを選択します。

f:id:vxd-naoshi-19961205-maro:20200712025130p:plain


コントローラーが複数のオブジェクトに触れている場合でも一番近くのオブジェクト一つを選択します。

f:id:vxd-naoshi-19961205-maro:20200712030332p:plain



オブジェクトが離れたら登録を消す

コントローラーからオブジェクトが離れた際に登録から消します。

f:id:vxd-naoshi-19961205-maro:20200712025328p:plain



選択されているオブジェクトを所持する

オブジェクトを所持する際は現在選択されているオブジェクトをコントローラーの子に設定します。

f:id:vxd-naoshi-19961205-maro:20200818211254p:plain



実装

これらの処理を実装したプログラムです。 GrabManager.csはコントローラーにアタッチされます。

GrabManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UI;

public class GrabManager : MonoBehaviour
{
    [SerializeField]
    private GameObject m_controller;

    [SerializeField]
    private GameObject m_selectTextUI;

    private IControllerInput m_controllerInput;

    private GrabObject m_nowGrabbingObject;

    private GrabObject m_selectingObject;

    private List<GrabObject> m_touchingObjects = new List<GrabObject>();

    private GameObject m_InstancedSelectTextUI;

    public bool IsGrabbing { get { return m_nowGrabbingObject; } }

    // Start is called before the first frame update
    void Start()
    {
        
        m_controllerInput = m_controller.GetComponent<IControllerInput>();

        Assert.IsNotNull(m_controllerInput, name + " : IControllerInputが設定されていません!");

    }


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

        /*
         * 物を掴んでいる場合は物を離すか検知する
         * そうでない場合は、物を掴むか検知する
         */
        if(m_nowGrabbingObject) 
            CheckRelease();
        else
            CheckGrab();

    }

    private void CheckRelease() {

        if(m_controllerInput.IsGrabDown())
            Release();

    }

    private void CheckGrab() {
        
        var nearObject = GetNearObject();

        // 何も触れていないのにメッセージがある場合はメッセージを消す
        if(m_InstancedSelectTextUI && !m_selectingObject) 
            Destroy(m_InstancedSelectTextUI);

        // 一番近いオブジェクトに更新 メッセージの表示
        if(nearObject != m_selectingObject) {

            m_selectingObject = nearObject;
            DisplaySelectMessage();

        }

        if(!nearObject)
            return;

        if(m_controllerInput.IsGrabDown())
            Grab();
        
    }
    
    private void DisplaySelectMessage() {

        if(m_InstancedSelectTextUI != null)
            Destroy(m_InstancedSelectTextUI);

        if(m_selectingObject == null)
            return;

        Transform selectObjTransform = m_selectingObject.transform;
        m_InstancedSelectTextUI = Instantiate(m_selectTextUI, selectObjTransform.position, selectObjTransform.rotation);

        m_InstancedSelectTextUI.transform.GetChild(0).GetComponent<Text>().text = m_selectingObject.name;

    }

    private GrabObject GetNearObject() {

        GrabObject retObject = null;
        float minDisntace = float.MaxValue;

        Vector3 pos = transform.position;

        m_touchingObjects.RemoveAll(obj => obj == null);

        foreach(var obj in m_touchingObjects) {
            
            if(obj.IsGrabbed)
                continue;

            float distance = Vector3.SqrMagnitude(pos - obj.transform.position);

            if(distance < minDisntace) {
                retObject = obj;
                minDisntace = distance;
            }

        }

        return retObject;

    }


    // 持っているオブジェクトからも呼ばれる
    public void Release() {

        m_nowGrabbingObject.IsGrabbed = false;
        m_nowGrabbingObject.transform.SetParent(null);
        m_nowGrabbingObject.FinalizeRelease();

        m_nowGrabbingObject = null;

    }

    private void Grab() {

        // 有り得ないかもしれないけど、一応nullチェックしとく
        // 表示されてる選択メッセージを消す
        if(m_InstancedSelectTextUI != null) 
            Destroy(m_InstancedSelectTextUI);

        m_nowGrabbingObject = m_selectingObject;
        m_selectingObject = null;

        m_nowGrabbingObject.transform.SetParent(this.transform, true);
        m_nowGrabbingObject.IsGrabbed = true;
        m_nowGrabbingObject.InitializeGrab(m_controllerInput);
        m_nowGrabbingObject.SetGrabManager(this);

    }


    private void OnTriggerEnter(Collider other) {

        if(!other.CompareTag("GrabObject"))
            return;
        
        var grabObj = other.GetComponent<GrabObject>();

        Assert.IsNotNull(grabObj, other.name + " : GrabObjectが見つかりません");

        m_touchingObjects.Add(grabObj);

    }


    private void OnTriggerExit(Collider other) {

        if(!other.CompareTag("GrabObject"))
            return;

        m_touchingObjects.Remove(other.GetComponent<GrabObject>());


    }


}



また、オブジェクトにはGrabObject.csをアタッチします。

GrabObject.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


[RequireComponent(typeof(Rigidbody))] // OnTrigger~~を使うために必要
public class GrabObject : MonoBehaviour
{

    private bool m_isGrabbed = false;

    // GrabManagerから設定される
    public bool IsGrabbed { get { return m_isGrabbed; } set { m_isGrabbed = value; } }

    protected IControllerInput m_controllerInput;

    public virtual void InitializeGrab(IControllerInput controller) {

        m_controllerInput = controller;

    }


    public virtual void FinalizeRelease() {

        m_grabManager = null;

    }



解説

オブジェクトの登録、登録解除

この処理はOnTriggerEnterとOnTriggerExitで行います。 また、登録されたオブジェクトは m_touchingObjects に保存されます。

    private void OnTriggerEnter(Collider other) {

        if(!other.CompareTag("GrabObject"))
            return;
        
        var grabObj = other.GetComponent<GrabObject>();

        Assert.IsNotNull(grabObj, other.name + " : GrabObjectが見つかりません");

        m_touchingObjects.Add(grabObj);

    }


    private void OnTriggerExit(Collider other) {

        if(!other.CompareTag("GrabObject"))
            return;

        m_touchingObjects.Remove(other.GetComponent<GrabObject>());

    }



一番近いオブジェクトの選択

m_touchingObjectsの中からコントローラーとの距離が一番近いオブジェクトを返す関数GetNearObjectを作成します。もし、何も登録されていない場合はnullを返します。

距離を計算する際はDistance関数やmagnitude関数ではなくSqrMagnitude関数を使用します。(出来るだけ計算を軽くするため)

オブジェクトとの距離を計算する前にそのオブジェクトがもう片方のコントローラーに握られている際場合は無視されます。

    private GrabObject GetNearObject() {

        GrabObject retObject = null;
        float minDisntace = float.MaxValue;

        Vector3 pos = transform.position;

        m_touchingObjects.RemoveAll(obj => obj == null);

        foreach(var obj in m_touchingObjects) {

            // 片方のコントローラーに握られている場合は計算しない
            if(obj.IsGrabbed)
                continue;

            float distance = Vector3.SqrMagnitude(pos - obj.transform.position);

            if(distance < minDisntace) {
                retObject = obj;
                minDisntace = distance;
            }

        }

        return retObject;

    }


また、一番近いオブジェクトを選択した際にm_selectingObjectに登録します。

もし、新しく選択されたオブジェクトとm_selectingObjectが異なる場合はUIの更新を行います。

    private void CheckGrab() {
        
        var nearObject = GetNearObject();

        // 何も触れていないのにメッセージがある場合はメッセージを消す
        if(m_InstancedSelectTextUI && !m_selectingObject) 
            Destroy(m_InstancedSelectTextUI);

        // 一番近いオブジェクトに更新 メッセージの表示
        if(nearObject != m_selectingObject) {

            m_selectingObject = nearObject;
            DisplaySelectMessage();

        }

        if(!nearObject)
            return;

        if(m_controllerInput.IsGrabDown())
            Grab();
        
    }



オブジェクトを掴む、離す

オブジェクトを掴む、離す際の処理は次のようになります。

オブジェクトを掴むときに注意することはUIの表示を消すこと、掴んだオブジェクトの初期化処理を行うことです。

オブジェクトを離すときもほぼ同様です。

    // 持っているオブジェクトからも呼ばれる
    public void Release() {

        m_nowGrabbingObject.IsGrabbed = false;
        m_nowGrabbingObject.transform.SetParent(null);
        m_nowGrabbingObject.FinalizeRelease();

        m_nowGrabbingObject = null;

    }

    private void Grab() {

        // 有り得ないかもしれないけど、一応nullチェックしとく
        // 表示されてる選択メッセージを消す
        if(m_InstancedSelectTextUI != null) 
            Destroy(m_InstancedSelectTextUI);

        m_nowGrabbingObject = m_selectingObject;
        m_selectingObject = null;

        m_nowGrabbingObject.transform.SetParent(this.transform, true);
        m_nowGrabbingObject.IsGrabbed = true;
        m_nowGrabbingObject.InitializeGrab(m_controllerInput);

    }


オブジェクト側の初期化処理と終了処理は仮想関数になっているので、クラスを継承することによってオブジェクトごとに異なる処理を行うことが出来ます。

    public virtual void InitializeGrab(IControllerInput controller) {

        m_controllerInput = controller;

    }


    public virtual void FinalizeRelease() {

        m_controllerInput = null;

    }



結果

オブジェクトを選択する、UIを表示する、掴む、離す等違和感なく操作を行うことが出来ました。

f:id:vxd-naoshi-19961205-maro:20200818223527g:plain


また、このgif動画で出てくる指し棒はGrabObjectの仮想関数InitializeGrabとFinalizeReleaseでアニメーションを行っています。 GrabObjectの継承の例として指し棒のプログラムを載せておきます。

StickObject.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class StickObject : GrabObject
{

    [SerializeField]
    private TrailRenderer m_trail;

    [SerializeField]
    private Animator m_animator;

    private int m_extendID = Animator.StringToHash("Extend");

    private void Update() {

        if(IsGrabbed == false)
            return;

        if(m_controllerInput.IsTriggerDown()) {
            m_trail.enabled = true;
        }
        else if(m_controllerInput.IsTriggerUp()) {
            m_trail.enabled = false;
        }

    }

    public override void InitializeGrab(IControllerInput controller) {

        m_controllerInput = controller;
        IsGrabbed = true;
        m_animator.SetBool(m_extendID, true);

    }

    public override void FinalizeRelease() {

        IsGrabbed = false;
        m_animator.SetBool(m_extendID, false);

    }

}



最後に

オブジェクトを掴む処理って意外と複雑で難しかったと感じました。

今回はテキストで選択しているオブジェクトを表示しましたが、他の方法としてはマテリアルの色を変えたりアウトラインを表示するなど様々あると思うのでコンテンツにあったものでやってください。

また、いつかこのプログラムを使用することがあるかもしれないので変更があったら更新します。



参考

UIの表示を最前面にするために以下のサイトを参考にさせて頂きました。

tama-lab.net

www.gunsturn.com