Create your reality

This blog post is the continuation of the previous post where we've learned about setting up the Bow interaction component. This blog post will also be the final part where we’ll learn about the Arrow and Socket interaction. If you haven't checked out the previous two posts Part 1 and Part 2, then do check it out.

3.3  Arrow Interaction

In this section, we will set up the arrow so that it can be grabbed and fired using the bow.  Answering a few of the following questions will give you a better understanding.

  • How to fire the arrow upon release?
    We will apply a force in the forward direction.
  • How much force should be applied?
    The force applied will be proportionate to the string pull amount.
  • How to decide its path/ trajectory?
    By using physics calculations and LookRotation API method of Quaternion that does vector calculations.
  • What happened when it comes in contact with another object?
    The arrow will get attached to the object and it will impart some force to it if it had a Rigidbody component.

In the previous two sections, inheritance was used to add the new functionalities to the existing components. Arrow interaction can also be done in the same way, however, I would like to show a different approach that gives the same result. This approach involves adding the XRGrabInteractable component and a script to the game object. The script will take the instance of this XRGrabInteractable and makes use of events from that component to achieve the arrow interactions.

Before scripting, in the Unity editor, add the XRGrabInteractable component to the Arrow GameObject.

XRGrabInteractable

3.3.1 The Code

Create a new C# script, name it as ArrowInteraction and copy the following code. The code will apply the required force and physics to the arrow upon releasing it from the bow. It will also stop the arrow on colliding with another object and add an impulse force to the object if it is a rigid body.

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

using System.Collections;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;

[RequireComponent(typeof(XRGrabInteractable))]
public class ArrowInteraction : MonoBehaviour
{
    private XRGrabInteractable xRGrabInteractable = null;
    private bool inAir = false ;
    private Vector3 lastPosition = Vector3.one;
    private Rigidbody arrowRigidBody = null;

    [SerializeField] private float speed;
    [SerializeField] private Transform tipPosition;

    private void Awake()
    {
        this.arrowRigidBody = GetComponent<Rigidbody>();
        this.inAir = false;
        this.lastPosition = Vector3.zero;
        this.xRGrabInteractable = GetComponent<XRGrabInteractable>();
        this.arrowRigidBody.interpolation = RigidbodyInterpolation.Interpolate;
    }         

    private void FixedUpdate()
    {
        if(this.inAir)
        {
            this.CheckCollision();
            this.lastPosition = tipPosition.position;
        }
    }

    private void CheckCollision()
    {
        if (Physics.Linecast(lastPosition, tipPosition.position, out RaycastHit hitInfo))
        {
            if (hitInfo.transform.TryGetComponent(out Rigidbody body))
            {
                this.arrowRigidBody.interpolation = RigidbodyInterpolation.None;
                this.transform.parent = hitInfo.transform;
                body.AddForce(arrowRigidBody.velocity, ForceMode.Impulse);
            }
            this.StopArrow();
        }
    }

    private void StopArrow()
    {
        this.inAir = false;
        this.SetPhysics(false);
    }

    private void SetPhysics(bool usePhysics)
    {
        this.arrowRigidBody.useGravity = usePhysics;
        this.arrowRigidBody.isKinematic = !usePhysics;
    }

    public void ReleaseArrow(float value)
    {
        this.inAir = true;
        SetPhysics(true);
        MaskAndFire(value);
        StartCoroutine(RotateWithVelocity());
        this.lastPosition = tipPosition.position;

    }

    private void MaskAndFire(float power)
    {
        this.xRGrabInteractable.colliders[0].enabled = false;
        this.xRGrabInteractable.interactionLayerMask = 1 << LayerMask.NameToLayer("Ignore");
        Vector3 force = transform.forward * power * speed;
        this.arrowRigidBody.AddForce(force, ForceMode.Impulse);
    }

    private IEnumerator RotateWithVelocity()
    {
        yield return new WaitForFixedUpdate();
        while(this.inAir)
        {
            Quaternion newRotation = Quaternion.LookRotation(arrowRigidBody.velocity);
            this.transform.rotation = newRotation;
            yield return null;
        }
    }
}

3.3.2 The code breakdown

This section will help you understand the code, feel free to skip to Section 3.4 (Socket Interaction) if you understood the program already.

Declarations

using System.Collections;

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

[RequireComponent(typeof(XRGrabInteractable))]
public class ArrowInteraction : MonoBehaviour
{
    private XRGrabInteractable xRGrabInteractable = null;
    private bool inAir = false ;
    private Vector3 lastPosition = Vector3.one;
    private Rigidbody arrowRigidBody = null;

