Enemies that chase and hit back
Assets/
Scenes/
Arena.unity
Scripts/
Prefabs/
Project settings
you already havethis lesson addsthis lesson retires
The Dummy from the last lesson takes damage and dies, and that is all it does. It never moves, never hits back, and never makes you lose. Right now you cannot lose — so there is no game yet.
This lesson makes it stand up. Same capsule, same CharacterStats, three new behaviours: notice you, walk to you, hit you.
Distance decides everything: far away it patrols, inside the ring it chases, within arm's reach it swings.
Step 1 — three states, one number
You do not need a pathfinding system for an arena this size. One distance check picks the state, and the state picks the behaviour. That is the whole brain.
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class EnemyBrain : MonoBehaviour
{
public Transform target; // the Knight
public float aggroRange = 9f;
public float attackRange = 1.8f; // stop and swing
public float speed = 2.6f;
public float damage = 12f;
public float attackEvery = 1.2f;
CharacterController cc;
float nextAttack;
void Awake() => cc = GetComponent<CharacterController>();
void Update()
{
if (target == null) return;
// Flatten Y: a target standing on a block is still "close" horizontally.
Vector3 flat = target.position - transform.position;
flat.y = 0f;
float distance = flat.magnitude;
if (distance > aggroRange) return; // Patrol: do nothing yet
transform.rotation = Quaternion.LookRotation(flat);
if (distance > attackRange) // Chase
{
cc.Move(flat.normalized * speed * Time.deltaTime);
}
else if (Time.time >= nextAttack) // Attack
{
nextAttack = Time.time + attackEvery;
target.GetComponent<CharacterStats>().TakeDamage(damage);
}
cc.Move(Vector3.down * 9.8f * Time.deltaTime); // stay on the floor
}
}flat.y = 0f is the line that matters. Without it, a Knight standing on a block reads as far away and the enemy loses interest the moment you jump.
nextAttack is the same cooldown pattern as the player's skills — the enemy plays by your rules.
CharacterStats is the script you already wrote. The enemy calls TakeDamage on you; your skills call it on the enemy. One script, both directions.
Step 2 — wire it in the scene
- 1
Select the Dummy. It already has CharacterStats and the Enemy layer from the last lesson — keep both.
- 2
Add a Character Controller, then add EnemyBrain. Drag the Knight into the Target field.
- 3
Rename it Enemy and drag it into Assets/Prefabs. From here on you place enemies, not dummies.
- 4
Drop three of them around the block grid, at different distances. Press Play and walk towards one.
Step 3 — make the enemy killable
Your skills already deal damage to anything on the Enemy layer, so this half is nearly free. The only new part is what happens at zero — and CharacterStats already announces it: you added OnDied along with the health bar. The enemy just has to listen and remove itself.
CharacterStats stats;
void Awake()
{
cc = GetComponent<CharacterController>();
stats = GetComponent<CharacterStats>();
// The Knight has CharacterStats too, so that script must never destroy
// anything itself. The enemy listens for its own death instead.
stats.OnDied += () => Destroy(gameObject);
}Step 4 — a block should stop the attack
Right now the arena has blocks you can jump on, and nothing in the fight knows they exist. flat.y = 0f threw the height away, so an enemy on the floor swings at a Knight standing a metre above it, through solid geometry.
Two questions fix it, and they are separate on purpose: can it reach that high, and is there anything in the way. A wall blocks the second one even when you are both on the floor.
- 1
The Obstacle layer already exists: you made it when you stopped skills firing through blocks, and P_Cube is already on it. The enemies read the same layer.
- 2
On the Enemy prefab, right-click the name → Create Empty, name it Eye, set its Position to 0, 1.4, 0. That is where the enemy looks from.
- 3
Add the code below to EnemyBrain, then in the Inspector drag Eye into the Eye field and tick only Obstacle in the Obstacles mask.
[Header("Cover & height")]
public LayerMask obstacles; // the Obstacle layer: blocks, walls
public float reachHeight = 1.2f; // how far above itself it can still swing
public Transform eye; // an empty at head height
// A straight line from its eye to your chest. Anything on the Obstacle
// layer in the way means it has no shot.
bool CanSee()
{
Vector3 chest = target.position + Vector3.up;
return !Physics.Linecast(eye.position, chest, obstacles);
}
// You standing on a block are out of swinging range, even if the
// flattened distance says you are right next to it.
bool CanReach()
{
return target.position.y - transform.position.y <= reachHeight;
}Then the attack line asks both questions before it swings. Nothing else in Update changes.
else if (Time.time >= nextAttack && CanReach() && CanSee())
{
nextAttack = Time.time + attackEvery;
target.GetComponent<CharacterStats>().TakeDamage(damage);
}Step 5 — a brain that works out the route
The chase is still a straight line. Put a block between the enemy and the Knight and it walks into the side of it and stays there, pressing forward against a wall it will never get through. It does not go round because nothing told it the way round exists.
Unity can work the route out for it. Bake the walkable floor once and a NavMeshAgent follows that map: round the block, through the gap, to you.
- 1
Open . If the tab is not there, install the AI Navigation package from .
- 2
Select Floor and the Blocks parent, and tick Navigation Static in the top-right of the Inspector. That is what says 'this is part of the map'.
- 3
In the Navigation window, Bake tab → Bake. A blue skin appears over the floor and stops at the blocks — that blue is everywhere an enemy may walk.
- 4
On the Enemy prefab, remove the Character Controller and add a Nav Mesh Agent. The agent moves the enemy now, so the old cc.Move lines go with it.
- 5
Set the agent's Radius to about 0.4 and Height to 2 so it fits between two blocks instead of refusing the gap.
using UnityEngine;
using UnityEngine.AI; // NavMeshAgent lives here
[RequireComponent(typeof(NavMeshAgent))]
public class EnemyBrain : MonoBehaviour
{
public Transform target;
public float aggroRange = 9f;
public float attackRange = 1.8f;
public float speed = 2.6f;
NavMeshAgent agent;
void Awake()
{
agent = GetComponent<NavMeshAgent>();
agent.speed = speed;
agent.stoppingDistance = attackRange * 0.9f; // stop before it hugs you
}
void Update()
{
if (target == null) return;
Vector3 flat = target.position - transform.position;
flat.y = 0f;
float distance = flat.magnitude;
if (distance > aggroRange)
{
agent.ResetPath(); // lost interest
return;
}
if (distance > attackRange)
agent.SetDestination(target.position); // it works out the route
else
agent.ResetPath(); // close enough: stand and swing
}
}SetDestination is the whole request: you give it a point, it finds the route. You never write pathfinding yourself.
stoppingDistance keeps the agent from walking into you before it swings, and it replaces the distance check that used to stop the chase.
Gravity is gone from the script because the agent keeps itself on the baked surface. An agent cannot fall off the map, which is also why it will not follow you onto a block.
Keep CanSee and CanReach exactly as they are. Pathfinding decides how it gets to you; those two still decide whether it may hit you.
✖ Chasing with transform.position +=
✓ Chasing with CharacterController.Move