U
tab 02Character & Combat9 lessons — tap to open
08 / 09 · 13 min
Character & Combat

Enemies that chase and hit back

your project so far

Assets/

Scenes/

Arena.unity

KnightDummy ×3→Enemy ×3 (chases and hits back)

Scripts/

SkillCaster.csHealthBar.csEnemyBrain.cs

Prefabs/

Enemy.prefab

Project settings

Enemy layerObstacle layerNavMesh bake

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.

animated diagram

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.

EnemyBrain.cs
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. 1

    Select the Dummy. It already has CharacterStats and the Enemy layer from the last lesson — keep both.

  2. 2

    Add a Character Controller, then add EnemyBrain. Drag the Knight into the Target field.

  3. 3

    Rename it Enemy and drag it into Assets/Prefabs. From here on you place enemies, not dummies.

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

EnemyBrain.cs — replace Awake with this
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. 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. 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. 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.

add to EnemyBrain.cs — what stops an attack
[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.

the attack line, now with two conditions
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. 1

    Open Window▸AI▸Navigation. If the tab is not there, install the AI Navigation package from Window▸Package Manager.

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

EnemyBrain.cs — chasing with a NavMeshAgent
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 +=

The enemy clips into the very blocks you jump on, then stands inside one.

✓ Chasing with CharacterController.Move

It is stopped by the block instead of passing through it — and once it is a NavMeshAgent it walks around, which is what makes jumping worth doing.