We recently released CAT Game Builder, our framework for quickly building games in Unity, and several people have already asked about integrating new or existing systems into CAT.
CAT was designed to be highly extensible, so let’s go through an example of doing just that, based on the previous post of Building a Waypoint Pathing System In Unity. If you’re already comfortable with waypoint pathing, you can skip that post. Otherwise go read it – we’ll be here when you get back.
In this tutorial, we’ll learn about:
- Creating services and actions
- Using the value system
- CAT’s event system
- CAT validation
While typical CAT system integration may not require all of this work, we’ll cover all of these bases here. By the end of the article, you should have a working CAT integrated pathing system in which you can build something like this:
Creating a Service
Creating a Service
A service isn’t strictly necessary, and we could certainly get away without one. But there are clear advantages to using a service, particularly for functionality like pathing where we may have many agents that need to make expensive Unity method calls.
In fact that is the case, looking at our two classes (PathManager.cs
and Waypoint.cs
) from the previous article. Using a service here will improve performance by cutting down on the number of FindGameObjectsWithTag
calls. When integrating existing systems, it might be useful to add a service, but usually you would call existing code from it. In this case, we’re pulling the existing code into the service directly because there was so little of it and it wasn’t built with an external API.
We’ll start by re-using PathManager.cs
and renaming it to PathingService.cs
(renaming is optional, but is the convention). We’re going to use the PathingService
to keep track of all the waypoints and also do the work of computing paths:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using TrickyFast; namespace TrickyFast.AI { public interface IPathingService : IService { void RegisterWaypoint(Waypoint waypoint); void UnregisterWaypoint (Waypoint waypoint); Stack<Vector3> FindPath (Vector3 start, Vector3 destination); Waypoint FindClosestWaypoint(Vector3 target); } public class PathingService : ServiceBehaviour, IPathingService { //public float walkSpeed = 5.0f; //private Stack<Vector3> currentPath; //private Vector3 currentWaypointPosition; //private float moveTimeTotal; //private float moveTimeCurrent; private List<Waypoint> waypoints = new List<Waypoint>(); public void RegisterWaypoint(Waypoint waypoint) { waypoints.Add (waypoint); } public void UnregisterWaypoint(Waypoint waypoint) { waypoints.Remove (waypoint); } public void Initialize (Conductor conductor) { } public Deferred Stop() { return null; } /*public void Stop() { currentPath = null; moveTimeTotal = 0; moveTimeCurrent = 0; } void Update() { if (currentPath != null && currentPath.Count > 0) { if (moveTimeCurrent < moveTimeTotal) { moveTimeCurrent += Time.deltaTime; if (moveTimeCurrent > moveTimeTotal) moveTimeCurrent = moveTimeTotal; transform.position = Vector3.Lerp (currentWaypointPosition, currentPath.Peek (), moveTimeCurrent / moveTimeTotal); } else { currentWaypointPosition = currentPath.Pop (); if (currentPath.Count == 0) Stop (); else { moveTimeCurrent = 0; moveTimeTotal = (currentWaypointPosition - currentPath.Peek ()).magnitude / walkSpeed; } } } }*/ ... |
Notice that we’ve also created a new TrickyFast.AI
namespace for this service. This is optional but highly recommended. Our refactor here is pretty straightforward:
- Add the interface
IPathingService
. This is important, since the way to get a service out of the CAT Conductor is by callingGetLocalServiceByInterface
. - Define the public methods from the new
PathingService
- Make the
PathingService
inheritServiceBehaviour
and implementIService
. These are both in theTrickyFast
namespace, so we’ll need to include that in a using statement. - Remove the existing parameters and add a new list of
Waypoint
s. - Add methods to register and unregister nodes with the system.
- Nuke the original
Stop
andUpdate
methods, though you may want to keep them around since they’re going essentially to move elsewhere. - Implement
IService
, which includesInitialize
andStop
.
Let’s think about what we want from this system. The most important logic computes a path from one point to another. The NavigateTo
method is what did this before, but it needs to change a bit since it doesn’t return the path. We should also rename it to FindPath
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public Stack<Vector3> FindPath(Vector3 start, Vector3 destination) { var currentPath = new Stack<Vector3> (); var currentNode = FindClosestWaypoint (start); var endNode = FindClosestWaypoint (destination); if (currentNode == null || endNode == null || currentNode == endNode) return null; var openList = new SortedList<float, Waypoint> (); var closedList = new List<Waypoint> (); openList.Add (0, currentNode); currentNode.previous = null; currentNode.distance = 0f; while (openList.Count > 0) { currentNode = openList.Values[0]; openList.RemoveAt (0); var dist = currentNode.distance; closedList.Add (currentNode); if (currentNode == endNode) { break; } foreach (var neighbor in currentNode.neighbors) { if (closedList.Contains (neighbor) || openList.ContainsValue (neighbor)) continue; neighbor.previous = currentNode; neighbor.distance = dist + (neighbor.transform.position - currentNode.transform.position).magnitude; var distanceToTarget = (neighbor.transform.position - endNode.transform.position).magnitude; openList.Add (neighbor.distance + distanceToTarget, neighbor); } } if (currentNode == endNode) { while (currentNode.previous != null) { currentPath.Push (currentNode.transform.position); currentNode = currentNode.previous; } currentPath.Push (start); } return currentPath; } |
Other than renaming, we need to add a start parameter, make it return the path (a Stack<Vector3>
), make currentPath
a local variable, and use the start
parameter to find the initial currentNode
. Then at the end, we just return the currentPath
.
Next up is fixing the FindClosestWaypoint
function so that it uses our waypoints
list. We want this method to be public so that we can call it from a CAT Action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public Waypoint FindClosestWaypoint(Vector3 target) { Waypoint closest = null; float closestDist = Mathf.Infinity; for (int index = 0; index < waypoints.Count; ++index) { var waypoint = waypoints [index]; var dist = (waypoint.transform.position - target).magnitude; if (dist < closestDist) { closest = waypoint; closestDist = dist; } } if (closest != null) { return closest; } return null; } |
Finally, let’s get rid that expensive FindGameObjectsWithTag
use our explicit list of waypoints instead.
That’s it for the PathingService
.
Servicing Our Waypoints
Servicing Our Waypoints
We’ve already got a cleaner, more performant setup. Let’s keep the improvements coming by tackling our Waypoint
class. First, we’ll import the TrickyFast
namespace and add the TrickyFast.AI
namespace again. Next, let’s make it so that Waypoint
s register with our service automatically on Start
, and unregister on OnDestroy
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using TrickyFast; namespace TrickyFast.AI { public class Waypoint : MonoBehaviour { public List<Waypoint> neighbors; public Waypoint previous { get; set; } public float distance { get; set; } void OnDrawGizmos() { if (neighbors == null) return; Gizmos.color = new Color (0f, 0f, 0f); foreach(var neighbor in neighbors) { if (neighbor != null) Gizmos.DrawLine (transform.position, neighbor.transform.position); } } void Start() { var cond = Conductor.GetConductor (); if (cond == null) { Debug.LogError ("A Conductor is required for Waypoint pathing."); return; } var svc = cond.GetLocalServiceByInterface<IPathingService> (); if (svc == null) { Debug.LogError ("A PathingService is required for Waypoint pathing."); return; } svc.RegisterWaypoint (this); } void OnDestroy() { var cond = Conductor.GetConductor (); if (cond == null) { return; } var svc = cond.GetLocalServiceByInterface<IPathingService> (); if (svc == null) { return; } svc.UnregisterWaypoint (this); } } } |
In both Start
and OnDestroy
, we try to get the Conductor
’s singleton instance. If it isn’t there, we’ll return, and optionally log an error. Otherwise, we’ll call GetLocalServiceByInterface
to query the Conductor
for a service that implements IPathingService
. If we don’t have one of those, then we need to return as well.
The important part about querying for services by interface is that you can replace the implementation of a service with another one easily as long as it implements the correct interface.
Finally, we call RegisterWaypoint
or UnregisterWaypoint
accordingly on the service.
That’s it – we’re done with our refactor from the previous code, and ready to have real fun – adding CATs!
Adding CATs
Adding CATs
Let’s think about what CATs we want out of this system. As it turns out, we can just mirror existing CATs from CAT’s NavMesh pathing and make a Waypoint version of:
NavigateToAction
SetNavigationDestinationAction
ClearNavigationDestinationAction
CalculateNavMeshPathAction
SetNavigationSpeedAction
Let’s call the new versions:
WaypointPathToAction
SetWaypointDestinationAction
ClearWaypointDestinationAction
CalculateWaypointPathAction
SetWaypointPathSpeedAction
The first thing to do is create a new MonoBehaviour
that will manage moving a character through a path. This is where the stuff we removed from PathManager
/ PathingService
will go. Let’s call this WaypointPathAgent
to mirror the NavMeshAgent
component that comes with Unity.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using TrickyFast.CAT; namespace TrickyFast.AI { public class WaypointPathAgent : CATEventManager { public float walkSpeed = 5.0f; private Stack<Vector3> currentPath; private Vector3 currentWaypointPosition; private float moveTimeTotal; private float moveTimeCurrent; public void SetPath(Stack<Vector3> newPath) { currentPath = newPath; moveTimeTotal = 0f; moveTimeCurrent = 0f; FireEvent ("StartedPathing", currentPath); } public void Stop() { FireEvent ("StoppedPathing", currentPath); currentPath = null; moveTimeTotal = 0f; moveTimeCurrent = 0f; } void Update() { if (currentPath != null && currentPath.Count > 0) { if (moveTimeCurrent < moveTimeTotal) { moveTimeCurrent += Time.deltaTime; if (moveTimeCurrent > moveTimeTotal) moveTimeCurrent = moveTimeTotal; transform.position = Vector3.Lerp (currentWaypointPosition, currentPath.Peek (), moveTimeCurrent / moveTimeTotal); } else { currentWaypointPosition = currentPath.Pop (); FireEvent ("NewWaypointTarget", currentWaypointPosition); if (currentPath.Count == 0) Stop (); else { moveTimeCurrent = 0; moveTimeTotal = (currentWaypointPosition - currentPath.Peek ()).magnitude / walkSpeed; } } } } } } |
In this class, we’re mostly including code from the original waypoint system which performs movement along the path. Depending on the system you’re integrating, you may not have to do this step. We’ve also added a public method to set a new path, and we’ve also made it inherit from CATEventManager
which allows us to fire off events that others monitor. Finally we’ve added events for when pathing is stopped, a new path is added, and a new waypoint is reached.
Now let’s add some CAT Actions!
CAT Attributes
CAT Attributes
Note that for each new Action
, we’ll need to include both TrickyFast.CAT
and TrickyFast.CAT.Values
namespaces. Next, we need to define some attributes in order for the Action
to show up in the CAT Selector and the generated documentation:
- The
CATegory
attribute specifies where in the CAT browser and menus thisAction
will appear. - The
CATDescription
attribute provides the text that shows up in the CAT Selector as well as in the generated glossary, in addition to describing all the parameters for the glossary.
CalculateWaypointPathAction
CalculateWaypointPathAction
CalculateWaypointPathAction
is just a CATInstantAction
. This is a subclass of the main CATAction
class which makes it very easy to define instant Action
s or Action
s that complete synchronously.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using TrickyFast.CAT.Values; using TrickyFast.CAT; namespace TrickyFast.AI { [CATegory("Action/AI")] [CATDescription("Calculates a path through waypoints and stores it in a Vector3 List Value.", "start: Start position for the path.", "end: End position for the path.", "clear: Clear the list first.")] public class CalculateWaypointPathAction : CATInstantAction { [Tooltip("Start position for the path.")] public CATargetPosition start; [Tooltip("End position for the path.")] public CATargetPosition end; [Tooltip("Vector3 List Value to store the resulting path in.")] public ValueReference storeIn; [Tooltip("Clear the list first.")] public BoolValue clear = new BoolValue(true); protected override void DoAction (CATContext context) { var cond = Conductor.GetConductor (); if (cond == null) { Debug.LogError ("A Conductor and Pathing Service is required for CalculateWaypointPathAction.", gameObject); return; } var svc = cond.GetLocalServiceByInterface<IPathingService> (); if (svc == null) { Debug.LogError ("The Pathing Service is required for CalculateWaypointPathAction.", gameObject); return; } bool clr = clear.GetValue (context); start.WithPositions (context, delegate(Vector3 svect) { end.WithPositions(context, delegate(Vector3 evect) { var path = svc.FindPath(svect, evect); List<Vector3> points; if (clr) { points = new List<Vector3>(); storeIn.SetValue(context, points); } else points = storeIn.GetValue<List<Vector3>>(context); var start = points.Count; while (path.Count > 0) { points.Insert (start, path.Pop()); } }); }); } public override List<ValidationResult> Validate() { var res = base.Validate(); res.AddRange(ValidateField("start")); res.AddRange(ValidateField("end")); res.AddRange(ValidateField("storeIn")); res.AddRange(ValidateField("clear")); return res; } } } |
The parameters that we’re defining include a CATargetPosition
for both start
and end
. This lets the user select a target with a position. For more information on CATargetPosition
, see the Targeting section of the user manual.
The next two parameters are Value
s. The first one is a ValueReference
. These can be used when you want to require that the user references a value rather than just entering something directly. They are also useful when you want to accept references to multiple types of values.
The last parameter is a BoolValue
. For almost all regular fields in CATs, you’ll want to use a Value
instead of a simple field. The Value
types like BoolValue
are what allow you to either directly specify a value or to reference another value elsewhere. In this case, we’d normally have a bool field here, so we’ll use a BoolValue
instead. We also want to default it to true, so we initialize it by passing true to the constructor.
For CATInstantAction
, the only functions you need to override are DoAction
and Validate
. DoAction
, as the name implies, is simply where you implement your Action
code. It takes one parameter — a CATContext
– which stores information about the current state including:
- What the owner is
- Who the targets are
- The local scope
You’ll recognize some familiar things at the top of DoAction
: We’re getting the conductor and service, then the next line shows how you retrieve the value:
1 |
bool clr = clear.GetValue (context); |
All Value
fields define GetValue
and SetValue
, and this is how you retrieve the actual value they reference. This may either be a value stored directly or one pointed to on a ValueHolder
, local, or global.
1 |
start.WithPositions (context, delegate(Vector3 svect) { |
This bit of code shows an easy way to loop through the positions that the starting CATargetPosition
picks up; in cases where it’s set to player or owner, there will be only one position, but it’s always possible for the user to set it to tagged or targeted which could pick up multiple targets and thus multiple positions.
CATargetPosition.WithPositions
takes a context and a callback with one parameter, which is the position. It calls the callback function with each position that is targeted.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
List<Vector3> points; if (clr) { points = new List<Vector3>(); storeIn.SetValue(context, points); } else points = storeIn.GetValue<List<Vector3>>(context); var start = points.Count; while (path.Count > 0) { points.Insert (start, path.Pop()); } |
Next, let’s take a look at the part of the code that deals with values. In this case, if we set the clear parameter, we create a new list of points and call SetValue
on storeIn
with it. Otherwise, we pull the existing list of points from storeIn
. After this point, we just manipulate the point list directly, because we have a reference to the value pointed to by storeIn
and any changes will be saved there.
1 2 3 4 5 6 7 8 9 |
public override List<ValidationResult> Validate() { var res = base.Validate(); res.AddRange(ValidateField("start")); res.AddRange(ValidateField("end")); res.AddRange(ValidateField("storeIn")); res.AddRange(ValidateField("clear")); return res; } |
Finally, notice the Validate
function – it’s called to determine if there are any warnings or errors with the configuration of this Action
. In this case, we only need to check the parameters, so we can use the helper method called ValidateField
. It takes the parameter name as a string and will check it for errors and return a list of ValidationResult
s which should be included in the return value of Validate
.
ClearWaypointDestinationAction
ClearWaypointDestinationAction
ClearWaypointDestinationAction
is pretty simple. It’s another CATInstantAction
, and it only takes a target parameter. In DoAction
, we use CATarget.WithTargets
which is very similar to CATargetPosition.WithPositions
. In this case, it runs the callback function with each target that gets selected. All we need to do is check for a WaypointPathAgent
and call Stop
on it if it exists.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using TrickyFast.CAT.Values; using TrickyFast.CAT; namespace TrickyFast.AI { [CATegory("Action/AI")] [CATDescription("Clears the current waypoint path from an agent.", "target: The target(s) to clear the path of.")] public class ClearWaypointDestinationAction : CATInstantAction { [Tooltip("The target(s) to clear the path of.")] public CATarget target; protected override void DoAction (CATContext context) { target.WithTargets (context, delegate(GameObject obj) { var agent = obj.GetComponent<WaypointPathAgent>(); if (agent == null) return; agent.Stop(); }); } public override List<ValidationResult> Validate() { var res = base.Validate(); res.AddRange(ValidateField("target")); if (target != null && target.type == CATargetType.None) { res.Add(ValidationResult.Warning(this, "target", "Setting target to None will make this action do nothing.")); } return res; } } } |
One thing to note is in the validation, we’re checking if the target
’s type is None
and if so, we add a warning because this configuration doesn’t make sense (there would be nothing for the Action
to do).
SetWaypointDestinationAction
SetWaypointDestinationAction
SetWaypointDestinationAction
is yet again a CATInstantAction
. For this one, we get the PathingService
out of the Conductor
. Then, using CATargetPosition.FirstPosition
, we retrieve the first start and end point from their respective fields.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using TrickyFast.CAT.Values; using TrickyFast.CAT; namespace TrickyFast.AI { [CATegory("Action/AI")] [CATDescription("Calculates a path and then sets target agent(s) on that path.", "target: The target agent(s) to path.", "start: Start position for the path.", "end: End position for the path.")] public class SetWaypointDestinationAction : CATInstantAction { [Tooltip("The target agent(s) to path.")] public CATarget target; [Tooltip("Start position for the path.")] public CATargetPosition start; [Tooltip("End position for the path.")] public CATargetPosition end; protected override void DoAction (CATContext context) { var cond = Conductor.GetConductor (); if (cond == null) { Debug.LogError ("A Conductor and Pathing Service is required for CalculateWaypointPathAction.", gameObject); return; } var svc = cond.GetLocalServiceByInterface<IPathingService> (); if (svc == null) { Debug.LogError ("The Pathing Service is required for CalculateWaypointPathAction.", gameObject); return; } var svect = start.FirstPosition (context); var evect = end.FirstPosition (context); var path = svc.FindPath (svect, evect); if (path == null || path.Count == 0) return; target.WithTargets (context, delegate(GameObject obj) { var agent = obj.GetOrAddComponent<WaypointPathAgent>(); agent.SetPath (path); }); } public override List<ValidationResult> Validate() { var res = base.Validate(); res.AddRange(ValidateField("target")); res.AddRange(ValidateField("start")); res.AddRange(ValidateField("end")); if (target != null && target.type == CATargetType.None) { res.Add(ValidationResult.Warning(this, "target", "Setting target to None will make this action do nothing.")); } return res; } } } |
FirstPosition
returns the first position that the CATargetPosition
points to; if there are no positions, it returns null
. Then we just call FindPath
on the service and cycle through our targets and set their paths. Note that in order to get the WaypointPathAgent
here, we’re using GetOrAddComponent
. This is an extension that’s part of CAT. If the component isn’t found, it will automatically add it to the GameObject
. The rest is straightforward.
SetWaypointPathSpeedAction
SetWaypointPathSpeedAction
SetWaypointPathSpeedAction
is almost identical to ClearWaypointDestinationAction
, except instead of stopping the agent, it sets the walkSpeed
using a FloatValue
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using TrickyFast.CAT.Values; using TrickyFast.CAT; namespace TrickyFast.AI { [CATegory("Action/AI")] [CATDescription("Sets the speed of waypoint path navigation on an agent.", "target: The target(s) to set the speed of.", "speed: The speed to set.")] public class SetWaypointPathSpeedAction : CATInstantAction { [Tooltip("The target(s) to clear the path of.")] public CATarget target; [Tooltip("The speed to set.")] public FloatValue speed = new FloatValue (5f); protected override void DoAction (CATContext context) { target.WithTargets (context, delegate(GameObject obj) { var agent = obj.GetComponent<WaypointPathAgent>(); if (agent == null) return; agent.walkSpeed = speed.GetValue(context); }); } public override List<ValidationResult> Validate() { var res = base.Validate(); res.AddRange(ValidateField("target")); res.AddRange(ValidateField("speed")); if (target != null && target.type == CATargetType.None) { res.Add(ValidationResult.Warning(this, "target", "Setting target to None will make this action do nothing.")); } return res; } } } |
WaypointPathToAction
WaypointPathToAction
WaypointPathToAction
is the most complicated of our new Action
s. After setting the path destination, it keeps the Action
running until either all target agents are stopped, given new paths or reach their destinations. This Action
makes use of the events that we added earlier in WaypointPathingAgent
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using TrickyFast.CAT.Values; using TrickyFast.CAT; namespace TrickyFast.AI { [CATegory("Action/AI")] [CATDescription("Makes target agent(s) walk along a path between start and end.", "target: The target agent(s) to path.", "start: Start position for the path.", "end: End position for the path.")] public class WaypointPathToAction : CATAction { [Tooltip("The target agent(s) to path.")] public CATarget target; [Tooltip("Start position for the path.")] public CATargetPosition start; [Tooltip("End position for the path.")] public CATargetPosition end; private List<EventSubscription> subscriptions; private List<WaypointPathAgent> agents; public override Deferred Run (CATContext context) { if (IsRunning) return base.Run (context); var dfrd = base.Run (context); subscriptions = new List<EventSubscription> (); agents = new List<WaypointPathAgent> (); var cond = Conductor.GetConductor (); if (cond == null) { Debug.LogError ("A Conductor and Pathing Service is required for CalculateWaypointPathAction.", gameObject); return dfrd; } var svc = cond.GetLocalServiceByInterface<IPathingService> (); if (svc == null) { Debug.LogError ("The Pathing Service is required for CalculateWaypointPathAction.", gameObject); return dfrd; } var svect = start.FirstPosition (context); var evect = end.FirstPosition (context); var path = svc.FindPath (svect, evect); if (path == null || path.Count == 0) { Stop (); return dfrd; } target.WithTargets (context, delegate(GameObject obj) { var agent = obj.GetOrAddComponent<WaypointPathAgent>(); agents.Add (agent); agent.SetPath (path); subscriptions.Add(agent.Subscribe("StartedPathing", StoppedAgent)); subscriptions.Add(agent.Subscribe("StoppedPathing", StoppedAgent)); }); return dfrd; } private void StoppedAgent(CATEvent evt) { var agent = evt.source.GetComponent<WaypointPathAgent> (); agents.Remove (agent); for (int index = subscriptions.Count; index >= 0; --index) { if (subscriptions [index].manager == agent) { subscriptions [index].Unsubscribe (); subscriptions.RemoveAt (index); } } if (agents.Count == 0) Stop (); } public override bool Stop () { if (!IsRunning) return false; for (int index = 0; index < subscriptions.Count - 1; ++index) { subscriptions [index].Unsubscribe (); } subscriptions = null; agents = null; return base.Stop (); } public override List<ValidationResult> Validate() { var res = base.Validate(); res.AddRange(ValidateField("target")); res.AddRange(ValidateField("start")); res.AddRange(ValidateField("end")); if (target != null && target.type == CATargetType.None) { res.Add(ValidationResult.Warning(this, "target", "Setting target to None will make this action do nothing.")); } return res; } } } |
This action doesn’t inherit from CATInstantAction
and instead inherits from CATAction
, which allows it to keep running over time while the agent is pathing. Because of this, instead of DoAction
, we need to implement Run
and Stop
. Run
returns a new type of object called a Deferred
. Deferred
s represent a promise that at some point in the future they will have a value. You can listen for that by adding callbacks.
Most of Run
looks like DoAction
in SetWaypointDestinationAction
. The main difference is subscribing to events:
1 2 |
subscriptions.Add(agent.Subscribe("StartedPathing", StoppedAgent)); subscriptions.Add(agent.Subscribe("StoppedPathing", StoppedAgent)); |
The Subscribe
method returns an EventSubscription
which we need to hang on to (in the subscriptions list) so that we can later Unsubscribe
. If we don’t clean that up, this instance will keep getting callbacks and won’t be garbage collected until the path agent that it is subscribed to events on is. The callback we’re passing to Subscribe
is what gets called when the event is fired. In this case, we’re just calling StoppedAgent
.
In StoppedAgent
, we’re just removing the agent from our list of active agents, unsubscribing any events on it, and then checking if there are still any agents that we’re waiting on. If we aren’t waiting on any agents, then the Action
is done and we call Stop
.
Note that It’s crucial to call Stop
when the action is done: Not doing so will cause CAT to think it is still running. If it’s in a serial ActionList
, the next item in the list will never run for instance.
It’s also important to make sure that the Action
is actually running at the top of this function. Otherwise, agents and subscriptions will be null and we’ll get an NRE when referencing them. Otherwise, it’s just cleanup. Unsubscribe
all remaining subscriptions and null out subscriptions
and agents
.
Remember to call Stop
on the base class. This is what actually marks the Action
as done.
CAT Menus
CAT Menus
One last thing you’ll need to do is refresh all the menus in CAT. Before you do that, you’ll need to make sure to include any new namespaces you created in the autogenerated files. Open up CAT/Editor/CATMenuItemEditor.cs
and in the GenerateMenuItems
function, add a new line which includes your namespace.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
private static int GenerateMenuItems(List<CATMenuItem> items, string className, string mainMenuPath, string contextPath, int startPriority = 0, string createFunction = "AddComponent") { StringBuilder sb = new StringBuilder(); sb.AppendLine("// This class is Auto-Generated"); sb.AppendLine("using System;"); sb.AppendLine("using UnityEngine;"); sb.AppendLine("using UnityEditor;"); sb.AppendLine("using TrickyFast.CAT;"); sb.AppendLine("using TrickyFast.Quests;"); sb.AppendLine("using TrickyFast.Localization;"); sb.AppendLine("using TrickyFast.Storage;"); sb.AppendLine("using TrickyFast.Player;"); sb.AppendLine("using TrickyFast.Realms;"); sb.AppendLine("using TrickyFast.Areas;"); sb.AppendLine("using TrickyFast.Name;"); sb.AppendLine("using TrickyFast.AI;"); // <-- new entry here |
After that, you can generate the menus by going to the menu under CAT -> New -> Refresh
Example Usage
Example Usage
Here’s an example usage on a player:
The code from this article will be available in CAT Game Builder version 1.03.
That should be it! Of course, there are plenty of features we could add here, but those will have to wait for another article. You should now be able to place Waypoints in your scene, link them up, and then use these CATs to make your State Machines path using the Waypoints! Happy pathing!
About Tricky Fast Studios
Tricky Fast Studios is a US-based game studio featuring long-time industry veterans. We provide a full spectrum of game development services including bug fixing, feature development, porting, temporary staffing, and complete development. Our recent work includes The Walking Dead: March To War for Disruptor Beam, Poptropica Worlds for StoryArc Media, the Star Trek: Timelines Facebook and Steam ports for Disruptor Beam, and Wheel of Fortune Slots Casino for The Game Show Network. We’re here to build your story!