    [SerializeField] private float speed;
    [SerializeField] private Transform tipPosition;
 

As we’ll be instantiating the XRGrabInteractable Class, we need to make sure the GameObject has the XRGrabInteractable component attached as well. To ensure this, we use the line of code [RequireComponent(typeof(XRGrabInteractable))]

Variable Name Type Use
bowString is of type LineRenderer To update the mid-point of the line rendered based on the pullAmount
stringInteraction is of type StringInteraction To get the value of PullAmount
socketTransform is of type Transform To store the transform value of the socket interactor which is nothing but the transform of the GameObject String
BowHeld is a Property of type bool To 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

    private void Awake()
    {
        this.arrowRigidBody = GetComponent<Rigidbody>();
        this.xRGrabInteractable = GetComponent<XRGrabInteractable>();  
				this.inAir = false;
        this.lastPosition = Vector3.zero;
				this.arrowRigidBody.interpolation = RigidbodyInterpolation.Interpolate;
    }

The function Awake() is always called before any Start functions and also just after a prefab is instantiated. (If a GameObject is inactive during start-up Awake is not called until it is made active.) So, on Awake:

  • The variables are initialized to get the respective components.
  • The variable inAir is set to false, as the arrow is stationary at the beginning,
  • The last position is initialized to a zero vector.
  • The RigidbodyInterpolation parameter of the Arrow's RigidBody component is set as interpolate. Interpolation allows you to smooth out the effect of running physics at a fixed frame rate.

Update and Calculation
This section is a bit lengthy so I have broken it down into smaller parts

a) FixedUpdate()

	private void FixedUpdate()
    {
        if(this.inAir)
        {
            this.CheckCollision();
            this.lastPosition = tip.position;
        }
    }

Update runs once per frame, FixedUpdate can run once, zero, or several times per frame, depending on how many physics frames per second are set in the time settings, and how fast/slow the frame rate is. So, at the end of every frame, a check is being made to see if the arrow has been launched. If the value is true, the CheckCollusion() method is called and the last position variable is updated to the current tip position.

b) CheckCollision()

	private void CheckCollision()
    {
        if (Physics.Linecast(lastPosition, tip.position, out RaycastHit hitInfo))
        {
            if (hitInfo.transform.TryGetComponent(out Rigidbody body))
            {
                this.arrowRigidBody.interpolation = RigidbodyInterpolation.None;
                this.transform.parent = hitInfo.transform;
                body.AddForce(arrowRigidBody.velocity, ForceMode.Impulse);
            }
            this.StopArrow();
        }
    }

It is a private method that initially checks if the arrow has made contact with any object. That check is done by using line cast API. Line cast takes a starting point and ending point, draws a line and returns true if there is any collider intersecting that line. It can also return information about the object that it has come in contact with using the RaycastHits.

When the arrow is launched, the variable inAir is set to true and the FixedUpdate() method is called at the end of each frame which in turn calls this method. As the arrow moves, the tip position is changing every frame, the line cast draws a line between the previous tip position and current tip position every frame and if this line cast enters a collider at any point, a boolean true is returned.

Next, it checks for a Rigidbody component attached to the collided object. If found, the RigidbodyInterpolation of the arrow is set to none. The arrow is made to be the child of the object so that when the object moves the arrow can move along.  Depending upon the velocity of the arrow, an equivalent impulse force is applied to the object it collides with.

Finally, the arrow is made to stop using the StopArrow() method.

c) StopArrow()

	private void StopArrow()
    {
        this.inAir = false;
        SetPhysics(false);
    }

It is a private method. This method assigns a boolean false to the inAir variable and sets the physics of the arrow to false using the SetPhysics() method.

d) SetPhysics(bool usePhysics)

	private void SetPhysics(bool usePhysics)
    {
        this.arrowRigidBody.useGravity = usePhysics;
        this.arrowRigidBody.isKinematic = !usePhysics;
    }

It is a private method. This methods takes a boolean value and enables or disables the gravity and kinematic of the rigidbody component attached to the arrow.

When this method is called by passing a Boolean true, the gravity is enabled and kinematic is disabled and when Boolean false is passed as a parameter, the gravity is disabled and kinematic is enabled.

e) ReleaseArrow(float value)

	public void ReleaseArrow(float value)
    {
        this.inAir = true;
        SetPhysics(true);
        MaskAndFire(value);
				this.lastPosition = tipPosition.position;
        StartCoroutine(RotateWithVelocity());
    }

It is a public method that can be called by other classes (This function will be called by the SocketInteraction script ).

It takes a float value and it passes this value to MaskAndFire() method. It sets the variable inAir and the physics to Boolean true. It also sets the last position to the current tip position when released. Finally, it starts a coroutine RotateWithVelocity(). Both the function MaskAndFire and RotateWithVelocity are explained below.

