Virtual Reality
Creating a Bow and Arrow experience for VR: Part 1
by
Ashray Pai
November 2021
12 MIN READ
Virtual Reality

Creating a Bow and Arrow experience for VR: Part 1

by
Ashray Pai
November 2021
12 MIN READ
FREE
Crucial Mistakes New
XR Devs Must Avoid
Level up your XR coding game
by dodging common mistakes!
Grab yourS NOW

The bow and arrow are considered as high damage and stealth weapon in most games today. If you have played games like FarCry or Assassins Creed you will know how amazing they are to play with. With VR, we can take that experience to the next level. With the freedom to choose the type of bow, visual effects, sound effects, etc. we can create an immersive experience for the user.

This blog was inspired by Unity's prototype series.

1. Prerequisites

You must have a basic knowledge of installing the XR Interaction Toolkit, its components and properties, working with prefabs, and the basics of C# as well.

You should know how to set up a scene with a ground plane and an XR Rig. If not, this tutorial will help you get started with the XR Interaction Toolkit. You can learn more about prefabs here.

Note: This was built and tested in Unity version 2020.3.19 LTS and XR Interaction Toolkit version 1.0.0-pre.8.

2. Importing and setting up the models

2.1 Bow GameObject

Let's start by importing the models. We'll set up the Bow in such a way that the bow model can be changed easily without affecting any of the interactions.

  • Create an empty GameObject and name it Bow.
  • Drag the Bow prefab from the project folder and place it as a child of the GameObject Bow. Rename the bow prefab as BowPrefab.
  • Add a Box Collider to the BowPrefab where the user can grab the bow.
Adding box collider to GameObjects
  • For the string, we'll use a line renderer. So, create an empty GameObject as a child of the GameObject Bow and name it Line.  Add the Line Renderer component→ Make sure the Use World Space field is unchecked → Set the Sizeof the position field to 3 and adjust its values so that it matches the bow → Add a material to the string, you can either create your own or make use of the String material provided along with the asset  → Reduce the width of the line to a value of your choice.
Creating line renderer
Setting the line renderer

The reason for using the LineRenderer with 3 points is that its value at index 1 can be used to visualize the stretching effect of the string and the attachment point of the arrow.

Line renderer visualization
  • Create two empty GameObjects as children of the GameObject Bow. → Name them StartingPoint and EndingPoint. The transform values of these GameObjescts will later be used as the starting and the ending point of the BowString.
    So, adjust the transform value of the GameObject StartingPoint such that it has the same value as the vertex at position 1 of the LineRenderer. Also, adjust the transform of the GameObject EndingPoint to a point where you feel the string would reach its maximum pull position.
Bow sting starting point
Bow sting ending point
  • Create an empty GameObject and name it String → The transform value of this should be the same as the value of Line Rendered at index 1 → Add a spherical collider and check the "Is trigger" box → Adjust the collider radius. The collider radius will determine the space within which the string can be grabbed. This GameObject will later be used as an interface to pull the string (with or without the arrow).
Setting up sphere collider

2.2 Arrow GameObject

With the Bow GameObject created, next up is the Arrow GameObject.

  • Create an empty game object and name it Arrow.
  • Drag the Arrow prefab from the project folder and place it as a child of the GameObject Arrow. Rename the Arrow prefab as ArrowPrefab.
  • Originally the arrow prefab will be facing downwards, so rotate it by -90 units about the x-axis such that the prefab is always facing the parent's local z-axis.
Creating arrow gameobject
  • Add a Box Collider to the ArrowPrefab where the user can grab the arrow.
  • Create an empty game object as a child of the and name it as tip. Align it to the arrow's tip. (The transform of this game object will be used later while scripting the arrow interaction)

<div class=callout><div class="callout-emoji">💡</div><p style="margin-bottom:0px;">Note: If you change the orientation of the arrow prefab, make sure to change the transform of the Tip GameObject as well!<p></div>

  • Create a tag named Arrow and assign it to the Arrow GameObject.
Creating tag in unity

In the next section, we will learn how to add various interactions to the Bow and Arrow GameObject.

3. Scripting the Interactions

There are 4 main features:

  1. String Interaction
  2. Bow Interaction
  3. Arrow Interaction
  4. Socket Interaction

