AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Tools Unity State Machine

skill-tjboudreaux-cc-plugin-unity-gamedev-tools-unity-state-machine · by tjboudreaux

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

No reviews yet
0 installs
10 views
0.0% view→install

Install

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

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-tjboudreaux-cc-plugin-unity-gamedev-tools-unity-state-machine)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
6mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Tools Unity State Machine? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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

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

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

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

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

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

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

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

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

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

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.

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

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.