A lightweight Unity state machine library that unifies FSM, push-down automata, and behavior trees into a single elegant model.
- One class, three paradigms —
CyclopsStateworks as FSM states, PDA stack frames, or BT nodes - Composition over inheritance — No subclassing required; configure states with delegates
- Visible control flow — Transitions are declared upfront, making state graphs easy to read and reason about
- Async-friendly — Built-in Unity Awaitable integration with automatic cancellation
- Deterministic cleanup — States unwind in order with guaranteed
OnExitcalls
Add to your Unity project via the Package Manager using the git URL.
var fsm = new CyclopsStateMachine();
var gameplay = new CyclopsState
{
Name = "Gameplay",
OnEnter = () => Debug.Log("Game started!"),
OnUpdate = () => { /* game logic */ }
};
var gameOver = new CyclopsState
{
Name = "GameOver",
OnEnter = () => Debug.Log("Game Over")
};
// Declare transition: gameplay → gameOver when player dies
gameplay.AddTransition(gameOver, () => player.IsDead);
fsm.PushState(gameplay);
// In your update loop:
void Update() => fsm.Update();var hud = new CyclopsState { Name = "HUD" };
var pauseMenu = new CyclopsState { Name = "PauseMenu" };
// Escape pushes pause menu on top of HUD
hud.AddPushTransition(pauseMenu, () => Keyboard.current.escapeKey.wasPressedThisFrame);
// Escape pops pause menu, returning to HUD
pauseMenu.AddPopTransition(() => Keyboard.current.escapeKey.wasPressedThisFrame);
fsm.PushState(hud);using Cyclops.States.BehaviorTree;
var ai = Bt.Selector("AI",
Bt.Sequence("Attack",
Bt.Condition("HasTarget", () => target != null),
Bt.Action("Fire", () => weapon.Fire())
),
Bt.Sequence("Search",
Bt.Action("Scan", () =>
{
target = ScanForEnemy();
return target != null ? BtResult.Success : BtResult.Failure;
})
),
Bt.Action("Patrol", () => patrol.Step())
);
fsm.PushState(ai);Start() → OnEnter → [OnUpdate | OnBackgroundUpdate] → OnExit
↑
OnEnterBackground / OnExitBackground
| Hook | When It's Called |
|---|---|
OnEnter |
State becomes active |
OnUpdate |
Each frame while foreground (top of stack) |
OnBackgroundUpdate |
Each frame while background (below top) |
OnEnterBackground |
Another state is pushed on top |
OnExitBackground |
State returns to foreground |
OnExit |
State is stopped or replaced |
| Operation | What It Does |
|---|---|
AddTransition |
Replace current state with target |
AddPushTransition |
Push target on top, keep current |
AddPopTransition |
Pop current, resume state below |
AddExitTransition |
Trigger when Stop() is called |
// Predicate-based (checked each frame)
state.AddTransition(target, () => someCondition);
// Action-based (fires when action is invoked)
state.AddTransition(target, ref onPlayerDied);The Bt factory creates states that work as BT nodes:
Composites:
Bt.Sequence(name, children...) // Succeeds if ALL children succeed
Bt.Selector(name, children...) // Succeeds if ANY child succeedsDecorators:
Bt.Inverter(name, child) // Success ↔ Failure
Bt.Succeeder(name, child) // Always succeeds
Bt.Failer(name, child) // Always fails
Bt.Repeat(name, count, child) // Repeat N times (-1 = forever)
Bt.Retry(name, attempts, child) // Retry on failureLeaves:
Bt.Action(name, () => { }) // One-shot, succeeds immediately
Bt.Action(name, () => BtResult.Running) // Tick until non-Running
Bt.Condition(name, () => true) // Success if true, else failure
Bt.WaitFrames(name, frames) // Wait N frames, then succeedStates provide async helpers tied to their lifetime:
var loading = new CyclopsState
{
Name = "Loading",
OnEnter = async () =>
{
await loading.WaitForSecondsAsync(2f); // Auto-cancelled on exit
await loading.NextFrameAsync();
await loading.FromAsyncOperation(SceneManager.LoadSceneAsync("Game"));
loading.Stop();
}
};Each state has an ExitCancellationToken that cancels when the state exits — no manual cleanup needed.
Configure everything upfront, then just tick:
// Bootstrap: wire the state graph once, then drive with async
var fsm = new CyclopsStateMachine();
var stateA = new CyclopsState();
var stateB = new CyclopsState();
stateA.AddTransition(stateB, () => someCondition);
fsm.PushState(stateA);
// Drive the loop — no MonoBehaviour needed
while (!fsm.IsIdle && !Application.exitCancellationToken.IsCancellationRequested)
{
await Awaitable.NextFrameAsync(Application.exitCancellationToken);
fsm.Update();
}States don't hold references to the machine — they signal via Stop(), Succeed(), or Fail(), and transitions handle the rest. This makes state graphs predictable and testable.
| Method | Description |
|---|---|
PushState(state) |
Add state to top of stack |
Update() |
Tick all states, process transitions |
ForceStop() |
Immediately stop all states in order |
IsIdle |
True when stack is empty |
StateCount |
Number of active states |
| Property | Description |
|---|---|
Name |
Debug name for the state |
IsActive |
True while state is running |
IsForegroundState |
True if top of stack |
Result |
BtResult.Running, Success, or Failure |
ExitCancellationToken |
Cancelled when state exits |
| Method | Description |
|---|---|
Stop() |
Request graceful exit |
Succeed() |
Set result to Success |
Fail() |
Set result to Failure |
AddTransition(target, condition) |
Replace on condition |
AddPushTransition(target, condition) |
Push on condition |
AddPopTransition(condition) |
Pop on condition |
OnSuccess(target) |
Transition on Success result |
OnFailure(target) |
Transition on Failure result |
- Unity 2023.3+ or Unity 6000+
- Uses:
UnityEngine.Pool,UnityEngine.Awaitable
| Folder | Contents |
|---|---|
Runtime/States |
Core state machine classes |
Runtime/BehaviorTree |
BT node factory |
Tests |
Unit tests |