AceLand Unity Packages
  • Home
  • Getting Started
    • Installation
    • Project Settings
    • Architecture Graph
    • Development Level
  • Tutorial
    • Create Your Package
    • Create Project Settings
  • Packages
    • Library
      • Change Log
      • Editor Tools
      • Mono
        • Follow Object
        • Singleton
      • Attributes
        • Conditional Show
        • Inspector Button
        • ReadOnly Field
      • Build Leveling
      • CVS
      • DataTools
      • Disposable Object
      • Extensions
      • Json
      • Kalman Filter
      • Optional
      • Project Setting
      • Serialization Surrogate
      • Utils
    • Event Driven
      • Change Log
      • Event Bus
      • Signal
    • Input
      • Change Log
    • Memento Service
      • Change Log
    • Node Framework
      • Change Log
      • Mono Node
    • Node For Mono (dev)
    • Player Loop Hack
      • Change Log
    • Pool
      • Change Log
    • Security (dev)
    • States
      • Change Log
    • Task Utils
      • Change Log
    • WebRequest
      • Change Log
Powered by GitBook
On this page
  • In One Line
  • Overview
  • Package Info
  • Key Features
  • Core Components
  • How It Works
  • Performance Considerations
  • Usage Example
  • Best Practices
  1. Packages

States

A robust and flexible state machine implementation for Unity

In One Line

States that flow like water, solid as rock.

Overview

The States Package is designed to manage complex state transitions and behaviors in your game objects and systems. It provides a clean, builder-pattern-based API for creating and managing state machines with support for nested states and system integration.

Package Info

display name

AceLand States

package name

latest version

1.0.10

namespace

git repository

dependencies

Key Features

  • Builder pattern for intuitive state machine construction

  • Support for any-state transitions

  • Sub state machine capabilities

  • System integration through Unity's PlayerLoop

  • Automatic resource management and cleanup

  • Asynchronous state machine retrieval

  • Custom state action injection

  • Debug logging support


Core Components

StateMachine

The main class for creating and managing states. Provides:

  • State transitions management

  • Update cycle handling

  • Resource cleanup

  • System integration capabilities

State

Represents individual states within the state machine:

  • Customizable Enter/Update/Exit behaviors

  • Sub State Machine support

  • Action injection capabilities

  • Unique state identification


How It Works

Core Architecture

The States Package operates on a hierarchical system with several key components working together:

  1. State Machine Core

    • Maintains a registry of all states

    • Manages transitions between states

    • Handles the update cycle

    • Controls state lifecycle (Enter → Update → Exit)

  2. State Transitions

    • Uses a double-buffered transition system to prevent race conditions

    • Evaluates transitions in priority order:

      1. Any-state transitions

      2. Specific state transitions

    • Prevents invalid transitions through state validation


Performance Considerations

  1. Update Optimization

    • Minimal garbage collection impact

    • Cached transition evaluations

    • Efficient state lookup through dictionary storage

  2. Memory Efficiency

    • States are shared when possible

    • Action delegates are cached

    • Transition conditions are optimized


Usage Example

Task Utils and Player Loop Hack is already installed with this package. To try this example, please install AceLand Input.

Create State Machine Ids

public enum StateMachineId
{
    Test1 = 900,
    Test2 = 901,
}

Create StateMachine [Test1]

using AceLand.Input.Events;
using AceLand.Input.State;
using AceLand.PlayerLoopHack;
using AceLand.States;
using UnityEngine;

public class StateMachineTest1 : MonoBehaviour, IButtonPressed
{
    // create all states name
    public enum States
    {
        Init, Splash, Title, Running, Quit,
    }
    
    // reference of StateMachine
    private IStateMachine _stateMachine;
    // state of machine
    private States _state;

    private void Awake()
    {
        InitStateMachine();
    }

    private void Start()
    {
        _stateMachine.Start();
    }

    private void OnDestroy()
    {
        _stateMachine.Stop();
        _stateMachine.Dispose();
    }

    private void InitStateMachine()
    {
        // create all states at one.
        IState[] states = State.Builders()
            .WithNames<States>()
            .Build();
        
        // inject actions to each state
        // there are 3 stages in all states [enter, update, exit]
        IState initState = states[0].InjectActions(enter: OnInitEnter);
        IState splashState = states[1].InjectActions(enter: OnSplashEnter);
        IState titleState = states[2].InjectActions(enter: OnTitleEnter);
        IState runningState = states[3].InjectActions(enter: OnRunningEnter);
        IState quitState = states[4].InjectActions(enter: OnQuitEnter);
        
        // create state machine with states and transitions
        _stateMachine = StateMachine.Builder()
            .WithStates(states)    // add all states to machine
            .WithEntryTransition(initState)    // set first state on machine start
            // add transitions with specified state to specified state
            .WithTransition(initState, splashState, () => _state is States.Splash)
            .WithTransition(splashState, titleState, () => _state is States.Title)
            .WithTransition(titleState, runningState, () => _state is States.Running)
            .WithTransition(runningState, titleState, () => _state is States.Title)
            // add transitions with any state to specified state
            .WithAnyTransition(quitState, () => _state is States.Quit)
            .WithId(StateMachineId.Test1)    // optional: set machine id
            .BuildAsSystem(PlayerLoopState.EarlyUpdate);
         // .BuildAsSystem(state) will run automatically when start
    }
    
