Instinctive Interaction System for SteamVR

The code in this tutorial is available for Patreons on the downloads page.

In my personal opinion, the interaction system is the most important part of any VR game. For most games, you have to interact with your favorite worlds through a screen using buttons and mouse clicks. But VR is different in the fact that it allows you to literally reach in and grab things, that is the magic of VR. But there is a problem, if programmers don’t make this interaction seamless it breaks the illusion. And as you probably know, since you were searching for this tutorial, making an interaction system is hard and complicated. It took me forever to come up with mine but here it is:

Features:

(First off go and look at my last two tutorials as they serve the base of the program. )

Unfortunately for this one, I can’t just say it’s easier than you think. This one is complicated. We are going to use four scripts, two prefabs, and over 250 lines of code, Multiple configurable joints, and lose a bit of our sanity. But by the end of it all, we should have a system capable of holding an object with collision, holding an object with both hands, and telling and object that we are holding it. And all of this comes with the added benefit of SteamVR Skeletons integration.

The workflow

So our eventual goal is to be able to take a random 3D object (say… a gun since that seems like the first thing everyone uses) and add a Rigidbody, an Interactible Script, and a Grip Point Prefab and it’s instantly ready for use, If we want to add additional Grip Points, it’s as easy as just dragging in more grip point prefabs and marking them as secondary grip points. Making it easy to make anything Grabbable. (optional SteamVR Skeletons can be set for each Grip point)

Getting Started

So first off let’s get the simple scripts out of the way:

Interactible

This script is attached to the root object of whatever object you are making grabbable, or wherever you put the rigidbody. Its purpose is to keep track of collisions and if it’s been grabbed. This is important so we can keep track of which type of grip (physics joint) to use.

using UnityEngine;
using Valve.VR;
public class Interactable : MonoBehaviour
{
    public int touchCount;//how many objects are currently in contact
    public SteamVR_Input_Sources Hand;//keep track of the active/first hand
    public bool gripped;
    public bool SecondGripped;
    void start()
    {
        if (gameObject.tag != "Grabbable") {
        }
    }
    private void OnCollisionEnter(Collision collision)//on a collison update touchcount
    {
        touchCount++;
    }
    private void OnCollisionExit(Collision collision)
    {
        touchCount--;
    }
}

Grip Point Prefab

Code

This is the script that we put on our Grip Point prefabs, yes I know. One Variable. Along with storing this one variable it also lets us get around using a tag.

using UnityEngine;
public class GrabPoint : MonoBehaviour
{
    public bool SubGrip;
}

The Prefab

The prefab is really just a collider and a few variables as you see below. Create an empty, set it up as below and save it as a prefab. Make sure the collider/s are set as triggers. The collider size doesn’t matter since you’re going to change it depending on the object.

The Grabber Prefab

This Prefab contains all of our physics joints, the rigid body, and the collider so they aren’t all clogging up the [cameraRig] Hand object. It also contains a script to keep track of all the Joints and nearby grabbable objects.

The Script

using System.Collections.Generic;
using UnityEngine;

public class Grabber : MonoBehaviour
{
    public ConfigurableJoint StrongGrip;//keep track of our jounts 
    public ConfigurableJoint WeakGrip;
    public FixedJoint FixedJoint;

    public List<GameObject> NearObjects = new List<GameObject>();//list of near grabbable objects

    void OnTriggerEnter(Collider other)//when we touch an object cheak if it has a GrabPoint Script and we can grab then add it to the list if so.
    {
        if (other.GetComponent<GrabPoint>())
        {
            if (other.GetComponent<GrabPoint>().SubGrip && other.transform.parent.GetComponent<Interactable>().gripped)
            {
                NearObjects.Add(other.gameObject);
            }
            else if (!other.GetComponent<GrabPoint>().SubGrip && !other.transform.parent.GetComponent<Interactable>().gripped)
            {
                NearObjects.Add(other.gameObject);
            }
        }
        Debug.Log(NearObjects);
    }

    void OnTriggerExit(Collider other)//remove items from the list
    {
        if (other.GetComponent<GrabPoint>())
        {
            NearObjects.Remove(other.gameObject);
        }
    }
    public GameObject ClosestGrabbable()//cheak the list and return the closest grabbable object.
    {
        GameObject ClosestGameObj = null;
        float Distance = float.MaxValue;
        if (NearObjects != null)
        {
            foreach (GameObject GameObj in NearObjects)
            {
                if ((GameObj.transform.position - transform.position).sqrMagnitude < Distance)
                {
                    ClosestGameObj = GameObj;
                    Distance = (GameObj.transform.position - transform.position).sqrMagnitude;
                }
            }
        }
        return ClosestGameObj;
    }
}

