4.2 — Enemy Animation: A Different State Machine
You wired the Player's animator in 1.6, so you know the shape: a parameter goes in, a state comes out. The enemy's animator is that same API with none of your conventions — its rules were decided by somebody else, and reading them before you write a line is the lesson.
The enemy slides around as a single frozen frame. The art exists, the controller exists, and nothing drives either of them — so "make the enemy animate" is not one problem, it is the job of finding out what somebody else's state machine wants.
First, before any code: cut that into parts small enough that you could look each one up on its own.
- Find the Animator somebody else attachedwhat does this part do?Read: the
GetComponentInChildren<T>()row of this lesson's reference table, and the same line you wrote for the Player in 1.6 - Read the contract: the parameter names, their types, and every value they takewhat does this part do?Read: the reference table below, and the Animator window you learned to read in 1.6
- Turn "which way is it moving" into one of those four integerswhat does this part do?Read:
Mathf.Abs(float f)from 1.6 and theSetInteger(string name, int value)row of this lesson's reference table - Decide which lifecycle method writes the valuewhat does this part do?Read: the
Update()andFixedUpdate()row of this lesson's reference table, and the lifecycle split from 1.3 - Fire the attack, and know what will consume itwhat does this part do?Read: the
SetTrigger(string name)row of this lesson's reference table, and the mouse-click trigger you wrote in 1.6
Notice the shape: find the machine, read its contract, choose a value, decide when to write it. Three of those four are things you already did in 1.6 for the Player. The new one is that the contract is not yours — in 1.6 you invented the numbers 0, 1 and 2 for WalkDir, and here you inherit four of them from a controller somebody else built. A wrong one is not an error message; it is an enemy that walks the wrong way.
The test for a finished split: does the part name something you could point at? A component, a window, a parameter, a method. "Make the enemy animate" points at nothing; "which object carries the Animator" points at one object on one prefab.
Your turn
Pick two of the parts above and say in one line what each one actually does.
The reference table below names four setters. This controller uses two of them. Say which two, and what each of the other two would cost you if you reached for it out of habit.
This controller has no state the enemy can stand still in. Before you write anything: what do you want the player to see when the enemy stops walking, and is that something your code can produce at all?
-
Project window →
Assets/Prefabs/MobileEnemy, then Inspector → the child named Sprite. The Animator and the Sprite Renderer both live there, not on the root — the same arrangement your Player has. -
Window → Animation → Animator with the prefab selected. The
Parameters list is the entire contract:
Direction(Int) andAttack(Trigger). Two entries, and no Bool at all. -
Animator window → click each transition arrow and read its
Conditions, then count the states and look for one the enemy can stand still
in. Which value of
Directiondoes each arrow test, and why can the enemy never stop walking? The four integers exist nowhere else, and a line copied from your player's animator compiles happily while aiming the enemy the wrong way. Project window → the same folder holds fourIdleclips that the controller never references. -
Project window →
Assets/Scripts/Package Scripts/TopDown_EnemyAnimator.cs. Read it: short, commented, complete, attached to nothing. Somebody else's solution to today's problem — you will either attach it or write your own. -
Game view → press Play and watch a full beat with that window open. The
Directionvalue never changes, and neither does the sprite. Nothing is broken; nothing has been asked for yet.
Socratic: Reading Someone Else's Machine
- Your player's
WalkDirand the enemy'sDirectionare both Ints set bySetInteger(string name, int value). Before copying any animator line from one script to the other, what exactly must you check — and where do you check it? - The enemy's controller has no idle states. If your code simply stops calling
SetInteger(string name, int value)when the enemy stops walking, what does the player see on screen — and is that a bug in your code or a gap in the asset? - Your player mirrors one clip with
flipX; the enemy uses two separate clips and never flips. What does the enemy's approach give you that yours cannot — and what does it cost?
| API | What you get | What it costs |
|---|---|---|
void SetInteger(string name, int value) |
The setter you used in 1.6 for the player's WalkDir. Here the parameter is Direction, with four values instead of three, and whatever you set stays set until you set it again. |
The name is a string, not a keyword: the controller must already have a parameter spelled exactly that way, and a near miss is a Console warning and a frozen sprite rather than a crash. The value is the harder half — 2 is not a direction, it is a number some transition compares against, so a wrong value that is still a valid number moves the enemy into the wrong clip in complete silence. |
void SetBool(string name, bool value) and void SetFloat(string name, float value) |
The two other setters from 1.6's table: a Bool that stays set until you clear it, and a Float for smooth values like a walk-to-run blend. | Neither fits this controller. It declares no Bool at all, so any name you invent there is a warning and nothing else; and Direction is declared as an Int, so a setter whose type does not match the parameter is a warning and no change — the value never arrives, and nothing on screen tells you so. |
void SetTrigger(string name) |
The one-shot you fired from 1.6's mouse click. No value is passed, because being fired is the whole message, and the animator clears the trigger once a transition has consumed it. | It is an event, so it may not be fired every frame — that is 1.6's Input.GetMouseButton(int button) mistake in a new costume, where holding the button restarted the swing sixty times a second. And a trigger that no transition is listening for is cleared without doing anything at all, which looks exactly like code that never ran. |
T GetComponentInChildren<T>() or T GetComponent<T>() |
The lookup from 1.6, in both flavours: the first searches the object this script sits on and everything beneath it; the second looks only at the object it is attached to. | On the enemy root the plain version finds nothing and hands you null — 1.6's most-repeated bug, and nastier here: the null is created in Start() and used in the frame loop, so the error points at the line that used it rather than the line that failed. The downward search takes the first match it meets and is slower, which is why it belongs in Start(), once, and not in the method that runs every frame. |
float Mathf.Abs(float f) |
The size of a number with the sign thrown away — the comparison you used in 1.6 to work out which axis was being pushed harder, so that walking hard left would not lose to a gentle nudge upward. | It throws the sign away, which is exactly what you want in order to choose a direction and exactly what you must not use in order to name one. The losing axis has to be kept raw: on the horizontal axis the sign is the entire difference between the value that means Right and the value that means Left. |
void Update() and void FixedUpdate() |
Update() is the frame loop you have used since 1.4, once per rendered frame. FixedUpdate() runs once per physics step, in step with movement written onto a Rigidbody2D. |
FixedUpdate() can run twice in one frame or not at all, so a parameter written there can be a rendered frame stale — visible as a half-step of lag on the frame the direction changes. Match the loop to whatever is actually moving the enemy, and the answer is not the same for every enemy you will write. |
- Which quantity tells you which of the four values to write — the movement the enemy was asked for, or the movement that actually happened? On a diagonal step, how do you choose between two axes moving at once with
Mathf.Abs(float f)rather than their signed values, and which axis is the one whose sign you must keep? - Which loop writes the value —
Update()orFixedUpdate()— and what is moving your enemy? Read the package script to see which it chose, then say whether you would have made the same choice. - Nothing in the enemy attacks yet, so which parameter is waiting for it, which call fires it, and how often may you set it? Then argue the fork: should the animation decide when damage happens, or damage decide when the animation plays?
Save, play, and walk a full beat: the enemy faces the way it walks in all four directions — and stops animating only if you worked out how to make it stop.
Checkpoint — before moving on, you must be able to:
- Name the enemy's two animator parameters and their types, by reading the controller rather than your memory
- Recite the four
Directionvalues — and say how they differ from your player's - Explain why the enemy's animator needs
GetComponentInChildren<T>()and notGetComponent<T>() - Say which of the four setters this controller's two parameters call for, and what the other two would cost
- Break this feature into parts small enough to look each one up on its own, naming an earlier lesson for each
- Commit and push
Reference Docs
Need a deeper explanation? These pages cover the same concepts:
- How do I…? index — find this lesson again by the question you were asking
- Unit 4: The Enemy — animation and the state machine
- Enemy State Machines — how an Animator controller relates to a code state machine
- Unity + C# Quick Reference — Animator parameters and
GetComponentInChildren<T>()
Next: 4.3 — Chase: The Aggro Radius, where the enemy stops walking its beat and starts walking yours.
Saved on this device only — the Dashboard tracks your progress.