Finite State Machines in Unity for AI and Animation (Practical Patterns)

Finite state machines are one of the simplest ways to make game characters feel intentional. An enemy does not need to “decide everything” every frame. It needs to be in a behavior state, react to the world, and transition at the right moments.

In Unity, the trick is not the theory. The trick is the integration. You want your AI state logic to switch cleanly, to avoid animation glitches, and to stay debuggable when the state count inevitably grows.

This guide focuses on practical FSM patterns for Unity AI and animation. You will see how to structure your code, how to handle transitions safely, how to coordinate with the Animator, and how to debug behavior without guesswork.


The mental model that prevents most FSM bugs

Start with two rules that keep your state machine stable.

  1. A state owns behavior while it is active
  2. Transitions are deliberate events, not random checks spread across the project

If you follow those rules, you can treat “what the agent is doing right now” as a single source of truth. Your AI update loop becomes predictable. Your animation layer stops fighting with AI logic. Your future self stops asking “why is it doing that” at 2 AM.

At a practical level, your code should have four responsibilities:

  • Enter runs once when you enter a state
  • Tick runs every frame while the state is active
  • Exit runs once when you leave a state
  • Transition logic decides when to move, with guard conditions that prevent thrashing

FSM for AI vs FSM for animation

Unity already has an animation state machine built into the Animator. That does not mean you should discard FSMs for AI. It means you should coordinate them.

In most projects you will end up with two related state systems:

  • AI FSM: decides behavior, like Idle, Patrol, Chase, Attack, Flee
  • Animation FSM: decides motion and presentation, like locomotion blend trees, attack clips, turn-in-place

If you let both systems pick states independently, you will get issues like:

  • AI enters Attack but animation is still in a locomotion transition
  • AI rapidly toggles between Chase and Attack because a guard condition flickers
  • An animation event fires after you have already changed AI state

The practical pattern is to treat the AI FSM as the authority for behavior and use animation as a renderer. When the AI state changes, you “handoff” to the appropriate animation parameters.


Practical FSM patterns for Unity AI and animation

Pattern 1: Keep states as plain C# objects

It is tempting to make each state a MonoBehaviour. Do it only if you truly need Unity lifecycle hooks per state. Otherwise, prefer plain C# classes.

Benefits:

  • States are easy to unit test
  • You avoid a giant scene full of components
  • You can share data and services without fighting Unity serialization

In this pattern, a StateMachine lives on one agent component. The states are lightweight.

Pattern 2: Use guard conditions with hysteresis to prevent transition thrashing

Many FSM bugs are not logic bugs. They are guard bugs.

Example:

  • Condition to switch to Chase: target distance < 10
  • Condition to switch back to Patrol: target distance > 11

That one extra unit of distance is hysteresis. It prevents the agent from flipping every frame around the threshold. This matters even more when physics and navmesh updates have latency.

You can apply hysteresis to:

  • distance thresholds
  • line of sight checks
  • health percentages
  • “heard sound recently” timers

Pattern 3: Make transitions event driven when the situation is discrete

Some transitions should not be checked every frame as boolean expressions.

Examples:

  • “Reached destination”
  • “Animation finished attack windup”
  • “Took damage”

For those transitions, prefer event-driven triggers. Your state can subscribe to events in Enter, unsubscribe in Exit, and transition only when the event fires.

This reduces CPU waste and makes behavior easier to reason about.

Pattern 4: Use a shared blackboard for cross-state memory

FSMs often need memory that persists across states. A common mistake is duplicating that memory per state.

Instead, keep a shared blackboard on the agent:

  • target reference
  • last known position
  • current nav destination
  • timers like “time since last seen”
  • cached sensory results like “can see player”

States read and write the blackboard. Transitions depend on it. Debugging becomes far easier because you can inspect one structure.

If you want to go further, you can store portions of the blackboard in ScriptableObjects for data driven tuning, which pairs nicely with broader data layer decisions like the ones discussed in our post on Unity ScriptableObjects vs JSON vs SQLite in Unity.

Pattern 5: Define a single animation handoff method per AI state

When the AI enters a state, immediately set the animation parameters needed for that state.

For example:

  • Idle state sets Speed = 0 and clears IsAttacking
  • Chase state sets Speed = currentNavSpeed and clears IsAttacking
  • Attack state sets a trigger DoAttack and disables root motion if you do not want animation driving movement

