Notifications
Article
Improve Waypoints for John Lemon's Haunted Jaunt
Published a month ago
117
0
Easily edit waypoints from the editor
Hi everyone,
In this article I will teach you how to improve the Waypoint System over the John Lemon's Haunted Jaunt project. This allows the creator to manage waypoints for ghost enemies in an easier way.
If you have followed the Unity tutorial, you will have something like this:

Summary

  • Add the Waypoints icon
  • Add and remove waypoints using the Editor
  • Move waypoints using the Transform gizmo in the Editor

Adding the Waypoints icon

First of all add a Gizmo to the waypoint Transform objects. Download an image for your waypoints. If you don't have one you can use the one I'm using : waypoint-image.
Drop the image in the Gizmos folder in your project. Select the image and change the Texture Type to Sprite (2D and UI) in the Inspector window
Open the WaypointPatrol script. Other than Start and Update the MonoBehaviour gives us another special function called OnDrawGizmos.
private void OnDrawGizmos() { }
This function is handled by the editor to draw Gizmos for your gameobjects. Before accessing the waypoints array, we should check that the waypoints variable is not null because we don't want to incur in a NullReferenceException (NRE)
if (waypoints != null) { }
Then we loop through all the waypoints and, for each one, we draw the Gizmo.
foreach is a special keyword in C# that allows us to loop through objects that are IEnumerables. Examples of this are Arrays, Lists.
Let's take a look at the syntax of the foreach loop here:
foreach(var waypoint in waypoints) { }
waypoints is the IEnumerable we want to loop through. waypoint is the element we are currently selecting. If we want to make a comparison with another loop like for or while, the waypoint variable is the same as waypoints[i], where i is the current loop index. Inside the loop in the end, we can finally draw the image imported previously
Gizmos.DrawIcon(waypoint.position, "waypoint");
Gizmos is a special class from Unity that is used for drawing things. It has a lots of other functions to draw Cubes, Spheres and other objects. In our case we are using the DrawIcon.
  • The first parameter is the position where the Gizmo should appear in world space
  • The second parameter is the name of the image without the extension
The final script
private void OnDrawGizmos() { if (waypoints!= null) { foreach(var waypoint in waypoints) { Gizmos.DrawIcon(waypoint.position, "waypoint"); } } }
If you go back to the Scene View, you should be able to view the waypoints.
The waypoint icon itself is already a nice addition. With this easy tip, we can see where the waypoints are.

Add and remove Waypoints

Now we want to be able to add and remove them when we select the ghost game object. We need to create an Editor script but first we have to clean up the current WaypointPatrol script:
  • Delete the waypoints from the Hierarchy and open the WaypointPatrol script
  • In the script rename the property waypoints to waypointsPositions and change its type to List<Vector3>
Previously, the waypoints variable was an array of Transform but, in that case, we were only using the position property of the Transform object (which is a Vector3). If you are using Visual Studio you can rename a property by having the cursor on it and pressing F2 on the keyboard. This action will automatically rename all the occurences of the variable. Not renaming it can cause an error and the Editor may try to assign the old Transform values to the variable which is now of a different type (Vector3).
public List<Vector3> waypointsPositions;
This will break some parts of the script, so let's fix them.
  • public List<Vector3> waypointsPositions; The List class is in a namespace that we need to include. You can use Visual Studio help button to fix this problem or add the namespace by yourself adding this line at the top of the file using System.Collections.Generic;
  • navMeshAgent.SetDestination(waypointsPositions[0].position); We don't need the .position anymore since the waypointsPositions is already a Vector3
  • navMeshAgent.SetDestination(waypointsPositions[m_CurrentWaypointIndex].position); Same as above
  • Gizmos.DrawIcon(waypoint.position, "waypoint"); Same as above
Now we are all set to create the Editor script:
  • Create a new folder inside the Scripts folder and call it Editor. The name of the folder is very important because it tells Unity that the scripts inside it alter the Editor functionality
  • Create a new script inside this folder called WaypointPatrolEditor
  • Change MonoBehaviour to Editor. Add the namespace using UnityEditor; at the top
  • Remove the Start and Update methods
  • Add the CustomEditor attribute: [CustomEditor(typeof(WaypointPatrol))] This tells Unity that this script is a CustomEditor for the WaypointPatrol script