    // Event from AceLand.Input
    public void OnButtonPressed(in BtnStatus btnStatus)
    {
        switch (btnStatus.Name)
        {
            // switch state
            case "space":
                _state = _state switch
                {
                    States.Init => States.Splash,
                    States.Splash => States.Title,
                    States.Title => States.Running,
                    States.Running => States.Title,
                    _ => _state,
                };
                break;
            
            // any state to quit
            case "e":
                _state = States.Quit;
                break;
        }
    }

    private void OnInitEnter() => Debug.Log("A -----> Init Enter");
    private void OnSplashEnter() => Debug.Log("A -----> Splash Enter");
    private void OnTitleEnter() => Debug.Log("A -----> Title Enter");
    private void OnRunningEnter() => Debug.Log("A -----> Running Enter");
    private void OnQuitEnter() => Debug.Log("A -----> Quit Enter");
}

State Machine will run on calling Start(), and will stop on calling Stop().

Update state will be running on:

  • Build() - calling Update()

  • BuildAsSystem(state) - each PlayerLoop state.

Create StateMachine [Test2] and inject to State Running in [Test1]

using AceLand.Input.Events;
using AceLand.Input.State;
using AceLand.PlayerLoopHack;
using AceLand.States;
using AceLand.TaskUtils;
using UnityEngine;

public class StateMachineTest2 : MonoBehaviour, IButtonPressed
{
    public enum States
    {
        Loading, Play, Pause,
    }
    
    private IStateMachine _stateMachine;
    private States _state;

    private void Awake()
    {
        InitStateMachine();
        InjectStateMachine();
    }

    private void OnDestroy()
    {
        _stateMachine.Stop();
        _stateMachine.Dispose();
    }

    private void InitStateMachine()
    {
        IState[] states = State.Builders()
            .WithNames<States>()
            .Build();
        IState loadingState = states[0].InjectActions(enter: OnLoadingEnter);
        IState playState = states[1].InjectActions(enter: OnPlayEnter);
        IState pauseState = states[2].InjectActions(enter: OnPauseEnter);

        _stateMachine = StateMachine.Builder()
            .WithStates(states)
            .WithEntryTransition(loadingState)
            .WithTransition(loadingState, playState, () => _state is States.Play)
            .WithTransition(playState, pauseState, () => _state is States.Pause)
            .WithTransition(pauseState, playState, () => _state is States.Play)
            .WithId(StateMachineId.Test2)
            .Build();
         // .Build() will create a manual machine that update manually.
    }

    private void InjectStateMachine()
    {
        // Get machine Test1
        StateMachine.GetAsync(StateMachineId.Test1)
            .Then(machine =>
            {
                Debug.Log("Inject StateMachine B to Machine A Running State");
                // Get State from machine Test1
                machine.GetState(StateMachineTest1.States.Running)
                    // inject Test2 as sub machine
                    .InjectSubStateMachine(_stateMachine)
                    // reset Test2 state on exit
                    .InjectActions(exit: () => _state = States.Loading);
            })
            .Catch(e => Debug.Log(e, this));
    }
    
    public void OnButtonPressed(in BtnStatus btnStatus)
    {
        // do not update state if Test2 is not active
        if (!_stateMachine.IsActive) return;
        
        switch (btnStatus.Name)
        {
            // switch state
            case "z":
                _state = _state switch
                {
                    States.Loading => States.Play,
                    States.Play => States.Pause,
                    States.Pause => States.Play,
                    _ => _state,
                };
                break;
        }
    }

    private void OnLoadingEnter() => Debug.Log("B -----------> Loading Enter");
    private void OnPlayEnter() => Debug.Log("B -----------> Play Enter");
    private void OnPauseEnter() => Debug.Log("B -----------> Pause Enter");
}

State Machine will become a Sub State Machine when injected to state in other machine.

Sub State Machine will start and stop on injected state Enter and Exit.

Update state will be running on:

  • Build() - will update on same PlayerLoop state of machine injected to

  • BuildAsSystem(state) - will update on set PlayerLoop state.


Best Practices

  • Always dispose of state machines when no longer needed

  • Use meaningful state names for better debugging

  • Implement transition conditions as pure functions

  • Utilize sub state machines for complex state hierarchies

  • Consider using system integration for performance-critical scenarios


Last updated 4 months ago

This example works with Packages , and for easy settings and handling.

com.aceland.states
AceLand.States
com.aceland.library: 1.0.10
com.aceland.playerloophack: 1.0.5
com.aceland.taskutils: 1.0.5
Task Utils
Player Loop Hack
Input
https://github.com/parsue/com.aceland.states.git