Advanced VR Mechanics With Unity and the HTC Vive Part 1

Learn how to create a powerful, flexible, and re-useable interaction system for your HTC Vive games in Unity! By Eric Van de Kerckhove.

Leave a rating/review
Save for later
Share
Note: This tutorial is using SteamVR 1.2.3 and is not compatible with SteamVR 2.0 that was released in September 2018. Please use an older version of SteamVR instead of the Asset Store version if you wish to follow along with this tutorial and stay tuned for an update!

VR is more popular than ever, and making games has never been easier. But to offer a really immersive experience, your in-game mechanics and physics need to feel very, very real, especially when you’re interacting with in-game objects.

In the first part of this advanced HTC Vive tutorial, you’ll learn how to create an expandable interaction system and implement multiple ways to grab virtual objects inside that system, and fling them around like nobody’s business.

By the time you’re done, you’ll have some flexible interaction systems that you can use right in your own VR projects!

Note: This tutorial is intended for an advanced audience, and won’t cover things such as adding components, creating new GameObjects scripts, or C# syntax. If you need to level up your Unity skills, work through our tutorials on getting started with Unity and introduction to Unity Scripting first, then return to this tutorial.

Getting Started

You’ll need the following things for this tutorial:

If you haven’t worked with the HTC Vive before, you might want to check out this previous HTC Vive tutorial to get a feel for the basics of working with the HTC Vive in Unity. The HTC Vive is one of the best head-mounted displays at the moment and offers an excellent immersive experience because of its room-scale gameplay capabilities.

Download the starter project, unzip it somewhere and open the folder inside Unity.

Take a look at the folder structure in the Project window:

Here’s what each contains:

  • Materials: All the materials for the scene.
  • Models: All models for this tutorial.
  • Prefabs: For now, this only contains the prefab for the poles that are scattered around the level. You’ll place your own objects in here for later use.
  • Scenes: The game scene and some lighting data.
  • Scripts: A few premade scripts; you’ll save your own scripts in here as well.
  • Sounds: The sound for shooting an arrow from the bow.
  • SteamVR: The SteamVR plugin and all related scripts, prefabs and examples.
  • Textures: Contains the main texture shared by almost all models (for the sake of efficiency) as well as the texture for the book object.

Open up the Game scene inside the Scenes folder.

Look at the Game view and you’ll notice there’s no camera present in the scene:

In the next section you’ll fix this by adding everything necessary for the HTC Vive to work.

Scene Setup

Select and drag the [CameraRig] and [SteamVR] prefabs from the SteamVR\Prefabs folder to the Hierarchy.

The camera rig will now be on the ground, but it should be on the wooden tower. Change the position of [CameraRig] to (X:0, Y:3.35, Z:0) to correct this. This is what it should look like in the Game view:

Now save the scene and press the play button to test if everything works as intended. Be sure to look around and use at least one controller to see if you can see the in-game controller moving around.

If the controllers didn’t work, don’t panic! At the time of writing, there’s a bug in the latest SteamVR plugin (version 1.2.1) when using Unity 5.6 which causes the movement of the controllers to not register.

To fix this, select Camera (eye) under [CameraRig]/Camera (head) and add the SteamVR_Update_Poses component to it:

This script manually updates the position and rotation of the controllers. Try playing the scene again, and things should work much better.

Before doing any scripting, take a look at these tags in the project:

These tags make it easier to detect which type of object collided or triggered with another.

Interaction System: InteractionObject

An interaction system allows for a flexible, modular approach to interactions between the player and objects in the scene. Instead of rewriting the boilerplate code for every object and the controllers, you’ll be making some classes from which other scripts can be derived.

The first script you’ll be making is the RWVR_InteractionObject class; all objects that can be interacted with will be derived from this class. This base class will hold some essential variables and methods.

Note: To avoid conflicts with the SteamVR plugin and make searching easier, all VR scripts in this tutorial will have the “RWVR” prefix.

Create a new folder in the Scripts folder and name it RWVR. Create a new C# script in there and name it RWVR_InteractionObject.

Open up the script in your favorite code editor and remove both the Start() and Update() methods.

Add the following variables to the top of the script, right underneath the class declaration:

protected Transform cachedTransform; // 1
[HideInInspector] // 2
public  RWVR_InteractionController currentController; // 3

You’ll probably get an error saying RWVR_InteractionController couldn’t be found. Ignore this for now, as you’ll be creating that class next.

Taking each commented line in turn:

  1. You cache the value of the transform to improve performance.
  2. This attribute makes the variable underneath invisible in the Inspector window, even though it’s public.
  3. This is the controller this object is currently interacting with. You’ll visit the controller in detail later on.

Save this script for now and return to the editor.

Create a new C# script inside the RWVR folder named RWVR_InteractionController. Open it up, remove the Start() and Update() methods and save your work.

Open the RWVR_InteractionObject script again, and the error you received before should be gone.

Note: If you’re still getting the error, close your code editor, give focus to Unity and open the script again from there.

Now add the following three methods below the variables you just added:

public virtual void OnTriggerWasPressed(RWVR_InteractionController controller)
{
    currentController = controller; 
}

public virtual void OnTriggerIsBeingPressed(RWVR_InteractionController controller)
{
}

public virtual void OnTriggerWasReleased(RWVR_InteractionController controller)
{
    currentController = null;
}

These methods will be called by the controller when its trigger is either pressed, held or released. A reference to the controller is stored when it’s pressed, and removed again when it’s released.

All of these methods are virtual and will be overridden by more sophisticated scripts later on so they can benefit from these controller callbacks.

Add the following method below OnTriggerWasReleased:

public virtual void Awake()
{
    cachedTransform = transform; // 1
    if (!gameObject.CompareTag("InteractionObject")) // 2
    {
        Debug.LogWarning("This InteractionObject does not have the correct tag, setting it now.", gameObject); // 3
        gameObject.tag = "InteractionObject"; // 4
    }
}

Taking it comment-by-comment:

  1. Cache the transform for better performance.
  2. Check to see if this InteractionObject has the proper tag assigned. Execute the code below if it doesn’t.
  3. Log a warning in the inspector to warn the developer of a forgotten tag.
  4. Assign the tag just in time so this object functions as expected.

The interaction system will depend heavily upon the InteractionObject and Controller tags to differentiate those special objects from the rest of the scene. It’s quite easy to forget to add this tag to objects every time you add a script to it. That’s why this failsafe is in place. Better to be safe than sorry! :]

Finally, add these methods below Awake():

public bool IsFree() // 1
{
    return currentController == null;
}

public virtual void OnDestroy() // 2
{
    if (currentController)
    {
        OnTriggerWasReleased(currentController);
    }
}

Here’s what these methods do:

  1. This is a public Boolean that indicates whether or not this object is currently in use by a controller.
  2. When this object gets destroyed, you release it from the current controller (if there are any). This helps to avoid weird bugs later on when working with objects that can be held.

Save this script and open the RWVR_InteractionController script again.

It’s empty at the moment. But you’ll soon fill it up with functionality!