f) MaskAndFire(float power)

	private void MaskAndFire(float power)
    {
        this.xRGrabInteractable.colliders[0].enabled = false;
        this.xRGrabInteractable.interactionLayerMask = 1 << LayerMask.NameToLayer("Ignore");
        Vector3 force = transform.forward * power * speed;
        this.arrowRigidBody.AddForce(force, ForceMode.Impulse);
    }

It is a private method, it takes in a float value and adds a force to the arrow. The magnitude of the force is going to be the speed (set in the editor) times the power (the value of pull amount) and it applies the force in the forward direction (local z-axis).  The force is applied by using the AddForce API method of the rigidbody component.

It also disables the collider on the arrow so that it does not snap back onto the socket. Apart from that, this method updates the layer mask to "Ignore", so that no object can interact with the arrow.

g) Enumerator RotateWithVelocity()

    private IEnumerator RotateWithVelocity()
    {
        yield return new WaitForFixedUpdate();
        while(inAir)
        {
            Quaternion newRotation = Quaternion.LookRotation(arrowRigidBody.velocity, transform.up);
            this.transform.rotation = newRotation;
            yield return null;
        }
    }
}

This coroutine rotates the arrow in the direction of the velocity. If you know how a projectile works, the velocity direction changes as the speed reduce with height i.e. if an arrow is shot straight up, after reaching its peak the arrow will rotate and fall to the ground with its tip pointing downwards. Without this the arrow will move and in the direction of launch and will remain the same till it falls down i.e. if an arrow is shot straight up, it will fall down with its tip pointing up. To know more about projectile motion you can click here.

projectile motion

Quaternion.LookRotation() takes a forward vector and an upward vector and creates a rotation about its axis. More on LookRotatation [1] and [2]

Note: ArrowInteraction can be tested only after completing the SocketInteraction. So moving on to the next interaction i.e SocketInteraction.

3.4  Socket Interaction

In this section, we’ll script the socket interaction whose functionality would be :

  • To release the arrow when the string is let go.
  • To pass the interactor(VR hands) from the Arrow interactable to the String Interactable. What this means is that, when the user brings the arrow close to the string, the script will automatically detach the hand from the arrow and attach it to the string(String Interactaction). Simultaneously, the arrow gets attached to the socket.
    And why are we doing this? To have one smooth flow instead of having 2 additional steps i.e release the arrow in the socket and then grab the string to pull.

3.4.1 The Code

Create a new C# script, name it as SocketInteraction and copy the following code. The code will allow the user to attach the arrow to the string and fire it in a single flow.  This script will be inherited from the XRSocketInteractor class.

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

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

[RequireComponent(typeof(StringInteraction))]
public class SocketInteraction : XRSocketInteractor
{

    private XRBaseInteractor handHoldingArrow = null;
    private XRBaseInteractable currentArrow = null;
    private StringInteraction stringInteraction = null;
    private BowInteraction bowInteraction = null;
    private ArrowInteraction currentArrowInteraction = null;
   
    protected override void Awake()
    {
        base.Awake();
        this.stringInteraction = GetComponent<StringInteraction>();
        this.bowInteraction = GetComponentInParent<BowInteraction>();
    }

    protected override void OnEnable()
    {
        base.OnEnable();
        stringInteraction.selectExited.AddListener(ReleasaeArrow);
    }

    protected override void OnDisable()
    {
        base.OnDisable();
        stringInteraction.selectExited.RemoveListener(ReleasaeArrow);
    }

    protected override void OnHoverEntered(HoverEnterEventArgs args)
    {
        base.OnHoverEntered(args);
        this.handHoldingArrow = args.interactable.selectingInteractor;
        if (args.interactable.tag == "Arrow" && bowInteraction.BowHeld)
        {
            interactionManager.SelectExit(handHoldingArrow, args.interactable);
            interactionManager.SelectEnter(handHoldingArrow, stringInteraction);
        }
    }

    protected override void OnSelectEntered(SelectEnterEventArgs args)
    {
        base.OnSelectEntered(args);
        StoreArrow(args.interactable);
    }

    private void StoreArrow(XRBaseInteractable interactable)
    {
        if (interactable.tag == "Arrow")
        {
            this.currentArrow = interactable;
            this.currentArrowInteraction = currentArrow.gameObject.GetComponent<ArrowInteraction>();
        }
    }

    private void ReleasaeArrow(SelectExitEventArgs arg0)
    {
        if (currentArrow && bowInteraction.BowHeld)
        {
            ForceDetach();
            ReleaseArrowFromSocket();
            ClearVariables();
        }
    }

