Advanced VR Mechanics With Unity and the HTC Vive – Part 2

Eric Van de Kerckhove

unity-htc-vive

Introduction

In the first part of this HTC Vive in Unity tutorial, you learned how to create an interaction system and use it to grab, snap and throw objects.

In this second part of this advanced HTC Vive tutorial, you’ll learn how to:

  • Make a functional bow and arrow.
  • Create a virtual backpack.

This tutorial is intended for an advanced audience, and it will skip a lot of the details on how to add components and make new GameObjects, scripts and so on. It’s assumed you already know how to handle these things. If not, check out our series on beginning Unity here.

Getting Started

Download the starter project, unzip it somewhere and open the folder inside Unity. Here’s an overview of the folders in the Project window:

Here’s what each will be used for:

  • Materials: Contains all the materials for the scene.
  • Models: All models are in here.
  • Prefabs: This contains all prefabs that were made in the previous part.
  • Scenes: Contains the game scene and some lighting data.
  • Scripts: All scripts are in here.
  • Sounds: Contains the sound for shooting an arrow from the bow.
  • SteamVR: The SteamVR plugin and all related scripts, prefabs and examples are in here.
  • Textures: Contains the main texture that’s shared by almost all models for the sake of efficiency and the texture for the book.

Open up the Game scene inside the Scenes folder to get started.

Creating The Bow

At the moment there’s not even a bow present in the scene.

Create a new empty GameObject and name it Bow.

Set the Bow‘s position to (X:-0.1, Y:4.5, Z:-1) and its rotation to (X:0, Y:270, Z:80).

Now drag the Bow model from the Models folder onto Bow in the Hierarchy to parent it.

Rename it BowMesh and set its position, rotation and scale to (X:0, Y:0, Z:0), (X:-90, Y:0, Z:-180) and (X:0.7, Y:0.7, Z:0.7) respectively.

It should now look like this:

Before moving on, I’d like to show you how the string of the bow works.

Select BowMesh and take a look at its Skinned Mesh Renderer. Unfold the BlendShapes field to reveal the Bend blendshape value. This is where the magic happens.

Keep looking at the bow. Change the Bend value from 0 to 100 and back by dragging and holding down your cursor on the word Bend in the Inspector. You should see the bow bending and the string being pulled back:

Set Bend back to 0 for now.

Remove the Animator component from the BowMesh, all animations are done using blendshapes.

Now add an arrow by dragging an instance of RealArrow from the Prefabs folder onto Bow.

Name it BowArrow and reset its Transform component to move it into position relative to the Bow.

This arrow won’t be used as a regular arrow, so break the connection to its prefab by selecting GameObject\Break Prefab Instance from the top menu.

Unfold BowArrow and delete its child, Trail. This particle system is used by normal arrows only.

Remove the Rigidbody, second Box Collider and RWVR_Snap To Controller components from BowArrow.

All that should be left is a Transform and a Box Collider component.

Set the Box Collider‘s Center to (X:0, Y:0, Z:-0.28) and set its size to (X:0.1, Y:0.1, Z:0.2). This will be the part the player can grab and pull back.

Select Bow again and add a Rigidbody and a Box Collider to it. This will make sure it has a physical presence in the world when not in use.

Change the Box Collider‘s Center and Size to (X:0, Y:0, Z:-0.15) and (X:0.1, Y:1.45, Z:0.45) respectively.

Now add a RWVR_Snap To Controller component to it. Enable Hide Controller Model, set Snap Position Offset to (X:0, Y:0.08, Z:0) and Snap Rotation Offset to (X:90, Y:0, Z:0).

Play the scene and test if you can pick up the bow.

Before moving on, set up the tags on the controllers so future scripts will function correctly.

Unfold [CameraRig], select both controllers and set their tag to Controller.

In the next part you’ll make the bow work by doing some scripting.

Creating Arrows

The bow system you’ll create consists of three key parts:

  • The bow.
  • The arrow in the bow.
  • A regular arrow that shoots out.

