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
com.aceland.states
latest version
1.0.10
namespace
AceLand.States
git repository
dependencies
com.aceland.library: 1.0.10
com.aceland.playerloophack: 1.0.5
com.aceland.taskutils: 1.0.5
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:
State Machine Core
Maintains a registry of all states
Manages transitions between states
Handles the update cycle
Controls state lifecycle (Enter → Update → Exit)
State Transitions
Uses a double-buffered transition system to prevent race conditions
Evaluates transitions in priority order:
Any-state transitions
Specific state transitions
Prevents invalid transitions through state validation
Performance Considerations
Update Optimization
Minimal garbage collection impact
Cached transition evaluations
Efficient state lookup through dictionary storage
Memory Efficiency
States are shared when possible
Action delegates are cached
Transition conditions are optimized
Usage Example
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");
}
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");
}
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