Your resulting script at this point should be this:
using UnityEngine; [CustomEditor(typeof(WaypointPatrol))] public class WaypointPatrolEditor : Editor { }
Every editor script has a reference to the actual object we will be working with. This object (the instance) is stored in a variable called target. When the GameObject is selected from the Hierarchy window, Unity calls the OnEnable method on the CustomEditor script
private WaypointPatrol m_WaypointPatrol; private void OnEnable() { m_WaypointPatrol = (WaypointPatrol)target; }
We also need to make sure that the List of waypoints is initialised
if (m_WaypointPatrol.waypointsPositions == null) { m_WaypointPatrol.waypointsPositions = new List<Vector3>(); }
using System.Collections.Generic; is now required at the top and we are ready to do something with the object. Override the method OnSceneGUI: this is called once every frame, by the Unity Editor when the GameObject is selected. Inside the method we can alter the Scene View to add GUI elements:
private void OnSceneGUI() { }
Now, we want to add two buttons:
  • A button to add a new waypoint
  • A button to remove the last added waypoint
Let's start with the Add Waypoint button. We tell the Editor that we intend drawing some GUI Elements
Handles.BeginGUI();
Then, we create our button
GUI.Button(new Rect(10, 10, 150, 25), "Add Waypoint")
The GUI.Button function draws a Button at the coordinates expressed in the first parameter. The coordinates are intended to have (0,0) in the top left corner and (width, height) at the bottom right corner. The second parameter is the text that will be inside the button. GUI.Button accepts also other parameters but we won't be talking about those today. You can check out the documentation. We now need to tell Unity that we have finished drawing GUI Elements
Handles.EndGUI();
Now head to Unity and you should be able to see your button in the top left corner of the Scene View while you have a Ghost GameObject selected.
This new button is nice, but it doesn't do anything yet! Let's fix it!
The GUI.Button function returns a boolean and this boolean is True when the user clicks on the button, False otherwise. We can wrap the entire function inside an if statement
if (GUI.Button(new Rect(10, 10, 150, 25), "Add Waypoint")) { }
Inside the if we can now create the new waypoint. What I want to do is to create the new waypoint where the last waypoint is but, if we don't have a waypoint yet, we should use the current Ghost position. After we get this position, we will move the point forward so it won't overlap with the previous one.
var newPosition = m_WaypointPatrol.waypointsPositions.Any() ? m_WaypointPatrol.waypointsPositions.Last() : m_WaypointPatrol.transform.position; newPosition = newPosition + m_WaypointPatrol.transform.forward; m_WaypointPatrol.waypointsPositions.Add(newPosition);
If you have never seen this syntax don't panic:
  • Any() and Last() are extension methods from the package System.Linq. An extension method adds functionality to an existing object: in this case Any() returns True if the List has at least one element and Last() returns the last element of the Array. Add the namespace at the top to get rid of the errors: using System.Linq;
  • If you have never seen the ternary operator this might look very odd to you right now, so let's try to break it down: The syntax for the ternary operator is as follow: <condition> ? <true_result> : <false_result>. The condition in our code is m_WaypointPatrol.waypointsPositions.Any(), which means if we have at least one waypoint. If the result of the condition is True the true_result will be assigned to the newPosition variable, which is the last waypoint. If the result of the condition is False then the false_result will be assigned to the newPosition variable, which is the position of the Ghost GameObject.
  • Second statement we are altering this position by adding the forward Vector3. This will move the point forward by 1.
  • Third statement we add the newPosition to the List.