Make this animation handoff happen in Enter, not in Tick. That eliminates “animation lags behind one frame” problems.


A minimal Unity FSM implementation you can extend

Below is a simple, practical implementation. It focuses on clarity and debuggability rather than cleverness.

using System;
using UnityEngine;

public interface IState
{
    void Enter();
    void Tick(float deltaTime);
    void Exit();
}

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

    public void Initialize(IState initialState)
    {
        CurrentState = initialState;
        CurrentState.Enter();
    }

    public void ChangeState(IState nextState)
    {
        if (nextState == null || nextState == CurrentState) return;

        CurrentState.Exit();
        CurrentState = nextState;
        CurrentState.Enter();
    }

    public void Tick(float deltaTime)
    {
        CurrentState?.Tick(deltaTime);
    }
}

Now an agent component that owns the machine and shared blackboard:

using UnityEngine;

public class EnemyAI : MonoBehaviour
{
    [SerializeField] private Animator animator;
    [SerializeField] private float chaseDistance = 10f;
    [SerializeField] private float returnDistance = 12f;

    private readonly StateMachine fsm = new StateMachine();
    private readonly Blackboard bb = new Blackboard();

    private void Awake()
    {
        bb.animator = animator;
        fsm.Initialize(new IdleState(this, bb));
    }

    private void Update()
    {
        Sense();
        fsm.Tick(Time.deltaTime);
    }

    private void Sense()
    {
        // Example only. Replace with your own perception system.
        bb.canSeeTarget = bb.target != null && bb.distanceToTarget <= chaseDistance;
        bb.distanceToTarget = bb.targetDistance;
    }

    public void SetState(IState next) => fsm.ChangeState(next);
}

public class Blackboard
{
    public Animator animator;
    public Transform target;
    public float distanceToTarget;
    public bool canSeeTarget;

    public Vector3 lastKnownTargetPosition;
}

The key extension points are:

  • Blackboard carries memory
  • Each state can read guard conditions and decide transitions
  • Enter does the animation handoff

Example states with animation handoff and safe transitions

Imagine four AI behaviors:

  • Idle: do nothing, patrol lightly later
  • Patrol: move between points until interrupted
  • Chase: move toward last known target
  • Attack: play windup and strike, then decide what comes next

Idle state:

public class IdleState : IState
{
    private readonly EnemyAI agent;
    private readonly Blackboard bb;

    private const string HasTarget = "HasTarget";

    public IdleState(EnemyAI agent, Blackboard bb)
    {
        this.agent = agent;
        this.bb = bb;
    }

    public void Enter()
    {
        // Animation handoff happens here, once.
        bb.animator.SetFloat("Speed", 0f);
        bb.animator.SetBool("IsAttacking", false);
        bb.animator.SetBool(HasTarget, false);
    }

    public void Tick(float deltaTime)
    {
        // Guard with hysteresis by using returnDistance in other states.
        if (bb.canSeeTarget)
        {
            bb.lastKnownTargetPosition = bb.target.position;
            agent.SetState(new ChaseState(agent, bb));
        }
    }

    public void Exit()
    {
        // Clear optional parameters if you want a clean slate.
        bb.animator.SetBool(HasTarget, true);
    }
}

Chase state:

public class ChaseState : IState
{
    private readonly EnemyAI agent;
    private readonly Blackboard bb;

    public ChaseState(EnemyAI agent, Blackboard bb)
    {
        this.agent = agent;
        this.bb = bb;
    }

    public void Enter()
    {
        // Set animation parameters once on entry.
        bb.animator.SetBool("IsAttacking", false);
        bb.animator.SetFloat("Speed", 1f);
    }

    public void Tick(float deltaTime)
    {
        // Example transition guards.
        if (!bb.canSeeTarget)
        {
            // You can add additional logic like time since last seen.
            agent.SetState(new PatrolState(agent, bb));
            return;
        }

        // If you are close enough, switch to attack.
        if (bb.distanceToTarget <= 2.2f)
        {
            bb.animator.SetTrigger("DoAttack");
            agent.SetState(new AttackState(agent, bb));
        }
    }

    public void Exit()
    {
        bb.animator.ResetTrigger("DoAttack");
    }
}

Attack state:

public class AttackState : IState
{
    private readonly EnemyAI agent;
    private readonly Blackboard bb;

    private float attackTimer;
    private const float AttackDuration = 1.1f;