Each of these needs their own script to work together to make the bow shoot.

For starters, the normal arrows need some code to allow them to get stuck in objects and be picked up again later.

Create a new C# script inside the Scripts folder and name it RealArrow. Note this script doesn’t belong in the RWVR folder as it’s not a part of the interaction system.

Open it up and remove the Start() and Update() methods.

Add the following variable declarations below the class declaration:

public BoxCollider pickupCollider; // 1
private Rigidbody rb; // 2
private bool launched; // 3
private bool stuckInWall; // 4

Quite simply:

  1. The arrows have two colliders: one trigger to detect collisions when fired and a regular one that’s used for physical interaction and picking the arrow up again once fired. This variable references the latter.
  2. A cached reference to this arrow’s Rigidbody.
  3. Gets set to true when an arrow is launched from the bow.
  4. Will be set to true when this arrow hits a solid object.

Now add the Awake() method:

private void Awake()
{
    rb = GetComponent<Rigidbody>();
}

This simply caches the Rigidbody component that’s attached to this arrow.

Add the following method below Awake() :

private void FixedUpdate()
{
    if (launched && !stuckInWall && rb.velocity != Vector3.zero) // 1
    {
        rb.rotation = Quaternion.LookRotation(rb.velocity); // 2
    }
}

This snippet will make sure the arrow will keep facing the direction it’s headed. This allows for some cool skill shots, like shooting arrows in the sky and then watching them come down upon the ground again with their heads stuck in the soil. It also makes things more stable and prevents arrows from getting stuck in awkward positions.

FixedUpdate does the following:

  1. If this arrow is launched, not stuck in a wall and has at least some velocity
  2. Look in the direction of the velocity.

Add these methods below FixedUpdate():

public void SetAllowPickup(bool allow) // 1
{
    pickupCollider.enabled = allow;
}

public void Launch() // 2
{
    launched = true;
    SetAllowPickup(false);
}

Looking at the two commented sections:

  1. A small helper method that enables or disables the pickupCollider.
  2. Called when the arrow gets shot by the bow, sets the launched flag to true and doesn’t allow the arrow to be picked up.

Add the next method to make sure the arrow doesn’t move once it hits a solid object:

private void GetStuck(Collider other) // 1
{
    launched = false; // 2
    rb.isKinematic = true; // 3
    stuckInWall = true; // 4
    SetAllowPickup(true); // 5
    transform.SetParent(other.transform); // 6
}

Taking each commented section in turn:

  1. Gets a Collider as a parameter. This is what the arrow will attach itself to.
  2. Make the arrow kinematic so it’s not affected by physics.
  3. Sets the stuckInWall flag to true.
  4. Allow picking this arrow as it stopped moving.
  5. Parent the arrow to the object it hit. This makes sure the arrow stays firmly attached to any object, even if that object is moving.

The final piece of this script to add is OnTriggerEnter(), which is called when the arrow’s trigger hits something:

private void OnTriggerEnter(Collider other)
{
    if (other.CompareTag("Controller") || other.GetComponent<Bow>()) // 1
    {
        return;
    }

    if (launched && !stuckInWall) // 2
    {
        GetStuck(other);
    }
}

You’ll get an error saying Bow doesn’t exist yet. Ignore this for now: you’ll create the Bow script next.

Here’s what’s the code above does:

  1. If this arrow hit a controller or a bow, don’t get stuck. This prevents some annoying behavior, as the arrow could get stuck in the bow right after shooting it otherwise.
  2. This arrow was launched and isn’t stuck yet, so attach it to the object it hit.

Save this script, then create a new C# script in the Scripts folder named Bow. Open it in your code editor.

Coding the Bow

Remove the Start() method and add this line right above the class declaration:

[ExecuteInEditMode]

This will let this script execute its method, even while you’re working in the editor. You’ll see why this can be quite handy in just a bit.

Add these variables above Update():

public Transform attachedArrow; // 1
public SkinnedMeshRenderer BowSkinnedMesh; // 2