These features will enable us to have the bow and arrow interaction in VR, as soon as we combine them.

3.1 String Interaction

This section focuses on the string interaction. Answering a few of the following questions will give you a better understanding.

  • In which direction can we pull the string? In this project, the string movement is restricted only in one direction i.e backward. It cannot be pulled left or right.
  • What happens if the user pulls it in the other direction? If the string gets pulled towards the left or the right, visually the sting will get pulled backward, but the extent of the pull will be determined using vector calculation.
  • To what extent can the string be pulled? The extent to which the string can be pulled is determined by playtesting the interaction.
  • What should happen when the string is pulled? A floating type number should be returned which can be used later for implementing other features like moving the arrow, visualizing the string pull using a line renderer.

3.1.1 The Code

Let's code the string interaction that will enable us to pull the string between two fix points that we have already created above and return a value between 0 and 1 based on the position of the string.

In the next section 3.1.2 we have the breakdown of the code as well, so don't worry if you are not able to understand the math behind the code immediately.

Create a new C# script and name it StringInteraction and copy the following code. The code will return a value for the variable PullAmount when the string is grabbed and pulled. The value of PullAmount is based on the interactor's position and string position in world space.


using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;

public class StringInteraction : XRBaseInteractable
{
    [SerializeField] private Transform stringStartPoint;
    [SerializeField] private Transform stringEndPoint;

    private XRBaseInteractor stringInteractor = null; 
    private Vector3 pullPosition;
    private Vector3 pullDirection;
    private Vector3 targetDirection;

    public float PullAmount { get; private set; } = 0.0f;
    public Vector3 StringStartPoint { get => stringStartPoint.localPosition;}
    public Vector3 StringEndPoint { get => stringEndPoint.localPosition; }

    protected override void Awake()
    {
        base.Awake();
    }

    protected override void OnSelectEntered(SelectEnterEventArgs args)
    {
        base.OnSelectEntered(args);
        this.stringInteractor = args.interactor;
    }

    protected override void OnSelectExited(SelectExitEventArgs args)
    {
        base.OnSelectExited(args);
        this.stringInteractor = null;
        this.PullAmount = 0f;
    }

    public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase)
    {
        base.ProcessInteractable(updatePhase);
        
        if(updatePhase == XRInteractionUpdateOrder.UpdatePhase.Dynamic && isSelected)
        {
            this.pullPosition = this.stringInteractor.transform.position;
            this.PullAmount = CalculatePull(this.pullPosition);
						//Debug.Log("<<<<< Pull amount is "+ PullAmount+" >>>>>");
        }           
    }

    private float CalculatePull(Vector3 pullPosition)
    {
        this.pullDirection = pullPosition - stringStartPoint.position;
        this.targetDirection = stringEndPoint.position - stringStartPoint.position;
        float maxLength = targetDirection.magnitude;
        
        targetDirection.Normalize();

        float pullValue = Vector3.Dot(pullDirection, targetDirection) / maxLength;
        return Mathf.Clamp(pullValue, 0, 1);
    }
}

3.1.2 The code breakdown

This section will help you understand the code, feel free to skip to Section 3.1.3 (Testing) if you understood the program.

Declarations


using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;

public class StringInteraction : XRBaseInteractable
{
    [SerializeField] private Transform stringStartPoint;
    [SerializeField] private Transform stringEndPoint;

    private XRBaseInteractor stringInteractor = null; 
    private Vector3 pullPosition;
    private Vector3 pullDirection;
    private Vector3 targetDirection;

    public float PullAmount { get; private set; } = 0.0f;
    public Vector3 StringStartPoint { get => stringStartPoint.localPosition;}
    public Vector3 StringEndPoint { get => stringEndPoint.localPosition; }

The script inherits from XRBaseInteractable class.

Variable NameTypeUsestringStartPointis of type TransformTo store the transform values of the starting point of the stringstringEndPointis of type TransformTo store the transform values of the ending point of the stringstringInteractoris of type XRBaseInteractorTo assign the interactor that will be pulling the sting. In this case, it can either be the left hand or the right hand.pullPoistionis of type Vector3To store the position of the VR hand after pulling the stringpullDirectionis of type Vector3To store the direction of pulltargetDirectionis of type Vector3To store the direction the bow is facingPullAmountis a Property of type floatTo store the pull value privately in this class. It is read-only and other classes can read the pull value using this property.StringStartPointis a Property of type Vector3To store the local position of the variable stringStartPoint. It is read-only and other classes can read the local position value using this property.StringEndPointis a Property of type Vector3To store the local position of the variable stringEndPoint. It is read-only and other classes can read the local position value using this property.

Initialization


protected override void Awake()
    {
        base.Awake();
    }