    public override XRBaseInteractable.MovementType? selectedInteractableMovementTypeOverride
    {
        get { return XRBaseInteractable.MovementType.Instantaneous; }
    }

    private void ForceDetach()
    {
        interactionManager.SelectExit(this, currentArrow);
    }

    private void ReleaseArrowFromSocket()
    {
        currentArrowInteraction.ReleaseArrow(stringInteraction.PullAmount);
    }

    private void ClearVariables()
    {
        this.currentArrow = null;
        this.currentArrowInteraction = null;
        this.handHoldingArrow = null;
    }
}

3.4.2 The code breakdown

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

Declarations

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

[RequireComponent(typeof(StringInteraction))]
public class SocketInteraction : XRSocketInteractor
{

    private XRBaseInteractor handHoldingArrow = null;
    private XRBaseInteractable currentArrow = null;
    private StringInteraction stringInteraction = null;
    private BowInteraction bowInteraction = null;
    private ArrowInteraction currentArrowInteraction = null;

For the socket interaction to work as intended it requires the StringInteraction component. So we use the line of code [RequireComponent(typeof(StringInteraction))] to make sure the GameObject has StringInteraction as well.

Variable Name Type Use
handHoldingArrow is of type XRBaseInteractor To register the interactor which is holding the arrow. The interactor will be passed to the variable when it enters the socket and removed when the interactor leaves the socket.
currentArrow is of type XRBaseInteractable To register the interactable i.e arrow when it snaps into the socket.
stringInteraction is of type StringInteraction To make use of the events and property from the StringInteraction component.
bowInteraction is of type BowInteraction To make use of the property from the BowInteraction component.
currentArrowInteraction is of type ArrowInteraction To make use of the public method from arrow interaction component, which allows us to fire the arrow

Initialization

protected override void Awake()
    {
        base.Awake();
        this.stringInteraction = GetComponent<StringInteraction>();
        this.bowInteraction = GetComponentInParent<BowInteraction>();
    }

    protected override void OnEnable()
    {
        base.OnEnable();
        stringInteraction.selectExited.AddListener(ReleasaeArrow);
    }

    protected override void OnDisable()
    {
        base.OnDisable();
        stringInteraction.selectExited.RemoveListener(ReleasaeArrow);
    }

		protected override void OnHoverEntered(HoverEnterEventArgs args)
    {
        base.OnHoverEntered(args);
        handHoldingArrow = args.interactable.selectingInteractor;
        if (args.interactable.tag == "Arrow" && bowInteraction.BowHeld)
        {
            interactionManager.SelectExit(handHoldingArrow, args.interactable);
            interactionManager.SelectEnter(handHoldingArrow, stringInteraction);
        }
    }

    protected override void OnSelectEntered(SelectEnterEventArgs args)
    {
        base.OnSelectEntered(args);
        StoreArrow(args.interactable);
    }
  • The function Awake() is always called before any Start functions and also just after a prefab is instantiated. On Awake, the variables stringInteraction and bowInteraction are initialized to get the respective components. The rest of the variables will be initialized only when an event takes place or when a certain condition is met.
  • The functions OnEnable() and OnDisable() is called when the object becomes active and disabled respectively. OnEnable a listener is added to receive the call back from selectExited event of the string interaction. i.e when the hand let go of the string. When the listener gets that callback, the method ReleasaeArrow() is called. And, OnDisable the listener is removed.
  • When the interactor first initiates hovering over an Interactable the event OnHoverEntered() gets called. When this function is called, the interactor (VR hand) is assigned to the variable handHoldingArrow.
  • Next, only if the interactable is an Arrow and the bow is held, then the SelectExit and SelectEnter API methods of the interaction manager are used to first exit the hand - arrow connection and enter the hand - string connection.

Note: If you are wondering why not directly use args.interactor directly instead of assigning it to the variable? That's because the XRSocketInteractor is an interactor just like a VR hand. since this scrip is inherited from the XRSocketInteractor the args.interactor will return the socket interactor and not the VR Hand

  • When the hand exits the arrow, the socket interactor automatically snaps the arrow in place. This is the same as releasing the arrow into a socket
  • The event OnSelectEntered is called when the arrow snaps into the socket. This event calls a method to store the arrow.

Calculation
This section is a bit lengthy so I have broken it down into smaller parts:

a) StoreArrow(XRBaseInteractable interactable)

	private void StoreArrow(XRBaseInteractable interactable)
    {
        if (interactable.tag == "Arrow")
        {
            this.currentArrow = interactable;
            this.currentArrowInteraction = currentArrow.gameObject.GetComponent<ArrowInteraction>();
        }
    }