public float blendMultiplier = 255f; // 3
public GameObject realArrowPrefab; // 4

public float maxShootSpeed = 50; // 5

public AudioClip fireSound; // 6

This is what they’ll be used for:

  1. A reference to the BowArrow you added as a child of the bow.
  2. Reference to the skinned mesh of the bow. This will be used to manipulate the blend shape of the bow.
  3. The distance from the arrow to the bow will be multiplied by the blendMultiplier to get the final Bend value for the blend shape.
  4. Reference to the RealArrow prefab which will be spawned and shot if the bow’s string has been stretched and released.
  5. This is the velocity the arrow that gets shot will have when the bow’s string gets stretched at its maximum length.
  6. The sound that will be played when an arrow gets shot.

Add the following encapsulated field below the variables:

bool IsArmed()
{
    return attachedArrow.gameObject.activeSelf;
}

This simply returns true if the arrow is enabled. It’s much easier to reference this field than to write out attachedArrow.gameObject.activeSelf each time.

Add the following to the Update() method:

float distance = Vector3.Distance(transform.position, attachedArrow.position); // 1
BowSkinnedMesh.SetBlendShapeWeight(0, Mathf.Max(0, distance * blendMultiplier)); // 2

Here’s what each of these lines do:

  1. Calculates the distance between the bow and the arrow.
  2. Sets the bow’s blend shape to the distance calculated above multiplied by blendMultiplier.

Next, add these methods below Update():

private void Arm() // 1
{
    attachedArrow.gameObject.SetActive(true);
}

private void Disarm() 
{
    BowSkinnedMesh.SetBlendShapeWeight(0, 0); // 2
    attachedArrow.position = transform.position; // 3
    attachedArrow.gameObject.SetActive(false); // 4
}

These methods handle the loading and unloading of arrows in the bow.

  1. Arm the bow by enabling the attached arrow, making it visible.
  2. Reset the bow’s bend, which makes the string snap back to a straight line.
  3. Reset the attached arrow’s position.
  4. Hide the arrow by disabling it.

Add OnTriggerEnter() below Disarm():

private void OnTriggerEnter(Collider other) // 1
{
    if (
        !IsArmed() 
          && other.CompareTag("InteractionObject") 
          && other.GetComponent<RealArrow>() 
          && !other.GetComponent<RWVR_InteractionObject>().IsFree() // 2
    ) {
        Destroy(other.gameObject); // 3
        Arm(); // 4
    }
}

This handles what should happen when a trigger collides with the bow.

  1. Accept a Collider as a parameter. This is the trigger that hit the bow.
  2. This long if-statement returns true if the bow is unarmed and is hit by a RealArrow. There’s a few checks to make sure it only reacts to arrows that are held by the player.
  3. Destroy the RealArrow.
  4. Arm the bow.

This code is essential to make it possible for a player to rearm the bow once the intitally loaded arrow has been shot.

The final method shoots the arrow. Add this below OnTriggerEnter():

public void ShootArrow()
{
    GameObject arrow = Instantiate(realArrowPrefab, transform.position, transform.rotation); // 1
    float distance = Vector3.Distance(transform.position, attachedArrow.position); // 2

    arrow.GetComponent<Rigidbody>().velocity = arrow.transform.forward * distance * maxShootSpeed; // 3
    AudioSource.PlayClipAtPoint(fireSound, transform.position); // 4
    GetComponent<RWVR_InteractionObject>().currentController.Vibrate(3500); // 5
    arrow.GetComponent<RealArrow>().Launch(); // 6

    Disarm(); // 7
}

This might seem like a lot of code, but it’s quite simple:

  1. Spawn a new RealArrow based on its prefab. Set its position and rotation equal to that of the bow.
  2. Calculate the distance between the bow and the attached arrow and store it in distance.
  3. Give the RealArrow a forward velocity based on distance. The further the string gets pulled back, the more velocity the arrow will have.
  4. Play the bow firing sound.
  5. Vibrate the controller to give tactile feedback.
  6. Call the RealArrow’s Launch() method.
  7. Disarm the bow.

