CAT Game Builder In-Depth: Integrating a Waypoint Pathing System

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:

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.
  • 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.
  • Remove the existing parameters and add a new list of Waypoints.
  • 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.
  • 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:

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:

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 Waypoints register with our service automatically on Start, and unregister on OnDestroy.

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.

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.
  • 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 Actions or Actions that complete synchronously.

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 Values. 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:

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.

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.

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.

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 ValidationResults 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.

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.

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.

WaypointPathToAction

WaypointPathToAction is the most complicated of our new Actions. 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.

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. Deferreds 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:


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.

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!


Join Our Mailing List

No comments yet.

Leave Your Reply