This is the full script to this point
using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; [CustomEditor(typeof(WaypointPatrol))] public class WaypointPatrolEditor : Editor { private WaypointPatrol m_WaypointPatrol; private void OnEnable() { m_WaypointPatrol = (WaypointPatrol)target; if (m_WaypointPatrol.waypointsPositions == null) { m_WaypointPatrol.waypointsPositions = new List<Vector3>(); } } private void OnSceneGUI() { Handles.BeginGUI(); if (GUI.Button(new Rect(10, 10, 150, 25), "Add Waypoint")) { var newPosition = m_WaypointPatrol.waypointsPositions.Any() ? m_WaypointPatrol.waypointsPositions.Last() : m_WaypointPatrol.transform.position; newPosition = newPosition + m_WaypointPatrol.transform.forward; m_WaypointPatrol.waypointsPositions.Add(newPosition); } Handles.EndGUI(); } }
Head back to the Editor and test the button.
There is one small improvement that we can do: link the Unity Undo. Go back to the script and add this line before adding the newPosition to the List
Undo.RegisterCompleteObjectUndo(m_WaypointPatrol, "Add Waypoint");
Adding a waypoint now will register in the Undo.
It's time to add the second button. This will be very similar to the Add Button:
if (GUI.Button(new Rect(10, 45, 150, 25), "Remove Last Waypoint")) { var lastPoint = m_WaypointPatrol.waypointsPositions.LastOrDefault(); if (lastPoint != null) { Undo.RegisterCompleteObjectUndo(m_WaypointPatrol, "Remove Waypoint"); m_WaypointPatrol.waypointsPositions.Remove(lastPoint); } }
We are using the same GUI.Button but:
  • The coordinates are different, in fact this button will sits below the Add.
  • We are using a new extension method to get the last waypoint LastOrDefault() The difference between Last() and LastOrDefault() is very simple: When the List is empty Last will throw an Exception, while LastOrDefault() will return null
  • We remove the waypoints instead of adding it

Move waypoints using the Transform gizmo in the Editor

We can see all the waypoints being created but the position is not very interesting. It would be cool to have the Move Tool we have in all the other GameObjects. Luckily Unity allows us to do so in a very simple way. First let's clean up the script a bit.
Create a new function called Input
void Input() { }
Move the code from the OnSceneGUI method inside the Input function
void Input() { Handles.BeginGUI(); if (GUI.Button(new Rect(10, 10, 150, 25), "Add Waypoint")) { var newPosition = m_WaypointPatrol.waypointsPositions.Any() ? m_WaypointPatrol.waypointsPositions.Last() : m_WaypointPatrol.transform.position; newPosition = newPosition + m_WaypointPatrol.transform.forward; Undo.RegisterCompleteObjectUndo(m_WaypointPatrol, "Add Waypoint"); m_WaypointPatrol.waypointsPositions.Add(newPosition); } if (GUI.Button(new Rect(10, 45, 150, 25), "Remove Last Waypoint")) { var lastPoint = m_WaypointPatrol.waypointsPositions.LastOrDefault(); if (lastPoint != null) { Undo.RegisterCompleteObjectUndo(m_WaypointPatrol, "Remove Waypoint"); m_WaypointPatrol.waypointsPositions.Remove(lastPoint); } } Handles.EndGUI(); }
Call the Input fuction from the OnSceneGUI method
private void OnSceneGUI() { Input(); }
Create now a new function called Draw
void Draw() { }
And don't forget to call it from the OnSceneGUI method
private void OnSceneGUI() { Input(); Draw(); }
Unity has a common function for displaying a Move Tool Gizmo:
var newPosition = Handles.PositionHandle(position, rotation);
This function accepts: the position where to display the Move Tool, the rotation it should have and returns the new position if we move the handle. We need to display one Move Tool for each waypoint and, if the position has changed, alter it. Start with a basic for loop:
for (var i = 0; i < m_WaypointPatrol.waypointsPositions.Count; i++) { var newPos = Handles.PositionHandle(m_WaypointPatrol.waypointsPositions[i], Quaternion.identity); }
Inside the loop we call the Unity function to display the Move Tool. Unity gives us another Begin-End structure when it comes to check for changes happened in the Editor, so we can use it to check if the handle has been moved:
for (var i = 0; i < m_WaypointPatrol.waypointsPositions.Count; i++) { EditorGUI.BeginChangeCheck(); var newPos = Handles.PositionHandle(m_WaypointPatrol.waypointsPositions[i], Quaternion.identity); if (EditorGUI.EndChangeCheck()) { Undo.RegisterCompleteObjectUndo(m_WaypointPatrol, "Move Waypoint"); m_WaypointPatrol.waypointsPositions[i] = newPos; } }
EditorGUI.EndChangeCheck() returns True if the handle has been moved. Consequently, the position of the waypoint will be replaced with the new one. Go back to the Editor and test if you can move the waypoints:
If you start moving the waypoints around, you will notice that it can get messy. If you move the waypoints to have something similar to this, it would be impossible to understand the path the Ghost is going to take to walk through the waypoints
An easy way to address this would be to connect the waypoints with arrows indicating the path. Inside the Draw function, we make sure that there is at least one waypoint
if (m_WaypointPatrol.waypointsPositions.Any()) { }
and then we draw the first line
Handles.color = Color.blue; var firstPoint = m_WaypointPatrol.waypointsPositions.First(); Handles.DrawLine(m_WaypointPatrol.transform.position, firstPoint);
The function we use to draw a line is Handles.DrawLine(startPoint, endPoint);. This function creates a line between the two given points following these simple steps:
  • First we set the color of the lines: blue.
  • Then we get the first point in the list
  • At last we draw a line between two points: the Ghost position m_WaypointPatrol.transform.position and the firstPoint