Time to set up the bow in the inspector!

Save this script and return to the editor.

Select Bow in the Hierarchy and add a Bow component.

Expand Bow to reveal its children and drag BowArrow to the Attached Arrow field.

Now drag BowMesh to the Bow Skinned Mesh field and set Blend Multiplier to 353.

Drag a RealArrow prefab from the Prefabs folder onto the Real Arrow Prefab field and drag the FireBow sound from the Sounds folder to the Fire Sound.

This is what the Bow component should look like when you’re finished:

Remember how the skinned mesh renderer affected the bow model? Move the BowArrow in the Scene view on its local Z-axis to test what the full bow bend effect looks like:

That’s pretty sweet looking!

You’ll now need to set up the RealArrow to work as intended.

Select RealArrow in the Hierarchy and add a Real Arrow component to it.

Now drag the Box Collider with Is Trigger disabled to the Pickup Collider slot.

Click the Apply button at the top of the Inspector to apply this change to all RealArrow prefabs as well.

The final piece of the puzzle is the special arrow that sits in the bow.

Scripting The Arrow In The Bow

The arrow in the bow needs to be pulled back by the player in order to bend the bow, and then released to fire an arrow.

Create a new C# script inside the Scripts \ RWVR folder and name it RWVR_ArrowInBow. Open it up in a code editor and remove the Start() and Update() methods.

Make this class derive from RWVR_InteractionObject by replacing the following line:

public class RWVR_ArrowInBow : MonoBehaviour

With this:

public class RWVR_ArrowInBow : RWVR_InteractionObject

Add these variables below the class declaration:

public float minimumPosition; // 1
public float maximumPosition; // 2

private Transform attachedBow; // 3
private const float arrowCorrection = 0.3f; // 4

Here’s what they’re for:

  1. The minimum position on the Z-axis.
  2. The maximum position on the Z-axis. This is used together with the variable above to restrict the position of the arrow so it can’t be pulled too far back nor pushed into the bow.
  3. A reference to the Bow this arrow is attached to.
  4. This will be used to correct the arrow position relative to the bow later.

Add the following method below the variable declarations:

public override void Awake()
{
    base.Awake();
    attachedBow = transform.parent;
}

This calls the base class’ Awake() method to cache the transform and stores a reference to the bow in the attachedBow variable.

Add the following method to react while the player holds the trigger button:

public override void OnTriggerIsBeingPressed(RWVR_InteractionController controller) // 1
{
    base.OnTriggerIsBeingPressed(controller); // 2

    Vector3 arrowInBowSpace = attachedBow.InverseTransformPoint(controller.transform.position); // 3
    cachedTransform.localPosition = new Vector3(0, 0, arrowInBowSpace.z + arrowCorrection); // 4
}

Taking it step-by-step:

  1. Override OnTriggerIsBeingPressed() and get the controller that’s interacting with this arrow as a parameter.
  2. Call the base method. This doesn’t do anything at the moment, but it’s added for the sake of consistency.
  3. Get the arrow’s new position relative to the position of the bow and the controller by using InverseTransformPoint(). This allows for the arrow to be pulled back correctly, even though the controller isn’t perfectly aligned with the bow on its local Z-axis.
  4. Move the arrow to its new position and add the arrowCorrection to it on its Z-axis to get the correct value.

Now add the following method:

public override void OnTriggerWasReleased(RWVR_InteractionController controller) // 1
{
    attachedBow.GetComponent<Bow>().ShootArrow(); // 2
    currentController.Vibrate(3500); // 3
    base.OnTriggerWasReleased(controller); // 4
}

This method is called when the arrow is released.

  1. Override the OnTriggerWasReleased() method and get the controller that’s interacting with this arrow as a parameter.
  2. Shoot the arrow.
  3. Vibrate the controller.
  4. Call the base method to clear the currentController.

Add this method below OnTriggerWasReleased():

