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.
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.
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.
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.
This section will help you understand the code, feel free to skip to Section 3.4 (Socket Interaction) if you understood the program already.
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 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.
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:
This section is a bit lengthy so I have broken it down into smaller parts
a) FixedUpdate()
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()
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()
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)
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)
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)
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()
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.
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.
In this section, we’ll script the socket interaction whose functionality would be :
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.
This section will help you understand the code, feel free to skip to Section 3.4.3 (Testing) if you understood the program.
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 NameTypeUsehandHoldingArrowis of type XRBaseInteractorTo 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.currentArrowis of type XRBaseInteractableTo register the interactable i.e arrow when it snaps into the socket.stringInteractionis of type StringInteractionTo make use of the events and property from the StringInteraction component.bowInteractionis of type BowInteractionTo make use of the property from the BowInteraction component.currentArrowInteractionis of type ArrowInteractionTo make use of the public method from arrow interaction component, which allows us to fire the arrow
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
This section is a bit lengthy so I have broken it down into smaller parts:
a) StoreArrow(XRBaseInteractable interactable)
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)
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()
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()
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
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.
Now, let's test if this works as intended.
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:
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.
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.