Simple patrol & chase AI tutorial with Unity 2D and Mecanim
Finite State Machines & Unity Mecanim?
A finite state machine sounds complicated but at its simplest, it is just a way of keeping track of the situation (state) of an object and the rules which determine when that state will change.
If you want to build a finite state machine in code, there are dozens of tutorials out there already. Unity, however, provides a neat visual way to build an AI using Mecanim, the animation system built into Unity. If you are concerned that this tutorial is going to be some kind of dirty hack where we use the game engine in a way it was not designed to be used then let me assure you this is a legitimate use for Mecanim and as we will see the way we handle things like behaviours and communication between a finite state machine and its related objects is completely integrated into Unity. What you should be able to do by the end of this tutorial is implement your own, more complicated AI using these same simple techniques.
What kind of behavior will our zombie have?
We will see how to set up a set of waypoints and then get our zombies to choose one at random. The zombie will then set off towards it until it gets close and then it will pick another one. This simple behavior will continue forever unless the player enters the zombie’s field of view. Of course, the field of view is something we must define and we will do so with a triangle shaped sprite to which we will attach a trigger collider. If the player enters the trigger, then the zombie has “seen” the player. It would be simple to give the zombie more “sense” perhaps a circle collider for hearing or smell.
When the player is seen by a zombie it will abandon its waypoint and home in on the player. If the player escapes the zombies AI sight, the zombie will stop for a few seconds and then pick a random waypoint and go back to doing what it was doing before lunch wandered into its triangular field of view.
Take a look at this diagram read words in the rectangles that represent the states a zombie can be in and also look at the lines connecting the states. You will see that each line has an arrow indicating the direction that a state transitions from state to state as well as some notes I have added indicating the parameters(variables of the state machine) that need to be true for the transition to take place.
To explain the above, first of all, forget about the Exit and Any State states. We don’t need to do anything with them in this simple tutorial. When the state machine starts it enters the Entry state and immediately and unconditionally transitions to the Get New Waypoint state. When the Get New Waypoint state is entered it will communicate with the zombie and the zombie will pick a new waypoint. At this point, the distance from the chosen waypoint (distanceFromWaypoint) will be greater than one. This will cause a transition to the Move To Waypoint state. The state machine will communicate this change to the zombie object.
Every frame the zombie object will feed data into the state machine. One of these pieces of data is its distance from the current waypoint. When this distance is less than one a transition occurs back to the Get New Waypoint state and the zombie picks a new waypoint and sets off towards it… until it is less than one unit away and so this continues.
If however, the player stumbles into the zombie’s vision (playerInSight is true) the zombie object will let the state machine know and the state will change to Chase. The zombie’s behavior will be modified by the state machine and a battle for life and death will ensue. In order to keep this short enough for a single and functional tutorial, nothing happens when the zombie catches the player. You would probably want to kill the player or subtract hit points, etc. In addition, you would probably arm the player with a weapon etc.
If the player manages to escape the zombie’s sight then playerInSight will turn to false and the state machine will transition to the Wait state. After the Wait state the state machine either goes back to the Get New Waypoint state or if the player was careless enough to wander back into the field of view then the Chase state will resume.
There might be a bunch of unanswered questions like how do the zombie and the state machine communicate with each other or how do you associate a state machine with an object? All these questions and more will be answered as we proceed.
Starting the project
Create a new project in Unity, call it Zombie AI, choose the 2D option and click the Create Project button.
Create some new folders to stay organized as we proceed. You need an FSM, Prefabs, Scripts, and Sprites, like this.
Import the three images below and keep them in the Sprites folder.
To get started with the visuals grab yourself a fancy background from opengameart.org. Import it into the Sprites folder, drag it into the scene and repeat, positioning the background like tiles until you have covered a little bit more than the camera area. This step is not essential, the entire project will work without a background; however with just a plain background, when we set the camera to follow the player there will be a reduced sense of movement. One alternative if you don’t want to bother with the background is to simply play your game while observing the scene view which has handy grid-lines. Background or not, let’s move on to the project proper.
Here is what my Sprites folder looks like at this stage.
Let’s create some waypoints for the zombies to wander between.
Creating the waypoints
Next, we will create some waypoints that the zombies will randomly choose from to wander between. Once you see the code, you will see you could just as easily make your zombies follow a path of waypoints if you prefer.
Right click in the Hierarchy tab and select Create Empty. Now we have an empty object that is not visible to the player during the game. We want to make it easy for us to see in the Scene view, however. Rename the empty object to p1. Make sure that p1 is selected and in the Inspector window give it an icon to make it clearer in the Scene view. You do so by clicking this icon in the top-left of the inspector window.
Choose a new icon. I changed it to the red pill shape but you can choose whatever you prefer. You can see the new empty game object in the Scene view as shown in the next image.
We will now add a tag to this game object so we can easily find it through C# code.
- In the Inspector window select Tag | Add Tag
- Click the + icon
- Type p1 for the name of the new tag
- Select p1 in the Hierarchy window, click Tag in the Inspector window and choose p1
The empty game object called p1 now has a nice clear icon and a tag called p1.
Repeat this process (empty object, icon, and tag)four more times and name them p2, p3, p4, and p5. Actually, you can make as many or as few waypoints as you like but you will then need to remember to vary the code as we proceed. Note that for this particular project to work all of them must have a unique tag.
Arrange your waypoints around the scene. This is what my scene view looks like.
Now the zombies have some waypoints let’s get started with the zombies themselves.
Prefabricating an intelligent(ish) zombie
We will do this in 6 stages as follows.
- Create a game object in the scene from the zombie and view-cone sprites
- Add an AI script to the zombie
- Add a collision script to the view cone
- Build a finite state machine to receive data from the AI
- Add behaviors to the finite state machine to trigger the required behavior from the AI script
- Make a prefab from the finished zombie and add a whole bunch of them to the scene
Let’s get started.
The zombie and the vision cone sprites
In the Sprites folder select vision_cone. Then, in the Inspector window click the Pivot button, then choose bottom and click Apply. This has made the center-bottom(narrow part) of the cone the pivot point for this sprite. This will make things work when we combine two sprites next.
Drag the zombie and the sight_cone sprites into the scene. Then in the Hierarchy view, drag the vision_cone game object to be a child of the zombie game object. Set the X, Y and Z values of the vision_cone‘s Transform component to 0,0,0. This will make the vision_cone and zombie objects line up, just as we want them to.
This is what my Scene view looks like (zoomed in).
This is what your Hierarchy window should look like at this stage.
Add a Rigidbody 2D component to the zombie object and set it to Is Kinematic. We want the zombie to move but not collide.
Now for the first of the C# code.
Adding the AI script to the zombie
Right click in the Scripts folder and select Create | C# Script. Name it ZombieAi. Code the script as follows. Read through the code including the comments and then we can discuss it.
1 | using UnityEngine; using System.Collections; public class ZombieAi : MonoBehaviour { // Where is the player private Transform playerTransform; // FSM related variables private Animator animator; bool chasing = false; bool waiting = false; private float distanceFromTarget; public bool inViewCone; // Where is it going and how fast? Vector3 direction; private float walkSpeed = 2f; private int currentTarget; private Transform[] waypoints = null; // This runs when the zombie is added to the scene private void Awake() { // Get a reference to the player's transform playerTransform = GameObject.FindGameObjectWithTag("Player").transform; // Get a reference to the FSM (animator) animator = gameObject.GetComponent<Animator>(); // Add all our waypoints into the waypoints array Transform point1 = GameObject.Find("p1").transform; Transform point2 = GameObject.Find("p2").transform; Transform point3 = GameObject.Find("p3").transform; Transform point4 = GameObject.Find("p4").transform; Transform point5 = GameObject.Find("p5").transform; waypoints = new Transform[5] { point1, point2, point3, point4, point5 }; } private void Update() { // If chasing get the position of the player and point towards it if (chasing) { direction = playerTransform.position — transform.position; rotateZombie(); } // Unless the zombie is waiting then move if (!waiting) { transform.Translate(walkSpeed * direction * Time.deltaTime, Space.World); } } private void FixedUpdate() { // Give the values to the FSM (animator) distanceFromTarget = Vector3.Distance(waypoints[currentTarget].position, transform.position); animator.SetFloat("distanceFromWaypoint", distanceFromTarget); animator.SetBool("playerInSight", inViewCone); } public void SetNextPoint() { // Pick a random waypoint // But make sure it is not the same as the last one int nextPoint = -1; do { nextPoint = Random.Range(0, waypoints.Length — 1); } while (nextPoint == currentTarget); currentTarget = nextPoint; // Load the direction of the next waypoint direction = waypoints[currentTarget].position — transform.position; rotateZombie(); } public void Chase() { // Load the direction of the player direction = playerTransform.position — transform.position; rotateZombie(); } public void StopChasing() { chasing = false; } private void rotateZombie() { float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg; transform.rotation = Quaternion.Euler(new Vector3(0, 0, angle — 90)); direction = direction.normalized; } public void StartChasing() { chasing = true; } public void ToggleWaiting() { waiting = !waiting; } } |
Add the script to the zombie game object by selecting the zombie game object in the Hierarchy window and clicking Add Component in the Inspector window. Now choose Scripts | ZombieAi. This is how the code works.
At a glance, the code is long and sprawling but it really doesn’t hold too much that we haven’t seen in other tutorials previously.
The variables initialized at the start include a
1 | Transform |
that will be used to track the position of the player. The variables related to the state machine include an
1 | Animator |
. As we will see soon, an
1 | Animator |
is a state machine. Each frame we will send values to the
1 | Animator |
/state machine. These values are held in
1 | distanceFromTarget |
and
1 | inViewCone |
which will indicate how far away a zombie is from its current waypoint as well as if the player is currently inside its trigger collider. The chasing and waiting values do NOT get sent to the state machine. Rather, as we will see, the state machine will update theses variables when appropriate. We then declare a bunch of variables that will handle movement. A
1 | Vector3 |
called
1 | direction |
which keeps track of the zombies heading, a
1 | float |
called
1 | walkSpeed |
which is how fast the zombie can move. An
1 | int |
called
1 | currentTarget |
which indicates the position in an array which holds the
1 | Transform |
of the current waypoint. The
1 | waypoints |
array into which we will load all the
1 | Transform |
components of our waypoints in the scene.
In the
1 | Awake |
method we use the tags of the waypoints and the player to get a reference to them. All the waypoints are stashed in the
1 | waypoints |
array. The interesting thing that happens in
1 | Awake |
is we initialize
1 | animator |
by getting a reference to an
1 | Animator |
component which is the finite state machine we will soon build.
In
1 | Update |
there are just two
1 | if |
statements. If the zombie is currently
1 | chasing |
calculate the direction to the player and turn to face him. If the zombie is not
1 | waiting |
just walk in whichever direction it is facing. Note that this
1 | direction |
will have been previously set to a waypoint. This implies that if the zombie is
1 | waiting |
it won’t move at all. We will see the
1 | rotateZombie |
method soon.
In
1 | FixedUpdate |
is where the action happens. The first line of code calculates how far the zombie is from the current waypoint then calls
1 | SetFloat |
and
1 | SetBool |
on
1 | animator |
to set the values of
1 | distanceFromWaypoint |
and
1 | playerInSight |
, inside the state machine. These two values will be all that our finite state machine will need in order to make decisions and transition between states.
At this point, we have not seen any way that the state machine can communicate back to the zombie. We will get to the first part of the conundrum really soon.
Next up is the
1 | SetNextPoint |
method.
1 | SetNextPoint |
uses a
1 | do while |
loop to keep picking a new position in the
1 | waypoints |
array until it chooses one that is different to the current one. It then alters direction and calls
1 | rotateZombie |
to do a little math to face the zombie in the correct direction.
The
1 | Chase |
method sets direction relative to the player(rather than the next waypoint) then also calls
1 | rotateZombie |
.
1 | StopChasing |
does one thing. It sets
1 | chasing |
to
1 | false |
. When we build our state machine we will see how it accesses this method in order to control how the zombie behaves. Note there is a corresponding
1 | StartChasing |
method as well.
The
1 | RotateZombie |
The last method,
1 | ToggleWaiting |
will also be called by the state machine to toggle the waiting state of the zombie.
Adding collision detection to the vision-cone game object
Add a collider by selecting the vision-cone in the Hierarchy and clicking the Add Component | Physics2D | Polygon Collider 2D. If you check the scene view you will see that a nice neat collider has been added to the vision-cone object. Check the Is Trigger checkbox so that we can code a script to respond to objects entering the perimeter of the collider.
Right click in the Scripts folder and select Create | C# Script. Name it ConeOnTrigger. Code the script as follows.
1 | using UnityEngine; using System.Collections; public class ConeOnTrigger : MonoBehaviour { public ZombieAi zombieAi; void OnTriggerEnter2D(Collider2D o) { if (o.gameObject.tag == "Player") { zombieAi.inViewCone = true; } } void OnTriggerExit2D(Collider2D o) { if (o.gameObject.tag == "Player") { zombieAi.inViewCone = false; } } } |
Add the script to the vision-cone game object by selecting the vision-cone game object in the Hierarchy window and clicking Add Component in the Inspector window. Now choose Scripts | ConeOnTrigger. This is how the code works.
The previous code has a reference to the zombie object. Every time the player either enters or leaves the trigger the
1 | inConeView |
boolean variable is updated appropriately. Remember the value of
1 | inConeView |
is sent to the state machine every frame during the
1 | FixedUpdate |
method in the
1 | ZombieAi |
script. We can start to see all the pieces of how we talk to the state machine.
Now we get to build the finite state machine that will receive the inputs and initiate the behaviors we have just coded.
Building the Zombie Finite State Machine in Mecanim
To get started let’s add a couple of components to the zombie game object.
Make sure the zombie object is selected in the Hierarchy then click Add Component in the Inspector. Choose Miscellaneous | Animator. If you look closely at this new component you will see a slot for a Controller. This is where we will add the finite state machine when we have built it. Next, add another component to the zombie. Click Add Component and choose Physics 2D | Rigidbody 2D. Set Gravity Scale to 0 to stop our zombie falling to oblivion and set it to Is Kinematic. We want the zombie to move but not collide.
Let’s build the finite state machine in an animator controller. This sounds like a hack but it is a totally legitimate way to program AI in Unity. Select Window | Animator from the Unity main menu to create a workspace for this purpose. Dock the window (by dragging and dropping its tab) somewhere you will have a large workspace. I docked mine in the same space as the Scene.
Right-click in the FSM folder and select Create | Animator Controller. Name the Animator Controller ZombieFSM. Rearrange the states that are created for you like this following image. Note this is not necessary it is just keeping things tidy.
Add the following states in the following order by right-clicking and selecting Create State | Empty. You rename the states by selecting them in the Animator window and adjusting the Name in the Inspector window.
- Get New Waypoint
- Move To Waypoint
- Chase
- Wait
Notice the first state you create is a different color and was automatically connected to the Entry state. Rearrange your states to look like this next image. Notice we are already getting close to how we envisioned our finite state machine at the start of the article.
Now we can add the two parameters and the transitions that their different values will trigger. To add a parameter click Parameters in the top left of the Animator window and then click the + icon. Add the following parameters as the following types.
playerInSight as a bool
distanceFromWaypoint as a float
Remember the FixedUpdate method in the ZombieAI class constantly updates these parameters. Here is a reminder of the code which executes each frame.
1 | private void FixedUpdate() { // Give the values to the FSM (animator) distanceFromTarget = Vector3.Distance(waypoints[currentTarget].position, transform.position); animator.SetFloat("distanceFromWaypoint", distanceFromTarget); animator.SetBool("playerInSight", inViewCone); } |
To add the transitions that values of these parameters will trigger we will start by adding the transition links and then plug in the values after that. To create a transition right-click on the state you are starting from, choose Make Transition and then left-click on the state you want to transition to. The direction is very important or our zombies will not behave as intended. Create the following transition links.
Get New Waypoint to Move To Waypoint
Move To Waypoint to Get New Waypoint
Move To Waypoint to Chase
Chase to Wait
Wait to Chase
Wait to Get New Waypoint
Check you have the exact same state machine as shown in this image.
Now we can plug in the values of the parameters that will initiate the transitions. Select the transition that goes from Get New Waypoint to Move To Waypoint by left clicking it. Notice the options that appear in the inspector window.
Uncheck Has exit time
Click the + icon of the Conditions option
Configure the new condition as distanceFromWaypoint Greater 1
This is how the inspector should look after you have done this.
The Has Exit Time option optionally delays the exit when the condition is true. The Conditions is the actual condition that would cause this transition link to be made. So as the zombie is closer than one unit to the current waypoint it will get a new waypoint. Fill out all the rest of the transition parameters and exit times as detailed next.
- Move To Waypoint to Get New Waypoint: Uncheck Has Exit Time. distanceFromWayPoint Less 1
- Move To Waypoint to Chase: Uncheck Has Exit Time. playerInSight = True
- Chase to Wait: Uncheck Has Exit Time. playerInSight = false
- Wait to Chase: Uncheck Has Exit Time. playerInSight = true
- Wait to Get New Waypoint: Check Has Exit Time and set to 3.0 under Settings. playerInSight = False
We have so far seen how the ZombieAi class repeatedly updates the state machine with the values it needs and we have just programmed how and when the state machine will step back and forth between states. Now we can see how the state machine communicates back to the ZombieAi class using behaviors.
Adding behaviors to the finite state machine
IMPORTANT NOTE: I am using an English|UK version of Unity and an English|US spell-checker. Note there is a disparity in the spelling of behavior/behaviour. It doesn’t matter which you use as long as you are consistent with regard to file names and class names. Throughout this tutorial, I have used behavior but I just noticed that in the code samples that follow I have used behaviour. Just make sure you are consistent and make any necessary changes if you are copy & pasting. End of important note.
Behaviors are how the FSM communicates with the AI. In this project, the behaviors don’t control HOW the AI does something they just choose WHICH ONE and WHEN.
To add a behavior we choose the state that we want to add a behavior to and then in the inspector window click the Add Behavior button. We can add behaviors on numerous different events as we will see.
Select the Get New Waypoint state and click Add Behavior. Choose New Script and type SelectWaypointState in the Name field. A new C# script has been added as a behavior. Open the script and edit the code to look like this.
1 | using UnityEngine; using System.Collections; public class SelectWaypointState : StateMachineBehaviour { // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { ZombieAi zombieAi = animator.gameObject.GetComponent<ZombieAi>(); zombieAi.SetNextPoint(); } } |
The first thing you will notice when you view the auto-generated script is there are a few more methods. The
1 | OnStateEnter |
method is called when the state is first entered. For this behavior, that is exactly what we want. The code above, therefore, when the Get Next Waypoint state is entered will get a reference to the appropriate instance of the
1 | ZombieAi |
script and call its
1 | SetNextPoint |
method. The
1 | ZombieAi |
script will take care of rotating the zombie and sending it on its way.
Select the Chase state and follow the previous steps to create a behavior called ChaseStateBehaviour. Edit the code to look like this
1 | using UnityEngine; using System.Collections; public class ChaseStateBehaviour : StateMachineBehaviour { // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { ZombieAi zombieAi = animator.gameObject.GetComponent<ZombieAi>(); zombieAi.StartChasing(); } // OnStateExit is called when a transition ends and the state machine finishes evaluating this state override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { ZombieAi zombieAi = animator.gameObject.GetComponent<ZombieAi>(); zombieAi.StopChasing(); } } |
In the Chase state, we get to use both
1 | OnStateEnter |
and
1 | OnStateExit |
. When the state machine first enters the state it calls
1 | StartChasing |
on the
1 | ZombieAi |
script which changes where the zombie is pointed and where it moves to each frame. In
1 | OnStateExit |
the same method is called which causes the zombie to revert to its previous behavior of wandering between waypoints.
Select the Wait state and follow the previous steps to create a behavior called WaitingStateBehaviour. Edit the code to look like this
1 | using UnityEngine; using System.Collections; public class WaitingStateBehaviour : StateMachineBehaviour { // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { ZombieAi zombieAi = animator.gameObject.GetComponent<ZombieAi>(); zombieAi.ToggleWaiting(); } // OnStateExit is called when a transition ends and the state machine finishes evaluating this state override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { ZombieAi zombieAi = animator.gameObject.GetComponent<ZombieAi>(); zombieAi.ToggleWaiting(); } } |
In the
1 | WaitingStateBehavior |
script the same methods are used as in the
1 | ChaseStateBehavior |
script. When the state machine enters the Waiting state the
1 | ToggleWaiting |
method sets the zombie into a motionless state and when the
1 | Waiting |
state is exited it sets the zombie moving again. Remember this movement could either be towards the player or towards a new waypoint, depending upon whether the player is currently visible.
Finally for this section of the tutorial, select the zombie object in the Hierarchy, drag ZombieFSM from the FSM folder to the Controller field in the Inspector.
Select the vision-cone object in the Hierarchy and drag the zombie object to the Inspector and drop it on the Zombie Ai field of the Cone On Trigger script.
We now have all the references in our scripts apart from the Player reference but we haven’t made that yet.
Making a zombie prefab and adding some instances to the scene
Our zombie is almost done. Drag it from the Hierarchy to the Prefabs folder to create a prefab from all of our hard work. Make sure the prefab has definitely been created in the Prefabs folder and delete the zombie from the Hierarchy. Drag and drop a whole load of zombies from the Prefabs folder into the scene.
Tip: If you can’t wait any longer, you could comment out the few lines of code in ZombieAi.cs that refer to playerTransform (and are causing an error), run the game and see all your zombies wandering around between the different waypoints.
We are so close now so let’s add the finishing touches.
Coding a controllable player
Get started by adding the player sprite to the scene. Add a tag called Player to the player object. If you need a reminder how to do this then refer back to when we created the waypoints. Add a Ridgidbody 2D and set its Gravity Scale to 0. Next, add a Box Collider 2D. Now add a new script component named PlayerController. Edit the script to be the same as this next code.
1 | using UnityEngine; using System.Collections; public class PlayerController : MonoBehaviour { private float speed = 4f; // Update is called once per frame void Update () { // Quit if the player presses Escape if (Input.GetKey("escape")) Application.Quit(); // Get input from the keyboard or controller float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); // Calculate the angle to rotate the player Vector3 facing = new Vector3(h, v, 0); float angle = Mathf.Atan2(facing.y, facing.x) * Mathf.Rad2Deg; // Rotate the player transform.rotation = Quaternion.Euler(new Vector3(0, 0, angle — 90)); // Move the player transform.Translate(facing * speed * Time.deltaTime, Space.World); } } |
This is very straight forward code to move the player and make him face the appropriate direction. Note that if you plug a controller into your PC/Mac you will get more refined rotations and movements compared to just using the cursor keys. We have discussed simple movement in several previous tutorials.
Making the camera follow the player
Select the Main Camera object in the hierarchy. Add a new C# script as a component in the Inspector and name it FollowCamera. Edit the code to be the same as this.
1 | using UnityEngine; using System.Collections; public class FollowCamera : MonoBehaviour { // Reference to the player's transform. public Transform player; void LateUpdate() { transform.position = new Vector3(player.transform.position.x, player.transform.position.y, transform.position.z); } } |
This script keeps a reference to the
1 | Transform |
component on the player so the camera follows the player as it moves.
Finally, select the Main Camera object and drag the player to the slot on the FollowCamera script.
Running the game
Run the game and observe it in the scene view if you didn’t bother with the fancy background or the game view if you did.
You can add hundreds of objects before even my fairly modest laptop starts to struggle. There are optimizations we could make to our code but that would be for another tutorial. The point is that our finite state machine tracks the states and makes all the decisions for us. It might not be obvious how much coding this is saving us. Think about the vast number of if statements that would be necessary to replace what the state machine is doing. Furthermore, it is much less error prone. Click on a zombie in the Scene view, switch to the Animator view and then run the game. You can see a nice animation of the states and when they transition. This is very clear to debug when things don’t behave as expected. The visual nature of the Animator is also really simple to add in extra states.
Congratulations on building the project.