The result is not very exciting
We have the line now, but we still don't have an arrow. To draw it, we want to look at this other function Handles.ArrowHandleCap(controlId, startPoint, rotation, size, eventType);
The parameters for this function are different from the DrawLine function.
  • The controlId. A controlId in Unity is a unique identifier for a control. We can set it to 0
  • The startPoint is the point where the arrow starts from. The start point in our case is the Ghost position
  • The rotation is where the arrow is pointing to. We can calculate it using some Vector and Quaternion Math
  • The size is the length of the arrow This will be half the size of the distance so, the arrow cap would be in the middle giving a nice effect
  • The eventType decides when the arrow gets drawn The ArrowHandleCap reacts only to Layout and Repaint events. We will use Repaint
To calculate the rotation we first need a Vector that goes from VectorA to VectorB. This is very easy to achieve: VectorB - VectorA = VectorDistance
Now, we can finally draw our arrow:
var distanceVector = firstPoint - m_WaypointPatrol.transform.position; Handles.ArrowHandleCap(0, m_WaypointPatrol.transform.position, Quaternion.LookRotation(distanceVector), distanceVector.magnitude / 2, EventType.Repaint);
Using the same logic, loop through the waypoints List and draw all the other lines and arrows:
for (var i = 0; i < m_WaypointPatrol.waypointsPositions.Count - 1; i++) { var currentPoint = m_WaypointPatrol.waypointsPositions[i]; var nextPoint = m_WaypointPatrol.waypointsPositions[i + 1]; distanceVector = nextPoint - currentPoint; Handles.ArrowHandleCap(i + 1, currentPoint, Quaternion.LookRotation(distanceVector), distanceVector.magnitude / 2, EventType.Repaint); Handles.DrawLine(currentPoint, nextPoint); }
In each iteration of the loop, we work with the currentPoint and nextPoint hence the condition is i < m_WaypointPatrol.waypointsPositions.Count - 1. We get the currentPoint as the i element and the nextPoint as the i+1 element, calculate the distanceVector and then we draw Line and Arrow. After the Ghost reaches the last waypoint, it will go back to the first one so it would be great to have another Line and Arrow that go from the last waypoint to the first one
if (m_WaypointPatrol.waypointsPositions.Count > 1) { var lastPoint = m_WaypointPatrol.waypointsPositions.Last(); distanceVector = firstPoint - lastPoint; Handles.ArrowHandleCap(m_WaypointPatrol.waypointsPositions.Count + 1, lastPoint, Quaternion.LookRotation(distanceVector), distanceVector.magnitude / 2, EventType.Repaint); Handles.DrawLine(lastPoint, firstPoint); }
Finally we can see what path is the Ghost taking!
There is an issue currently where the List will always go back to the list stored in the prefab. To avoid this issue, create a Prefab variant for each ghost.
That's about it for this article. Thank you for reading it!! If you have some question please leave them in the comment section.
If you liked this content then follow me to not miss out on the next article!

Pietro Carta
Developer - Programmer
15
Comments