    public AttackState(EnemyAI agent, Blackboard bb)
    {
        this.agent = agent;
        this.bb = bb;
    }

    public void Enter()
    {
        bb.animator.SetBool("IsAttacking", true);
        attackTimer = 0f;

        // You can also enable hitboxes here.
    }

    public void Tick(float deltaTime)
    {
        attackTimer += deltaTime;

        // If attack completes, transition based on the world.
        if (attackTimer >= AttackDuration)
        {
            bb.animator.SetBool("IsAttacking", false);

            if (bb.canSeeTarget && bb.distanceToTarget <= 2.5f)
                agent.SetState(new AttackState(agent, bb)); // Combo example
            else
                agent.SetState(new ChaseState(agent, bb));
        }
    }

    public void Exit()
    {
        // Disable hitboxes here if you enabled them in Enter.
    }
}

This is not a complete enemy navigation system. It is a complete example of what you should care about:

  • transitions happen in predictable places
  • animation handoff is tied to Enter
  • guards prevent thrashing
  • the blackboard carries cross-state information

Debugging an FSM without losing your mind

When your agent misbehaves, do not add random logs inside every state. Add an intentional debug layer.

Here are practical debugging techniques that work well in Unity:

  • Track the current state name in one place and show it with OnGUI or a debug panel
  • Log transitions only when state changes, not every frame
  • When a transition fails, print the guard values that blocked it
  • Visualize guard thresholds like chase distance and attack distance in the Scene view with gizmos

A very useful step is to include a “transition reason” string.

Example:

  • Idle -> Chase because canSeeTarget is true
  • Chase -> Patrol because canSeeTarget is false for more than a timer
  • Attack -> Chase because cooldown ended and distance is still within range

Once you have that, QA bug reports become actionable.


Performance notes when your state count grows

FSMs scale surprisingly well. Still, watch for these pitfalls:

  • Avoid allocations inside Tick (for example, creating new state objects every frame)
  • Prefer reusing state instances or using a small state cache per agent
  • Keep sensory computations separated from transition decisions
  • Use event-driven transitions for discrete events like “reached waypoint”

If you allocate new state objects on every transition, you may create garbage spikes. A simple improvement is to create your states once in Awake and switch between cached instances.


Common mistakes to avoid

  1. Putting transition logic inside animation callbacks only
  2. Setting animation parameters in Tick instead of Enter
  3. Using a single threshold without hysteresis
  4. Duplicating shared memory across states instead of using a blackboard
  5. Letting both AI and animation systems fight for authority

Most of these mistakes produce symptoms that look like “random AI behavior”, but the root cause is usually coordination and guard logic.


FAQ

Should each FSM state be its own script?

Not required. You can keep state classes as separate files for readability, but the logic should remain lightweight. If you make states MonoBehaviour components for convenience, you often add complexity you do not need.

How do I prevent the AI from switching states every frame?

Use guard conditions with hysteresis and time windows. For example, switch from Chase to Patrol only when the target has been out of sight for a short duration, not the instant it disappears.

How do I coordinate AI FSM states with Unity Animator?

Use the AI FSM as the authority for behavior. When the AI enters a state, set the Animator parameters needed for that state. Avoid driving AI decisions from animation state alone unless you truly want animation events to be the trigger.

What is a good way to debug transitions?

Log only when the state changes. Also include the guard values or transition reason that triggered the change. A single debug view showing current state and key blackboard fields is usually better than spammy logs.

Do FSMs replace behavior trees?

FSMs and behavior trees solve overlapping problems. FSMs are often faster to implement and easier to debug for small to medium complexity. Behavior trees can be better when decision logic becomes deeply hierarchical. Many games use both patterns in different layers.


Conclusion

A well-structured finite state machine in Unity is less about fancy architecture and more about coordination. When your AI state logic has clear Enter, Tick, and Exit responsibilities, when transitions are guarded against thrashing, and when animation handoff is tied to AI state changes, your characters behave like they have intent.

Use the practical patterns in this guide, then iterate based on what you see in debug views and transition reasons. Your goal is not to build the biggest FSM. Your goal is to build one you can trust.

If you want to keep your tuning data clean and iteration fast, pair your FSM with a data driven approach like ScriptableObjects and the broader data layer choices discussed in Unity ScriptableObjects vs JSON vs SQLite in Unity.

Found this useful? Bookmark it for your next AI refactor and share it with your team so everyone can debug state behavior faster.