July 17, 2019
Code & Design
The use of laser pointers as a form of input is a common requirement for VR developers; yet, since the launch of SteamVR 2.0, close to zero resources exist on how exactly to set this up. Worse, pointers pose a particular problem in VR because, per the Unity documentation, it’s suggested that one use different raycast types when targeting different entities such as 2D, 3D or UI items. Fortunately, the SteamVR 2.0 plugin itself comes with a “SteamVR_Laser_Pointer” script that handles the heavy functionality of creating a pointer, physics raycaster & line renderer. Unfortunately, it requires a bit more implementation to work properly if you’re going to raycast both 3D & UI items (hint: you almost always will).
The tutorial below aims to make it perfectly clear how to implement a SteamVR laser pointer that can target both 3D & canvas UI items. We’ll break this up into the two main sections:
I. Setting Up Our Scene & SteamVR_Laser_Pointer Script
II. Writing A Scene-Wide Pointer Handler Script
The overall logic is to create a scene-wide event handler that holds three giant switch statements. Each statement resides within one of the three following types of pointer interactions: on enter, on exit, & clicked. Additionally, in order to make sure UI items area also respond, we’ll add a box collider to our test button.
We’ll start from a blank slate — go ahead & start a fresh project on Unity v2019.1.7f. Next, import the key SteamVR plugin from the asset store. Whether you’re on an Oculus or HTC device, we start by initializing the headset & handheld devices. Do this by grabbing & dropping the [CameraRig] prefab found in the SteamVR/Prefabs directory onto your scene hierarchy window; with this prefab we no longer need the default “Main Camera” prefab, delete it.
Next, we’ll initialize the laser pointer. Grab & drop the “SteamVR_LaserPointer” script found in the SteamVR/Extras directory onto either of the “Controller” game objects found within the [CameraRig] prefab (I’m right-handed, so I chose the “Controller (Right)” game object). If done correctly, you should see the following public script properties:
We’ll leave all of the properties in their default position. If you go ahead & run this scene, you should now see a thin, black laser pointer protruding from your controller! To test our laser pointer, we’ll have it interact with a cube & a UI button — let’s set those up next.
Despite the great out-of-box functionality the SteamVR_Laser_Pointer script offers, it fails to address the single-most common frustration that developers come across with raycasting in VR: the ability to hit across both 3D entities & UI elements. Again, if you visit the documentation here, it’s clear that Unity defines different raycasts for those two purposes, so we have to implement a clever work-around: wrapping our UI button with a box collider.
First make a simple, bare 3D cube & place it in front of you. Next, setup a Canvas game object, change the render mode attribute to “World Space,” add a UI button within the canvas, add a box collider component to that button, & finally, position the button in front of you, next to the cube as such:
The second half of this tutorial revolves around correctly receiving & handling the three possible inputs from the SteamVR_Laser_Pointer: when the pointer enters a game object, when the pointer exits a game object, & when the pointer clicks on a game object. We’re going to handle all of the scene’s logic within a single script. Each of the three input handlers will act as giant switch statements that check the name of the game objected currently targeted for further instructions.
/* SceneHandler.cs*/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using Valve.VR.Extras;
public class SceneHandler : MonoBehaviour
{
public SteamVR_LaserPointer laserPointer;
void Awake()
{
laserPointer.PointerIn += PointerInside;
laserPointer.PointerOut += PointerOutside;
laserPointer.PointerClick += PointerClick;
}
public void PointerClick(object sender, PointerEventArgs e)
{
if (e.target.name == "Cube")
{
Debug.Log("Cube was clicked");
} else if (e.target.name == "Button")
{
Debug.Log("Button was clicked");
}
}
public void PointerInside(object sender, PointerEventArgs e)
{
if (e.target.name == "Cube")
{
Debug.Log("Cube was entered");
}
else if (e.target.name == "Button")
{
Debug.Log("Button was entered");
}
}
public void PointerOutside(object sender, PointerEventArgs e)
{
if (e.target.name == "Cube")
{
Debug.Log("Cube was exited");
}
else if (e.target.name == "Button")
{
Debug.Log("Button was exited");
}
}
}
Starting from the top, past the import statements, we first have our public declaration of “SteamVR_Laser_Pointer” game object. Next, we have our Awake() method which contains the three input functions extended. And finally, culminating in the majority of the code, are the three custom handlers we’ll use to check which game object is currently raycasted. As you can see by inspecting any of the “if” or “else if” statements, we’re checking against the name of the game object through “e.target.name.” Since we’re only testing against two objects, we need only check against the two strings: “Cube” & “Button.” For brevity, I’m only logging the interaction taking place.
Since this is a scene-wide handler, we only need to place the script onto our scene once. Feel free to attach it to any game object — placing the script on the [CameraRig] works just fine. Last but not least, make sure to grab & drop the active Controller game object in the now-visible “SteamVR_Laser_Pointer” public field. Everything is now ready.
Go ahead, fire up our scene & use the laser pointer to interact with the two game objects. If you followed along correctly, you should see all pointer events registering messages in the console! And, there, we go — a walk-through on adding a game object-agnostic laser pointer in SteamVR 2.0. As long as we remember to register, check & handle additional functionality, we can expand this scene handler with as many game objects as we’d like.
Additional Tutorials:
Indie Support:
If you enjoyed this tutorial/series please consider supporting my indie project, CryptoSpace. It’s a VR blockexplorer with the goal of educating people on cryptocurrency projects with a hands-on, interactive experience: