Tutorial2 min read

Enemy AI you can read at a glance

A finite state machine in plain C#: patrol, chase and search as small classes, with every transition in one place. No behaviour tree required.

My first enemy was one Update method, four hundred lines long, steered by a handful of booleans: isChasing, isSearching, hasLostPlayer. Every new behaviour broke an old one, because any flag could be true at the same time as any other.

A finite state machine removes that whole class of bug. The enemy is in exactly one state at a time, each state is a small class that does one job, and the rules for moving between states sit together where you can read them.

One interface, one class per state

C#
public interface IState
{
    void Enter();
    void Tick();
    void Exit();
}

public class StateMachine
{
    public IState Current { get; private set; }

    public void Set(IState next)
    {
        if (next == Current) return;
        Current?.Exit();
        Current = next;
        Current.Enter();
    }

    public void Tick() => Current?.Tick();
}

Enter and Exit are where the setup and clean-up live: start an animation, change the agent’s speed, stop a sound. They run exactly once per change, which is precisely what the boolean version kept getting wrong.

A state does one job

C#
public class Patrol : IState
{
    readonly Enemy enemy;
    int index;

    public Patrol(Enemy enemy) => this.enemy = enemy;

    public void Enter() => GoTo(index);

    public void Tick()
    {
        var agent = enemy.Agent;
        if (agent.pathPending || agent.remainingDistance > agent.stoppingDistance) return;
        index = (index + 1) % enemy.Waypoints.Length;
        GoTo(index);
    }

    public void Exit() { }

    void GoTo(int i) => enemy.Agent.SetDestination(enemy.Waypoints[i].position);
}

The pathPending check matters: straight after SetDestination, remainingDistance can still read zero while the path is being calculated, and the enemy skips every other waypoint.

Transitions live in one place

C#
public class Enemy : MonoBehaviour
{
    public NavMeshAgent Agent;
    public Transform[] Waypoints;
    public Transform Player;
    [SerializeField] float viewDistance = 12f;
    [SerializeField] float viewAngle = 90f;
    [SerializeField] LayerMask obstacles;

    readonly StateMachine brain = new();
    Patrol patrol;
    Chase chase;
    Search search;

    void Awake()
    {
        patrol = new Patrol(this);
        chase = new Chase(this);
        search = new Search(this);
        brain.Set(patrol);
    }

    void Update()
    {
        if (CanSeePlayer()) brain.Set(chase);
        else if (brain.Current == chase) brain.Set(search);
        else if (brain.Current == search && search.GaveUp) brain.Set(patrol);
        brain.Tick();
    }
}

That Update is the enemy’s whole personality in five lines. When a designer asks “what happens if it loses you?”, the answer is right there.

Seeing the player

C#
bool CanSeePlayer()
{
    Vector3 toPlayer = Player.position - transform.position;
    if (toPlayer.sqrMagnitude > viewDistance * viewDistance) return false;
    if (Vector3.Angle(transform.forward, toPlayer) > viewAngle * 0.5f) return false;

    // Eye to eye, so a knee-high crate doesn't hide the player.
    Vector3 eye = Vector3.up * 1.6f;
    return !Physics.Linecast(transform.position + eye, Player.position + eye, obstacles);
}

Cheapest checks first: the distance check costs a few multiplications, the angle check a little more, and only an enemy that passes both pays for the raycast.

Make the state visible

Draw the current state’s name above each enemy while you work on them:

C#
#if UNITY_EDITOR
void OnDrawGizmos()
{
    if (brain.Current != null)
        UnityEditor.Handles.Label(transform.position + Vector3.up * 2.2f, brain.Current.GetType().Name);
}
#endif
  • C#
  • AI
  • NavMesh

Written by

Logical Programmer

Solo indie developer. I build games in Unity, ship them on Google Play, and document all of it.

Built with this package

AI

88 sales

AI Enemy Controller

NavMesh state-machine AI — patrol, chase, attack, flee. Fully inspector-driven, no code required.

$34.99

Details

Checkout

Online checkout opens soon

Need AI Enemy Controller today? Email me and I’ll sort out payment and the download for you.

AI Enemy Controller$34.99

Keep reading

All articles
  • Tutorial2 min read

    Object pooling in Unity, the built-in way

    Unity has shipped a generic object pool since 2021. How to use UnityEngine.Pool properly: prewarming, resetting state, and the one mistake that corrupts a pool.

    Read

  • Tutorial2 min read

    Save files that survive your next update

    Every update that changes your save format can wipe a player’s progress. A version number, one migration step per version and a safe write stop it from ever happening.

    Read

  • Game design2 min read

    Why your mobile joystick feels wrong

    Three numbers decide whether an on-screen joystick feels sharp or mushy: the dead zone, the travel radius and where the stick’s centre sits. How to tune each one.

    Read