Last week’s episode of .GAME started preparations for an item system that we’ll be using for equipment and merchant mechanics. The episode covered some Unity fundamentals, such as methods of object rotation, tags, layers, sorting layers and filtering. We wrapped up with a development challenge, the scenario of which was: When the player moves towards a merchant, the merchant should rotate towards the player.
This post will go over the design I took into consideration which fed into the solution that I came up with. Of course, this is by no means the only way you can solve the challenge. The result that I ended up with was:
I left this challenge a bit vague so that you can interpret what rotating the merchant means to you. As I started to design my solution, I had to consider what it meant to me. Sure, I could just make the merchant rotate the second that the player clicks on them – but was that the experience I wanted? It certainly didn’t feel like an immersive experience for the player.
If triggered right when the player clicks on the merchant, the merchant will rotate to face the player right away. This is problematic when you consider the distance a character might be from the merchant. If the player was far away, how would the merchant know to turn to them? The player’s character could have yelled the merchant’s name, but we’re not showing anything on screen to indicate to the player that is the case.
Now consider a scenario where the player is not only far away, but has several obstacles that they need to avoid, making them ping pong around the scene before reaching the merchant. If triggered right away, the behavior that we’d encounter is that the merchant would continue to evaluate and adjust to the player’s location. In other words, they would be constantly rotating around to face the player as the player was attempting to reach them.
Next I considered what a merchant would do when the player moved away from them. If it was a real person, they’d probably go back to doing whatever they were doing before the player arrived. From a mechanics standpoint, this means that it’d be weird if the merchant kept rotating to look at the player or continued to face the direction that they were looking when the player left.
Considering this, I added a few additional requirements to the scenario:
- The merchant must rotate to look at the player’s character only when they are in a certain range.
- The merchant should restore its rotation back to its default position when the player leaves.
I used colliders to solve the distance requirement. Colliders are components which can be used for detecting physical collisions. If you want to perform a zone type detection you can enable the
isTrigger property. Enabling the
isTrigger property will cause the collider to be ignored by physics calculations and will instead detect when an object has passed through it. Using a Trigger, we can detect when the player object has entered the zone of our merchant and tell the merchant to rotate. We can also detect when the player object has left the zone, using it to tell the merchant to return to its default position.
Setting up the collider
The goal is to setup a circular zone around the merchant that triggers when the player enters it, like so:
To do this, you add a Sphere Collider component to the root of the Merchant game object. Set the Radius to 7 and isTrigger to true.
One of the game objects needs to have a RigidBody component to detect the collision. We’ll add that to the Merchant game object as well. Since we’re only controlling the rotation, we’ll freeze the XYZ positions under the constraints section. We’ll also turn off gravity.
Coding the new behavior
We’ll need a new script to control the NPC. To do this, create a new class called
GeneralInteraction and have it derive from
MonoBehavior. To start, we’ll store the default rotation of the merchant and add the logic in for rotating. This is very similar to what was reviewed in the episode, with the exception of a few additional conditions in the
Colliders have several callbacks that can occur. The two that we’ll be working with are
OnTriggerEnter, which is called when another collider enters the trigger, and
OnTriggerExit which is called when another collider exits the trigger.
OnTriggerEnter we’ll need to do a check to ensure we’re only storing a reference of the object if it is the Player. For the check to work, we’ll need to set the Player object’s Tag to “Player”, which can be done in the inspector:
OnTriggerExit to clear out the reference to the Player object and tell our NPC to begin rotating. The
RotateTowards() method (referenced above) already has the logic to determine that the merchant should rotate back to the original location.
Fixing Adverse Behaviors
Unfortunately, this solution conflicted with the way I’d originally coded up the player’s movement which caused a few issues that needed to be fixed.
Design Issue #0: The navigation system fights with the rotation
Sometimes when the player navigates to a merchant, it’ll start twitching as it slowly tries to complete its rotation. Technically this was an issue that was likely to happen regardless of the challenge, I just hadn’t caught it yet. 🙂 This could have had significant performance issues had it not been caught and was implemented on all NPC characters.
The reason this is happening is because the navigation system is constantly trying to run and as a result is fighting with the
Quaternion.Slerp logic. The navigation should have been stopped once it’d reached it’s location and resumed once it had a new one.
To fix this, I needed to adjust the check that was used in the
Update() method to consider the distance the player game object was from the NavMeshes destination and compare it against the stopping distance of the NavMesh. I also added the logic to stop and resume the NavMeshes pathing. You can learn more about Unity’s navigation system by watching the Unity Navigation – Part 1 and Part 2 episodes of .GAME.
Design Issue #1 The player navigates too far from the merchant
The player always navigates to the edge of the sphere collider, which creates a robotic feel. This can also have adverse behaviors, depending on the size of the Trigger that is used.
This occurs because of the original way that I was providing the path destination to the character. The Raycast was passing in the position of the hit point, rather than the position of the object that was being hit. The fix for this was pretty simple – I just needed to change this line:
Move(hit.point + (transform.forward * -6)); to say
Move(hit.transform.position + (transform.forward * -6));.
Design Issue #2: The player navigates through the merchant
Fixing the last design issue introduced a new problem – Now the player was navigating too close to the merchant and bumping into the object.
This is an easy fix, as the merchant was missing the NavMeshObstacle component which is used to tell the navigation system to go around it. After adding the NavMeshObstacle component to the root of the Merchant game object, I set the XYZ size values to 6, enabled carve and disabled Carve Only Stationary.
In the end, I mostly got the behavior that I wanted. I know that there’s a few other areas that I’d need to address if I wanted this to be a robust system. For example, consider a scenario where the player is already in the trigger and the merchant has already completed their rotation. If the player were to move to a new position in the trigger, but never actually exit/re-enter it, the merchant would never be notified that they need to rotate to a new location.
I hope you enjoyed reading about my approach to this challenge. I’d love to hear about how you’ve chosen to solve it. You can either tweet to me at@yecats131 or email dotgame at microsoft dot com. Be sure to check out more episodes of .GAME on Channel 9!