The Prefab

Put this prefab as the child of a hand object in the [camera rig] and make it a prefab so you can duplicate it to both hands. Make sure the collider/s are set as triggers. As with the one above, your Collider values will be different, adjust them so that they fit your hand models.

Strong Grip:

Weak Grip:

The Grip Controller

This is the final piece of the puzzle that brings everything together. We attach it to both Hands in the [camera rig] object. This script does EVERYTHING.

The Script

using UnityEngine;
using Valve.VR;

public class GripController : MonoBehaviour
{
    public SteamVR_Input_Sources Hand;//Get all of our inputs
    public SteamVR_Action_Boolean ToggleGripButton;
    public SteamVR_Action_Pose position;
    public SteamVR_Behaviour_Skeleton HandSkeleton;//I had two hands connected two one, one hand was just an outline so I could use it as a indication of a grabbable object.
    public SteamVR_Behaviour_Skeleton PreviewSkeleton;
    public Grabber grabber;//get our grabber prefab we made earlier

    private GameObject ConnectedObject;//The object we a currently holding
    private Transform OffsetObject;//used to stor our Grip point prefabs
    private bool SecondGrip;//are we the second hand to grip the object
    private void Update()
    {

        if (ConnectedObject != null )//If we are holding something
        {
            if (!SecondGrip)//and we are not the second hand
            {


                if (ConnectedObject.GetComponent<Interactable>().touchCount == 0&& !ConnectedObject.GetComponent<Interactable>().SecondGripped)//If the held object isn't touching anything 
                {
                    grabber.FixedJoint.connectedBody = null;//disconnect the rigid jont to reset it
                    grabber.StrongGrip.connectedBody = null;//and all the other joints just to be sure
                    //these two lines move the connected object to the hands position over time to give a snappy feel to picking stuff up, otherwise you could just teleport the object to the right position
                    ConnectedObject.transform.position = Vector3.MoveTowards(ConnectedObject.transform.position, transform.position - ConnectedObject.transform.rotation * OffsetObject.localPosition, .25f);//move and rotate the object to the hands position 
                    ConnectedObject.transform.rotation = Quaternion.RotateTowards(ConnectedObject.transform.rotation, transform.rotation * Quaternion.Inverse(OffsetObject.localRotation), 10);
                    grabber.FixedJoint.connectedBody = ConnectedObject.GetComponent<Rigidbody>();//reconnect the rigid joint
                }
                else if (ConnectedObject.GetComponent<Interactable>().touchCount > 0|| ConnectedObject.GetComponent<Interactable>().SecondGripped)//the object is touching something so use a configurable joint.
                {

                    grabber.FixedJoint.connectedBody = null;
                    grabber.StrongGrip.connectedAnchor = OffsetObject.localPosition;
                    grabber.StrongGrip.connectedBody = ConnectedObject.GetComponent<Rigidbody>();

                }
                
            }
            else if (SecondGrip)// if we are the second hand just use the weak grip that doesnt have an agular drive
            {
                
                if(!ConnectedObject.GetComponent<Interactable>().gripped)//if the other hand lets go use a configurable joint
                {
                    grabber.FixedJoint.connectedBody = null;
                    grabber.StrongGrip.connectedAnchor = OffsetObject.localPosition;
                    grabber.StrongGrip.connectedBody = ConnectedObject.GetComponent<Rigidbody>();
                }
                else
                {
                    grabber.FixedJoint.connectedBody = null;
                    grabber.StrongGrip.connectedBody = null;
                    grabber.WeakGrip.connectedBody = ConnectedObject.GetComponent<Rigidbody>();
                }
            }
            if (ToggleGripButton.GetStateUp(Hand))//check for if we want to let go
            {
                Release();
            }
            if(PreviewSkeleton)//hide the preview hand since we are holding somthing 
                PreviewSkeleton.transform.gameObject.SetActive(false);
        }
        else//if we arn't holding anything
        {
            if (grabber.ClosestGrabbable() && PreviewSkeleton)//if there is somthing close and we have a preview hand assigned 
            {
                PreviewSkeleton.transform.gameObject.SetActive(true);//activate the hand 
                OffsetObject =grabber.ClosestGrabbable().transform;//parent it to the object
                if (grabber.ClosestGrabbable().GetComponent<SteamVR_Skeleton_Poser>())//if the object has a skeleton poser 
                {
                    if (!OffsetObject.GetComponent<GrabPoint>().SubGrip && !OffsetObject.transform.parent.GetComponent<Interactable>().gripped || OffsetObject.GetComponent<GrabPoint>().SubGrip && OffsetObject.transform.parent.GetComponent<Interactable>().gripped)//if the object is not gripped at all or is gripped by both hands
                    {
                        PreviewSkeleton.transform.SetParent(OffsetObject, false);
                        PreviewSkeleton.BlendToPoser(OffsetObject.GetComponent<SteamVR_Skeleton_Poser>(), 0f);
                    }
                }
            }
            else//if there is no object in range deactivate the hand
            {
                PreviewSkeleton.transform.gameObject.SetActive(false);
            }
            if (ToggleGripButton.GetStateDown(Hand))//check for if we want to grip
            {
                Grip();
            }
        }
    }
    private void Grip()
    {
        GameObject NewObject = grabber.ClosestGrabbable();//get the closest grabbable 
        if (NewObject != null)//if it is defined
        {
            OffsetObject = grabber.ClosestGrabbable().transform;//set the offset
            ConnectedObject = OffsetObject.transform.parent.gameObject;//set the root of the grabbable as the connected object
            ConnectedObject.GetComponent<Rigidbody>().useGravity = false;
            if (ConnectedObject.GetComponent<Interactable>().gripped)//if the object has already been grabbed (we know that it's not a grip point that has already been grabbed since those arn't put in the grabbable array)
            {
                SecondGrip = true;
                ConnectedObject.GetComponent<Interactable>().SecondGripped = true;
                grabber.WeakGrip.connectedBody = ConnectedObject.GetComponent<Rigidbody>();//attach the correct joint.
                grabber.WeakGrip.connectedAnchor = OffsetObject.localPosition;//set the offest 
            }
            else//if we are the first to grab it 
            {
                ConnectedObject.GetComponent<Interactable>().Hand = Hand;
                ConnectedObject.GetComponent<Interactable>().gripped = true;
            }
            if (OffsetObject.GetComponent<SteamVR_Skeleton_Poser>()&&HandSkeleton)//if the object has a defined skeleton poser update our hand
            {
                HandSkeleton.transform.SetParent(OffsetObject, false);
                HandSkeleton.BlendToPoser(OffsetObject.GetComponent<SteamVR_Skeleton_Poser>(), 0f);
            }


        }
    }
    private void Release()
    {
        grabber.FixedJoint.connectedBody = null;//disconnect everything
        grabber.StrongGrip.connectedBody = null;
        grabber.WeakGrip.connectedBody = null;
        ConnectedObject.GetComponent<Rigidbody>().velocity = position.GetVelocity(Hand) + transform.parent.GetComponent<Rigidbody>().velocity; //set the velocitiy of the object, edit if you ar not using my walking script so that it doesn't try to get the speed of the player
        ConnectedObject.GetComponent<Rigidbody>().angularVelocity = position.GetAngularVelocity(Hand) + transform.parent.GetComponent<Rigidbody>().angularVelocity;//set the rotational velocitiy too
        ConnectedObject.GetComponent<Rigidbody>().useGravity = true;
        if (!SecondGrip)//if we were the first to grab the object
        {
            
            ConnectedObject.GetComponent<Interactable>().gripped = false;
            
        }
        else//if we were  the second hand
        {
            ConnectedObject.GetComponent<Interactable>().SecondGripped = false;
            SecondGrip = false;
        }
        
        ConnectedObject = null;
        if (OffsetObject.GetComponent<SteamVR_Skeleton_Poser>() && HandSkeleton)//disconnect the hand if needed.
        {
            HandSkeleton.transform.SetParent(transform, false);
            HandSkeleton.BlendToSkeleton();
        }

        OffsetObject = null;
    } 
}

Final Advice

Alright so that should be all you need to get up and running but in case you are still confused, this is how you put it together. Make sure your prefabs are assembled first though. First, add a Grip Controller script to both hands, and a Gripper prefab as a child to each then set the Grip Controller script as needed. Your hands should now be ready to go and you shouldn’t ever have to touch them again. Now to set up an object to be grabbed add a riggidbody if you don’t already have one and an Interactible script. Now as a direct child of that object add the Grip Point Prefabs, think of them as handles and put them everywhere you want to be able to grab the object. If it’s a secondary grip like the forestock of a gun set it as a second grip so that your trigger hand doesn’t have to fight it.

Final Words

Man, it took too long for me to make this tutorial, but now that I finally have I can start putting out tutorials on everything that uses a grabbing mechanic (ie everything) so be hyped for that and I’ll see you there.

Liked it? Take a second to support WireWhiz on Patreon!
Become a patron at Patreon!