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

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:

PathingService.cs 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; } } } }*/ ... 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 calling GetLocalServiceByInterface .

. This is important, since the way to get a service out of the CAT Conductor is by calling . Define the public methods from the new PathingService

Make the PathingService inherit ServiceBehaviour and implement IService . These are both in the TrickyFast namespace, so we’ll need to include that in a using statement.

inherit and implement . These are both in the namespace, so we’ll need to include that in a using statement. Remove the existing parameters and add a new list of Waypoint s.

s. Add methods to register and unregister nodes with the system.

Nuke the original Stop and Update methods, though you may want to keep them around since they’re going essentially to move elsewhere.

and methods, though you may want to keep them around since they’re going essentially to move elsewhere. Implement IService , which includes Initialize and Stop .

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 :

PathingService.cs 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; } 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:

PathingService.cs 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; } 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

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 .

Waypoint.cs 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); } } } 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

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.

WaypointPathAgent.cs 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; } } } } } } 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

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 this Action will appear.

attribute specifies where in the CAT browser and menus this 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 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.

CalculateWaypointPathAction.cs 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; } } } 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:

CalculateWaypointPathAction.cs bool clr = clear.GetValue (context); 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.

CalculateWaypointPathAction.cs start.WithPositions (context, delegate(Vector3 svect) { 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.

CalculateWaypointPathAction.cs 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()); } 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.

CalculateWaypointPathAction.cs 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; } 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 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.

ClearWaypointDestinationAction.cs 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; } } } 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 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.

SetWaypointDestinationAction.cs 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; } } } 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 is almost identical to ClearWaypointDestinationAction , except instead of stopping the agent, it sets the walkSpeed using a FloatValue .

SetWaypointPathSpeedAction.cs 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; } } } 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 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 .

WaypointPathToAction.cs 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; } } } 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:

SetWaypointDestinationAction.cs subscriptions.Add(agent.Subscribe("StartedPathing", StoppedAgent)); subscriptions.Add(agent.Subscribe("StoppedPathing", StoppedAgent)); 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

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.

CAT/Editor/CATMenuItemEditor.cs 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 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

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!