Skip to content

Repository files navigation

Visual State Machine V3

License: MITUnity 6+Status: Alpha

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

  • 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:

  1. Open Window > Package Manager
  2. Click + > Add package from git URL
  3. Enter:
https://www.pkglnk.dev/visualstatemachinev3.git

pkglnk

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:

usingSystem;usingNonatomic.VSM.Attributes;usingNonatomic.VSM.Core;usingUnityEngine;publicclassIdleState:State{[DataIn("Movement Input")]publicVector2MovementInput;[DataOut("Idle Duration")]publicfloatIdleDuration;[Transition("On Move")]publicActionOnMove;[Transition("On Jump")]publicActionOnJump;privatefloat_enterTime;publicoverridevoidOnEnter(){_enterTime=Time.time;Debug.Log("Entered Idle State");}publicoverridevoidOnUpdate(){IdleDuration=Time.time-_enterTime;if(MovementInput.magnitude>0.1f){OnMove?.Invoke();}}publicoverridevoidOnExit(){Debug.Log($"Exited Idle State after {IdleDuration:F2}s");}}

2. Create a State Machine Graph

  1. Right-click in the Project window
  2. Select Create > Visual State Machine > State Machine Graph
  3. Double-click the created asset to open the Graph Editor

3. Build Your Graph

  1. Right-click in the graph to add states
  2. Connect transition ports to define state flow
  3. Add an Entry node to define the starting state
  4. Optionally add Exit nodes for completion states

4. Run the State Machine

Add a StateMachineRunner component to a GameObject and assign your graph:

