Virtual Reality
Creating a Bow and Arrow experience for VR: Part 2
by
Ashray Pai
December 2021
7 MIN READ
Virtual Reality

Creating a Bow and Arrow experience for VR: Part 2

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

This blog post is the continuation of the previous post where we've learned about setting up the prefabs and adding the string interaction component to it. If you haven't checked it out already, then do check out Part 1.

3.2 Bow Interaction

This section focuses on the overall bow interaction. The bow interaction involves:

  • Making the bow grab interactable.
  • Updating the line renderer to show the sting movement.
  • Moving the GameObject String within the starting and ending point when pulled. (In the later stage, a socket interaction component will be added to the GameObject String. The socket will hold the arrow in place. So moving the GameObject String will in turn move the arrow.

3.2.1 The Code

In the next section 3.2.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.

Let's implement the bow interaction by writing the code for the same, so create a new C# script and name it BowInteraction and copy the following code. The code will update the line renderer and the transform of the GameObject String as per PullAmount.


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

public class BowInteraction : XRGrabInteractable
{
    private LineRenderer bowString; 
    private StringInteraction stringInteraction; 

    [SerializeField] private Transform socketTransform;
    public bool BowHeld { get; private set; }
    

    protected override void Awake()
    {
        base.Awake();
        stringInteraction = GetComponentInChildren();
        bowString = GetComponentInChildren();
        this.movementType = MovementType.Instantaneous;
    }

    protected override void OnSelectEntered(SelectEnterEventArgs args)
    {
        BowHeld = true;
        base.OnSelectEntered(args);
    }

    protected override void OnSelectExited(SelectExitEventArgs args)
    {
        BowHeld = false;
        base.OnSelectExited(args);       
    }

    public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase)
    {
        base.ProcessInteractable(updatePhase);
        
        if (updatePhase == XRInteractionUpdateOrder.UpdatePhase.Dynamic)
        {
                UpdateBow(stringInteraction.PullAmount);
        }
    }

    private void UpdateBow(float pullAmount)
    {
        float xPositionStart = stringInteraction.stringStartPoint.localPosition.x;
        float xPositionEnd = stringInteraction.stringEndPoint.localPosition.x;

        Vector3 linePosition = Vector3.right * Mathf.Lerp(xPositionStart, xPositionEnd, pullAmount);

        bowString.SetPosition(1, linePosition);
        socketTransform.localPosition = linePosition;
        
    }

}

3.2.2 The code breakdown

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

Declarations


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

public class BowInteraction : XRGrabInteractable
{
    private LineRenderer bowString; 
    private StringInteraction stringInteraction; 

    [SerializeField] private Transform socketTransform;

    public bool BowHeld { get; private set; }

The script inherits from XRBaseInteractable class.

Variable NameTypeUsebowStringis of type LineRendererTo update the mid-point of the line rendered based on the pullAmountstringInteractionis of type StringInteractionTo get the value of PullAmountsocketTransformis of type TransformTo store the transform value of the socket interactor which is nothing but the transform of the GameObject StringBowHeldis a Property of type boolTo store the value as true if the bow is grabbed and false if it is not grabbed. It is read-only and other classes can read the boolean value.

Initialization


protected override void Awake()
    {
        base.Awake();
        stringInteraction = GetComponentInChildren();
        bowString = GetComponentInChildren();
        this.movementType = MovementType.Instantaneous;
    }

    protected override void OnSelectEntered(SelectEnterEventArgs args)
    {
        BowHeld = true;
        base.OnSelectEntered(args);
    }

