My Take on Action State Management in Game Development

March 20, 2026
Daniel LuFull-Stack Engineer | Content Creator

An exploration of common bugs involving action state overriding and failed recovery. Drawing from my experience with a startup team, I share how to build a high-extensibility, low-coupling action state machine in Unity using an Active State Pool and dynamic priority evaluation.

CategoriesTechnologyUnityWeb Development

The Pain Point: Action State Overlap Bug

I recently joined a startup team that is still in its early stages. While helping them audit their combat system and presentation logic, I encountered a classic and incredibly frustrating bug.

Here is the scenario: A character is hit with a "Freeze" debuff lasting 5 seconds. The animation switches to [Freeze] (the character is frozen in place, shivering). Two seconds into this, the character is struck by a heavy enemy attack that triggers a 2-second [Stun] animation (the character clutches their head in a daze).

The real problem occurs after the stun ends: When the 2-second stun animation finishes, the underlying logic dictates that the character should still have 1 second of the "Freeze" debuff remaining. However, instead of returning to the [Freeze] animation, the character snaps back to the default [Idle] state. The player sees the character breathing normally in an idle stance but finds they cannot move (because the logic is still "frozen"). This creates a massive disconnect between visual feedback and game logic.

Upon digging through the code, I found the cause was simple: the team’s existing system was trigger-based. When a stun occurred, it forced the Animator to play the stun; when the stun ended, it crudely told the Animator to return to the default state, completely ignoring any other ongoing logical effects.

The Generality of the Problem and Limitations of Traditional Solutions

In my opinion, this issue is extremely common in game development. As long as your game involves complex Buff systems or overlapping skill effects, you will inevitably face the challenge of overriding and recovering action states.

Many teams, in an effort to get quick results early on, tend to adopt two approaches that I personally advise against due to their inherent risks:

Risk A: Direct Manipulation of Animator Variables

Different systems (Buffs, skills, hit reactions) directly call animator.SetBool("isFrozen", true). As the project grows, it becomes nearly impossible to track which piece of code set a boolean to false and when, leading to a messy State Machine that is a nightmare to debug.

Risk B: Over-reliance on Unity's Mecanim Transition Web

Unity’s Animator window is intuitive when you have only a few transitions. However, if you bind business logic to these transitions—setting dozens of conditions like Speed > 0, isFrozen == true, isStunned == false—the State Machine eventually turns into a dense "spider web." Adding a single new state requires connecting countless new lines, causing maintenance costs to rise exponentially.

My Approach: Active State Pool and Dynamic Priority Evaluation

To solve the issues of state overriding, failed recovery, and high coupling, I introduced a specific workflow to the team: the Active State Pool combined with Dynamic Priority Evaluation.

The core philosophy is simple: State requests must be completely decoupled from state performance.

The business layer (Buffs or skill systems) is only responsible for Registering and Unregistering the action states they desire. A centralized AnimationStateManager then decides, in any given frame, exactly which animation command should be sent to the Animator.

Core Mechanism Flowchart:

Loading diagram...

Under this architecture, when the priority-100 [Stun] ends and is unregistered, the manager re-evaluates the pool. It finds that the highest remaining priority is the 50-priority [Freeze], so it automatically commands the Animator to switch back to the freeze animation. The bug is solved naturally.

Unity Implementation: Keeping the Animator Pure

Once this architecture is in place, the way we use Unity's Animator changes significantly. We can abandon complex transition lines and Trigger/Bool conditions, allowing the Animator to function as a pure Action Player.

In the Unity Animator Controller, you simply drop in all the required states without connecting them with lines (or connect them all from AnyState with no conditions). In the code, you drive the transitions directly using Animator.CrossFade() or Animator.Play().

Core Manager Pseudo-code Example:

// 1. Define the Action State data structure
public class AnimStateRequest
{
    public string StateName; // State name in the Animator
    public int Priority;     // Priority level
    public object Source;    // The source (used for unregistering, e.g., a specific Buff instance)
}

// 2. The Unified Manager
public class AnimationStateManager : MonoBehaviour
{
    private Animator _animator;
    private List<AnimStateRequest> _activeStates = new List<AnimStateRequest>();
    private string _currentPlayingState;

    // Register a state
    public void AddStateRequest(string stateName, int priority, object source)
    {
        _activeStates.Add(new AnimStateRequest { StateName = stateName, Priority = priority, Source = source });
        EvaluateHighestPriorityState();
    }

    // Unregister a state
    public void RemoveStateRequest(object source)
    {
        _activeStates.RemoveAll(req => req.Source == source);
        EvaluateHighestPriorityState();
    }

    // Dynamically evaluate the current highest priority state
    private void EvaluateHighestPriorityState()
    {
        if (_activeStates.Count == 0)
        {
            PlayAnimation("Idle"); // Default state
            return;
        }

        // Sort to find the request with the highest priority
        _activeStates.Sort((a, b) => b.Priority.CompareTo(a.Priority));
        string targetState = _activeStates[0].StateName;

        if (_currentPlayingState != targetState)
        {
            PlayAnimation(targetState);
        }
    }

    // Drive the Unity Animator
    private void PlayAnimation(string stateName)
    {
        _currentPlayingState = stateName;
        // Use CrossFade for smooth transitions; 0.1f is the transition duration
        _animator.CrossFade(stateName, 0.1f); 
    }
}

Extending the Architecture: My Thoughts on Complex Systems

This Active Pool + Priority Evaluation architecture offers excellent scalability for future complexity. During our development, I’ve considered several extensions:

  1. Handling Priority Ties: If two requests share the same priority (e.g., both are 50), you can expand the evaluation logic to favor the most recently added request or the one with the longest remaining duration.
  2. Avatar Masking: In 3D games, you often need the upper body to fire a weapon while the lower body is hit or running. You can set up two independent managers (e.g., UpperBodyPool and LowerBodyPool) and pass different Layer indices to CrossFade to achieve perfect decoupling.
  3. Embracing the Playable API: For skills with strict timing, Animator might suffer from update latency. In such cases, this state manager can be integrated with Unity’s low-level Playable Graph to mix AnimationClips directly via C#, giving you absolute control over every frame.

These are my thoughts and summaries regarding action state management based on my recent project experience. This solution addressed the specific pain points our team was facing, though it is certainly not a "silver bullet" for every scenario—actual implementation should always be adjusted based on the specific game genre. My perspective is naturally limited, so if you have better approaches or notice any oversights in my logic, I’d love to hear from you. Please feel free to correct me.


This article was originally created by the iknowabit team. Technical support: Exploration of advanced architectural patterns in C# and the Unity engine.