4.5 — The Full State Machine: Patrol, Chase, Attack
You have written every line of this enemy already: the patrol, the chase, the contact damage. What you do not have is a field saying which one it is doing right now — and that missing field is the difference between a pile of behaviours and a machine.
The enemy should walk its beat, come for you when you get close, and hurt you when it reaches you — and on every single frame it should be doing exactly one of those three. "It behaves correctly, but nobody can say which one it is doing" is the whole problem.
First, before any code: cut that into parts small enough that you could look each one up on its own.
- Name the states this enemy can be inThree names — PATROL, CHASE, ATTACK — and nothing else. Written down once, in one type, so a fourth value cannot arrive by accident and the enemy cannot be two of them at once.Read: the Making states explicit section of the Enemy State Machines reference.
- write this partRead: the
enumrow in the Unity + C# Quick Reference, and the field-scope rule from 1.3 (the deadline you wrote asnextReportTime). - Decide what moves it from one state to the nextOne assignment per arrow.
state = EnemyState.Chase;is the transition — the comparison that used to decide whether to chase now only decides whether the machine becomes the chasing machine.Read: the four-part upgrade list in the Enemy State Machines reference, next to the transition table you drew at the top of this lesson. - Keep the three behaviours you already wroteThe beat from 4.1, the pursuit vector from 4.3 and the contact damage from 4.4 are not rewritten. They move behind the state that owns them, and not one line of them changes.Read: your own
EnemyController.csafter 4.1, 4.3 and 4.4 — the walkthrough below shows where each piece lands.
Three of those four parts are code you have already written, and the fourth is one field plus the line that reads it. That is the honest size of this lesson. What you have never had is a place in the file that says which behaviour is live — the state is implied by which branch happened to run, which is why you cannot print it, cannot drive an animator from it, and cannot answer "what is this enemy doing?" without reading the whole file.
The test: could you look this part up on its own, right now, in one search? "Make the enemy switch between behaviours" cannot be looked up. "Declare an enum, hold one value of it, and switch on that value" can — and the reference page writes out all four pieces.
Your turn
The table below prices three ways to hold the states. Which did you choose, and what does your choice cost you — in your own words, not the table's?
ATTACK is entered by a physics callback rather than by a comparison. If you make it a real state, what changes about when the enemy moves — and is that the fight you designed in 4.4?
Your animator's Direction parameter is an Int with four values and no idle states. Now that the machine has a state you can read, what could you drive from it that you could not drive before?
-
Scene view → save the Overworld, then Project window →
Assets/Scripts/EnemyController.cs. Read it as it stands: the patrol runs unconditionally, the chase hides inside anif, the attack is a collision callback, and no line anywhere says what the enemy is doing. - Game view → press Play, walk into a patrolling enemy and stand still. Note exactly what happens and when — that observation is what the table below has to be able to explain.
-
Inspector → the child named Sprite, then Window →
Animation → Animator: the two parameters from 4.2,
Directionand theAttacktrigger, are waiting for something that can say which behaviour is live. - Scene view → watch one full beat and mark the instant the enemy ought to switch behaviour. That instant is an arrow, and every arrow needs a condition written on it.
The machine, as the player experiences it — and then the question of which of these rows your code actually implements:
| From | Condition or event | To |
|---|---|---|
| PATROL | distance to the player is inside the aggro radius | CHASE |
| CHASE | every frame: move toward the player, normalized | CHASE (stay) |
| CHASE | contact with the player, and the damage cooldown has elapsed | ATTACK |
| ATTACK | damage delivered, cooldown started | CHASE |
| CHASE | distance is outside the radius (or outside the give-up radius if you built one) | PATROL |
One row of that table is a lie. In your code ATTACK is not a state — it is a callback that fires when the two colliders meet, so it can fire mid-chase or mid-patrol. Drawing it as a state is still right, because that is how it feels to the player; just be able to say which rows your code implements and which it only feels like.
There are three ways to hold those states, and they are priced rather than ranked — the question is never which is correct, but which cost you will pay for this enemy, this week:
| Representation | What it looks like in the file | What it buys you | What it costs you |
|---|---|---|---|
| Boolean flags — one bool per behaviour | isChasing, isAttacking, and one per behaviour after that. |
Cheap to add the first flag, and genuinely readable with exactly two behaviours. It is also the smallest change from what you have. | Every flag doubles the number of combinations: four flags describe sixteen situations and most of them are nonsense. Nothing in the language stops you being "chasing and attacking and fleeing" at once — you get a bug instead of a compile error. |
| An enum state variable | One enum listing the states, plus one field of that type, read by a switch. |
The set of legal states is written down in exactly one place, so illegal combinations cannot even be expressed. You can print the state name, drive the animator from it, and read every transition out of one switch. |
One field, one enum declaration and one transition rule — and you must decide every exit on purpose. The version you have gave you the exits for free; here the one you forget is a state the enemy can never leave. |
| An enum, and one method per state | The same enum and field, with each case in the switch reduced to a single call: case EnemyState.Chase: ChaseTick(distance); break; |
Each state's code becomes a named block you can read on its own, and the dispatch at the top of the frame reads as a table of contents. | You have to decide what each state is handed — the distance? the target? — and the transition now sits one hop away from the selection that reads the field. |
Socratic: Naming the Machine
- Right now, is there any field in
EnemyController.csthat holds what the enemy is currently doing? If not, what is holding it — and what does that cost you when you want to debug? - With two behaviours, a single bool is perfectly readable. How many combinations do four booleans describe, and how many of those are real states? What does that ratio tell you about scaling the flag approach?
- Your attack is a callback, not a state. Can it fire while the enemy is patrolling? Walk into a patrolling enemy and find out — then say whether your diagram is a description of the code or a description of the player's experience.
The question this answers“how do I make an enemy switch between patrolling, chasing and attacking?”
You are editing Assets/Scripts/EnemyController.cs. Everything you built in 4.1, 4.3 and 4.4 stays: the beat, the pursuit, the contact damage and its cooldown. Today adds two lines to the class body and one selection inside Update() — the line that was never there. The names below are the ones this course uses; if you named yours differently, keep your names and read the shape.
This build leaves 1 line for you to write. Everything around it is given, so you always know the shape of what you are producing.
Step 1Name the states, and give the class one field to hold the current one
Both lines go inside the class braces and outside every method — the placement rule from 1.3.
// ...the fields you wrote in 4.1, 4.3 and 4.4: speed, direction, the patrol memory, the aggro radius, the damage knob and its cooldown...
private enum EnemyState { Patrol, Chase, Attack }
private EnemyState state = EnemyState.Patrol;- 1Not one of those fields is deleted or renamed today. The two lines below are the only additions to the class body.
- 3The legal states, written down in exactly one place. Three names, and from here on the compiler knows them.The type is the point.
EnemyState.Patrolis the only spelling that compiles, so a typo is an error on the line you typed rather than a fourth state that silently matches nothing. Astringholding"patrol"would compile happily — that is 4.2's lesson, whereDirectionmeant Right on one controller and Up on the other and no error message ever mentioned it.private, because this is one enemy's own vocabulary and no other script has business naming its states. - 5The field this whole lesson is about. One variable holding which of those three names the enemy is doing right now — the thing you could not print, could not drive an animator from, and could not ask about without reading the file.Two things to notice, and both are 1.3. Where it lives: in the class body, not inside
Update(). Declared inside a method it would be rebuilt from nothing sixty times a second and carry nothing between frames — the same mistake aslastAttackTimein 3.3. What it starts at:Patrol. Frame one has to be some state, and a guard that spawns already hunting you has no first state at all; the beat you built in 4.1 is the machine at rest. Leave the starting value off and you get0, which is whichever name you happened to list first rather than the one you meant.
Step 2Let the state decide which behaviour runs
This is the line that did not exist. Until today, behaviour was decided by where the code sat — and nothing stopped two behaviours running in the same frame.
void Update()
{
float distance = DistanceToPlayer();
if (playerTransform == null)
{
state = EnemyState.Patrol;
}
________ (________)
{
// one branch per state — the next step fills them in
}
}
// No player is never in range, so the beat is never interrupted
private float DistanceToPlayer()
{
if (playerTransform == null)
{
return float.MaxValue;
}
return Vector3.Distance(transform.position, playerTransform.position);
}- 3One measurement, made once, by a small method that answers the only question the machine ever asks about the player: how far away is it?Why a distance and not a bool. Every branch below compares a number against a radius, so "there is no player" has to arrive as a number that is never inside one. The helper below is where that number is produced.
- 5The guard from 4.3, with a new job: it no longer stops the frame, it forces the state.
- 7An enemy with no target is a patrolling enemy. That is a state, not an exit — and it is the answer to what this machine does the moment your death rule from 2.2 takes the player out of the scene.An early return here would delete the machine. Return before the selection and no state is ever consulted again: the enemy freezes mid-beat, and the animator from 4.2 keeps playing the last clip it was given. The fallback for "no target" has to be a state, or the machine has a hole where its default should be.
- Your turn10The selection. Read the field from step 1 and choose which of the three behaviours runs this frame. One construct, three branches, exactly one of them running — this line is the machine.The multi-branch selection statement you met in 1.5: the one that reads a value and picks a branch to run. The value is the field you declared in step 1, and its type is the enum you wrote beside it.This is the line whose absence was the bug. Before today, behaviour was decided by where the code sat — the patrol ran because it was there, the chase ran if its
ifmatched — so on any frame where both matched, both moved the transform. That is 4.3's two-writers bug, the hunter that crabs sideways because it is patroling and chasing at once. Now a value decides, the value can be printed, and there is no arrangement of this file in which two branches run in the same frame. - 12The three branches land here. Nothing else in
Update()changes: this selection replaces the unconditional patrol and the hidden chase, and the distance is still measured exactly once, above it. - 16The small helper the line above calls. Two lines of body and one idea.
- 17A method with one job: return a number the machine can compare against a radius, whether or not there is anything to measure.
- 19The case that used to freeze the enemy in 4.3.
- 21A number so far outside every radius in the Inspector that no comparison in the machine can match it. "There is no player" is expressed as a distance, so the rest of the file never has to ask the question again.
- 23The measurement from 4.3, unchanged — the same two positions, the same call. If you chose
sqrMagnitudethere instead, keep it: what matters is that one place in the file answers this question.
Step 3Move patrol and chase behind their states, and write both exits down
Every behaviour stays exactly as you wrote it — it only changes which lines it sits behind. What is new is the arrows: each state = ... line is one arrow from your diagram, in code.
case EnemyState.Patrol:
if (distance <= aggroRadius)
{
state = EnemyState.Chase;
break;
}
// ...your beat from 4.1, unchanged: the anchor or waypoint check, the direction flip, the movement call and your animator line...
break;
case EnemyState.Chase:
if (distance > aggroRadius)
{
state = EnemyState.Patrol;
break;
}
// ...your pursuit from 4.3, unchanged: the normalized (player - me) vector, the movement call and your animator line...
break;- 1The state the machine starts in, and the one that runs when nothing is hunting you.
- 2The comparison from 4.3, measuring the same two things it always did. Its job changed: it no longer decides whether to chase this frame, it decides whether the machine becomes the chasing machine.The exit and the entry have to agree about the boundary. Your entry uses
<=, so the chase's way out uses the other side of it —distance > aggroRadius. Use the same side in both places and an enemy standing exactly on the line changes state on every frame and stutters there instead of committing. - 4The arrow. One assignment is the transition: the machine is now the chasing machine, and it stays that way until some line changes it back.One arrow, one assignment, and it lives in the case it leaves. If the chase code could also write
state, two places would be deciding this machine's next move and which one won would depend on the order of the lines — the same class of bug as two writers on one transform in 4.3. A transition belongs with the state you are leaving, never with the state you are entering. - 5Ends this case for this frame.The one line a
switchhas that anif/elsechain does not. Leave it out and the machine falls through into the next case and runs the chase as well — in the same frame, from the state it was just leaving. Nothing crashes and nothing is underlined in red: the enemy simply behaves like two states at once, which is the exact thing this lesson exists to remove. - 8The patrol itself is untouched. It now runs on the frames where the machine is in PATROL and on no others — which is the whole difference from 4.1, where it ran on every frame there was.
- 9Ends the PATROL case.
- 11The hunting state: entered from PATROL, and left by the two lines inside it.
- 12The return-to-patrol arrow, written down by hand. There was no such line in 4.3 — the chase ended when its
ifstopped matching, and the patrol was simply what was left. Now an exit nobody writes is an exit that does not exist.This is the line teams forget. Delete it and the enemy follows you across the entire map, forever, because nothing in the machine can leave CHASE. Notice also which radius it uses: if you built a second, larger give-up radius in 4.3, this is where it belongs, and the gap between chase-in and give-up is what stops the state flickering on the boundary. - 14The arrow back to the beat. The beat resumes where it was — the anchor and the direction field are still holding the values 4.1 gave them, which is a decision you can make the other way if you want a fresh beat.
- 15Without it the machine falls through and runs the chase body on the same frame it decided to stop chasing.
- 18The chase body — the same subtraction order, the same
.normalized, the same movement call. Nothing about the pursuit changes; it has just been given a state to live in. - 19Ends the CHASE case.
Step 4Say what the machine does while it is attacking, and let physics enter it
ATTACK is the state your contact damage reaches — but not from a comparison. It arrives from the collision callback you wrote in 4.4, which is the only place in the file where the physics engine tells you the fight started.
case EnemyState.Attack:
if (Time.time >= lastDamageTime + damageCooldown)
{
state = EnemyState.Chase;
}
break;
}
}
private void OnCollisionStay2D(Collision2D collision)
{
if (!collision.gameObject.CompareTag("Player")) return;
if (Time.time < lastDamageTime + damageCooldown) return;
// ...the single damage call you wrote in 4.4, unchanged...
lastDamageTime = Time.time;
state = EnemyState.Attack;
}- 1The state a landing hit puts the machine into. Be honest about this one out loud: in your file the attack is not a movement — it is a callback — and this case is where you decide what the enemy does while a hit is landing. In this build that is nothing: no movement line runs here, so it stops for the length of its cooldown.Whether ATTACK is a state is a design decision, and this is where you make it. Your diagram draws ATTACK as a box because that is how the fight feels to the player; your code reaches it from physics, which means it can fire while the enemy is calmly patrolling. Both are defensible. What is not defensible is a state that nothing ever enters.
- 2The cooldown gate from 4.4 doing a second job. The same pair of fields that stopped the damage running every frame now says when the swing is over.The seed you chose in 4.4 matters here too.
lastDamageTimeis stamped the moment a hit lands, so this comparison is false for exactly one cooldown — and if you never seeded it inStart(), an enemy that has never hit you is already "finished attacking" on frame one. - 4The exit: the swing is over, back to the hunt.
- 6Ends the ATTACK case. Every case in the selection ends this way, and every exit in the machine is a line you typed.
- 10Whichever callback you chose in 4.4 —
OnCollisionStay2D(Collision2D collision),OnTriggerStay2D(Collider2D other), or theirEnterversions. The name is yours; the shape below is the same for all of them. - 12The tag check from 4.4, unchanged — one extra hop through
.gameObjectfor aCollision2D, direct for aCollider2D. - 13The cooldown from 4.4, unchanged: a second touch inside the window is discarded, not queued.
- 15Your
GetComponent(PlayerController)call and the negative number you pass to the health method onPlayerController. Nothing about the damage itself changes today. - 16The stamp from 4.4 that closes the damage gate. It is also the clock the ATTACK case reads to decide when the swing is over.
- 18The one new line here: the machine is now attacking. This is what makes ATTACK reachable — without it the state exists, compiles, and is never entered.This is the row of your diagram that your code does not implement the way you drew it. You drew an arrow from CHASE to ATTACK as if chasing led to attacking. In your file the arrow comes from physics instead: the enemy enters ATTACK from PATROL just as readily, which is why walking into a guard on its beat costs you health. Say that out loud once and you will never again mistake the diagram for the code.
Check your work: the finished filetry every gap first — then open this to compare
Compare against your own file rather than copying it. This is the same machine arranged differently on purpose: the reference puts each state's code in its own method, so you can see that the arrangement was never the point — the state field, the selection that reads it, and the exits you wrote by hand are. Your names, your beat, your movement call and your damage flow are yours; the machine is what has to match.
// ------------------------------------------------------------------
// REFERENCE FILE — Assets/Scripts/EnemyController.cs
// Written to be read, not pasted. Build it with the walkthrough
// above, one step at a time, then open this and compare.
// If it is sitting in your project unchanged, that is the thing
// to go and do now — the walkthrough is still above you.
// ------------------------------------------------------------------
using UnityEngine;
public class EnemyController : MonoBehaviour
{
[SerializeField] private float moveSpeed = 2f; // your number from 4.1
[SerializeField] private float patrolDistance = 3f; // the size of the beat (4.1)
[SerializeField] private float aggroRadius = 5f; // your number from 4.3
[SerializeField] private int contactDamage = 1; // your number from 4.4
[SerializeField] private float damageCooldown = 0.75f; // your number from 4.4
private Vector3 spawnPoint; // 4.1 - the anchor, saved once in Start()
private int direction = 1; // 4.1 - the sign that IS the beat
private Transform playerTransform; // 4.3 - however you acquired it
private float lastDamageTime; // 4.4 - the moment of the last hit
// ---- all of today, in two lines: three names, and which one is live ----
private enum EnemyState
{
Patrol,
Chase,
Attack
}
private EnemyState state = EnemyState.Patrol; // frame one is a calm guard
void Start()
{
spawnPoint = transform.position;
playerTransform = GameObject.FindWithTag("Player").transform; // or your wired reference
lastDamageTime = -damageCooldown; // "there has been no last hit"
}
private void Update()
{
// One measurement, and no early return: an enemy with no target is a
// patrolling enemy, not an enemy that stops thinking.
float distance = DistanceToPlayer();
if (playerTransform == null)
{
state = EnemyState.Patrol;
}
switch (state)
{
case EnemyState.Patrol: PatrolTick(distance); break;
case EnemyState.Chase: ChaseTick(distance); break;
case EnemyState.Attack: AttackTick(); break;
}
}
// Same machine, arranged differently: each state's code is its own method,
// and the transition lives inside the method of the state it leaves.
private float DistanceToPlayer()
{
if (playerTransform == null)
{
return float.MaxValue; // no player is never in range
}
return Vector3.Distance(transform.position, playerTransform.position);
}
private void PatrolTick(float distance)
{
if (distance <= aggroRadius)
{
state = EnemyState.Chase;
return;
}
// ...your beat from 4.1, unchanged: the anchor check, the direction
// flip, the movement call and your SetInteger("Direction", ...)...
}
private void ChaseTick(float distance)
{
if (distance > aggroRadius)
{
state = EnemyState.Patrol; // the exit nobody writes
return;
}
// ...your pursuit from 4.3, unchanged: the normalized (player - me)
// vector, the movement call and your animator line...
}
private void AttackTick()
{
if (Time.time >= lastDamageTime + damageCooldown)
{
state = EnemyState.Chase; // the swing is over
return;
}
// Nothing here moves the enemy, so it stands still for the length of
// its cooldown. If you would rather it kept coming, that is the one
// line you move into this method - and it is a design decision.
}
private void OnCollisionStay2D(Collision2D collision)
{
if (!collision.gameObject.CompareTag("Player")) return;
if (Time.time < lastDamageTime + damageCooldown) return;
// ...the damage call you wrote in 4.4, unchanged...
lastDamageTime = Time.time;
state = EnemyState.Attack; // physics is what enters ATTACK here
}
}- Which of the three representations did you choose, what type holds the state, and where does that field live — class body or method — with what first value?
- Where does each transition live, and what condition is written on the arrow back to PATROL? When the machine returns to PATROL, does the enemy resume its old beat or start a fresh one, and what does the player see either way?
- How do you print the current state's name so you can read it in the Console, and which animator parameter from 4.2 can now be driven from the state directly rather than guessed from the movement?
Checkpoint — before moving on, you must be able to:
- Break "the enemy should switch between patrolling, chasing and attacking" into its parts before you write any code, and name the earlier lesson you lifted each one from
- Draw the enemy's state machine from memory, with a condition written on every arrow
- Compare the flag-based and enum-based representations, and defend the one you chose
- Say where the return-to-patrol transition lives in your code, and what condition it tests
- Label sequencing, selection and iteration in real code
- Explain why your drawn ATTACK state and your coded attack are not quite the same thing
- Print the current state's name without a UI
- Commit and push
Reference Docs
Need a deeper explanation? These pages cover the same concepts:
- Enemy State Machines — the full reference, with the four-part upgrade list for your own file
- Unit 4: The Enemy — every branch of the enemy annotated
- Unity + C# Quick Reference — enums, switch statements and the field-scope rule
Next: 5.1 — The Axe: Item Pickups & Inventory Bools, where Unit 5 begins and your stats finally have a reason to leave the player.
Saved on this device only — the Dashboard tracks your progress.