    protected override void OnSelectExited(SelectExitEventArgs args)
    {
        BowHeld = false;
        base.OnSelectExited(args);       
    }
  • 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 is executed already, before/ after adding our code.
  • The function Awake() is called when the script instance is being loaded. On Awake, the variables are initialized to get the respective components.
  • The functions OnSelectEntered or OnSelectExited are called when the Interactor (in our case, its the VR hands) grabs or releases the Interactable (Bow) respectively.
  • When the function OnSelectEntered gets called, the variable BowHeld is set to true.
  • Similarly, when the function OnSelectExited gets called, the variable BowHeld is set to false.

Update and Calculation


public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase)
    {
        base.ProcessInteractable(updatePhase);      
        if (updatePhase == XRInteractionUpdateOrder.UpdatePhase.Dynamic)
        {
                UpdateBow(stringInteraction.PullAmount);
        }
    }

    private void UpdateBow(float pullAmount)
    {
        float xPositionStart = stringInteraction.stringStartPoint.localPosition.x;
        float xPositionEnd = stringInteraction.stringEndPoint.localPosition.x;

        Vector3 linePosition = Vector3.right * Mathf.Lerp(xPositionStart, xPositionEnd, pullAmount);

        bowString.SetPosition(1, linePosition);
        socketTransform.localPosition = linePosition;      
    }

}
  • As stated earlier, 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 calls the function UpdateBow and passes the PullAmount as its parameter.
  • The UpdateBow function takes a float value and updates the position of the line renderer and the socket. Wondering how it works?
  • The positions array of the line renderer is set to a size of 3. It holds the start, center and endpoint of the bowString. The first and last points are fixed. So the positions of the middle point (index 1) have to be changed to visualize the string getting pulled, mainly the local x component of the position.
Line Renderer
  • float xPositionStart = stringPosition.stringStartPoint.localPosition.x;
    The local x position of the string's starting point is stored in the variable xPositionStart.
    In our case the starting position is (0.25, 0, 0) . So the value of xPositionStart is going to be 0.25.
  • float xPositionEnd = stringPosition.stringEndPoint.localPosition.x;
    The local x position of the string's ending point is stored in the variable xPositionEnd .
    In our case the end position is (0.5, 0, 0) . So the xPositionEnd is going to be 0.5
  • Vector3 linePosition = Vector3.right * Mathf.Lerp(xPositionStart, xPositionEnd, pullAmount);
    Vector3.right gives a vector with component only in the x-axis i.e it is a shorthand for writing Vector3 (1, 0, 0).
    The lerp function linearly interpolates the pullAmount between the values of starting point and ending point. In our case it gives back the value in the range from 0.25 to 0.5 for a given value t. t is clamped from 0 to 1 and represents a percentage.
    For example, t=0.5 gives back the middle of the range, which would be: 0.375. t=0 gives back the starting value of the range, which would be: 0.25 and t=1 gives back the ending value of the range, which would be: 0.5.
Lerp
  • Finally, the vector(1, 0, 0) is multiplied with the returned value which gives the current position of the string. That value is stored in the variable linePosition.
  • bowString.SetPosition(1, linePosition);
    SetPosition is a method is an API method of the LineRenderer component, which takes an index as integer, which corresponds to the vertex in the line and sets it to a given position defined as Vector3.
  • socketPosition.socketTransform.localPosition = linePosition;
    When the sting gets pulled/released, this line of code moves the socket along with the string. So that the arrow when attached to the socket can also move along as well.

3.2.3 Testing

Now, let's test if this works as intended.

  • Add the BowInteraction script to the GameObject Bow.
  • The Bow has two colliders, one on the BowPrefab and one on the String. To make sure that the bow is grabbed only from the collider in prefab, drag and drop the BowPrefab in the collider field of the BowInteraction component.
Bow's local target direction
  • Later, a socket interaction will be added to the String GameObject which will arrow in place. To move the arrow along with the string we need to drag and drop the String GameObject into the socket transform field.
Bow interaction
  • Play the scene and test it by grabbing the bow. While doing that, there is possibility that the orientation could be wrong, as you can see below.
Grab Orientation
  • To correct this, create an empty object as a child of Bow and name it AttachTransform. Set the rotation and position of the attach transform to the desired orientation. For this case the bow is turning about y-axis by -90 units, so set the rotation of the attach transform to -90 units in y-axis.
Attatch transform
  • Drag and drop this gameobject inside of the BowInteraction component's attach transform field.
Attatch transform 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 once again and now you will be able to grab the bow. Also, you will be able to see the string getting pulled visually as well.
Attatch transform component

🎉 With this we have completed the Bow Interaction, let's move on to the Arrow 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