# Tools Unity State Machine

> State machine patterns for Unity including hierarchical states, async transitions, and cleanup safety.

- **Type:** Skill
- **Install:** `agentstack add skill-tjboudreaux-cc-plugin-unity-gamedev-tools-unity-state-machine`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [tjboudreaux](https://agentstack.voostack.com/s/tjboudreaux)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [tjboudreaux](https://github.com/tjboudreaux)
- **Source:** https://github.com/tjboudreaux/cc-plugin-unity-gamedev/tree/main/skills/tools-unity-state-machine

## Install

```sh
agentstack add skill-tjboudreaux-cc-plugin-unity-gamedev-tools-unity-state-machine
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Unity State Machine Patterns

## Overview

State machines manage game flow, UI navigation, and entity behavior. This skill covers safe implementation patterns for async state transitions and proper cleanup.

## When to Use

- Game state management (menu, loading, gameplay)
- UI navigation flows
- AI behavior states
- Animation state control
- Feature/activity controllers

## Basic State Machine

### State Interface

```csharp
public interface IState
{
    UniTask Enter(IStateTransitionData data);
    UniTask Exit(bool applicationExiting);
    UniTask Suspend(IStateTransitionData nextStateData);
    UniTask Resume();
    IStateTransitionData GetTransitionData();
}

public interface IStateTransitionData
{
    Type Installer { get; }
}
```

### Simple State Implementation

```csharp
public abstract class BaseState : IState
{
    protected bool IsActive { get; private set; }
    protected bool IsSuspended { get; private set; }
    
    public virtual async UniTask Enter(IStateTransitionData data)
    {
        IsActive = true;
        IsSuspended = false;
        await OnEnter(data);
    }
    
    public virtual async UniTask Exit(bool applicationExiting)
    {
        if (!IsActive) return;
        
        IsActive = false;
        await OnExit(applicationExiting);
    }
    
    public virtual async UniTask Suspend(IStateTransitionData nextStateData)
    {
        IsSuspended = true;
        await OnSuspend();
    }
    
    public virtual async UniTask Resume()
    {
        IsSuspended = false;
        await OnResume();
    }
    
    protected abstract UniTask OnEnter(IStateTransitionData data);
    protected abstract UniTask OnExit(bool applicationExiting);
    protected virtual UniTask OnSuspend() => UniTask.CompletedTask;
    protected virtual UniTask OnResume() => UniTask.CompletedTask;
    
    public abstract IStateTransitionData GetTransitionData();
}
```

## State Navigation Controller

### Core Navigation Controller

```csharp
public class StateNavigationController : IStateNavigator
{
    private readonly LifetimeScope _rootScope;
    private readonly List _stateStack = new();
    private bool _navigationInProgress;
    
    public StateNavigationController(LifetimeScope rootScope)
    {
        _rootScope = rootScope;
    }
    
    public async UniTask NavigateTo(IStateTransitionData transitionData)
    {
        // Wait for any ongoing navigation
        if (_navigationInProgress)
        {
            var waited = await UniTask.WaitUntil(() => !_navigationInProgress)
                .TimeoutWithoutException(TimeSpan.FromSeconds(5));
            
            if (!waited)
            {
                Debug.LogError("Navigation timeout - forcing through");
                _navigationInProgress = false;
            }
        }
        
        _navigationInProgress = true;
        
        try
        {
            // Exit all current states
            await ExitAllStates();
            
            // Enter new state
            await EnterState(transitionData);
        }
        finally
        {
            _navigationInProgress = false;
        }
    }
    
    public async UniTask NavigateToAdditive(IStateTransitionData transitionData)
    {
        if (_navigationInProgress)
        {
            await UniTask.WaitUntil(() => !_navigationInProgress);
        }
        
        _navigationInProgress = true;
        
        try
        {
            // Suspend current state
            if (_stateStack.Count > 0)
            {
                var currentScope = _stateStack[^1];
                if (TryResolveState(currentScope, out var currentState))
                {
                    await currentState.Suspend(transitionData);
                }
            }
            
            // Enter new state additively
            await EnterState(transitionData);
        }
        finally
        {
            _navigationInProgress = false;
        }
    }
    
    public async UniTask ExitActiveState()
    {
        if (_navigationInProgress || _stateStack.Count == 0)
            return;
        
        _navigationInProgress = true;
        
        try
        {
            // Pop and exit top state
            var exitScope = PopState();
            
            if (TryResolveState(exitScope, out var exitState))
            {
                await exitState.Exit(false);
            }
            
            // CRITICAL: Wait before disposal
            await UniTask.Yield();
            
            exitScope.Dispose();
            
            // Resume underlying state
            if (_stateStack.Count > 0)
            {
                var revealedScope = _stateStack[^1];
                if (TryResolveState(revealedScope, out var revealedState))
                {
                    await revealedState.Resume();
                }
            }
        }
        finally
        {
            _navigationInProgress = false;
        }
    }
    
    private async UniTask EnterState(IStateTransitionData transitionData)
    {
        var installer = (IInstaller)Activator.CreateInstance(transitionData.Installer);
        var stateScope = _rootScope.CreateChild(installer);
        
        _stateStack.Add(stateScope);
        
        if (TryResolveState(stateScope, out var state))
        {
            await state.Enter(transitionData);
        }
    }
    
    private async UniTask ExitAllStates()
    {
        while (_stateStack.Count > 0)
        {
            var scope = PopState();
            
            if (TryResolveState(scope, out var state))
            {
                await state.Exit(false);
            }
            
            await UniTask.Yield();
            scope.Dispose();
        }
    }
    
    private LifetimeScope PopState()
    {
        var index = _stateStack.Count - 1;
        var scope = _stateStack[index];
        _stateStack.RemoveAt(index);
        return scope;
    }
    
    private bool TryResolveState(LifetimeScope scope, out IState state)
    {
        state = null;
        try
        {
            if (scope?.Container == null) return false;
            state = scope.Container.Resolve();
            return state != null;
        }
        catch
        {
            return false;
        }
    }
    
    public async UniTask Shutdown()
    {
        for (int i = _stateStack.Count - 1; i >= 0; i--)
        {
            var scope = _stateStack[i];
            
            if (TryResolveState(scope, out var state))
            {
                await state.Exit(true);
            }
            
            scope.Dispose();
        }
        
        _stateStack.Clear();
    }
}
```

## Activity State Pattern

### Activity State Base

```csharp
public abstract class ActivityState : IState 
    where TController : IActivityController
{
    private readonly TController _controller;
    private bool _disposed;
    
    protected ActivityState(TController controller)
    {
        _controller = controller;
    }
    
    public async UniTask Enter(IStateTransitionData data)
    {
        if (_disposed) return;
        await _controller.Initialize(data);
    }
    
    public async UniTask Exit(bool applicationExiting)
    {
        if (_disposed) return;
        _disposed = true;
        
        await _controller.Shutdown(applicationExiting);
    }
    
    public async UniTask Suspend(IStateTransitionData nextStateData)
    {
        if (_disposed) return;
        await _controller.Suspend(nextStateData);
    }
    
    public async UniTask Resume()
    {
        if (_disposed) return;
        await _controller.Resume();
    }
    
    public IStateTransitionData GetTransitionData()
    {
        return _controller.GetTransitionData();
    }
}
```

### Activity Controller Base

```csharp
public abstract class ActivityController : IActivityController
    where TData : IStateTransitionData
    where TView : ActivityView
{
    protected TData TransitionData { get; private set; }
    protected TView View { get; private set; }
    protected CancellationTokenSource LifecycleCts { get; private set; }
    
    private bool _isShuttingDown;
    
    public async UniTask Initialize(IStateTransitionData data)
    {
        TransitionData = (TData)data;
        LifecycleCts = new CancellationTokenSource();
        
        await LoadView();
        await SetupView();
        await OnInitialized();
        
        View.SetActive(true);
    }
    
    public async UniTask Shutdown(bool applicationExiting)
    {
        if (_isShuttingDown) return;
        _isShuttingDown = true;
        
        // Cancel pending operations
        LifecycleCts?.Cancel();
        
        if (!applicationExiting)
        {
            await View.PlayExitAnimation();
        }
        
        await OnShutdown(applicationExiting);
        
        ReleaseView();
        
        LifecycleCts?.Dispose();
        LifecycleCts = null;
    }
    
    public virtual async UniTask Suspend(IStateTransitionData nextStateData)
    {
        View.SetInteractable(false);
        await OnSuspended();
    }
    
    public virtual async UniTask Resume()
    {
        View.SetInteractable(true);
        await OnResumed();
    }
    
    protected abstract UniTask OnInitialized();
    protected abstract UniTask OnShutdown(bool applicationExiting);
    protected virtual UniTask OnSuspended() => UniTask.CompletedTask;
    protected virtual UniTask OnResumed() => UniTask.CompletedTask;
    
    public abstract IStateTransitionData GetTransitionData();
}
```

## Hierarchical State Machine

### HSM Implementation

```csharp
public class HierarchicalStateMachine
{
    private readonly Dictionary _states = new();
    private readonly Dictionary _parentStates = new();
    private readonly Stack _activeStateStack = new();
    
    public string CurrentState => _activeStateStack.Count > 0 ? _activeStateStack.Peek() : null;
    
    public void AddState(string id, IState state, string parentId = null)
    {
        _states[id] = state;
        if (parentId != null)
            _parentStates[id] = parentId;
    }
    
    public async UniTask TransitionTo(string targetState)
    {
        // Find common ancestor
        var currentPath = GetStatePath(CurrentState);
        var targetPath = GetStatePath(targetState);
        
        var commonAncestor = FindCommonAncestor(currentPath, targetPath);
        
        // Exit states up to common ancestor
        while (_activeStateStack.Count > 0 && _activeStateStack.Peek() != commonAncestor)
        {
            var stateId = _activeStateStack.Pop();
            if (_states.TryGetValue(stateId, out var state))
            {
                await state.Exit(false);
            }
        }
        
        // Enter states from common ancestor to target
        var enterStates = new List();
        var current = targetState;
        
        while (current != null && current != commonAncestor)
        {
            enterStates.Insert(0, current);
            _parentStates.TryGetValue(current, out current);
        }
        
        foreach (var stateId in enterStates)
        {
            _activeStateStack.Push(stateId);
            if (_states.TryGetValue(stateId, out var state))
            {
                await state.Enter(null);
            }
        }
    }
    
    private List GetStatePath(string stateId)
    {
        var path = new List();
        var current = stateId;
        
        while (current != null)
        {
            path.Insert(0, current);
            _parentStates.TryGetValue(current, out current);
        }
        
        return path;
    }
    
    private string FindCommonAncestor(List path1, List path2)
    {
        string common = null;
        var minLength = Math.Min(path1.Count, path2.Count);
        
        for (int i = 0; i > _guards = new();
    
    public void AddGuard(string fromState, string toState, Func guard)
    {
        _guards[(fromState, toState)] = guard;
    }
    
    public bool CanTransition(string from, string to)
    {
        if (_guards.TryGetValue((from, to), out var guard))
        {
            return guard();
        }
        return true; // No guard = allowed
    }
    
    public async UniTask TryTransitionTo(string targetState)
    {
        if (!CanTransition(CurrentState, targetState))
        {
            Debug.Log($"Transition from {CurrentState} to {targetState} blocked by guard");
            return false;
        }
        
        await TransitionTo(targetState);
        return true;
    }
}
```

### Transition Actions

```csharp
public class StateMachineWithActions
{
    public event Action OnStateTransition;
    public event Action OnStateEntered;
    public event Action OnStateExited;
    
    public async UniTask TransitionTo(string targetState)
    {
        var previousState = CurrentState;
        
        if (_states.TryGetValue(previousState, out var exitingState))
        {
            await exitingState.Exit(false);
            OnStateExited?.Invoke(previousState);
        }
        
        CurrentState = targetState;
        OnStateTransition?.Invoke(previousState, targetState);
        
        if (_states.TryGetValue(targetState, out var enteringState))
        {
            await enteringState.Enter(null);
            OnStateEntered?.Invoke(targetState);
        }
    }
}
```

### State History

```csharp
public class StateMachineWithHistory
{
    private readonly Stack _history = new();
    private const int MaxHistorySize = 20;
    
    public bool CanGoBack => _history.Count > 0;
    
    public async UniTask TransitionTo(string targetState)
    {
        if (CurrentState != null)
        {
            _history.Push(CurrentState);
            
            // Limit history size
            while (_history.Count > MaxHistorySize)
            {
                // Remove oldest entries (need to convert to list)
            }
        }
        
        await DoTransition(targetState);
    }
    
    public async UniTask GoBack()
    {
        if (!CanGoBack) return;
        
        var previousState = _history.Pop();
        await DoTransition(previousState);
    }
    
    public void ClearHistory()
    {
        _history.Clear();
    }
}
```

## Cleanup Patterns

### Safe Async Disposal

```csharp
public class SafeStateController
{
    private CancellationTokenSource _cts;
    private bool _disposed;
    
    public async UniTask Initialize()
    {
        _cts = new CancellationTokenSource();
        await StartBackgroundTasks(_cts.Token);
    }
    
    public async UniTask Shutdown(bool immediate)
    {
        if (_disposed) return;
        _disposed = true;
        
        // Cancel all pending operations
        _cts?.Cancel();
        
        if (!immediate)
        {
            // Give time for graceful cancellation
            try
            {
                await UniTask.Delay(100, cancellationToken: default);
            }
            catch { }
        }
        
        _cts?.Dispose();
        _cts = null;
    }
    
    private async UniTask StartBackgroundTasks(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            await DoWork(ct);
            await UniTask.Delay(1000, cancellationToken: ct);
        }
    }
}
```

### Force Hidden State Pattern

```csharp
public interface IForceHideable
{
    void ForceHiddenSuspend();
    void ReleaseForceHidden();
}

public class ActivityState : IState, IForceHideable
{
    private bool _isForcedHidden;
    
    public void ForceHiddenSuspend()
    {
        if (_isForcedHidden) return;
        _isForcedHidden = true;
        
        View.SetActive(false, immediate: true);
    }
    
    public void ReleaseForceHidden()
    {
        if (!_isForcedHidden) return;
        _isForcedHidden = false;
        
        View.SetActive(true, immediate: true);
    }
}
```

## Best Practices

1. **Guard against concurrent navigation** - Use flags/locks
2. **Wait b

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [tjboudreaux](https://github.com/tjboudreaux)
- **Source:** [tjboudreaux/cc-plugin-unity-gamedev](https://github.com/tjboudreaux/cc-plugin-unity-gamedev)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-tjboudreaux-cc-plugin-unity-gamedev-tools-unity-state-machine
- Seller: https://agentstack.voostack.com/s/tjboudreaux
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