The StoreArrow is a private function that takes the interactable and stores it in the variable currentArrow only if the interactable is an arrow. This check is making sure that only arrows are registered.

Then, the variable currentArrowInteraction is initialized to get the ArrowInteraction component from the stored arrow.

b) ReleasaeArrow(SelectExitEventArgs arg0)

    private void ReleasaeArrow(SelectExitEventArgs arg0)
    {
        if (currentArrow && bowInteraction.BowHeld)
        {
            ForceDetach();
            ReleaseArrowFromSocket();
            ClearVariables();
        }
    }

This method is invoked from the ExitEvent call back of the StringInteraction component. The event arguments are passed from the StringInteraction component.

The string can be exited in two ways. One is when the string is pulled and let go and the other is when the bow itself is released while pulling the string.

Now, to make sure the following functions are called only when the string is released while holding the bow with an arrow attached to the socket, the if conditional statement is used. When this statement is true, the methods ForceDetach, ReleaseArrowFromSocket and ClearVariables are called.

c) ForceDetach() and ReleaseArrow()

    private void ForceDetach()
    {
        interactionManager.SelectExit(this, currentArrow);
    }

    private void ReleaseArrowFromSocket()
    {
        currentArrowInteraction.ReleaseArrow(stringInteraction.PullAmount);
    }

The method ForceDetach uses the SelectExit API method of the interactionManager to forcefully exit the arrow from the socket interaction. The keyword this refers to the socket interactor.

The method ReleaseArrowFromSocket invokes the method ReleaseArrow of the ArrowInteraction component and passes the PullAmount as a parameter.

d) ClearVariables()

    private void ClearVariables()
    {
        this.currentArrow = null;
        this.currentArrowInteraction = null;
        this.handHoldingArrow = null;
    }

This method ensures that all the variables currentArrow, currentArrowInteraction and handHoldingArrow are set to null so that the variables are clean when new interactor(left or right hand) and interactable( next arrow) enter into the socket.

e) Movement type

    public override XRBaseInteractable.MovementType? selectedInteractableMovementTypeOverride
    {
        get { return XRBaseInteractable.MovementType.Instantaneous; }
    }

There are three types of Movement type Instantaneous, Kinematic and Velocity tracking. Instantaneous movement enables the arrow in the socket to move along with the bow in sync. The other two give a lagging effect.

So the property XRBaseInteractable.MovementType?, which is inherited from the XRSocketInteractor is used to select the type of movement as Instantaneous.

Note: Changing the movement type to Instantaneous can be done in Unity Editor as well but there are chances that it could be accidentally changed to any of the other types.

3.4.3 Testing

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

  • Add the ArrowInteraction.cs script to the arrow GameObject, then drag and drop the tip game object as component's tip position. Set the speed to 20.

    Arrow interaction component

  • Add the SocketInteraction.cs script to the string GameObject.
  • Create an empty game object as a child of String and name it to AttachTransform → Enter -90 units in y-direction rotation → Drag and drop this into the Attach Transform field of Socket Interaction component. This ensures that the arrow is facing the correct direction when attached to the socket.

    Socket interaction

  • Create a new layer named Ignore and from the interaction layer mask field of the Socket Interaction, uncheck the Ignore layer.

    Ignore layer

  • Play the scene and test the interactions. As you can see, the arrow snaps into place without having to let go. And when the string is let go, a force is applied to the arrow and it's is fired.

    Final testing

4. Conclusion

In this series, you have learnt to create a bow and arrow experience in VR. So what’s next??

What we've learned was just the basic interaction. You can polish this further and add more features like:

  • Creating a quiver interaction that spawns an arrow as and when you use one.
  • A script to enable and disable the socket when the bow is held and released respectively, so that arrow can be attached only when the bow is held.
  • Particle effects for the arrow as it flies through the air.
  • Change the colour of the string as it gets pulled and particle effects after the string has been released.
  • Adding sound effects, etc

I hope you will try to create some of those features mentioned earlier on your own, so in that way, you learn a lot more. But no worries if you find it difficult, we will have separate tutorials for that as well in the future.

🎉 With this we have completed creating the bow and arrow experience for VR.

_____________________

Thank you

Thanks for reading this blog post. 🧡 If you are interested in creating your own AR and VR apps, you can learn more about it here on immersive insiders. Also, if you have any questions, don't hesitate to reach out! We're always happy to help.

You’ve successfully subscribed to immersive insiders
Welcome back! You’ve successfully signed in.
Great! You’ve successfully signed up.
Your link has expired
Success! Check your email for magic link to sign-in.