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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Advanced VR Mechanics With Unity and the HTC Vive Part 1
30 mins
Grabbing Objects Using The Interaction System
You may have noticed these objects laying around:
You can take a good look at them, but you can’t pick them up yet. You’d better fix that soon, or how will you ever learn how awesome our Unity book is?! :]
In order to interact with rigidbodies like these, you’ll need to create a new derivative class of RWVR_InteractionObject that will let you grab and throw objects.
Create a new C# script in the Scripts/RWVR folder and name it RWVR_SimpleGrab.
Open it up in your code editor and remove the Start()
and Update()
methods.
Replace the following:
public class RWVR_SimpleGrab : MonoBehaviour
…with:
public class RWVR_SimpleGrab : RWVR_InteractionObject
This makes this script derive from RWVR_InteractionObject, which provides all the hooks onto the controller’s input so it can appropriately handle the input.
Add these variables below the class declaration:
public bool hideControllerModelOnGrab; // 1
private Rigidbody rb; // 2
Quite simply:
- A flag indicating whether or not the controller model should be hidden when this object is picked up.
- Cache the Rigidbody component for performance and ease of use.
Add the following methods below those variables:
public override void Awake()
{
base.Awake(); // 1
rb = GetComponent<Rigidbody>(); // 2
}
Short and sweet:
- Call
Awake()
on the base classRWVR_InteractionObject
. This caches the object’s Transform component and checks if the InteractionObject tag is assigned. - Store the attached Rigidbody component for later use.
Now you need some helper methods that will attach and release the object to and from the controller by using a FixedJoint.
Add the following methods below Awake()
:
private void AddFixedJointToController(RWVR_InteractionController controller) // 1
{
FixedJoint fx = controller.gameObject.AddComponent<FixedJoint>();
fx.breakForce = 20000;
fx.breakTorque = 20000;
fx.connectedBody = rb;
}
private void RemoveFixedJointFromController(RWVR_InteractionController controller) // 2
{
if (controller.gameObject.GetComponent<FixedJoint>())
{
FixedJoint fx = controller.gameObject.GetComponent<FixedJoint>();
fx.connectedBody = null;
Destroy(fx);
}
}
Here’s what these methods do:
- This method accepts a controller to “stick” to as a parameter and then proceeds to create a FixedJoint component. Attach it to the controller, configure it so it won’t break easily and finally connect it to the current InteractionObject. The reason you set a finite break force is to prevent users from moving objects through other solid objects, which might result in weird physics glitches.
- The controller passed as a parameter is relieved from its FixedJoint component (if there is one). The connection to this object is removed and the FixedJoint is destroyed.
With those methods in place, you can take care of the actual player input by implementing some OnTrigger methods from the base class. To start off with, add OnTriggerWasPressed()
:
public override void OnTriggerWasPressed(RWVR_InteractionController controller) // 1
{
base.OnTriggerWasPressed(controller); // 2
if (hideControllerModelOnGrab) // 3
{
controller.HideControllerModel();
}
AddFixedJointToController(controller); // 4
}
This method adds the FixedJoint when the player presses the trigger button to interact with the object. Here’s what you do in each part:
- Override the base
OnTriggerWasPressed()
method. - Call the base method to intialize the
controller
. - If the
hideControllerModelOnGrab
flag was set, hide the controller model. - Add a FixedJoint to the controller.
The final step for this simple grab class is to add OnTriggerWasReleased()
:
public override void OnTriggerWasReleased(RWVR_InteractionController controller) // 1
{
base.OnTriggerWasReleased(controller); //2
if (hideControllerModelOnGrab) // 3
{
controller.ShowControllerModel();
}
rb.velocity = controller.velocity; // 4
rb.angularVelocity = controller.angularVelocity;
RemoveFixedJointFromController(controller); // 5
}
This method removes the FixedJoint and passes the controller’s velocities to create a realistic throwing effect. Comment-by-comment:
-
Override the base
OnTriggerWasReleased()
method. - Call the base method to unassign the
controller
. - If the
hideControllerModelOnGrab
flag was set, show the controller model again. - Pass the controller’s velocity and angular velocity to this object’s rigidbody. This means the object will react in a realistic manner when you release. For example, if you’re throwing a ball, you move the controller from back-to-front in an arc. The ball should gain rotation and a forward-acting force as if you had passed your actual kinetic energy in real life.
- Remove the FixedJoint.
Save this script and return to the editor.
The dice and books are linked to their respective prefabs in the Prefabs folder. Open this folder in the Project view:
Select the Book and Die prefabs and add the RWVR_Simple Grab component to both. Also enable Hide Controller Model.
Save and run the scene. Try grabbing some of the books and dice and throwing them around.
In the next section I’ll explain another way of grabbing objects: via snapping.
Grabbing and Snapping Objects
Grabbing objects at the position and rotation of your controller usually works, but in some cases snapping the object to a certain position might be desirable. For example, when the player sees a gun, they would expect the gun to be pointing in the right direction once they’ve picked it up. This is where snapping comes into play.
In order for snapping to work, you’ll need to create another script. Create a new C# script inside the Scripts/RWVR folder and name it RWVR_SnapToController. Open it in your favorite code editor and remove the Start()
and Update()
methods.
Replace the following:
public class RWVR_SnapToController : MonoBehaviour
..with:
public class RWVR_SnapToController : RWVR_InteractionObject
This lets this script use all of the InteractionObject capabilities.
Add the following variable declarations:
public bool hideControllerModel; // 1
public Vector3 snapPositionOffset; // 2
public Vector3 snapRotationOffset; // 3
private Rigidbody rb; // 4
Here’s what these variables are for:
- A flag to tell whether the controller’s model should be hidden once the player grabs this object.
- The position added after snapping. The object snaps to the controller’s position by default.
- Same as above, except this handles the rotation.
- A cached reference of this object’s Rigidbody component.
Add the following method below the variables:
public override void Awake()
{
base.Awake();
rb = GetComponent<Rigidbody>();
}
Just as in the SimpleGrab script, this overrides the base Awake()
method, calls the base and caches the RigidBody component.
Next up are the helper methods, which form the real meat of this script.
Add the following method below Awake()
:
private void ConnectToController(RWVR_InteractionController controller) // 1
{
cachedTransform.SetParent(controller.transform); // 2
cachedTransform.rotation = controller.transform.rotation; // 3
cachedTransform.Rotate(snapRotationOffset);
cachedTransform.position = controller.snapColliderOrigin.position; // 4
cachedTransform.Translate(snapPositionOffset, Space.Self);
rb.useGravity = false; // 5
rb.isKinematic = true; // 6
}
The way this script attaches the object differs from the SimpleGrab script, as it doesn’t use a FixedJoint, but instead makes itself a child of the controller. This means the connection between the controller and snap objects can’t be broken by force. This will keep everything stable for this tutorial, but you might prefer to use a FixedJoint in your own projects.
Taking it play-by-play:
- Accept a controller as a parameter to connect to.
- Set this object’s parent to be the controller.
- Make this object’s rotation the same as the controller and add the offset.
- Make this object’s position the same as the controller and add the offset.
- Disable the gravity on this object; otherwise, it would fall out of your hand.
- Make this object kinematic. While attached to the controller, this object won’t be under the influence of the physics engine.
Now add the matching method to release the object by adding the following method:
private void ReleaseFromController(RWVR_InteractionController controller) // 1
{
cachedTransform.SetParent(null); // 2
rb.useGravity = true; // 3
rb.isKinematic = false;
rb.velocity = controller.velocity; // 4
rb.angularVelocity = controller.angularVelocity;
}
This simply unparents the object, resets the rigidbody and applies the controller velocities. In more detail:
- Accept the controller to release as a parameter.
- Unparent the object.
- Re-enable gravity and make the object non-kinematic again.
- Apply the controller’s velocities to this object.
Add the following override method to perform the snapping:
public override void OnTriggerWasPressed(RWVR_InteractionController controller) // 1
{
base.OnTriggerWasPressed(controller); // 2
if (hideControllerModel) // 3
{
controller.HideControllerModel();
}
ConnectToController(controller); // 4
}
This one is fairly straightforward:
- Override
OnTriggerWasPressed()
to add the snap code. - Call the base method.
- If the
hideControllerModel
flag was set, hide the controller model. - Connect this object to the controller.
Now add the release method below:
public override void OnTriggerWasReleased(RWVR_InteractionController controller) // 1
{
base.OnTriggerWasReleased(controller); // 2
if (hideControllerModel) // 3
{
controller.ShowControllerModel();
}
ReleaseFromController(controller); // 4
}
Again, fairly simple:
- Override
OnTriggerWasReleased()
to add the release code. - Call the base method.
- If the
hideControllerModel
flag was set, show the controller model again. - Release this object to the controller.
Save this script and return to the editor. Drag the RealArrow prefab out of the Prefabs folder into the Hierarchy window.
Select the arrow and set its position to (X:0.5, Y:4.5, Z:-0.8). It should be floating above the stone slab now:
Attach the RWVR_Snap To Controller component to the new arrow in the Hierarchy so you can interact with it and set its Hide Controller Model bool to true
. Finally, press the Apply button at the top of the Inspector window to apply the changes to this prefab.
For this object, there’s no need to change the offsets; it should snap to an acceptable position by default.
Save the scene and run it. Grab the arrow and throw it away. Let your inner beast out!
Notice that the arrow will always be positioned properly in your hand, no matter how you pick it up.
You’re all done with this tutorial; play around with the game a bit to get a feel for the dynamics of the interactions.