usingNonatomic.VSM.Core;usingNonatomic.VSM.Graph;usingUnityEngine;publicclassPlayerController:MonoBehaviour{[SerializeField]privateStateMachineGraph_graph;privateStateMachineRunner_runner;privatevoidStart(){_runner=gameObject.AddComponent<StateMachineRunner>();_runner.Initialize(_graph);_runner.StartStateMachine();}privatevoidUpdate(){varinput=newVector2(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:

publicclassExampleState:State{// --- Synchronous ---publicoverridevoidOnEnter(){}// Called when entering the statepublicoverridevoidOnUpdate(){}// Called every frame (per UpdateMode)publicoverridevoidOnFixedUpdate(){}// Called at fixed timesteppublicoverridevoidOnLateUpdate(){}// Called at end of framepublicoverridevoidOnExit(){}// Called when leaving the statepublicoverridevoidOnReset(){}// Called when returning to pool// --- Asynchronous (with cancellation support) ---publicoverrideTaskOnEnterAsync(CancellationTokenct){}publicoverrideTaskOnEnterAsync(){}publicoverrideTaskOnUpdateAsync(CancellationTokenct){}publicoverrideTaskOnUpdateAsync(){}publicoverrideTaskOnExitAsync(CancellationTokenct){}publicoverrideTaskOnExitAsync(){}}

Available properties within a state:

PropertyTypeDescription
StateMachineStateMachineInstanceThe parent state machine instance
BlackboardBlackboardShared key-value store
GameObjectGameObjectThe owning GameObject
TransformTransformThe owning Transform
ContextIStateMachineContextShared context object
IsActiveboolWhether the state is currently active

Utility methods:

MethodDescription
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")]publicfloatSpeed=5f;[DataIn]publicTransformTarget;

[DataOut]

Marks a field as an output port. Values are written to the Blackboard after OnExit().

[DataOut("Result")]publicfloatCalculatedValue;[DataOut]publicVector3FinalPosition;

[Transition]

Marks an Action field as a transition trigger. Invoke to transition to the connected state.

[Transition("On Complete",Color="#00FF00")]publicActionOnComplete;[Transition("On Fail")]publicActionOnFail;

[Exposed]

Marks a Blackboard variable for Inspector visibility.

// In your blackboard setup[Exposed("Player Health")]publicintHealth=100;

Blackboard

The Blackboard is a shared key-value store for passing data between states.

// Set a valueBlackboard.Set("playerHealth",100);Blackboard.Set("targetPosition",newVector3(10,0,5));// Get a valuevarhealth=Blackboard.Get<int>("playerHealth");varposition=Blackboard.Get<Vector3>("targetPosition");// Try get (safe)if(Blackboard.TryGet<int>("playerHealth",outvarhp)){Debug.Log($"Health: {hp}");}// Subscribe to changesBlackboard.Subscribe<int>("playerHealth",(oldValue,newValue)=>{Debug.Log($"Health changed from {oldValue} to {newValue}");});// Check if key existsif(Blackboard.HasKey("playerHealth")){// ...}

Blackboard Variable Nodes

You can visually connect blackboard variables to state inputs in the graph editor:

  1. Create a variable in the Blackboard panel
  2. Drag the variable onto the graph to create a Get node
  3. 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:

  1. In the Blackboard panel, toggle the "Expose" checkbox on a variable
  2. The variable will appear in the StateMachineRunner Inspector
  3. Each runner instance can override the default value
// Access overridden values at runtimevarrunner=GetComponent<StateMachineRunner>();// Values are automatically applied from overrides when the state machine starts// You can also set values programmatically:runner.SetBlackboardValue("Speed",10f);varspeed=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:

usingNonatomic.VSM.Attributes;[BlackboardType]publicenumGamePhase{Menu,Playing,Paused,GameOver}[BlackboardType(DisplayName="AI State",Category="AI")]publicenumAIBehavior{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:

usingNonatomic.VSM.Blackboard;usingNonatomic.VSM.Editor.Blackboard;[InitializeOnLoad]publicstaticclassExternalTypeRegistration{staticExternalTypeRegistration(){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:

usingSystem;usingNonatomic.VSM.Attributes;[Serializable][BlackboardType(Category="Combat")]publicstructDamageInfo{publicintAmount;publicfloatKnockback;}

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 graphpublicclassScoreState:State{[DataIn("Points")]publicintPoints;[DataOut("Total Score")]publicintTotalScore;publicoverridevoidOnEnter(){TotalScore+=Points;}}// Avoid: hidden dependency on a blackboard keypublicclassScoreState:State{publicoverridevoidOnEnter(){varpoints=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:

  1. Blackboard Variables - Shared data accessible from any state
  2. Port Connections - Visual connections between state outputs and inputs
  3. Optional Typed Context - Strongly-typed shared context object
// Access blackboard from within a statepublicclassDamageState:State{[DataIn]publicintDamageAmount;publicoverridevoidOnEnter(){varcurrentHealth=Blackboard.Get<int>("Health");Blackboard.Set("Health",currentHealth-DamageAmount);}}

Typed Context

For strongly-typed data sharing between states, use State<TContext> and StateMachineController<TContext>:

publicclassEnemy:MonoBehaviour{[SerializeField]privateStateMachineGraph_graph;privateStateMachineController<Enemy>_controller;// Context data - states access these via SharedContextpublicfloatHealth{get;set;}=100f;publicTransformTarget{get;set;}privatevoidAwake(){_controller=newStateMachineController<Enemy>(_graph,this);_controller.OnStateChanged+=(prev,next)=>Debug.Log($"State changed: {prev?.GetType().Name} -> {next?.GetType().Name}");_controller.Start();}privatevoidUpdate()=>_controller.Update();privatevoidFixedUpdate()=>_controller.FixedUpdate();privatevoidOnDestroy()=>_controller.Dispose();}

States access the typed context through SharedContext:

publicclassChaseState:State<Enemy>{[Transition("Reached")]publicActionOnReached;publicoverridevoidOnUpdate(){varenemy=SharedContext;vardirection=(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>:

publicclassEnemyRunner:StateMachineRunner<EnemyContext>{protectedoverrideEnemyContextCreateSharedContext(){returnnewEnemyContext{AlertLevel=0};}}

Sub-State Machines

Create hierarchical state machines by embedding one graph inside another using SubStateMachine nodes:

  1. Create a separate StateMachineGraph asset for the sub-state machine
  2. In the parent graph, right-click and add a Sub State Machine node
  3. Assign the sub-graph to the node
  4. Connect the node's Enter port from a transition and Exit port to continue flow

How It Works

When execution reaches a SubStateMachine node:

  1. The parent creates a child StateMachineInstance for the sub-graph
  2. The child runs independently (parent delegates Update calls to it)
  3. When the child reaches an Exit node, it fires OnExited
  4. The parent resumes execution from the SubStateMachine node's exit connection

Data Modes

Configure how the child accesses parent data:

ModeDescription
InheritChild blackboard inherits from parent (can read parent variables)
IsolatedChild has independent blackboard; use explicit input/output mappings
SharedChild 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 provides PlayerController)
  • 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

LimitValueDescription
Max Nesting Depth16Maximum levels of nested sub-state machines
Circular ReferencesBlockedGraph 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
  1. Open the sub-graph asset in the graph editor
  2. In the Blackboard panel, expand a variable to reveal its settings
  3. Set the Sub-Graph Port dropdown to control how it appears on the parent's SubStateMachine node:
DirectionDescription
NoneVariable is not exposed (default)
InputAppears as an input port — parent provides the value, child reads it
OutputAppears as an output port — child writes the value, parent reads it
InputOutputAppears 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):

  1. Values from connected parent blackboard variables are written to the child's blackboard
  2. If no connection exists, the inline value set on the SubStateMachine node is used instead

On child exit (output ports):

  1. 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

  1. Add a Portal Out node where you want to "jump from"
  2. Add a Portal In node where you want to "jump to"
  3. Set both nodes to the same Channel (color-coded for easy identification)
  4. 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 StateMachineInstancestateMachine.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:

usingNonatomic.VSM.Attributes;[StateStyle(Color=StateColors.Red)]publicclassCombatState:State{}[StateStyle(Color=StateColors.Blue)]publicclassIdleState:State{}[StateStyle(Color="#FF6B35")]// Custom hex colorpublicclassCustomState:State{}

Available Colors (StateColors class):

NameHexAliases
Grey#444444Gray
Red#990e23
Orange#B06101
LimeGreen#6d9111
Green#116f1c
ForestGreen#08704a
Teal#066670Cyan
LightBlue#037091
Blue#084870
Purple#4a0e99Indigo
Violet#740e99
Pink#750b55
Cantera#551C25Brown
Dijon#a89d46Yellow
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")]publicclassAttackState:State{}

Code-First State Machines

You can create state machines entirely in code using the fluent StateMachineBuilder API:

usingNonatomic.VSM.Core;usingUnityEngine;publicclassCodeBasedController:MonoBehaviour{privateStateMachineInstance_stateMachine;privatevoidStart(){_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();}privatevoidUpdate(){_stateMachine?.Update();}privatevoidOnDestroy(){_stateMachine?.Stop();}}

Builder Methods

MethodReturnsDescription
Create(gameObject)StateMachineBuilderCreates a builder with GameObject context
Create()StateMachineBuilderCreates a builder without GameObject
AddState<T>(key)StateRefAdds a state type with optional key (defaults to type name)
AddState(type, key)StateRefAdds a state type by Type reference
SetEntryState<T>()StateMachineBuilderSets which state to start in
SetEntryState(stateRef)StateMachineBuilderSets entry state from a StateRef
AddTransition<TFrom, TTo>(name, delay)StateMachineBuilderAdds a transition between state types
AddTransition(from, name, to, delay)StateMachineBuilderAdds a transition with string keys
AddVariable<T>(key, default, exposed)StateMachineBuilderAdds a blackboard variable
WithContext(context)StateMachineBuilderSets a custom context
WithLayout(mode)StateMachineBuilderSets layout mode: Hierarchical (default), Grid, or None
WithoutLayout()StateMachineBuilderDisables automatic layout
Build()StateMachineInstanceCreates the instance
BuildAndStart()StateMachineInstanceCreates and immediately starts the instance

Node Positioning

By default, the builder automatically arranges nodes using hierarchical layout. You can customize this:

// Manual positioningvaridle=builder.AddState<IdleState>().AtPosition(100,100);varmove=builder.AddState<MoveState>().AtPosition(400,100);varjump=builder.AddState<JumpState>().AtPosition(400,250);// Or disable auto-layout entirelybuilder.WithoutLayout();// Or use grid layout insteadbuilder.WithLayout(GraphLayoutMode.Grid);

Multiple Instances of the Same State

When you need multiple instances of the same state type, use custom keys and StateRef:

varbuilder=StateMachineBuilder.Create(gameObject);// Store references to statesvarpatrolA=builder.AddState<PatrolState>("patrol_a");varpatrolB=builder.AddState<PatrolState>("patrol_b");varchase=builder.AddState<ChaseState>();// Use references for type-safe transitionsvarsm=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:

varbuilder=StateMachineBuilder.Create(gameObject);varidle=builder.AddState<IdleState>();varmove=builder.AddState<MoveState>();varjump=builder.AddState<JumpState>();// Chain transitions from each stateidle.TransitionTo("OnMove",move).TransitionTo("OnJump",jump);move.TransitionTo("OnStop",idle);jump.TransitionTo("OnLand",idle);varsm=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

  1. Select a StateMachineGraph asset in the Project window
  2. Right-click and select Create Variant
  3. A new StateMachineGraphVariant asset is created referencing the base graph

Alternatively, use Create > Visual State Machine > State Machine Graph Variant from the Assets menu.

What Can Be Overridden

ElementOverride Options
NodesModify properties, replace state type, or disable entirely
ConnectionsOverride transition time, easing curve, or remove
Blackboard VariablesOverride 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]privateStateMachineGraphVariant_hardModeVariant;privatevoidStart(){varrunner=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

StateDescriptionKey InputsTransitions
DelayStateWaits for a duration before transitioningDuration, UseUnscaledTimeComplete
RandomDelayStateWaits for a random duration within a rangeMinDuration, MaxDurationComplete

Logic & Branching

StateDescriptionKey InputsTransitions
ConditionStateBranches on a boolean valueCondition, InvertTrue, False
CompareStateCompares two numeric valuesValueA, ValueB, OperatorTrue, False
CompareStringStateCompares two stringsValueA, ValueB, IgnoreCaseEqual, Not Equal
RandomBranchStateRandom two-way branch by probabilityProbabilityAPath A, Path B
RandomBranch3StateRandom three-way branch by weightWeightA, WeightB, WeightCPath A, Path B, Path C

Behavior Tree Patterns

StateDescriptionKey InputsTransitions
SequenceGateStateAND-gate: all conditions must be trueConditionA-DSuccess, Failure
SelectorGateStatePriority selector: transitions on first trueConditionA-DA, B, C, D, None
RepeaterStateLoop counter with configurable repeat countRepeatCount, InfiniteLoop, Done
CooldownStatePrevents re-entry for a cooldown durationDurationReady, On Cooldown
TimeoutStateFires timeout after a duration elapsesDurationTimeout

Animator

StateDescriptionKey InputsTransitions
SetAnimatorTriggerStateSets an Animator trigger parameterAnimator, TriggerNameDone
ResetAnimatorTriggerStateResets an Animator trigger parameterAnimator, TriggerNameDone
SetAnimatorBoolStateSets an Animator bool parameterAnimator, ParameterName, ValueDone
SetAnimatorFloatStateSets an Animator float parameterAnimator, ParameterName, ValueDone
SetAnimatorIntStateSets an Animator int parameterAnimator, ParameterName, ValueDone
SetAnimatorSpeedStateSets Animator playback speedAnimator, SpeedDone
SetAnimatorLayerWeightStateSets an Animator layer weightAnimator, Layer, WeightDone
GetAnimatorFloatStateReads an Animator float parameterAnimator, ParameterNameDone
GetAnimatorBoolStateReads an Animator bool and branchesAnimator, ParameterNameTrue, False
GetAnimatorIntStateReads an Animator int parameterAnimator, ParameterNameDone
PlayAnimatorStatePlays an Animator state directlyAnimator, StateNameDone
CrossFadeAnimatorStateCrossfades to an Animator stateAnimator, StateName, TransitionDurationDone
WaitForAnimatorStateStateWaits for Animator to reach a stateAnimator, StateNameComplete

Audio

StateDescriptionKey InputsTransitions
PlayAudioStatePlays an audio clip (fire and forget)Clip, AudioSource, VolumeDone
PlayAudioAndWaitStatePlays a clip and waits for it to finishClip, AudioSource, VolumeComplete
StopAudioStateStops audio playbackAudioSourceDone

Transform

StateDescriptionKey InputsTransitions
SetPositionStateSets a transform's positionTarget, Position, UseLocalSpaceDone
SetRotationStateSets a transform's rotationTarget, EulerAngles, UseLocalSpaceDone
SetScaleStateSets a transform's local scaleTarget, ScaleDone
LookAtStateMakes a transform look at a targetSource, TargetTransformDone
MoveToStateTranslates a transform over timeTarget, Destination, DurationComplete

GameObject

StateDescriptionKey InputsTransitions
SetActiveStateEnables or disables a GameObjectTarget, ActiveDone
SpawnPrefabStateInstantiates a prefabPrefab, Position, ParentDone
DestroyStateDestroys a GameObjectTarget, DelayDone
DestroyImmediateStateDestroys a GameObject immediatelyTargetDone

Particles

StateDescriptionKey InputsTransitions
PlayParticleStatePlays a ParticleSystem (fire and forget)ParticleSystem, WithChildrenDone
PlayParticleAndWaitStatePlays and waits for completionParticleSystem, WithChildrenComplete
StopParticleStateStops a ParticleSystemParticleSystem, Clear, StopBehaviorDone
SetParticleEmissionStateSets emission rateParticleSystem, Enabled, RateOverTimeDone

Timeline

StateDescriptionKey InputsTransitions
PlayTimelineStatePlays a Timeline and waits for completionAsset, Director, SpeedComplete
PauseTimelineStatePauses a playing timelineDirectorDone
ResumeTimelineStateResumes a paused timelineDirector, SpeedDone
StopTimelineStateStops a playing timelineDirectorDone
WaitForPlaybackTimeStateWaits for a specific playback timeDirector, TargetTimeReached

Input

StateDescriptionKey InputsTransitions
WaitForKeyStateWaits for a specific key pressKey, InputModePressed
WaitForAnyKeyStateWaits for any key press-Pressed
WaitForMouseButtonStateWaits for a mouse button clickButton, InputModeClicked

Dialog

StateDescriptionKey InputsTransitions
DialogStateDisplays dialog text, waits for player advanceText, Speaker, PortraitContinue
TimedDialogStateDisplays dialog, auto-advances after delayText, Speaker, DurationContinue
DialogChoiceStatePresents up to four choicesChoiceA-DChoice A-D
ClearDialogStateClears the dialog UIOnClear (UnityEvent)Done

Events

StateDescriptionKey InputsTransitions
InvokeEventStateInvokes a UnityEventEventDone
InvokeStringEventStateInvokes a UnityEvent<string>Event, ValueDone
InvokeFloatEventStateInvokes a UnityEvent<float>Event, ValueDone
InvokeIntEventStateInvokes a UnityEvent<int>Event, ValueDone

Utility

StateDescriptionKey InputsTransitions
LogStateLogs a message to the consoleMessage, LevelDone
DebugBreakStatePauses the editor for inspectionMessage, BreakOnceContinue

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

  1. Right-click on the graph canvas
  2. Select Add Node > Organization > Region
  3. 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

  1. Click the Peek button on any SubStateMachine node
  2. A floating preview popup appears showing a miniature view of the sub-graph
  3. Click Peek again (or press Escape) to close the popup

Preview Controls

ActionControl
PanDrag with left or middle mouse button
ZoomScroll wheel (0.25x to 4x)
RecenterClick the recenter button in the header or press F
Move popupDrag the popup header
CloseClick 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

  1. Right-click a state node in the graph editor
  2. Select Add Breakpoint - a red dot appears in the node's corner
  3. Enter play mode - the editor pauses when that state is entered, by any path

Transition breakpoints

  1. Right-click a transition edge
  2. Select Add Transition Breakpoint - the edge turns bold red with a red dot at its midpoint
  3. 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

TypeIndicatorPauses when
On EntryRed dot on the nodethe state is entered by any transition
On TransitionRed dot on the edgethe state is entered through that specific transition
ConditionalOrange dota condition expression evaluates to true on entry
DisabledGray dotthe 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:

  1. Enable Diff Mode in the timeline toolbar
  2. Right-click a transition to set it as the diff anchor (highlighted orange)
  3. Click another transition to compare
  4. View side-by-side changes: orange = changed, green = added, red = removed

Replay Controller

Step through recorded transition history to review state machine behavior:

ControlDescription
PlayAuto-play through transitions at configurable speed
PauseStop on the current transition
Step ForwardAdvance one transition
Step BackwardGo back one transition
Return to LiveExit replay and follow the live state
SpeedCycle through 0.5x, 1x, 2x, 4x playback

Transition History Export

Export the complete transition history to JSON for external analysis or bug reports:

  1. Click the export button in the Debug Timeline panel
  2. Choose a save location
  3. 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:

ModeDescription
UpdateTicks in MonoBehaviour.Update() (default)
FixedUpdateTicks in MonoBehaviour.FixedUpdate() for physics-driven states
LateUpdateTicks in MonoBehaviour.LateUpdate() for camera/follow states
ManualNo 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:

varpool=newStateMachinePool();// Pre-warm during loading to avoid runtime allocation spikespool.Prewarm(enemyGraph,count:10);// Optional: cap pool size per graph (0 = unlimited)pool.SetMaxPoolSize(enemyGraph,maxSize:20);// Rent an instance instead of creating onevarinstance=pool.Rent(enemyGraph,context);instance.Start();// Return to pool when done instead of destroyinginstance.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:

EventDescription
OnRentedInstance rented (includes wasNewlyCreated flag)
OnReturnedInstance returned to pool
OnPoolGrewPool 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.xml shipped inside the package preserves VSM3's own runtime assemblies.
  • A build-time processor scans your project for State subclasses 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.staticvoidAotHint()=>newBlackboardVariable<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:

StateDescriptionKey InputsTransitions
LoadAddressableStateLoads an addressable asset asynchronouslyAssetReferenceLoaded, Failed
SpawnAddressableStateInstantiates an addressable prefabAssetReference, Parent, PositionSpawned, Failed
ReleaseAddressableStateReleases an addressable instanceInstanceDone

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:

UnitCategoryDescription
Complete StateVSMTriggers the Done transition to exit the state
Get VSM VariableVSM/BlackboardReads a variable from the state machine blackboard
Set VSM VariableVSM/BlackboardWrites a variable to the state machine blackboard

API Reference

StateMachineBuilder

publicclassStateMachineBuilder{// CreationstaticStateMachineBuilderCreate(GameObjectgameObject);staticStateMachineBuilderCreate();// States (AddState returns StateRef for positioning and chaining)StateRefAddState<T>(stringkey=null);StateRefAddState(TypestateType,stringkey=null);StateMachineBuilderSetEntryState(stringstateKey);StateMachineBuilderSetEntryState<T>();StateMachineBuilderSetEntryState(StateRefstateRef);// TransitionsStateMachineBuilderAddTransition(stringfromKey,stringtransitionName,stringtoKey,floatdelay=0);StateMachineBuilderAddTransition<TFrom,TTo>(stringtransitionName,floatdelay=0);// VariablesStateMachineBuilderAddVariable<T>(stringkey,TdefaultValue=default,boolexposed=false);// ContextStateMachineBuilderWithContext(IStateMachineContextcontext);// LayoutStateMachineBuilderWithLayout(GraphLayoutModemode);StateMachineBuilderWithoutLayout();// BuildStateMachineInstanceBuild();StateMachineInstanceBuildAndStart();// AdvancedStateMachineGraphGetGraph();StateNodeGetStateNode(stringkey);}

StateMachineRunner

publicclassStateMachineRunner:MonoBehaviour{// Initialize with a graph assetvoidInitialize(StateMachineGraphgraph);voidInitialize(StateMachineGraphgraph,IStateMachineContextcontext);// ControlvoidStartStateMachine();voidStopStateMachine();voidRestartStateMachine();voidPauseStateMachine();voidResumeStateMachine();// ConfigurationUpdateModeUpdateMode{get;set;}// AccessBlackboardBlackboard{get;}StateMachineInstanceInstance{get;}StateMachineGraphGraph{get;set;}boolIsRunning{get;}StateCurrentState{get;}// Blackboard shortcutsvoidSetBlackboardValue<T>(stringkey,Tvalue);TGetBlackboardValue<T>(stringkey,TdefaultValue=default);// Per-instance variable overrides (set via Inspector)List<BlackboardOverride>VariableOverrides{get;}// EventseventAction<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 StateMachineRunner when 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
publicclassStateMachineController<TContext>whereTContext:class{// ConstructorStateMachineController(StateMachineGraphgraph,TContextcontext,GameObjectowner=null);// ControlvoidStart();voidStop();voidPause();voidResume();voidRestart();// Lifecycle - call from your MonoBehaviourvoidUpdate();voidFixedUpdate();voidLateUpdate();// AccessStateMachineInstanceInstance{get;}TContextContext{get;}StateCurrentState{get;}BlackboardBlackboard{get;}boolIsRunning{get;}boolIsPaused{get;}// Blackboard shortcutsvoidSetBlackboardValue<T>(stringkey,Tvalue);TGetBlackboardValue<T>(stringkey,TdefaultValue=default);// CleanupvoidDispose();// EventseventAction<State,State>OnStateChanged;eventActionOnStarted;eventActionOnStopped;}// Non-generic version (no typed context)publicclassStateMachineController:StateMachineController<object>{StateMachineController(StateMachineGraphgraph,GameObjectowner);}

StateMachineInstance

publicclassStateMachineInstance{// EventseventAction<State,State>OnStateChanged;eventActionOnStarted;eventActionOnStopped;eventAction<string>OnExited;// Fired when reaching an Exit nodeeventAction<string,string>OnTransitionTriggered;// (fromNodeId, toNodeId)// PropertiesStateCurrentState{get;}stringCurrentStateNodeId{get;}BlackboardBlackboard{get;}StateMachineGraphGraph{get;}StateMachineInstanceParent{get;}// Parent if this is a sub-state machineStateMachineInstanceActiveChildInstance{get;}// Active sub-state machineboolIsRunning{get;}boolIsPaused{get;}boolIsInSubStateMachine{get;}intNestingDepth{get;}// 0 for root, 1+ for nestedobjectSharedContext{get;}// For State<TContext> states// ConfigurationintMaxSameFrameTransitions{get;set;}// Default: 100constintMaxSubStateMachineDepth=16;// LifecyclevoidStart();TaskStartAsync();voidStop();TaskStopAsync();voidPause();voidResume();// Update (call from MonoBehaviour)voidUpdate();voidFixedUpdate();voidLateUpdate();TaskUpdateAsync();// ContextvoidSetSharedContext<TContext>(TContextcontext);}

Blackboard

publicclassBlackboard{// EventseventAction<string,object,object>OnVariableChanged;// Get/SetTGet<T>(stringkey);TGet<T>(stringkey,TdefaultValue);voidSet<T>(stringkey,Tvalue);boolTryGet<T>(stringkey,outTvalue);// QueriesboolHasKey(stringkey);IEnumerable<string>GetAllKeys();TypeGetValueType(stringkey);// SubscriptionsIDisposableSubscribe<T>(stringkey,Action<T,T>onChange);// HierarchyvoidSetParent(Blackboardparent);}

State

publicabstractclassState{// ReferencesStateMachineInstanceStateMachine{get;}IStateMachineContextContext{get;}BlackboardBlackboard{get;}GameObjectGameObject{get;}TransformTransform{get;}boolIsActive{get;}// Synchronous lifecycle (override these)virtualvoidOnEnter(){}virtualvoidOnUpdate(){}virtualvoidOnFixedUpdate(){}virtualvoidOnLateUpdate(){}virtualvoidOnExit(){}virtualvoidOnReset(){}// Called when returning to pool// Asynchronous lifecycle (override these)virtualTaskOnEnterAsync(CancellationTokenct){}virtualTaskOnEnterAsync(){}virtualTaskOnUpdateAsync(CancellationTokenct){}virtualTaskOnUpdateAsync(){}virtualTaskOnExitAsync(CancellationTokenct){}virtualTaskOnExitAsync(){}// UtilitiesTGetComponent<T>()whereT:Component;TGetOrAddComponent<T>()whereT:Component;voidLog(stringmessage);voidLogWarning(stringmessage);voidLogError(stringmessage);}// Generic version with typed contextpublicabstractclassState<TContext>:StatewhereTContext:class{TContextSharedContext{get;}TypeRequiredContextType{get;}boolHasValidContext{get;}}

StateMachinePool

publicclassStateMachinePool{// Pre-warmingvoidPrewarm(StateMachineGraphgraph,intcount,IStateMachineContextcontext=null);voidSetMaxPoolSize(StateMachineGraphgraph,intmaxSize);// Rent/ReturnStateMachineInstanceRent(StateMachineGraphgraph,IStateMachineContextcontext,objectsharedContext=null,StateMachineInstanceparent=null);voidReturn(StateMachineInstanceinstance);// StatsintTotalPooled{get;}intTotalCreated{get;}// EventseventAction<StateMachineInstance,bool>OnRented;// (instance, wasNewlyCreated)eventAction<StateMachineInstance>OnReturned;eventAction<StateMachineGraph,int>OnPoolGrew;// (graph, newSize)}

Extending VSM3

The supported extension points in 0.4:

Extension pointHow
Custom statesSubclass 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 appearanceRuntimeGraphTheme 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:

usingNUnit.Framework;usingNonatomic.VSM.Core;usingNonatomic.VSM.Graph;usingUnityEngine;[TestFixture]publicclassMyStateTests{privateStateMachineGraph_graph;privateStateMachineInstance_instance;[SetUp]publicvoidSetUp(){_graph=ScriptableObject.CreateInstance<StateMachineGraph>();}[TearDown]publicvoidTearDown(){_instance?.Stop();_instance=null;if(_graph!=null){Object.DestroyImmediate(_graph);}}}

Building a Test Graph

Construct a graph with entry node, state nodes, and connections:

privatevoidSetupGraph(){varentry=newEntryNode{Id="entry"};varstateA=newStateNode{Id="stateA",DisplayName="State A",StateTypeAssemblyQualifiedName=typeof(MyStateA).AssemblyQualifiedName};varexit=newExitNode{Id="exit",ExitName="Done"};_graph.AddNode(entry);_graph.AddNode(stateA);_graph.AddNode(exit);_graph.EntryNodeId="entry";// Entry -> State A_graph.AddConnection(newConnection{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:

usingSystem;usingNonatomic.VSM.Attributes;usingNonatomic.VSM.Core;privateclassTestState:State{publicstaticintEnterCount;publicstaticintExitCount;[Transition("Next")]publicActionnext;publicoverridevoidOnEnter()=>EnterCount++;publicoverridevoidOnExit()=>ExitCount++;publicstaticvoidResetCounters(){EnterCount=0;ExitCount=0;}}[Test]publicvoidStart_EntersFirstState(){TestState.ResetCounters();SetupGraph();_instance=newStateMachineInstance(_graph);_instance.Start();Assert.AreEqual(1,TestState.EnterCount);}

Testing Transitions

Verify state transitions by invoking transition actions:

[Test]publicvoidTransition_MovesToNextState(){SetupGraph();// Graph with StateA -> StateB connection_instance=newStateMachineInstance(_graph);_instance.Start();// Trigger the transitionvarcurrentState=_instance.CurrentStateasTestStateA;currentState?.next?.Invoke();Assert.IsInstanceOf<TestStateB>(_instance.CurrentState);}

Testing Blackboard Data Flow

Test that blackboard values are available to states:

[Test]publicvoidBlackboard_ValuesAccessibleFromInstance(){SetupGraph();_instance=newStateMachineInstance(_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(newBlackboardVariableDefinition{Key="speed",ValueType=typeof(float),DefaultValue=5.0f});// Add a blackboard Get nodevarbbNode=newBlackboardVariableNode{Id="bbSpeed",VariableKey="speed",Mode=BlackboardNodeMode.Get};_graph.AddNode(bbNode);// Connect blackboard output to state input_graph.AddConnection(newConnection{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]publicvoidBuilder_CreatesWorkingStateMachine(){varinstance=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 (for State, StateMachineInstance, etc.)
  • UnityEngine.TestRunner
  • UnityEditor.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., MonoBehaviour instead of specific class)
  • Use State instead of State<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.

About

A visual state machine editor and runtime for Unity: graph-based authoring, a code-first builder API, blackboard data flow, sub-state machines, async states, breakpoint debugging, and IL2CPP/WebGL support.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages