$ git show visual-state-machine-v3
Visual State Machine V3
ACTIVE+ UPM › add package from git URL
https://www.pkglnk.dev/visual-state-machine-v3.git
✓ via pkglnk
// OVERVIEW
The third-generation rewrite of the Unity visual state machine editor, targeting Unity 6+. Same drag-and-drop node graph as V1 and V2, now with sub-state machines for hierarchy, portal nodes to tame spaghetti connections, a runtime monitor showing live instance states, transition breakpoints on both nodes and edges, a transition timeline with blackboard snapshots, a replay controller, and JSON export for bug analysis. Type-safe context sharing via State<TContext>, plus [DataIn], [DataOut] and [Transition] attributes that auto-bind ports to the blackboard. Instance pooling keeps the GC quiet, IL2CPP and WebGL ship without a preservation dance thanks to build-time processors, and delta-based graph variants let you fork a base graph for difficulty levels or config variants without duplicating it. Includes a fluent StateMachineBuilder API for code-first authoring and 50+ built-in states across timing, logic, behaviour trees, animator, audio, transform, particles, timeline, input, dialog, events, plus Addressables and Visual Scripting integrations. Distributed via Unity Package Manager (git URL), MIT licensed.
// DETAIL
The third-generation rewrite of the Unity visual state machine, targeting Unity 6+. Node-graph editor with sub-state machines and portal nodes, plus a debugging arsenal: live runtime monitor, node and edge breakpoints, transition timeline with blackboard snapshots, replay controller, and JSON export. Type-safe context sharing, instance pooling to keep the GC quiet, and IL2CPP + WebGL ready via build-time processors. Delta-based graph variants let you fork a base graph without duplicating it. 50+ built-in states across timing, logic, behaviour trees, animator, audio, transform, particles, timeline, input, dialog, events, plus Addressables and Visual Scripting integrations. MIT.
// README
Visual State Machine V3
A Unity 6+ package for visually building and monitoring state machines using Unity's experimental GraphView API. Open source under the MIT licence and under active development.
Status
VSM3 is pre-1.0 (currently 0.x). The API surface is stabilising but breaking changes may land between minor versions until 1.0. Pin to a specific tag if you're using it in production work, and watch the CHANGELOG for migration notes.
Feedback, bug reports, and PRs are welcome — open an issue or discussion on the GitHub repo.
Table of Contents
- Requirements
- Installation
- Quick Start
- Core Concepts
- Code-First State Machines
- Graph Variants
- Built-in States
- Editor Features
- Debugging Tools
- Performance
- Building for IL2CPP & WebGL
- Integrations
- API Reference
- Extending VSM3
- Testing Custom States
- Sample Projects
- Troubleshooting
- Licence
- Contributing
Requirements
- Unity 6 (6000.0) or later - Required for GraphView API compatibility
- .NET Standard 2.1
Note: This package uses Unity's experimental GraphView API (
UnityEditor.Experimental.GraphView) for its visual node-based editor. While this API has been available in earlier Unity versions, Unity 6 provides improved stability and features.
Installation
Add Visual State Machine V3 to your Unity project via Package Manager:
- Open Window > Package Manager
- Click + > Add package from git URL
- Enter:
https://www.pkglnk.dev/visualstatemachinev3.git
Manual Installation
Clone or download this repository into your project's Packages folder.
Quick Start
1. Create a State
Create a new C# script that inherits from State:
using System;
using Nonatomic.VSM.Attributes;
using Nonatomic.VSM.Core;
using UnityEngine;
public class IdleState : State
{
[DataIn("Movement Input")]
public Vector2 MovementInput;
[DataOut("Idle Duration")]
public float IdleDuration;
[Transition("On Move")]
public Action OnMove;
[Transition("On Jump")]
public Action OnJump;
private float _enterTime;
public override void OnEnter()
{
_enterTime = Time.time;
Debug.Log("Entered Idle State");
}
public override void OnUpdate()
{
IdleDuration = Time.time - _enterTime;
if (MovementInput.magnitude > 0.1f)
{
OnMove?.Invoke();
}
}
public override void OnExit()
{
Debug.Log($"Exited Idle State after {IdleDuration:F2}s");
}
}
2. Create a State Machine Graph
- Right-click in the Project window
- Select Create > Visual State Machine > State Machine Graph
- Double-click the created asset to open the Graph Editor
3. Build Your Graph
- Right-click in the graph to add states
- Connect transition ports to define state flow
- Add an Entry node to define the starting state
- Optionally add Exit nodes for completion states
4. Run the State Machine
Add a StateMachineRunner component to a GameObject and assign your graph:
using Nonatomic.VSM.Core;
using Nonatomic.VSM.Graph;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
[SerializeField]
private StateMachineGraph _graph;
private StateMachineRunner _runner;
private void Start()
{
_runner = gameObject.AddComponent<StateMachineRunner>();
_runner.Initialize(_graph);
_runner.StartStateMachine();
}
private void Update()
{
var input = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
_runner.Blackboard.Set("Movement Input", input);
}
}
Core Concepts
States
States are the building blocks of your state machine. Each state inherits from State (or State<TContext> for typed context access) and overrides lifecycle methods to define
behavior.
State Lifecycle
States have both synchronous and asynchronous lifecycle methods:
public class ExampleState : State
{
// --- Synchronous ---
public override void OnEnter() { } // Called when entering the state
public override void OnUpdate() { } // Called every frame (per UpdateMode)
public override void OnFixedUpdate() { } // Called at fixed timestep
public override void OnLateUpdate() { } // Called at end of frame
public override void OnExit() { } // Called when leaving the state
public override void OnReset() { } // Called when returning to pool
// --- Asynchronous (with cancellation support) ---
public override Task OnEnterAsync(CancellationToken ct) { }
public override Task OnEnterAsync() { }
public override Task OnUpdateAsync(CancellationToken ct) { }
public override Task OnUpdateAsync() { }
public override Task OnExitAsync(CancellationToken ct) { }
public override Task OnExitAsync() { }
}
Available properties within a state:
| Property | Type | Description |
|---|---|---|
StateMachine |
StateMachineInstance |
The parent state machine instance |
Blackboard |
Blackboard |
Shared key-value store |
GameObject |
GameObject |
The owning GameObject |
Transform |
Transform |
The owning Transform |
Context |
IStateMachineContext |
Shared context object |
IsActive |
bool |
Whether the state is currently active |
Utility methods:
| Method | Description |
|---|---|
GetComponent<T>() |
Gets a component from the owning GameObject |
GetOrAddComponent<T>() |
Gets or adds a component on the owning GameObject |
Log(message) |
Logs a message with state context |
LogWarning(message) |
Logs a warning with state context |
LogError(message) |
Logs an error with state context |
Attributes
[DataIn]
Marks a field as an input port. Values are populated from the Blackboard before OnEnter().
[DataIn("Speed", Required = true, Tooltip = "Movement speed")]
public float Speed = 5f;
[DataIn]
public Transform Target;
[DataOut]
Marks a field as an output port. Values are written to the Blackboard after OnExit().
[DataOut("Result")]
public float CalculatedValue;
[DataOut]
public Vector3 FinalPosition;
[Transition]
Marks an Action field as a transition trigger. Invoke to transition to the connected state.
[Transition("On Complete", Color = "#00FF00")]
public Action OnComplete;
[Transition("On Fail")]
public Action OnFail;
[Exposed]
Marks a Blackboard variable for Inspector visibility.
// In your blackboard setup
[Exposed("Player Health")]
public int Health = 100;
Blackboard
The Blackboard is a shared key-value store for passing data between states.
// Set a value
Blackboard.Set("playerHealth", 100);
Blackboard.Set("targetPosition", new Vector3(10, 0, 5));
// Get a value
var health = Blackboard.Get<int>("playerHealth");
var position = Blackboard.Get<Vector3>("targetPosition");
// Try get (safe)
if (Blackboard.TryGet<int>("playerHealth", out var hp))
{
Debug.Log($"Health: {hp}");
}
// Subscribe to changes
Blackboard.Subscribe<int>("playerHealth", (oldValue, newValue) =>
{
Debug.Log($"Health changed from {oldValue} to {newValue}");
});
// Check if key exists
if (Blackboard.HasKey("playerHealth"))
{
// ...
}
Blackboard Variable Nodes
You can visually connect blackboard variables to state inputs in the graph editor:
- Create a variable in the Blackboard panel
- Drag the variable onto the graph to create a Get node
- Connect the Get node's output to a state's
[DataIn]port
This provides a visual representation of data flow and ensures values are bound when entering states.
Exposed Variables & Per-Instance Overrides
Variables can be exposed in the graph editor for per-instance customization:
- In the Blackboard panel, toggle the "Expose" checkbox on a variable
- The variable will appear in the
StateMachineRunnerInspector - Each runner instance can override the default value
// Access overridden values at runtime
var runner = GetComponent<StateMachineRunner>();
// Values are automatically applied from overrides when the state machine starts
// You can also set values programmatically:
runner.SetBlackboardValue("Speed", 10f);
var speed = runner.GetBlackboardValue<float>("Speed");
Supported override types: int, float, bool, string, Vector2, Vector3, Color, and UnityEngine.Object references (GameObjects, ScriptableObjects, etc.).
Custom Blackboard Types
By default the blackboard supports primitives, math types, and Unity objects. To add custom types (such as enums), use the [BlackboardType] attribute:
using Nonatomic.VSM.Attributes;
[BlackboardType]
public enum GamePhase { Menu, Playing, Paused, GameOver }
[BlackboardType(DisplayName = "AI State", Category = "AI")]
public enum AIBehavior { Idle, Patrol, Chase, Attack }
Attributed types are auto-discovered at startup and appear in the blackboard "Add Variable" menu.
For types defined in immutable packages where you cannot add the attribute, use the manual registration API in an [InitializeOnLoad] class:
using Nonatomic.VSM.Blackboard;
using Nonatomic.VSM.Editor.Blackboard;
[InitializeOnLoad]
public static class ExternalTypeRegistration
{
static ExternalTypeRegistration()
{
BlackboardTypeRegistry.RegisterEnum<SomePlugin.Direction>();
BlackboardTypeEditorRegistry.RegisterEnum<SomePlugin.Direction>();
}
}
[BlackboardType] also works on serializable structs and classes. They serialize through
JsonUtility and get a reflected default-value editor in the blackboard panel:
using System;
using Nonatomic.VSM.Attributes;
[Serializable]
[BlackboardType(Category = "Combat")]
public struct DamageInfo
{
public int Amount;
public float Knockback;
}
Custom types work under IL2CPP with no extra configuration — see Building for IL2CPP & WebGL.
Best Practices
Prefer [DataIn]/[DataOut] ports over direct blackboard access. While states can read and write blackboard variables in code via Blackboard.Set() and Blackboard.Get(), this hides data flow from the graph. Instead, use [DataIn] and [DataOut] fields connected to blackboard variable nodes:
// Prefer: data flow is visible in the graph
public class ScoreState : State
{
[DataIn("Points")]
public int Points;
[DataOut("Total Score")]
public int TotalScore;
public override void OnEnter()
{
TotalScore += Points;
}
}
// Avoid: hidden dependency on a blackboard key
public class ScoreState : State
{
public override void OnEnter()
{
var points = Blackboard.Get<int>("Points");
Blackboard.Set("Total Score", points + Blackboard.Get<int>("Total Score"));
}
}
Using ports keeps the graph self-documenting — you can see where data comes from and where it goes by following the connections. It also makes states more reusable since they have no hidden dependencies on specific blackboard key names.
Data Flow
Data flows through the state machine via:
- Blackboard Variables - Shared data accessible from any state
- Port Connections - Visual connections between state outputs and inputs
- Optional Typed Context - Strongly-typed shared context object
// Access blackboard from within a state
public class DamageState : State
{
[DataIn]
public int DamageAmount;
public override void OnEnter()
{
var currentHealth = Blackboard.Get<int>("Health");
Blackboard.Set("Health", currentHealth - DamageAmount);
}
}
Typed Context
For strongly-typed data sharing between states, use State<TContext> and StateMachineController<TContext>:
public class Enemy : MonoBehaviour
{
[SerializeField] private StateMachineGraph _graph;
private StateMachineController<Enemy> _controller;
// Context data - states access these via SharedContext
public float Health { get; set; } = 100f;
public Transform Target { get; set; }
private void Awake()
{
_controller = new StateMachineController<Enemy>(_graph, this);
_controller.OnStateChanged += (prev, next) =>
Debug.Log($"State changed: {prev?.GetType().Name} -> {next?.GetType().Name}");
_controller.Start();
}
private void Update() => _controller.Update();
private void FixedUpdate() => _controller.FixedUpdate();
private void OnDestroy() => _controller.Dispose();
}
States access the typed context through SharedContext:
public class ChaseState : State<Enemy>
{
[Transition("Reached")] public Action OnReached;
public override void OnUpdate()
{
var enemy = SharedContext;
var direction = (enemy.Target.position - enemy.Transform.position).normalized;
enemy.Transform.position += direction * Time.deltaTime * 5f;
if (Vector3.Distance(enemy.Transform.position, enemy.Target.position) < 1f)
{
OnReached?.Invoke();
}
}
}
You can also create a typed runner by subclassing StateMachineRunner<TContext>:
public class EnemyRunner : StateMachineRunner<EnemyContext>
{
protected override EnemyContext CreateSharedContext()
{
return new EnemyContext { AlertLevel = 0 };
}
}
Sub-State Machines
Create hierarchical state machines by embedding one graph inside another using SubStateMachine nodes:
- Create a separate
StateMachineGraphasset for the sub-state machine - In the parent graph, right-click and add a Sub State Machine node
- Assign the sub-graph to the node
- Connect the node's Enter port from a transition and Exit port to continue flow
How It Works
When execution reaches a SubStateMachine node:
- The parent creates a child
StateMachineInstancefor the sub-graph - The child runs independently (parent delegates Update calls to it)
- When the child reaches an Exit node, it fires
OnExited - The parent resumes execution from the SubStateMachine node's exit connection
Data Modes
Configure how the child accesses parent data:
| Mode | Description |
|---|---|
| Inherit | Child blackboard inherits from parent (can read parent variables) |
| Isolated | Child has independent blackboard; use explicit input/output mappings |
| Shared | Child uses the same blackboard instance as parent (tight coupling) |
Context Inheritance
Sub-state machines inherit the parent's typed context (State<TContext>):
- Same context type: Works seamlessly - child states access the same context
- Compatible context type: Works if sub-graph expects a base type (e.g., sub-graph uses
MonoBehaviour, parent providesPlayerController) - Incompatible context type: Validation error - sub-graph requires a type the parent can't provide
- No context required: Always works - sub-graph doesn't need typed context
Limitations
| Limit | Value | Description |
|---|---|---|
| Max Nesting Depth | 16 | Maximum levels of nested sub-state machines |
| Circular References | Blocked | Graph A -> B -> A is detected and prevented |
Both limits are enforced at edit-time (validation) and runtime (with error logging and graceful continuation).
Exposed Variable Ports
Sub-state machine nodes can display typed data ports for the child graph's blackboard variables, making data flow between parent and child fully visible in the graph editor.
Exposing Variables in a Child Graph
- Open the sub-graph asset in the graph editor
- In the Blackboard panel, expand a variable to reveal its settings
- Set the Sub-Graph Port dropdown to control how it appears on the parent's SubStateMachine node:
| Direction | Description |
|---|---|
| None | Variable is not exposed (default) |
| Input | Appears as an input port — parent provides the value, child reads it |
| Output | Appears as an output port — child writes the value, parent reads it |
| InputOutput | Appears as both an input and output port |
Using Exposed Ports in the Parent Graph
Once a child graph has exposed variables, the parent graph's SubStateMachine node automatically shows typed data ports for them:
- Input ports accept connections from blackboard Get nodes or state
[DataOut]ports in the parent graph. When no connection is present, an inline field lets you set the value directly on the node — just like any other data input port. - Output ports can be connected to blackboard Set nodes in the parent graph, writing child values back to the parent's blackboard when the sub-graph exits.
How Data Flows at Runtime
On child start (input ports):
- Values from connected parent blackboard variables are written to the child's blackboard
- If no connection exists, the inline value set on the SubStateMachine node is used instead
On child exit (output ports):
- Values from the child's blackboard are written to connected parent blackboard variables
This layered approach means connections take priority over inline values, and inline values take priority over the child graph's defaults.
Example: Reusable Traffic Light with Direction Input
Child graph (TrafficLightGraph):
Blackboard:
- Direction (enum, exposed as Input)
- IsActive (bool, exposed as Output)
Parent graph (IntersectionGraph):
[Blackboard: NorthSouthDir] --Get--> [SubStateMachine: TrafficLightGraph].Direction
[SubStateMachine: TrafficLightGraph].IsActive --Set--> [Blackboard: NSActive]
The same TrafficLightGraph can be reused multiple times with different direction values, each wired visually in the parent graph.
Portal Nodes
Portal nodes allow "teleporting" transitions across the graph without visible connections. Useful for:
- Reducing visual clutter in complex graphs
- Creating common exit points that multiple states can reach
- Organizing large graphs into logical sections
Usage
- Add a Portal Out node where you want to "jump from"
- Add a Portal In node where you want to "jump to"
- Set both nodes to the same Channel (color-coded for easy identification)
- Connect states to the Portal Out's input, and the Portal In's output to destination states
[State A] --> [Portal Out (Ch 0)] ~~~~ [Portal In (Ch 0)] --> [State B]
^ |
(no visible connection - matched by channel)
Rules
- Many-to-One: Multiple Portal Out nodes can target the same Portal In channel
- One Portal In per Channel: Only one Portal In node allowed per channel (validated)
- 16 Channels: Color-coded channels (0-15) for visual organization
- Custom Labels: Each portal node can have a custom label for clarity
Transition Timing
Transitions can have configurable delay times:
- Instant (0s): Transition happens immediately
- Delayed: Waits specified time before transitioning
Same-Frame Protection: The system limits instant transitions to prevent infinite loops (default: 100 per frame).
// Configure in StateMachineInstance
stateMachine.MaxSameFrameTransitions = 50;
State Styling
Customize how states appear in the graph editor using attributes.
[StateStyle]
Set the background color and icon of a state node:
using Nonatomic.VSM.Attributes;
[StateStyle(Color = StateColors.Red)]
public class CombatState : State { }
[StateStyle(Color = StateColors.Blue)]
public class IdleState : State { }
[StateStyle(Color = "#FF6B35")] // Custom hex color
public class CustomState : State { }
Available Colors (StateColors class):
| Name | Hex | Aliases |
|---|---|---|
Grey |
#444444 |
Gray |
Red |
#990e23 |
|
Orange |
#B06101 |
|
LimeGreen |
#6d9111 |
|
Green |
#116f1c |
|
ForestGreen |
#08704a |
|
Teal |
#066670 |
Cyan |
LightBlue |
#037091 |
|
Blue |
#084870 |
|
Purple |
#4a0e99 |
Indigo |
Violet |
#740e99 |
|
Pink |
#750b55 |
|
Cantera |
#551C25 |
Brown |
Dijon |
#a89d46 |
Yellow |
Black |
#000000 |
|
White |
#ffffff |
[StateInfo]
Add documentation that appears in the editor:
[StateInfo(
Tooltip = "Brief hover text",
Description = "Detailed description shown in inspector",
Category = "Combat/Melee"
)]
public class AttackState : State { }
Code-First State Machines
You can create state machines entirely in code using the fluent StateMachineBuilder API:
using Nonatomic.VSM.Core;
using UnityEngine;
public class CodeBasedController : MonoBehaviour
{
private StateMachineInstance _stateMachine;
private void Start()
{
_stateMachine = StateMachineBuilder.Create(gameObject)
.AddState<IdleState>()
.AddState<MoveState>()
.AddState<JumpState>()
.SetEntryState<IdleState>()
.AddTransition<IdleState, MoveState>("OnMove")
.AddTransition<IdleState, JumpState>("OnJump")
.AddTransition<MoveState, IdleState>("OnStop")
.AddTransition<JumpState, IdleState>("OnLand")
.AddVariable("Speed", 5f)
.AddVariable("JumpForce", 10f)
.BuildAndStart();
}
private void Update()
{
_stateMachine?.Update();
}
private void OnDestroy()
{
_stateMachine?.Stop();
}
}
Builder Methods
| Method | Returns | Description |
|---|---|---|
Create(gameObject) |
StateMachineBuilder |
Creates a builder with GameObject context |
Create() |
StateMachineBuilder |
Creates a builder without GameObject |
AddState<T>(key) |
StateRef |
Adds a state type with optional key (defaults to type name) |
AddState(type, key) |
StateRef |
Adds a state type by Type reference |
SetEntryState<T>() |
StateMachineBuilder |
Sets which state to start in |
SetEntryState(stateRef) |
StateMachineBuilder |
Sets entry state from a StateRef |
AddTransition<TFrom, TTo>(name, delay) |
StateMachineBuilder |
Adds a transition between state types |
AddTransition(from, name, to, delay) |
StateMachineBuilder |
Adds a transition with string keys |
AddVariable<T>(key, default, exposed) |
StateMachineBuilder |
Adds a blackboard variable |
WithContext(context) |
StateMachineBuilder |
Sets a custom context |
WithLayout(mode) |
StateMachineBuilder |
Sets layout mode: Hierarchical (default), Grid, or None |
WithoutLayout() |
StateMachineBuilder |
Disables automatic layout |
Build() |
StateMachineInstance |
Creates the instance |
BuildAndStart() |
StateMachineInstance |
Creates and immediately starts the instance |
Node Positioning
By default, the builder automatically arranges nodes using hierarchical layout. You can customize this:
// Manual positioning
var idle = builder.AddState<IdleState>().AtPosition(100, 100);
var move = builder.AddState<MoveState>().AtPosition(400, 100);
var jump = builder.AddState<JumpState>().AtPosition(400, 250);
// Or disable auto-layout entirely
builder.WithoutLayout();
// Or use grid layout instead
builder.WithLayout(GraphLayoutMode.Grid);
Multiple Instances of the Same State
When you need multiple instances of the same state type, use custom keys and StateRef:
var builder = StateMachineBuilder.Create(gameObject);
// Store references to states
var patrolA = builder.AddState<PatrolState>("patrol_a");
var patrolB = builder.AddState<PatrolState>("patrol_b");
var chase = builder.AddState<ChaseState>();
// Use references for type-safe transitions
var sm = builder
.SetEntryState(patrolA)
.AddTransition(patrolA, "OnComplete", patrolB)
.AddTransition(patrolB, "OnComplete", patrolA)
.AddTransition(patrolA, "OnDetect", chase)
.AddTransition(patrolB, "OnDetect", chase)
.BuildAndStart();
You can also chain transitions directly from StateRef:
var builder = StateMachineBuilder.Create(gameObject);
var idle = builder.AddState<IdleState>();
var move = builder.AddState<MoveState>();
var jump = builder.AddState<JumpState>();
// Chain transitions from each state
idle.TransitionTo("OnMove", move)
.TransitionTo("OnJump", jump);
move.TransitionTo("OnStop", idle);
jump.TransitionTo("OnLand", idle);
var sm = builder
.SetEntryState(idle)
.BuildAndStart();
Graph Variants
Graph Variants provide a delta-based inheritance system for state machine graphs. A variant inherits from a base graph and stores only the differences (overrides), keeping assets lightweight and version-control friendly.
Creating a Variant
- Select a
StateMachineGraphasset in the Project window - Right-click and select Create Variant
- A new
StateMachineGraphVariantasset is created referencing the base graph
Alternatively, use Create > Visual State Machine > State Machine Graph Variant from the Assets menu.
What Can Be Overridden
| Element | Override Options |
|---|---|
| Nodes | Modify properties, replace state type, or disable entirely |
| Connections | Override transition time, easing curve, or remove |
| Blackboard Variables | Override default value, exposed flag |
Variants can also add new nodes, connections, and blackboard variables that exist only in the variant.
How It Works
- Changes to the base graph automatically propagate to all variants
- Overrides are applied on top of the base graph at merge time
- Disabled nodes are excluded along with all their connections
- Variants support nesting up to 8 levels deep (variant of a variant)
- Circular references are detected and prevented
Usage with StateMachineRunner
Use a variant anywhere you would use a regular graph. Assign it to a StateMachineRunner or StateMachineController and it behaves like a fully resolved graph:
[SerializeField] private StateMachineGraphVariant _hardModeVariant;
private void Start()
{
var runner = gameObject.AddComponent<StateMachineRunner>();
runner.Initialize(_hardModeVariant);
runner.StartStateMachine();
}
Built-in States
VSM3 ships with a library of ready-to-use states organized by category. All built-in states follow a consistent pattern with [DataIn] inputs, [DataOut] outputs, and
[Transition] exit ports.
Timing
| State | Description | Key Inputs | Transitions |
|---|---|---|---|
| DelayState | Waits for a duration before transitioning | Duration, UseUnscaledTime |
Complete |
| RandomDelayState | Waits for a random duration within a range | MinDuration, MaxDuration |
Complete |
Logic & Branching
| State | Description | Key Inputs | Transitions |
|---|---|---|---|
| ConditionState | Branches on a boolean value | Condition, Invert |
True, False |
| CompareState | Compares two numeric values | ValueA, ValueB, Operator |
True, False |
| CompareStringState | Compares two strings | ValueA, ValueB, IgnoreCase |
Equal, Not Equal |
| RandomBranchState | Random two-way branch by probability | ProbabilityA |
Path A, Path B |
| RandomBranch3State | Random three-way branch by weight | WeightA, WeightB, WeightC |
Path A, Path B, Path C |
Behavior Tree Patterns
| State | Description | Key Inputs | Transitions |
|---|---|---|---|
| SequenceGateState | AND-gate: all conditions must be true | ConditionA-D |
Success, Failure |
| SelectorGateState | Priority selector: transitions on first true | ConditionA-D |
A, B, C, D, None |
| RepeaterState | Loop counter with configurable repeat count | RepeatCount, Infinite |
Loop, Done |
| CooldownState | Prevents re-entry for a cooldown duration | Duration |
Ready, On Cooldown |
| TimeoutState | Fires timeout after a duration elapses | Duration |
Timeout |
Animator
| State | Description | Key Inputs | Transitions |
|---|---|---|---|
| SetAnimatorTriggerState | Sets an Animator trigger parameter | Animator, TriggerName |
Done |
| ResetAnimatorTriggerState | Resets an Animator trigger parameter | Animator, TriggerName |
Done |
| SetAnimatorBoolState | Sets an Animator bool parameter | Animator, ParameterName, Value |
Done |
| SetAnimatorFloatState | Sets an Animator float parameter | Animator, ParameterName, Value |
Done |
| SetAnimatorIntState | Sets an Animator int parameter | Animator, ParameterName, Value |
Done |
| SetAnimatorSpeedState | Sets Animator playback speed | Animator, Speed |
Done |
| SetAnimatorLayerWeightState | Sets an Animator layer weight | Animator, Layer, Weight |
Done |
| GetAnimatorFloatState | Reads an Animator float parameter | Animator, ParameterName |
Done |
| GetAnimatorBoolState | Reads an Animator bool and branches | Animator, ParameterName |
True, False |
| GetAnimatorIntState | Reads an Animator int parameter | Animator, ParameterName |
Done |
| PlayAnimatorState | Plays an Animator state directly | Animator, StateName |
Done |
| CrossFadeAnimatorState | Crossfades to an Animator state | Animator, StateName, TransitionDuration |
Done |
| WaitForAnimatorStateState | Waits for Animator to reach a state | Animator, StateName |
Complete |
Audio
| State | Description | Key Inputs | Transitions |
|---|---|---|---|
| PlayAudioState | Plays an audio clip (fire and forget) | Clip, AudioSource, Volume |
Done |
| PlayAudioAndWaitState | Plays a clip and waits for it to finish | Clip, AudioSource, Volume |
Complete |
| StopAudioState | Stops audio playback | AudioSource |
Done |
Transform
| State | Description | Key Inputs | Transitions |
|---|---|---|---|
| SetPositionState | Sets a transform's position | Target, Position, UseLocalSpace |
Done |
| SetRotationState | Sets a transform's rotation | Target, EulerAngles, UseLocalSpace |
Done |
| SetScaleState | Sets a transform's local scale | Target, Scale |
Done |
| LookAtState | Makes a transform look at a target | Source, TargetTransform |
Done |
| MoveToState | Translates a transform over time | Target, Destination, Duration |
Complete |
GameObject
| State | Description | Key Inputs | Transitions |
|---|---|---|---|
| SetActiveState | Enables or disables a GameObject | Target, Active |
Done |
| SpawnPrefabState | Instantiates a prefab | Prefab, Position, Parent |
Done |
| DestroyState | Destroys a GameObject | Target, Delay |
Done |
| DestroyImmediateState | Destroys a GameObject immediately | Target |
Done |
Particles
| State | Description | Key Inputs | Transitions |
|---|---|---|---|
| PlayParticleState | Plays a ParticleSystem (fire and forget) | ParticleSystem, WithChildren |
Done |
| PlayParticleAndWaitState | Plays and waits for completion | ParticleSystem, WithChildren |
Complete |
| StopParticleState | Stops a ParticleSystem | ParticleSystem, Clear, StopBehavior |
Done |
| SetParticleEmissionState | Sets emission rate | ParticleSystem, Enabled, RateOverTime |
Done |
Timeline
| State | Description | Key Inputs | Transitions |
|---|---|---|---|
| PlayTimelineState | Plays a Timeline and waits for completion | Asset, Director, Speed |
Complete |
| PauseTimelineState | Pauses a playing timeline | Director |
Done |
| ResumeTimelineState | Resumes a paused timeline | Director, Speed |
Done |
| StopTimelineState | Stops a playing timeline | Director |
Done |
| WaitForPlaybackTimeState | Waits for a specific playback time | Director, TargetTime |
Reached |
Input
| State | Description | Key Inputs | Transitions |
|---|---|---|---|
| WaitForKeyState | Waits for a specific key press | Key, InputMode |
Pressed |
| WaitForAnyKeyState | Waits for any key press | - | Pressed |
| WaitForMouseButtonState | Waits for a mouse button click | Button, InputMode |
Clicked |
Dialog
| State | Description | Key Inputs | Transitions |
|---|---|---|---|
| DialogState | Displays dialog text, waits for player advance | Text, Speaker, Portrait |
Continue |
| TimedDialogState | Displays dialog, auto-advances after delay | Text, Speaker, Duration |
Continue |
| DialogChoiceState | Presents up to four choices | ChoiceA-D |
Choice A-D |
| ClearDialogState | Clears the dialog UI | OnClear (UnityEvent) |
Done |
Events
| State | Description | Key Inputs | Transitions |
|---|---|---|---|
| InvokeEventState | Invokes a UnityEvent | Event |
Done |
| InvokeStringEventState | Invokes a UnityEvent<string> | Event, Value |
Done |
| InvokeFloatEventState | Invokes a UnityEvent<float> | Event, Value |
Done |
| InvokeIntEventState | Invokes a UnityEvent<int> | Event, Value |
Done |
Utility
| State | Description | Key Inputs | Transitions |
|---|---|---|---|
| LogState | Logs a message to the console | Message, Level |
Done |
| DebugBreakState | Pauses the editor for inspection | Message, BreakOnce |
Continue |
Editor Features
Graph Window
Open via Window > Visual State Machine > Graph Editor
- Node Creation: Right-click to add states from a searchable menu
- Connections: Drag from port to port to create transitions
- Navigation: Use breadcrumbs for sub-state machines
- Zoom/Pan: Scroll to zoom, middle-click to pan
- Search: Ctrl+F to search for nodes and regions
Toolbar
The toolbar splits into a left group (navigation + frequent view actions) and a right group (menus, panels, and save):
- Frame All / Search / Layout (left, next to the breadcrumb): frame all nodes, search nodes and regions (Ctrl+F), and the hierarchical/grid auto-layout menu.
- View menu: framing actions (Frame Selection, Frame Entry Node) and view behaviour (auto-center on the active state, smooth centering, follow sub-state machines).
- Settings menu: persistent display preferences — connecting-line style (Wire/Default), port colours, type icons, and Debug Mode.
- Windows menu: show or hide the editor panels — Blackboard, Inspector, Mini-Map, State List, Breakpoints, and (in play mode) the Debug Timeline.
- Bookmarks, Split View, and Save sit on the right; a link-scroll toggle appears next to Split View when it is enabled.
Regions
Regions are visual containers for organizing nodes into logical groups. They are purely organizational and do not affect state machine execution.
Creating a Region
- Right-click on the graph canvas
- Select Add Node > Organization > Region
- A new region appears at the click location
Working with Regions
- Rename: Double-click the region header to edit the title (Enter to confirm, Escape to cancel)
- Add nodes: Drag any node into the region to group it
- Remove nodes: Drag a node out of the region to ungroup it
- Resize: Drag the region edges to adjust the size
- Move: Drag the region header to reposition it along with all contained nodes
- Delete: Right-click the region and select Delete Group (contained nodes are preserved)
Region layout is persistent across editor sessions. Regions also appear in the graph search bar for quick navigation.
SubGraph Preview
Quickly inspect the contents of a Sub State Machine without navigating into it. Each SubStateMachine node has a Peek button alongside the existing Open button.
Usage
- Click the Peek button on any SubStateMachine node
- A floating preview popup appears showing a miniature view of the sub-graph
- Click Peek again (or press Escape) to close the popup
Preview Controls
| Action | Control |
|---|---|
| Pan | Drag with left or middle mouse button |
| Zoom | Scroll wheel (0.25x to 4x) |
| Recenter | Click the recenter button in the header or press F |
| Move popup | Drag the popup header |
| Close | Click X, press Escape, or click Peek again |
The preview shows all nodes with color-coded types, connection curves with directional arrows, and a node count in the info bar. It auto-scales to fit all nodes and follows its owner node when repositioned.
Layout Tools
- Auto Layout: Automatically arranges nodes hierarchically from left to right based on connections
- Grid Layout: Arranges nodes in a simple grid pattern
- Use the toolbar Layout menu to organize messy graphs
Debugging Tools
Runtime Monitor
Open via Window > Visual State Machine > Runtime Monitor
The Runtime Monitor provides a live view of all running state machine instances during play mode:
- Instance list: All active instances grouped by graph, with status indicators (green = running, yellow = paused, gray = stopped, red = error)
- Current state tracking: See which state each instance is currently in
- Blackboard inspector: View live blackboard variable values and types
- Quick navigation: Click "Open" to jump to the active state in the graph editor
Breakpoints
Pause the editor during play mode when a state is entered, so you can inspect the machine where it actually is. There are two kinds: a breakpoint on a state node pauses whenever that state is entered, and a breakpoint on a transition edge pauses only when the state is entered through that specific transition.
Node breakpoints
- Right-click a state node in the graph editor
- Select Add Breakpoint - a red dot appears in the node's corner
- Enter play mode - the editor pauses when that state is entered, by any path
Transition breakpoints
- Right-click a transition edge
- Select Add Transition Breakpoint - the edge turns bold red with a red dot at its midpoint
- Enter play mode - the editor pauses only when the target state is entered through that edge
Transition breakpoints are edge-specific: a state fed by several transitions can hold a separate breakpoint on each, listed individually, and the dot follows the edge as you move nodes. Click a transition's dot to open its quick actions (enable, disable, remove) and highlight its row in the Breakpoints panel.
Breakpoint Types
| Type | Indicator | Pauses when |
|---|---|---|
| On Entry | Red dot on the node | the state is entered by any transition |
| On Transition | Red dot on the edge | the state is entered through that specific transition |
| Conditional | Orange dot | a condition expression evaluates to true on entry |
| Disabled | Gray dot | the breakpoint exists but is inactive |
Curve-editing handles are drawn green with a white outline, so they stay distinct from the red breakpoint markers.
When a breakpoint is hit
A pause should never be mistaken for a hang, so a hit is made obvious: the paused node shows a persistent red glow, the transition it stopped on turns a brighter red, and a Paused at breakpoint: <state> banner appears at the top of the graph. All three clear when you resume or stop play.
Conditional Breakpoint Expressions
Right-click a state node that has a breakpoint and select Edit Breakpoint Condition... to open the condition editor. Supported syntax:
Health < 10 // Numeric comparison
Score >= 100 // Greater-or-equal
IsAlive == false // Boolean comparison
Status == "Running" // String comparison
!GameOver // Boolean negation
GameOver // Simple truthy check
Breakpoint List Panel
A sidebar panel in the graph editor (open it from the toolbar's Windows menu) lists every breakpoint in the current graph: node breakpoints by state name, transition breakpoints by their source. From it you can:
- Toggle individual breakpoints on or off
- Toggle all breakpoints globally
- Click a name to navigate to it in the graph
- Remove a breakpoint, or clear them all at once
Transition Timeline
The transition timeline view (visible in the Runtime Monitor details panel) shows a chronological record of all state transitions:
- Timestamp: When each transition occurred (millisecond precision)
- Transition label: "FromState -> ToState" with trigger port name
- Duration badge: Time spent in the previous state
- Blackboard snapshots: Click any transition to see the blackboard state at that moment
Diff Mode
Compare blackboard state between any two transitions:
- Enable Diff Mode in the timeline toolbar
- Right-click a transition to set it as the diff anchor (highlighted orange)
- Click another transition to compare
- View side-by-side changes: orange = changed, green = added, red = removed
Replay Controller
Step through recorded transition history to review state machine behavior:
| Control | Description |
|---|---|
| Play | Auto-play through transitions at configurable speed |
| Pause | Stop on the current transition |
| Step Forward | Advance one transition |
| Step Backward | Go back one transition |
| Return to Live | Exit replay and follow the live state |
| Speed | Cycle through 0.5x, 1x, 2x, 4x playback |
Transition History Export
Export the complete transition history to JSON for external analysis or bug reports:
- Click the export button in the Debug Timeline panel
- Choose a save location
- The JSON file includes: instance metadata, all transitions with timestamps, trigger ports, durations, and full blackboard snapshots at each point
{
"instanceId": "...",
"graphName": "PlayerStateMachine",
"ownerName": "Player",
"exportedAt": "2025-02-15T14:30:00",
"transitions": [
{
"timestamp": "2025-02-15T14:29:50",
"from": { "nodeId": "...", "stateName": "Idle" },
"to": { "nodeId": "...", "stateName": "Running" },
"triggerPort": "OnMove",
"durationFromPrevious": "0.500s",
"blackboard": { "Speed": "5.5", "Direction": "Forward" }
}
]
}
Performance
Update Modes
Configure how the state machine updates via the StateMachineRunner component:
| Mode | Description |
|---|---|
| Update | Ticks in MonoBehaviour.Update() (default) |
| FixedUpdate | Ticks in MonoBehaviour.FixedUpdate() for physics-driven states |
| LateUpdate | Ticks in MonoBehaviour.LateUpdate() for camera/follow states |
| Manual | No automatic ticking; call Instance.Update() yourself |
Set in the Inspector on StateMachineRunner, or in code:
runner.UpdateMode = UpdateMode.FixedUpdate;
Instance Pooling
StateMachinePool reduces garbage collection pressure by reusing state machine instances:
var pool = new StateMachinePool();
// Pre-warm during loading to avoid runtime allocation spikes
pool.Prewarm(enemyGraph, count: 10);
// Optional: cap pool size per graph (0 = unlimited)
pool.SetMaxPoolSize(enemyGraph, maxSize: 20);
// Rent an instance instead of creating one
var instance = pool.Rent(enemyGraph, context);
instance.Start();
// Return to pool when done instead of destroying
instance.Stop();
pool.Return(instance);
The pool automatically grows beyond the pre-warmed capacity when needed. Instances call PrepareForReuse() and OnReset() on all states before being returned, ensuring clean
reuse.
Pool events:
| Event | Description |
|---|---|
OnRented |
Instance rented (includes wasNewlyCreated flag) |
OnReturned |
Instance returned to pool |
OnPoolGrew |
Pool auto-grew beyond pre-warmed capacity |
Building for IL2CPP & WebGL
VSM3 supports both the Mono and IL2CPP scripting backends. The 0.4 release is validated with Windows IL2CPP and WebGL builds at Managed Stripping Level = High, with sample state machines (including states defined in their own assemblies) running end-to-end.
Managed code stripping is handled automatically
VSM3 instantiates states from type names stored in graph assets and binds ports by reflection — references the Unity linker cannot see, which would normally cause custom states to be stripped from IL2CPP builds. The package handles this for you with two mechanisms:
- A
link.xmlshipped inside the package preserves VSM3's own runtime assemblies. - A build-time processor scans your project for
Statesubclasses and[BlackboardType]types — including those in your own assemblies — and automatically generates preservation entries for them.
Your custom states and blackboard types survive any Managed Stripping Level with no
configuration required. You do not need to write a link.xml or add [Preserve]
attributes for VSM3 types.
If generation ever fails you will see [VSM] Failed to generate link.xml in the build log.
The build still completes (the package's own assemblies remain preserved), but custom
states may be stripped — please report it as a bug; adding your own link.xml covering
your state assemblies works as a stopgap.
Custom value-type blackboard variables under IL2CPP
Blackboard variables are backed by generic classes (BlackboardVariable<T>). IL2CPP
requires generic code to exist ahead of time, but Unity's full generic sharing (always
available on VSM3's minimum supported version, 2022.3) generates it automatically — custom
[BlackboardType] structs work out of the box. If an exotic platform configuration ever
reports missing AOT code for one of your value types, force generation with an explicit
instantiation hint anywhere in your code:
// Never called — exists only so IL2CPP generates the concrete generic code.
static void AotHint() => new BlackboardVariable<DamageInfo>();
Integrations
VSM3 integrations are optional assemblies that only compile when their dependencies are present. No configuration is needed - install the required Unity package and the integration activates automatically.
Addressables
Requires: com.unity.addressables package
Provides async asset loading and instantiation through Unity's Addressables system:
| State | Description | Key Inputs | Transitions |
|---|---|---|---|
| LoadAddressableState | Loads an addressable asset asynchronously | AssetReference |
Loaded, Failed |
| SpawnAddressableState | Instantiates an addressable prefab | AssetReference, Parent, Position |
Spawned, Failed |
| ReleaseAddressableState | Releases an addressable instance | Instance |
Done |
All addressable states support cancellation and report progress during loading.
Visual Scripting
Requires: com.unity.visualscripting package
Allows designers to build state logic using Unity's Visual Scripting graphs instead of C#:
VisualScriptingState - Executes a ScriptGraphAsset as the state body. The graph receives StateEnter, StateUpdate, and StateExit custom events. When SyncBlackboard is
enabled (default), blackboard variables are synced to graph variables on enter and synced back on exit.
Custom Units available inside Visual Script graphs:
| Unit | Category | Description |
|---|---|---|
| Complete State | VSM | Triggers the Done transition to exit the state |
| Get VSM Variable | VSM/Blackboard | Reads a variable from the state machine blackboard |
| Set VSM Variable | VSM/Blackboard | Writes a variable to the state machine blackboard |
API Reference
StateMachineBuilder
public class StateMachineBuilder
{
// Creation
static StateMachineBuilder Create(GameObject gameObject);
static StateMachineBuilder Create();
// States (AddState returns StateRef for positioning and chaining)
StateRef AddState<T>(string key = null);
StateRef AddState(Type stateType, string key = null);
StateMachineBuilder SetEntryState(string stateKey);
StateMachineBuilder SetEntryState<T>();
StateMachineBuilder SetEntryState(StateRef stateRef);
// Transitions
StateMachineBuilder AddTransition(string fromKey, string transitionName, string toKey, float delay = 0);
StateMachineBuilder AddTransition<TFrom, TTo>(string transitionName, float delay = 0);
// Variables
StateMachineBuilder AddVariable<T>(string key, T defaultValue = default, bool exposed = false);
// Context
StateMachineBuilder WithContext(IStateMachineContext context);
// Layout
StateMachineBuilder WithLayout(GraphLayoutMode mode);
StateMachineBuilder WithoutLayout();
// Build
StateMachineInstance Build();
StateMachineInstance BuildAndStart();
// Advanced
StateMachineGraph GetGraph();
StateNode GetStateNode(string key);
}
StateMachineRunner
public class StateMachineRunner : MonoBehaviour
{
// Initialize with a graph asset
void Initialize(StateMachineGraph graph);
void Initialize(StateMachineGraph graph, IStateMachineContext context);
// Control
void StartStateMachine();
void StopStateMachine();
void RestartStateMachine();
void PauseStateMachine();
void ResumeStateMachine();
// Configuration
UpdateMode UpdateMode { get; set; }
// Access
Blackboard Blackboard { get; }
StateMachineInstance Instance { get; }
StateMachineGraph Graph { get; set; }
bool IsRunning { get; }
State CurrentState { get; }
// Blackboard shortcuts
void SetBlackboardValue<T>(string key, T value);
T GetBlackboardValue<T>(string key, T defaultValue = default);
// Per-instance variable overrides (set via Inspector)
List<BlackboardOverride> VariableOverrides { get; }
// Events
event Action<State, State> OnStateChanged;
}
StateMachineController<TContext>
A non-MonoBehaviour helper for running state machines with a typed context. Use this when you want to embed a state machine inside an existing MonoBehaviour and share strongly-typed data with states.
When to use Controller vs Runner:
- Use
StateMachineRunnerwhen you want a quick drag-and-drop MonoBehaviour with Inspector support and exposed variable overrides - Use
StateMachineController<TContext>when you want to embed a state machine inside an existing MonoBehaviour and share typed context data with states
public class StateMachineController<TContext> where TContext : class
{
// Constructor
StateMachineController(StateMachineGraph graph, TContext context, GameObject owner = null);
// Control
void Start();
void Stop();
void Pause();
void Resume();
void Restart();
// Lifecycle - call from your MonoBehaviour
void Update();
void FixedUpdate();
void LateUpdate();
// Access
StateMachineInstance Instance { get; }
TContext Context { get; }
State CurrentState { get; }
Blackboard Blackboard { get; }
bool IsRunning { get; }
bool IsPaused { get; }
// Blackboard shortcuts
void SetBlackboardValue<T>(string key, T value);
T GetBlackboardValue<T>(string key, T defaultValue = default);
// Cleanup
void Dispose();
// Events
event Action<State, State> OnStateChanged;
event Action OnStarted;
event Action OnStopped;
}
// Non-generic version (no typed context)
public class StateMachineController : StateMachineController<object>
{
StateMachineController(StateMachineGraph graph, GameObject owner);
}
StateMachineInstance
public class StateMachineInstance
{
// Events
event Action<State, State> OnStateChanged;
event Action OnStarted;
event Action OnStopped;
event Action<string> OnExited; // Fired when reaching an Exit node
event Action<string, string> OnTransitionTriggered; // (fromNodeId, toNodeId)
// Properties
State CurrentState { get; }
string CurrentStateNodeId { get; }
Blackboard Blackboard { get; }
StateMachineGraph Graph { get; }
StateMachineInstance Parent { get; } // Parent if this is a sub-state machine
StateMachineInstance ActiveChildInstance { get; } // Active sub-state machine
bool IsRunning { get; }
bool IsPaused { get; }
bool IsInSubStateMachine { get; }
int NestingDepth { get; } // 0 for root, 1+ for nested
object SharedContext { get; } // For State<TContext> states
// Configuration
int MaxSameFrameTransitions { get; set; } // Default: 100
const int MaxSubStateMachineDepth = 16;
// Lifecycle
void Start();
Task StartAsync();
void Stop();
Task StopAsync();
void Pause();
void Resume();
// Update (call from MonoBehaviour)
void Update();
void FixedUpdate();
void LateUpdate();
Task UpdateAsync();
// Context
void SetSharedContext<TContext>(TContext context);
}
Blackboard
public class Blackboard
{
// Events
event Action<string, object, object> OnVariableChanged;
// Get/Set
T Get<T>(string key);
T Get<T>(string key, T defaultValue);
void Set<T>(string key, T value);
bool TryGet<T>(string key, out T value);
// Queries
bool HasKey(string key);
IEnumerable<string> GetAllKeys();
Type GetValueType(string key);
// Subscriptions
IDisposable Subscribe<T>(string key, Action<T, T> onChange);
// Hierarchy
void SetParent(Blackboard parent);
}
State
public abstract class State
{
// References
StateMachineInstance StateMachine { get; }
IStateMachineContext Context { get; }
Blackboard Blackboard { get; }
GameObject GameObject { get; }
Transform Transform { get; }
bool IsActive { get; }
// Synchronous lifecycle (override these)
virtual void OnEnter() { }
virtual void OnUpdate() { }
virtual void OnFixedUpdate() { }
virtual void OnLateUpdate() { }
virtual void OnExit() { }
virtual void OnReset() { } // Called when returning to pool
// Asynchronous lifecycle (override these)
virtual Task OnEnterAsync(CancellationToken ct) { }
virtual Task OnEnterAsync() { }
virtual Task OnUpdateAsync(CancellationToken ct) { }
virtual Task OnUpdateAsync() { }
virtual Task OnExitAsync(CancellationToken ct) { }
virtual Task OnExitAsync() { }
// Utilities
T GetComponent<T>() where T : Component;
T GetOrAddComponent<T>() where T : Component;
void Log(string message);
void LogWarning(string message);
void LogError(string message);
}
// Generic version with typed context
public abstract class State<TContext> : State where TContext : class
{
TContext SharedContext { get; }
Type RequiredContextType { get; }
bool HasValidContext { get; }
}
StateMachinePool
public class StateMachinePool
{
// Pre-warming
void Prewarm(StateMachineGraph graph, int count, IStateMachineContext context = null);
void SetMaxPoolSize(StateMachineGraph graph, int maxSize);
// Rent/Return
StateMachineInstance Rent(StateMachineGraph graph, IStateMachineContext context,
object sharedContext = null, StateMachineInstance parent = null);
void Return(StateMachineInstance instance);
// Stats
int TotalPooled { get; }
int TotalCreated { get; }
// Events
event Action<StateMachineInstance, bool> OnRented; // (instance, wasNewlyCreated)
event Action<StateMachineInstance> OnReturned;
event Action<StateMachineGraph, int> OnPoolGrew; // (graph, newSize)
}
Extending VSM3
The supported extension points in 0.4:
| Extension point | How |
|---|---|
| Custom states | Subclass State or State<TContext> — see Quick Start and Attributes |
| Custom blackboard types | [BlackboardType] on enums and serializable structs/classes — see Custom Blackboard Types |
| State node appearance | [StateStyle] and [StateInfo] attributes — see State Styling |
| Runtime graph appearance | RuntimeGraphTheme asset (Create → Visual State Machine → Runtime Graph Theme) for the world-space runtime graph view |
Custom state conventions
- One state class per file, named after the state. This keeps type resolution predictable and is required by the rename-safe resolution tooling planned for 0.5.
- Treat state class names, namespaces, and assemblies as part of your save format. Graphs currently reference states by assembly-qualified name, so renaming a state class (or moving it to another namespace or assembly) breaks existing graph references to it. Rename-safe, GUID-based resolution is planned for 0.5; until then, rename with care and re-assign affected nodes in the graph editor afterwards.
- Builds need no extra work. Custom states are preserved automatically under IL2CPP managed stripping — see Building for IL2CPP & WebGL.
What is intentionally internal in 0.4
The graph's node-type hierarchy (GraphNode subclasses), port kinds, and editor node views
are not extension points in 0.4 — custom node types serialized into user assets would
inherit the type-rename fragility described above. Opening these hierarchies is planned for
0.5, after rename-safe type resolution lands. If you need a custom node vocabulary today,
model it with custom states.
Testing Custom States
You can unit test state machines in Unity's EditMode tests using NUnit. The key pattern is to build a StateMachineGraph programmatically and run it through a
StateMachineInstance.
Test Setup
Create a test class with setup and teardown that manages the graph lifecycle:
using NUnit.Framework;
using Nonatomic.VSM.Core;
using Nonatomic.VSM.Graph;
using UnityEngine;
[TestFixture]
public class MyStateTests
{
private StateMachineGraph _graph;
private StateMachineInstance _instance;
[SetUp]
public void SetUp()
{
_graph = ScriptableObject.CreateInstance<StateMachineGraph>();
}
[TearDown]
public void TearDown()
{
_instance?.Stop();
_instance = null;
if (_graph != null)
{
Object.DestroyImmediate(_graph);
}
}
}
Building a Test Graph
Construct a graph with entry node, state nodes, and connections:
private void SetupGraph()
{
var entry = new EntryNode { Id = "entry" };
var stateA = new StateNode
{
Id = "stateA",
DisplayName = "State A",
StateTypeAssemblyQualifiedName = typeof(MyStateA).AssemblyQualifiedName
};
var exit = new ExitNode { Id = "exit", ExitName = "Done" };
_graph.AddNode(entry);
_graph.AddNode(stateA);
_graph.AddNode(exit);
_graph.EntryNodeId = "entry";
// Entry -> State A
_graph.AddConnection(new Connection
{
Id = "c1",
SourceNodeId = "entry",
SourcePortName = "out",
TargetNodeId = "stateA",
TargetPortName = "in",
ConnectionType = ConnectionType.Transition
});
}
Testing State Lifecycle
Create test states with counters to verify enter/update/exit calls:
using System;
using Nonatomic.VSM.Attributes;
using Nonatomic.VSM.Core;
private class TestState : State
{
public static int EnterCount;
public static int ExitCount;
[Transition("Next")] public Action next;
public override void OnEnter() => EnterCount++;
public override void OnExit() => ExitCount++;
public static void ResetCounters()
{
EnterCount = 0;
ExitCount = 0;
}
}
[Test]
public void Start_EntersFirstState()
{
TestState.ResetCounters();
SetupGraph();
_instance = new StateMachineInstance(_graph);
_instance.Start();
Assert.AreEqual(1, TestState.EnterCount);
}
Testing Transitions
Verify state transitions by invoking transition actions:
[Test]
public void Transition_MovesToNextState()
{
SetupGraph(); // Graph with StateA -> StateB connection
_instance = new StateMachineInstance(_graph);
_instance.Start();
// Trigger the transition
var currentState = _instance.CurrentState as TestStateA;
currentState?.next?.Invoke();
Assert.IsInstanceOf<TestStateB>(_instance.CurrentState);
}
Testing Blackboard Data Flow
Test that blackboard values are available to states:
[Test]
public void Blackboard_ValuesAccessibleFromInstance()
{
SetupGraph();
_instance = new StateMachineInstance(_graph);
_instance.Blackboard.Set("health", 100);
Assert.AreEqual(100, _instance.Blackboard.Get<int>("health"));
}
To test [DataIn] bindings, add blackboard variable nodes and data connections to your test graph:
// Add a blackboard variable
_graph.AddBlackboardVariable(new BlackboardVariableDefinition
{
Key = "speed",
ValueType = typeof(float),
DefaultValue = 5.0f
});
// Add a blackboard Get node
var bbNode = new BlackboardVariableNode
{
Id = "bbSpeed",
VariableKey = "speed",
Mode = BlackboardNodeMode.Get
};
_graph.AddNode(bbNode);
// Connect blackboard output to state input
_graph.AddConnection(new Connection
{
Id = "dataConn",
SourceNodeId = "bbSpeed",
SourcePortName = "value",
TargetNodeId = "stateA",
TargetPortName = "_speed",
ConnectionType = ConnectionType.BlackboardRead
});
Testing with the Builder API
For simpler test setups, use StateMachineBuilder:
[Test]
public void Builder_CreatesWorkingStateMachine()
{
var instance = StateMachineBuilder.Create()
.AddState<IdleState>()
.AddState<MoveState>()
.SetEntryState<IdleState>()
.AddTransition<IdleState, MoveState>("OnMove")
.BuildAndStart();
Assert.IsInstanceOf<IdleState>(instance.CurrentState);
instance.Stop();
}
Test Assembly Setup
Place EditMode tests in Tests/EditMode/ with an assembly definition referencing:
Nonatomic.VSM.Runtime(forState,StateMachineInstance, etc.)UnityEngine.TestRunnerUnityEditor.TestRunner
Sample Projects
The sample projects are maintained on this repository's samples branch while they are
polished for release. They are exercised against the package continuously — including the
IL2CPP/WebGL build validation — and will ship as Package Manager-importable samples in a
future release.
Troubleshooting
"Possible infinite loop detected"
You have a cycle of instant transitions. Add transition delays or break the cycle.
"Entry node has no outgoing transition"
The entry node must be connected to an initial state. If you've just updated the package, try deleting and recreating the connection from the Entry node.
State inputs are null or have default values
- Ensure the blackboard variable is set before the transition
- Check that the blackboard variable node is connected to the state's input port
- Verify the variable has a value set (either default in the graph or override in the runner)
- Mark the input as not required with a default value if it's optional
Exposed variables not appearing in Runner Inspector
- Toggle the "Expose" checkbox in the Blackboard panel
- Ensure the graph asset is assigned to the StateMachineRunner
- Check that the variable type is supported (primitives, Vector2/3, Color, UnityEngine.Object)
Graph not updating at runtime
Make sure you're calling Update() on the StateMachineInstance, or use StateMachineRunner which handles this automatically. Check that UpdateMode is not set to Manual unless
you intend to update manually.
"SubStateMachine nesting depth exceeded"
You have more than 16 levels of nested sub-state machines. This usually indicates a circular reference or overly complex hierarchy. Simplify your graph structure.
"Circular sub-state machine reference detected"
Graph A contains Graph B which contains Graph A (directly or indirectly). Each sub-graph must be unique in the parent chain. Restructure to avoid cycles.
Sub-state machine context type error
The sub-graph requires a typed context that's incompatible with the parent's context. Solutions:
- Change the sub-graph to use the same context type as parent
- Change the sub-graph to use a base type (e.g.,
MonoBehaviourinstead of specific class) - Use
Stateinstead ofState<TContext>in the sub-graph if context isn't needed
Portal node has no matching target
A Portal Out node's channel doesn't have a corresponding Portal In node. Add a Portal In node with the same channel ID, or delete the orphaned Portal Out.
Breakpoint not pausing the editor
- Check that breakpoints are globally enabled (toggle in the Breakpoint List panel)
- Verify the individual breakpoint is enabled (not gray)
- For conditional breakpoints, verify the expression syntax matches a supported format
- Breakpoints only work during play mode in the editor
Licence
VSM3 is released under the MIT licence. You're free to use it in personal or commercial projects, modify it, and redistribute it. Attribution is appreciated but not required.
Contributing
VSM3 is developed on the develop branch with feature work happening on feat/* branches via git worktrees. Open an issue to discuss bigger changes before sending a PR.
Coding standards: tabs (4-width), Allman braces, _camelCase private fields, one State class per .cs file (required for GUID-based type resolution), no #region directives. Full house style: https://github.com/PaulNonatomic/CodingStandards.