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
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Interaction System: Controller

The controller script might be the most important piece of all, as it’s the direct link between the player and the game. It’s important to make use of as much input as possible and return appropriate feedback to the player.

To start off, add the following variables below the class declaration:

public Transform snapColliderOrigin; // 1
public GameObject ControllerModel; // 2

[HideInInspector]
public Vector3 velocity; // 3
[HideInInspector]
public Vector3 angularVelocity; // 4

private RWVR_InteractionObject objectBeingInteractedWith; // 5

private SteamVR_TrackedObject trackedObj; // 6

Looking at each piece in turn:

  1. Save a reference to the tip of the controller. You’ll be adding a transparent sphere later, which will act as a guide to where and how far you can reach:

  2. This is the visual representation of the controller, seen in white above.
  3. This is the speed and direction of the controller. You’ll use this to calculate how objects should fly when you throw them.
  4. This is the rotation of the controller, also used when calculating the motion of thrown objects.
  5. This is the InteractionObject this controller is currently interacting with. You use it to send events to the active object.
  6. SteamVR_TrackedObject can be used to get a reference to the actual controller.

Add this code below the variables you just added:

private SteamVR_Controller.Device Controller // 1
{
    get { return SteamVR_Controller.Input((int)trackedObj.index); }
}

public RWVR_InteractionObject InteractionObject // 2
{
    get { return objectBeingInteractedWith; }
}

void Awake() // 3
{
    trackedObj = GetComponent<SteamVR_TrackedObject>();
}

Here’s what’s going on in the code above:

  1. This variable acts as a handy shortcut to the actual SteamVR controller class from the tracked object.
  2. This returns the InteractionObject this controller is currently interacting with. It’s been encapsulated to ensure it stays read-only for other classes.
  3. Finally, save a reference to the TrackedObject component attached to this controller to use later.

Now add the following method:

private void CheckForInteractionObject()
{
    Collider[] overlappedColliders = Physics.OverlapSphere(snapColliderOrigin.position, snapColliderOrigin.lossyScale.x / 2f); // 1

    foreach (Collider overlappedCollider in overlappedColliders) // 2
    {
        if (overlappedCollider.CompareTag("InteractionObject") && overlappedCollider.GetComponent<RWVR_InteractionObject>().IsFree()) // 3
        {
            objectBeingInteractedWith = overlappedCollider.GetComponent<RWVR_InteractionObject>(); // 4
            objectBeingInteractedWith.OnTriggerWasPressed(this); // 5
            return; // 6
        }
    }
}

This method searches for InteractionObjects in a certain range from the controller’s snap collider. Once it finds one, it populates the objectBeingInteractedWith with a reference to it.

Here’s what each line does:

  1. Creates a new array of colliders and fills it with all colliders found by OverlapSphere() at the position and scale of the snapColliderOrigin, which is the transparent sphere shown above that you’ll add shortly.
  2. Iterates over the array.
  3. If any of the found colliders has an InteractionObject tag and is free, continue.
  4. Saves a reference to the RWVR_InteractionObject attached to the object that was overlapped in objectBeingInteractedWith.
  5. Calls OnTriggerWasPressed on objectBeingInteractedWith and gives it the current controller as a parameter.
  6. Breaks out of the loop once an InteractionObject is found.

Add the following method that makes use of the code you just added:

void Update()
{
    if (Controller.GetHairTriggerDown()) // 1
    {
        CheckForInteractionObject();
    }

    if (Controller.GetHairTrigger()) // 2
    {
        if (objectBeingInteractedWith)
        {
            objectBeingInteractedWith.OnTriggerIsBeingPressed(this);
        }
    }

    if (Controller.GetHairTriggerUp()) // 3
    {
        if (objectBeingInteractedWith)
        {
            objectBeingInteractedWith.OnTriggerWasReleased(this);
            objectBeingInteractedWith = null;
        }
    }
}

This is fairly straightforward:

  1. When the trigger is pressed, call CheckForInteractionObject() to prepare for a possible interaction.
  2. While the trigger is held down and there’s an object being interacted with, call the object’s OnTriggerIsBeingPressed().
  3. When the trigger is released and there’s an object that’s being interacted with, call that object’s OnTriggerWasReleased() and stop interacting with it.

These checks make sure that all of the player’s input is being passed to any InteractionObjects they are interacting with.

Add these two methods to keep track of the controller’s velocity and angular velocity:

private void UpdateVelocity()
{
    velocity = Controller.velocity;
    angularVelocity = Controller.angularVelocity;
}

void FixedUpdate()
{
    UpdateVelocity();
}

FixedUpdate() calls UpdateVelocity() every frame at the fixed framerate, which updates the velocity and angularVelocity variables. Later, you’ll pass these values to a RigidBody to make thrown objects move more realistically.

Sometimes you’ll want to hide a controller to make the experience more immersive and avoid blocking your view. Add the following two methods below the previous ones:

public void HideControllerModel()
{
    ControllerModel.SetActive(false);
}

public void ShowControllerModel()
{
    ControllerModel.SetActive(true);
}

These methods simply enable or disable the GameObject representing the controller.

Finally, add the following two methods:

public void Vibrate(ushort strength) // 1
{
    Controller.TriggerHapticPulse(strength);
}

public void SwitchInteractionObjectTo(RWVR_InteractionObject interactionObject) // 2
{
    objectBeingInteractedWith = interactionObject; // 3
    objectBeingInteractedWith.OnTriggerWasPressed(this); // 4
}

Here’s how these methods work:

  1. This method makes the piezoelectric linear actuators (no, I’m not making that up) inside the controller vibrate for a certain amount of time. The longer it vibrates, the stronger the vibration feels. Its range is between 1 and 3999.
  2. This switches the active InteractionObject to the one specified in the parameter.
  3. This makes the specified InteractionObject the active one.
  4. Call OnTriggerWasPressed() on the newly assigned InteractionObject and pass this controller.

Save this script and return to the editor. In order to get the controllers working as intended, you’ll need to make a few adjustments.

Select both controllers in the Hierarchy. They’re both children of [CameraRig].

Add a Rigidbody component to both. This will allow them to work with fixed joints and interact with other physics objects.

Uncheck Use Gravity and check Is Kinematic. The controllers don’t need be to affected by physics since they’re strapped to your hands in real life.

Now add the RWVR_Interaction Controller component to the controllers. You’ll configure those in a bit.

Unfold Controller (left) and add a Sphere to it as its child by right-clicking it and selecting 3D Object > Sphere.

Select Sphere, name it SnapOrigin and press F to focus on it in the Scene view. You should see a big white hemisphere at the center of the platform floor:

Set its Position to (X:0, Y:-0.045, Z:0.001) and its Scale to (X:0.1, Y:0.1, Z:0.1). This will position the sphere right at the front of the controller.

Remove the Sphere Collider component, as all physics checks are done in code.

Finally, make the sphere transparent by applying the Transparent material to its Mesh Renderer.

Now duplicate SnapOrigin and drag SnapOrigin (1) to Controller (right) to make it a child of the right controller. Name it SnapOrigin.

The final step is to set up the controllers to make use of their Model and SnapOrigin.

Select and unfold Controller (left), drag its child SnapOrigin to the Snap Collider Origin slot and drag Model to the Controller Model slot.

Do the same for Controller (right).

Now for a bit of fun! Power on your controllers and run the scene.

Move the controllers in front of the HMD to check if the spheres are clearly visible and attached to the controllers.

When you’re done testing, save the scene and prepare to actually use the interaction system!