void LateUpdate()
{
    // Limit position
    float zPos = cachedTransform.localPosition.z; // 1
    zPos = Mathf.Clamp(zPos, minimumPosition, maximumPosition); // 2
    cachedTransform.localPosition = new Vector3(0, 0, zPos); // 3

    //Limit rotation
    cachedTransform.localRotation = Quaternion.Euler(Vector3.zero); // 4

    if (currentController)
    {
        currentController.Vibrate(System.Convert.ToUInt16(500 * -zPos)); // 5
    }
}

LateUpdate() is called at the end of every frame. It’s used to limit the position and rotation of the arrow and vibrates the controller to simulate the effort needed to pull the arrow back.

  1. Store the Z-axis position of the arrow in zPos.
  2. Clamp zPos between the minimum and maximum allowed position.
  3. Reapply the position of the arrow with the clamped value.
  4. Limit the rotation of the arrow to Vector3.zero.
  5. Vibrate the controller. The more the arrow is pulled back, the more intense the vibration will be.

Save this script and return to the editor.

Unfold Bow in the Hierarchy and select its child BowArrow. Add a RWVR_Arrow In Bow component to it and set Minimum Position to -0.4.

Save the scene, and get your HMD and controllers ready to test out the game!

Pick up the bow with one controller and pull the arrow back with the other one.

Release the controller to shoot an arrow, and try rearming the bow by dragging an arrow from the table onto it.

The last thing you’ll create in this tutorial is a backpack (or a quiver, in this case) from which you can grab new arrows to load in the bow.

For that to work, you’ll need some new scripts.

Creating A Virtual Backpack

In order to know if the player is holding certain objects with the controllers, you’ll need a controller manager which references both controllers.

Create a new C# script in the Scripts/RWVR folder and name it RWVR_ControllerManager. Open it in a code editor.

Remove the Start() and Update() methods and add these variables:

public static RWVR_ControllerManager Instance; // 1

public RWVR_InteractionController leftController; // 2
public RWVR_InteractionController rightController; // 3

Here’s what the above variables are for:

  1. A public static reference to this script so it can be called from all other scripts.
  2. Reference to the left controller.
  3. A reference to the right controller.

Add the following method below the variables:

private void Awake()
{
    Instance = this;
}

This saves a reference to this script in the Instance variable.

Now add this method below Awake():

public bool AnyControllerIsInteractingWith<T>() // 1
{
    if (leftController.InteractionObject && leftController.InteractionObject.GetComponent<T>() != null) // 2
    {
        return true;
    }

    if (rightController.InteractionObject && rightController.InteractionObject.GetComponent<T>() != null) // 3
    {
        return true;
    }

    return false; // 4
}

This helper method checks if any of the controllers have a certain component attached to them:

  1. A generic method that accepts any type.
  2. If the left controller is interacting with an object and it has the requested component type attached, return true.
  3. If the right controller is interacting with an object and it has the requested component type attached, return true.
  4. If neither controller has the requested component attached, return false.

Save this script and return to the editor.

The final script is for the backpack itself.

Create a new C# script in the Scripts \ RWVR folder and name it RWVR_SpecialObjectSpawner.

Open it in your favorite code editor and replace this line:

public class RWVR_SpecialObjectSpawner : MonoBehaviour

With this:

public class RWVR_SpecialObjectSpawner : RWVR_InteractionObject

This makes the backpack inherit from RWVR_InteractionObject.

Now remove both the Start() and Update() methods and add the following variables in their place:

public GameObject arrowPrefab; // 1
public List<GameObject> randomPrefabs = new List<GameObject>(); // 2

These are the GameObjects which will be spawned out of the backpack.

  1. A reference to the RealArrow prefab.
  2. List of GameObjects to choose from when something is pulled out of the backpack.

Add the following method:

private void SpawnObjectInHand(GameObject prefab, RWVR_InteractionController controller) // 1
{
    GameObject spawnedObject = Instantiate(prefab, controller.snapColliderOrigin.position, controller.transform.rotation); // 2
    controller.SwitchInteractionObjectTo(spawnedObject.GetComponent<RWVR_InteractionObject>()); // 3
    OnTriggerWasReleased(controller); // 4
}