    protected override void OnSelectEntered(SelectEnterEventArgs args)
    {
        base.OnSelectEntered(args);
        this.stringInteractor = args.interactor;
    }

    protected override void OnSelectExited(SelectExitEventArgs args)
    {
        base.OnSelectExited(args);
        this.stringInteractor = null;
        this.PullAmount = 0f;
    }
  • As the script inherits from XRBaseInteractable class:
  • The access modifiers such as protected or public will be the same as the existing function.
  • The keyword override is used as we are adding functionalities to already existing functions.
  • The keyword base followed by the method call is used to call the same method of the base class and make sure its code was executed already, before adding our code.
  • The functions OnSelectEntered or OnSelectExited are called when the Interactor (in our case the VR hands) grabs or releases the Interactable (String) respectively.
  • When the function OnSelectEntered gets called, the Interactor is passed from the event arguments to the variable stringInteractor.
  • Similarly, when the function OnSelectExited gets called, the Interactor is removed from the variable stringInteractor and the value of the variable PullAmount is set to zero. Setting the value to zero would mean that the string goes back to its original position.

Update and Calculation


public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase)
    {
        base.ProcessInteractable(updatePhase);
        
        if(updatePhase == XRInteractionUpdateOrder.UpdatePhase.Dynamic && isSelected)
        {
            this.pullPoistion = stringInteractor.transform.position;
            this.PullAmount = CalculatePull(pullPoistion);
						//Debug.Log("<<<<< Pull amount is "+ PullAmount+" >>>>>");
        }           
    }

    private float CalculatePull(Vector3 pullPosition)
    {
        
        this.pullDirection = pullPoistion - stringStartPoint.position;
        this.targetDirection = stringEndPoint.position - stringStartPoint.position;
        float maxLength = targetDirection.magnitude;
        
        targetDirection.Normalize();

        float pullValue = Vector3.Dot(pullDirection, targetDirection) / maxLength;
        return Mathf.Clamp(pullValue, 0, 1);
    }
  • The ProcessInteractable function can be considered as the Update function for the XRInteractions. When the ProcessInteractable function gets called, ...
  • ... it first checks if the updatePhase is Dynamic. What it means by that is only if the Interactables are getting updated in real-time then the following code will be executed. If they are getting updated in the Fixed or Late updatePhase, the interactions will not be smooth.
  • ... it checks if the string is grabbed with the help of isSelected function.
  • If both the conditions are met, the position of the Interactor is assigned to the variable pullPosition, which is then passed as a parameter to the function CalculatePull.
  • CalculatePull is a function that takes a Vector 3 parameter ( in our case it's the position of the Interactor) and returns a value between 0 and 1. This function mainly involves mathematical calculations (vector calculations) and this is how it works:
  • Consider the following example where the starting position of the string is at (0, 0) coordinate and the string can stretch max up to (0, -2) coordinate.
Maths behind bow interaction
  • Vector3 pullDirection = pullPosition - start.position;
    The pullDirection vector is calculated by finding the difference in the position of the Interactor and the position of the string at the start.
    The value of the vector depends on how far behind the hand is with respect to the string's starting point and the direction depends on where the hand is positioned.
    For this example, let's consider three different pullPositions therefore three different pullDirection vectors.
  • Vector3 targetDirection = end.position - start.position;
    The targetDirection vector is calculated by finding the difference between the string's ending position and starting position.
    The value of the vector remains constant but the direction changes based on which direction in which the user is pointing the bow.
    For this example, let's consider the targetDirection as a vector starting from (0,-2) coordinate and passing through (0, 0) coordinate.
Bow's local target direction
Bow's local target direction
  • float maxLength = targetDirection.magnitude;
    maxLength is the magnitude of the targetDirection. The magnitude of the vector is the square root of (x^2*+y^2*+z^2).
    In this example, the targetDirection has the coordinates (0,0) and (-2,0), so the magnitude is sq.root of ( (x1-x2)^2 + (y1-y2)^2) = sq.root of ( (0^2) +(-2^2)) = 2 . So the value of maxLength is 2
  • targetDirection.Normalize();
    The function Normalize() changes a given vector to a vector with unit length but the direction remains the same.
    In this example, the vector is having a magnitude of 2. After using the Normalize() function the vector will be facing the same direction but its magnitude is now 1.
    Why Normalize? The reason for doing this is so that we can get the pullValue to as close as 0 and 1. You will have a better understanding after the next section.
  • float pullValue = Vector3.Dot(pullDirection, targetDirection) / maxLength;
    The pullValue is calculated by finding the dot product of the pullDirection and targetDirection followed by division with maxLength.
    The dot product is calculated by multiplying the magnitudes of the two vectors and the cosine of the angle between them.
    For this example let's consider the angles as 37deg, 83 deg, and 31deg for pullDirection1, pullDirection2, and pullDirection3 respectively.
    We know that magnitude of targetDirection is 1. Let calculate the magnitude for the pull directions:
    Case 1: The Pull Direction 1 vector has coordinate (0,0) and (-2.5, -1.0), on calculation, the magnitude equals 2.692.
    Case 2: The Pull Direction 2 vector has coordinate (0,0) and (1.0, -1.5), on calculation, the magnitude equals 1.802.
    Case 3: The Pull Direction 3 vector has coordinate (0,0) and (3.0, -1.0), on calculation, the magnitude equals 3.162.
  • Now that we have the magnitudes for both the vectors lets calculate the pullValue for both:
    Case 1: pullValue = 2.692 * 1* cos(37) / 2 = 1.03
    Case 2: pullValue = 1.802 * 1* cos(83) / 2 = 0.33
    Case 3: pullValue = 3.162 * 1* cos(31) / 2 = 1.123
  • If the target direction vector was not Normalized then the pull value would have been twice as big. This means, clamping the values between 0 and 1 would always return a number closer to 1.
  • return Mathf.Clamp(pullValue, 0, 1);
    The Clamp function is used to return the given value if it is within the min and max range.
    It returns the min value if the given float value is less than the min and returns the max value if the given value is greater than the max value. We need the value to be always between 0 and 1.
    For example, if the pullValue is -0.5, the value returned is 0. If the pullValue is 1.45, the value returned is 1. If the pullValue is anything in between 0 and 1, the same value is returned.

3.1.3 Testing

Before proceeding to the next steps, let's test if this works as intended.

  • In the StringInteraction script, uncomment the line 44: Debug.Log("<<<<< Pull amount is "+ PullAmount+" >>>>>");
  • Save the script and add it to the GameObject String.
  • Drag and drop the GameObjects StartingPoint and EndingPoint in the respective fields.
  • Add XRGrabInteractable component to the GameObject Bow → Add the collider of the BowPrefab → On the rigid body component check the box for Use gravity and Is Kinematic.
Adding serealized fields to component
Note: While developing this project I made use of the XR Simulator for testing. I felt it speeds up the development process:
  • Play the scene. Grab that string and slowly start pulling it.
  • Once you start pulling, you can see the Console window logging the value of pullAmount, which will always be a value between 0 and 1.

You will not be able to see the string getting pulled visually and that is because we are yet to implement that feature. In the next section, we will use the pullAmount value to move the line renderer to represent the pulling action.

Testing Bow interaction
  • Exit the play mode → go back the script and comment the line 44: Debug.Log("<<<<< Pull amount is "+ PullAmount+" >>>>>"); You can delete it as well.
  • First, remove the XRGrabInteractable component and then remove the RigidBody component from the GameObject Bow.
Removing components

🎉 With this we have completed the String Interaction, let's move on to the Bow Interaction.

Thanks for reading this blog post 🧡

If you've enjoyed the insights shared here, why not spread the word? Share the post with your friends and colleagues who might also find it valuable.

Your support means the world to us and helps us create more content you'll love.

Read more by this author

Continue reading