This method attaches an object to the player’s controller, as if they grabbed it from behind their back.

  1. Accept two parameters: prefab is the GameObject that will be spawned, while controller is the controller to which it will attach to.
  2. Spawn a new GameObject with the same position and rotation as the controller and store a reference to it in spawnedObject.
  3. Switch the controller’s active InteractionObject to the object that was just spawned.
  4. “Release” the backpack to give full focus to the spawned object.

The next method decides what kind of object should be spawned when the player pressed the trigger button over the backpack.

Add the following method below SpawnObjectInHand():

public override void OnTriggerWasPressed(RWVR_InteractionController controller) // 1
{
    base.OnTriggerWasPressed(controller); // 2

    if (RWVR_ControllerManager.Instance.AnyControllerIsInteractingWith<Bow>()) // 3
    {
        SpawnObjectInHand(arrowPrefab, controller);
    }
    else // 4
    {
        SpawnObjectInHand(randomPrefabs[UnityEngine.Random.Range(0, randomPrefabs.Count)], controller);
    }
}

Here’s what each part does:

  1. Overrides the base OnTriggerWasPressed() method.
  2. Calls the base OnTriggerWasPressed() method.
  3. If any of the controllers are holding a bow, spawns an arrow.
  4. If not, spawns a random GameObject from the randomPrefabs list.

Save this script and return to the editor.

Create a new Cube in the Hierarchy, name it BackPack and drag it onto [CameraRig]\ Camera (head) to parent it to the player’s head.

Set its position and scale to (X:0, Y:-0.25, Z:-0.45) and (X:0.6, Y:0.5, Z:0.5) respectively.

The backpack is now positioned right behind and under the player’s head.

Set the Box Collider‘s Is Trigger to true. this object doesn’t need to collide with anything.

Set Cast Shadows to Off and disable Receive Shadows on the Mesh Renderer component.

Now add a RWVR_Special Object Spawner component and drag a RealArrow from the Prefabs folder onto the Arrow Prefab field.

Finally, drag a Book and a Die prefab from the same folder to the Random Prefabs list.

Now add a new empty GameObject, name it ControllerManager and add a RWVR_Controller Manager component to it.

Expand [CameraRig] and drag Controller (left) to the Left Controller slot and Controller (right) to the Right Controller slot.

Now save the scene and test out the backpack. Try grabbing behind your back and see what stuff you’ll pull out!

That concludes this tutorial! You now have a fully functional bow and arrow and an interaction system you can expand with ease!

Where To Go From Here?

You can download the finished project here.

In this tutorial you’ve learned how to create the following features and updates for your HTC Vive game:

  • Expand upon the interaction system.
  • Make a functional bow and arrow.
  • Create a virtual backpack.

If you’re interested in learning more about creating killer games with Unity, check out our book, Unity Games By Tutorials.

In this book, you create four complete games from scratch:

  • A twin-stick shooter
  • A first-person shooter
  • A tower defense game (with VR support!)
  • A 2D platformer

By the end of this book, you’ll be ready to make your own games for Windows, macOS, iOS, and more!

This book is for complete beginners to Unity, as well as for those who’d like to bring their Unity skills to a professional level. The book assumes you have some prior programming experience (in a language of your choice).

If you have any comments or suggestions, please join the discussion below!

Team

Each tutorial at www.raywenderlich.com is created by a team of dedicated developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Eric Van de Kerckhove

Eric is a belgian hobbyist game dev and has been so for more than 10 years.
He started with DarkBasic, RPG Maker, Game Maker & XNA and now he makes games using Unity.
Eric also takes interest in 3D modeling, vector art and playing video games.
He is currently the Unity team lead.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 20 total!

iOS Team

... 74 total!

Android Team

... 30 total!

Unity Team

... 12 total!

Articles Team

... 14 total!

Resident Authors Team

... 25 total!

Podcast Team

... 7 total!

Recruitment Team

... 9 total!