A lightweight and flexible state machine library for C# and Unity, designed for managing game logic flow.
After cloning this repository, run the following command to set up git hooks:
.githooks/install.shThis configures git to prevent direct pushes to the main branch. All changes must go through feature branches and pull requests.
StateMachineLib provides tools to structure application logic into distinct states and define transitions between them. It allows for multiple state graphs running concurrently, supports hierarchical state machines (subgraphs), dynamic graph attachment/detachment, and a local event system alongside global event integration.
// --- Core Setup & Lifecycle ---
StateMachine stateMachine = new StateMachine();
StateGraph mainGraph = stateMachine.CreateGraph(); // Creates a graph hosted by the StateMachine
StateUnit state1 = mainGraph.CreateState(); // Preferred: Creates a state. Name is managed by user if needed.
mainGraph.InitialUnit = state1; // Or first created state is initial by default.
// Activate/Deactivate a graph
// Graphs created via stateMachine.CreateGraph() or stateUnit.CreateGraph() are turned on by default.
mainGraph.SetTurnedOn(false); // Explicitly deactivate a graph
mainGraph.SetTurnedOn(true); // Reactivate it
stateMachine.Start(); // Enters initial state of all 'turned on' and 'powered' graphs.
stateMachine.UpdateMachine(); // Call in game loop (e.g., Unity's Update)
stateMachine.FixedUpdateMachine(); // Call in game loop (e.g., Unity's FixedUpdate)
stateMachine.LateUpdateMachine(); // Call in game loop (e.g., Unity's LateUpdate)
stateMachine.Exit(); // Exits all graphs and their current states. Power state is also turned off.
// Important: Call when StateMachine is no longer needed if not using a wrapper like StateMachineMB.
stateMachine.Dispose();
// --- Hierarchical Graphs (Subgraphs) ---
// StateUnits can host subgraphs because StateUnit implements IGraphHost
StateUnit parentStateHostingSubgraph = mainGraph.CreateState();
StateGraph subGraph = parentStateHostingSubgraph.CreateGraph(); // Create a new subgraph under parentStateHostingSubgraph
StateUnit childStateInSubgraph = subGraph.CreateState();
subGraph.InitialUnit = childStateInSubgraph;
// Subgraphs can also host their own subgraphs, creating deeper hierarchies.
StateGraph subSubGraph = childStateInSubgraph.CreateGraph();
// ...
// --- Dynamic Graph Management (Attach/Detach) ---
// Create a graph independently
StateGraph independentGraph = new StateGraph();
StateUnit someIndependentState = independentGraph.CreateState();
independentGraph.InitialUnit = someIndependentState;
// ... configure independentGraph ...
// Attach it to the StateMachine or a StateUnit
stateMachine.AttachGraph(independentGraph); // Becomes a top-level graph
// parentStateHostingSubgraph.AttachGraph(anotherIndependentGraph); // Attaches under the parentState
// Detach a graph (preserves its current state, makes it inactive, disconnects power)
stateMachine.DetachGraph(independentGraph);
// parentStateHostingSubgraph.DetachGraph(subGraph);
// --- Local Event System ---
// StateMachine and StateUnit (if hosting graphs) act as IGraphHost and have a LocalEventBus
stateMachine.LocalRaise(new MyGameEvent()); // Event propagates to graphs hosted by stateMachine
parentStateHostingSubgraph.LocalRaise(new MySubEvent()); // Event propagates to subGraph
// --- StateUnit Logic Actions (OnEnter, OnUpdate, OnExit, etc.) ---
state1.OnEnter = () => { /* Logic for state1 Enter */ };
state1.OnUpdate = (timeInState) => { /* Logic for state1 Update */ };
// ... (similarly for OnExit, OnFixedUpdate, OnLateUpdate)
// --- Time-Based Callbacks (within StateUnit) ---
state1.At(2.0f, () => { /* Action after 2 seconds in state1 */ });
state1.AtEvery(1.0f, () => { /* Action every 1 second in state1 */ });
// --- Event Listening (within StateUnit, for EventBus events) ---
// state.On<MyEvent>() listens to both global EventBus and relevant LocalEventBus events
state1.On<MyGameEvent>(evt => { /* Handle MyGameEvent */ });
// --- Fluent Transition Creation API (Examples) ---
(state1 > childStateInSubgraph).When(elapsedTime => elapsedTime > 1.0f);
(childStateInSubgraph > state1).On<MyGameEvent>();
(StateGraph.Any > state1).Immediately(); // Any-state transition within a graph- Hierarchical State Machines (Subgraphs):
StateUnits can host their ownStateGraphs (StateUnitimplementsIGraphHost).- This allows for deeply nested state logic (graphs within graphs).
- Dynamic Graph Management:
- Attach/Detach:
StateGraphs can be dynamically attached (AttachGraph()) to or detached (DetachGraph()) from anIGraphHost(likeStateMachineorStateUnit) at runtime. - Detaching preserves the graph's current state but makes it inactive. It can be re-attached later to resume.
- Graphs can be created independently and then attached to an existing state hierarchy.
- Attach/Detach:
- Activation Control (
SetTurnedOn):StateGraphs (andStateMachines) can be individually turned on/off usingSetTurnedOn(bool).- A graph only receives updates and processes events if it's turned on and its parent in the power hierarchy is also active.
- Local Event System (
LocalRaise):StateMachineandStateUnit(when acting asIGraphHost) provideLocalRaise<T>(T busEvent)to dispatch events within their own hierarchy.- Events propagate downwards to hosted graphs.
- Dual Event Listening:
StateUnit.On<TEvent>()and event-based transitions ((s1 > s2).On<TEvent>()) automatically listen to both globalEventBusevents and relevantLocalEventBusevents from their parentIGraphHost(s).
- Lifecycle Management (
Dispose):StateMachineshould be disposed viaDispose()when no longer needed (if not using a managed wrapper likeStateMachineMB) to clean up resources and event subscriptions.- Disposing a
StateMachine(or aStateUnithosting graphs) will also dispose of all its hosted graphs. - Detached graphs are no longer managed by their previous host; if not re-attached and referenced, they are eligible for GC.
- Simplified State Creation:
StateGraph.CreateState()is the preferred way to createStateUnits. - Rich Transition System: Fluent API for time-based, event-based, conditional, and immediate transitions.
- Parallel Graphs:
StateMachinecan manage multiple top-levelStateGraphs. - Time-Based Callbacks:
StateUnit.At()andStateUnit.AtEvery()for timed actions within a state.
-
StateMachine: The top-level container. It holds and manages one or moreStateGraphinstances, propagatingUpdate,LateUpdate, andFixedUpdatecalls to them.// In your MonoBehaviour or main logic class StateMachine stateMachine = new StateMachine();
-
StateGraph: Represents a single state machine graph. It manages a collection ofStateUnitinstances, keeps track of theCurrentUnit, and handles entering/exiting the graph and starting specific states.StateGraph mainGraph = stateMachine.CreateGraph(); // Creating states: StateUnit unnamedState = mainGraph.CreateState(); // Preferred: Creates a state (name will be null by default). // StateUnit namedState = mainGraph.CreateUnit("MyNamedState"); // Deprecated: Creates a state with a specific name.
-
StateUnit: Represents a single state within aStateGraph. You assign behavior to it by setting its variousActionproperties (e.g.,OnEnter,OnUpdate). The olderAction-suffixed fields (e.g.,EnterStateFunction) are now deprecated. It contains a list of potentialTransitionswhich are checked during itsUpdateloop.// Using preferred CreateState(): StateUnit idleState = mainGraph.CreateState(); // If you need to identify the state for debugging/logging, you can manage that externally or rely on its object reference. // idleState.Name will be null if created with CreateState(). // If using the deprecated CreateUnit(name) for legacy reasons or specific identification: // StateUnit namedIdleState = mainGraph.CreateUnit("Idle"); // namedIdleState.Name will be "Idle". idleState.OnEnter = () => Debug.Log("Idling..."); // Preferred way to assign actions idleState.OnUpdate = (timeInState) => { /* Do idle stuff. timeInState is DeltaTimeSinceStart */ }; // Preferred // Older, deprecated way of assigning actions: // idleState.EnterStateFunction = () => Debug.Log("Idling..."); // idleState.UpdateStateFunction = (timeInState) => { /* Do idle stuff. timeInState is DeltaTimeSinceStart */ };
-
IStateTransition: The interface for all transition types that are checked during theUpdateloop of aStateUnit. Defines theCheckTransitionmethod. (Event/Action based transitions trigger more directly).
A powerful feature of StateMachineLib is the ability to listen to EventBusLib events only while a specific state is active. This is achieved using the StateUnit.On methods. The older Listen method for events is now deprecated.
// Listen to all MyEvent events, but only while this state is active
// myState.Listen<MyEvent>(...); // Deprecated
myState.On<MyEvent>(evt => { // Preferred
Debug.Log($"MyEvent received in state: {myState.Name}");
});// Listen to MyEvent events with a specific parameter, only while this state is active
var query = EventBus<MyEvent>.Where<MyParam>(myValue);
// myState.Listen(query, ...); // Deprecated
myState.On(query, evt => { // Preferred
Debug.Log($"MyEvent with param received in state: {myState.Name}");
});Why is this important?
- The callback is only invoked if the state is currently active, so you don't need to manually unsubscribe or check state inside the handler.
- With the
EventQueryoverload for EventBus events, you can listen to only a subset of events (e.g., only those with a certain parameter value), making your state logic more precise and efficient.
The StateMachine class can manage multiple StateGraph instances simultaneously. Each graph runs independently but is updated by the main StateMachine update calls (UpdateMachine, FixedUpdateMachine, etc.). This is useful for managing distinct aspects of an object or system concurrently.
// In your setup
StateMachine characterStateMachine = new StateMachine();
// Graph for movement logic
StateGraph movementGraph = characterStateMachine.CreateGraph();
// ... define movement states and transitions ...
// Graph for combat logic
StateGraph combatGraph = characterStateMachine.CreateGraph();
// ... define combat states and transitions ...
// Start both graphs
characterStateMachine.Start();
// In Update loop, both graphs will be updated
void Update()
{
characterStateMachine.UpdateMachine();
}A StateUnit can contain its own nested StateGraph, allowing for more complex and organized state logic. When the parent StateUnit is active, its subgraph is also active and updated.
// In your setup
StateGraph mainGraph = stateMachine.CreateGraph();
StateUnit combatState = mainGraph.CreateState();
StateUnit patrollingState = mainGraph.CreateState();
// Method 1: Create subgraph directly (Recommended for simple cases)
StateGraph combatSubgraph = combatState.GetSubStateGraph(); // Automatically gets LocalEventBus support
// Method 2: Create subgraph elsewhere and attach (Flexible approach)
StateGraph detailedCombatGraph = new StateGraph(); // Created independently
StateUnit aimingState = detailedCombatGraph.CreateState();
StateUnit shootingState = detailedCombatGraph.CreateState();
// ... define transitions within detailedCombatGraph ...
combatState.SetSubStateGraph(detailedCombatGraph); // Automatically gets LocalEventBus support when attached
// Method 3: Manual StateMachine reference (Advanced usage)
StateGraph manualGraph = new StateGraph();
manualGraph.SetParentStateMachine(stateMachine); // Explicit LocalEventBus support
// ... setup states and transitions ...
// Define transition into the combat state using the fluent API
// (patrollingState > combatState).On<EnemyDetectedEvent>(); // Example using Fluent API
// ... other setup ...
stateMachine.Start();
// When patrollingState transitions to combatState:
// 1. combatState.OnEnter runs.
// 2. combatSubgraph.EnterGraph() runs, starting its initial state (e.g., aimingState).
// 3. While combatState is active, combatSubgraph is updated via combatState's Update/FixedUpdate/LateUpdate.
// 4. All subgraphs have access to the same LocalEventBus as the parent StateMachine.StateUnit provides convenient methods to schedule actions based on the time elapsed since the state became active (DeltaTimeSinceStart). These callbacks are automatically managed and reset if the state is re-entered.
Schedules an Action to be invoked once when DeltaTimeSinceStart reaches or exceeds targetTime. If the state is re-entered, the timer is reset, and the action can be invoked again after the specified targetTime.
Usage:
StateUnit preparingState = mainGraph.CreateState();
preparingState.OnEnter = () => Debug.Log("Preparing action..."); // Preferred
// After 3 seconds in PreparingState, log a message
preparingState.At(3.0f, () => {
Debug.Log("Preparation complete after 3 seconds!");
});Schedules an Action to be invoked repeatedly at specified intervalTime periods while the state is active. The first invocation occurs when DeltaTimeSinceStart reaches or exceeds the first intervalTime. If the state is re-entered, the interval timing is reset.
Usage:
StateUnit activeState = mainGraph.CreateState();
activeState.OnEnter = () => Debug.Log("ActiveState started. Monitoring..."); // Preferred
// Every 5 seconds, print a monitoring message
activeState.AtEvery(5.0f, () => {
Debug.Log($"Monitoring... (Time in state: {activeState.DeltaTimeSinceStart}s)");
});Both At and AtEvery ensure that the callback is only invoked if the state is currently active. The provided Action delegate should be parameterless. If you need to access state-specific data like DeltaTimeSinceStart or the StateUnit instance itself within the callback, you can do so via a lambda closure:
myState.At(2.5f, () => {
Debug.Log($"Action at {myState.DeltaTimeSinceStart} seconds in state {myState.Name}");
});For the easiest and most robust way to use StateMachineLib with Unity's MonoBehaviour lifecycle, use the CreateManagedStateMachine() extension method. This method handles all the necessary setup for automatic updates and lifecycle management, tied directly to your MonoBehaviour.
How to Use:
Call CreateManagedStateMachine(Action<StateMachine> setupCallback) from your MonoBehaviour (typically in Awake()) with a setup callback. The callback receives the StateMachine instance for configuration, and then the StateMachine is automatically started.
Key Benefits:
- Automatic Lifecycle Management:
- The
StateMachinestarts automatically when theMonoBehaviouris active. - It processes
Update,FixedUpdate, andLateUpdatecalls automatically. - It pauses if the
MonoBehaviouris disabled. - It correctly calls
Exit()on theStateMachinewhen theMonoBehaviouris destroyed, ensuring proper cleanup.
- The
- Simplified Code: Eliminates the need to manually call
Start(),UpdateMachine(), orExit()from yourMonoBehaviour. - Focus on Logic: Allows you to focus on defining your states and transitions rather than lifecycle boilerplate.
Usage Example:
using UnityEngine;
using Nopnag.StateMachineLib; // Required for StateMachine and the CreateManagedStateMachine() extension method
public class EnemyAI : MonoBehaviour
{
// Optional: Store as a class field if other methods need to access the StateMachine.
private StateMachine aiStateMachine;
void Awake()
{
// Create a lifecycle-managed StateMachine with a setup callback.
// The callback configures the StateMachine, then it's automatically started.
aiStateMachine = this.CreateManagedStateMachine(sm =>
{
StateGraph brain = sm.CreateGraph();
StateUnit patrolState = brain.CreateState();
patrolState.OnEnter = () => Debug.Log("Enemy: Starting patrol.");
// ... add patrol logic and transitions ...
StateUnit chaseState = brain.CreateState();
chaseState.OnEnter = () => Debug.Log("Enemy: Chasing player!");
// ... add chase logic and transitions ...
brain.InitialUnit = patrolState;
});
// All lifecycle calls (Start, Update, Exit, etc.) are handled automatically.
// The StateMachine is already started and ready to receive events!
}
}This extension method provides the most straightforward and recommended way to use StateMachineLib within Unity projects.
Important Notes:
- The setup callback is executed immediately, and then
StateMachine.Start()is called automatically. - If the
MonoBehaviouris initially disabled, theStart()call is deferred until the component is enabled. - This ensures that event-based transitions work correctly even when events are raised in the same frame as creation (e.g., in
Awake()).
(Internally, CreateManagedStateMachine() utilizes a StateMachineWrapper component on the GameObject to manage lifecycle and updates. This underlying mechanism ensures the described automatic behaviors, but you typically don't need to interact with this component directly.)
A fluent syntax is available for defining transitions directly from StateUnit instances. This API uses operator overloading (> and <) and chained method calls.
This fluent approach is designed with structs to minimize garbage generation during transition setup.
Initiating a Transition:
You can start defining a transition using the > operator between a source and a target state, or the < operator between a target and a source state. Both achieve the same result of setting up a transition from the source to the target.
// These are equivalent ways to start defining a transition from stateA to stateB:
var transitionAB = (stateA > stateB);
var transitionAlsoAB = (stateB < stateA); // (target < source) also configures source -> targetThis returns a configurator struct. You then chain one of the following methods to define the transition logic:
This defines a BasicTransition that triggers when the provided predicate returns true. The predicate receives the elapsed time in the source state.
StateUnit stateA = myGraph.CreateState();
StateUnit stateB = myGraph.CreateState();
// Transition from stateA to stateB when health is low after 1 second
(stateA > stateB).When(elapsedTime => player.Health < 10 && elapsedTime > 1.0f);
// Equivalent using the < operator
(stateB < stateA).When(elapsedTime => player.Health < 10 && elapsedTime > 1.0f);This defines a BasicTransition that triggers after a specific duration (in seconds) has passed since the source state was entered.
StateUnit loadingState = myGraph.CreateState();
StateUnit readyState = myGraph.CreateState();
// Transition from loadingState to readyState after 2.5 seconds
(loadingState > readyState).After(2.5f);This defines a TransitionByEvent. It has several overloads:
On<TEvent>(): Triggers when any event ofTEventis raised.(stateA > stateB).On<PlayerDiedEvent>();
On<TEvent>(Func<TEvent, bool> predicate): Triggers ifTEventis raised AND the predicate returnstrue.(stateA > stateB).On<EnemySpottedEvent>(evt => evt.IsHighPriority);
On<TEvent>(EventQuery<TEvent> query, Func<TEvent, bool> predicate = null): Triggers ifTEventmatching thequeryis raised. If apredicateis also provided, it must also returntrue.// Define a marker IParameter type for your specific query. public class ItemTag : IParameter { } // Event publishing (example of how the event would be set up elsewhere): // var collectedEvent = new ItemCollectedEvent(); // collectedEvent.Set<ItemTag>("KeyCard"); // Set the string value with ItemTag as type key // EventBus.Raise(collectedEvent); // Fluent transition setup: (stateA > stateB).On( EventBus<ItemCollectedEvent>.Where<ItemTag>("KeyCard"), // Filter by the string value "KeyCard" evt => evt.Collector.IsPlayer // Optional additional predicate on the event object );
This defines a TransitionByAction that triggers when the provided C# Action or Action<T> delegate (signal) is invoked. The signal must be passed with the ref keyword. (The heading shows the parameterless version; Action<TActionParam> is also supported).
public Action PlayerJumped;
public Action<int> PlayerScoredPoints;
// ... in setup ...
(groundedState > jumpingState).On(ref PlayerJumped);
(anyState > scoreCelebrationState).On(ref PlayerScoredPoints);
// ... elsewhere ...
PlayerJumped?.Invoke();
PlayerScoredPoints?.Invoke(100);This defines a DirectTransition that occurs unconditionally as soon as the source state is entered or updated, causing an immediate transition to the target state. It's useful for states that are purely transitional or serve as entry points that should immediately redirect.
StateUnit entryPointState = myGraph.CreateState();
StateUnit actualStartState = myGraph.CreateState();
// From entryPointState, immediately go to actualStartState
(entryPointState > actualStartState).Immediately();You can define transitions from a single state to one of several possible target states based on an index returned by a condition function. This is useful for decision points where the next state depends on dynamic criteria.
The When method, when used with an array of target StateUnits, expects its predicate to return an integer.
- If the integer is a valid index into the array of target states (0 to N-1), a transition to the state at that index occurs.
- If the integer is -1 (or any out-of-bounds negative number), no transition occurs.
StateUnit decisionState = myGraph.CreateState();
StateUnit optionAState = myGraph.CreateState();
StateUnit optionBState = myGraph.CreateState();
StateUnit optionCState = myGraph.CreateState();
// From decisionState, transition to one of the new[] { optionAState, ... } based on index
(decisionState > new[] { optionAState, optionBState, optionCState }).When(elapsedTime => {
// Assuming 'player' and 'PlayerChoices' are defined elsewhere
// and 'elapsedTime' is the time since 'decisionState' became active.
if (player.Choice == PlayerChoices.A) return 0; // Transition to optionAState
if (player.Choice == PlayerChoices.B) return 1; // Transition to optionBState
if (elapsedTime > 10.0f && player.IsIdle) return 2; // Transition to optionCState
return -1; // No transition
});This defines a ConditionalTransition where the target state is determined at runtime by the dynamicTargetPredicate.
You initiate this by transitioning from a state to the special StateGraph.DynamicTarget marker. The subsequent .When() method then takes a predicate of type Func<float, StateUnit>.
dynamicTargetPredicate: A function that receives the elapsed time in the source state and should return:- A non-null
StateUnitto transition to that state. nullto indicate that no transition should occur at this time.
- A non-null
StateUnit patrollingState = myGraph.CreateState();
StateUnit chasingState = myGraph.CreateState();
StateUnit investigatingState = myGraph.CreateState();
// From patrollingState, transition to a dynamically chosen state
(patrollingState > StateGraph.DynamicTarget).When(elapsedTime => {
if (CanSeePlayer()) return chasingState;
if (HeardNoise()) return investigatingState;
return null; // Stay in patrolling state
});Future methods (like for conditional transitions to a dynamically chosen single state) will be added to this fluent API.
This example demonstrates a character controller with Idle, Moving, Jumping, and Stunned states, using various transitions and the recommended CreateManagedStateMachine() for easy integration.
using Nopnag.StateMachineLib;
using Nopnag.EventBusLib; // For MyEvent, DamageTakenEvent etc.
using UnityEngine;
using System;
// --- Define Events used for Transitions (if not already globally defined) ---
// public class DamageTakenEvent : BusEvent { }
// public class JumpInputEvent : BusEvent { } // Example if using event for jump
public class CharacterController : MonoBehaviour
{
private Rigidbody rb;
private float jumpForce = 5f;
private float _stunDuration = 0.5f;
void Awake()
{
rb = GetComponent<Rigidbody>();
if (rb == null) rb = gameObject.AddComponent<Rigidbody>();
// Create a lifecycle-managed StateMachine with setup callback.
this.CreateManagedStateMachine(stateMachine =>
{
// --- Initialize States and Transitions ---
StateGraph movementGraph = stateMachine.CreateGraph();
// Define States
StateUnit idleState = movementGraph.CreateState();
StateUnit movingState = movementGraph.CreateState();
StateUnit jumpingState = movementGraph.CreateState();
StateUnit stunnedState = movementGraph.CreateState();
// Assign State Logic
idleState.OnEnter = () => {
Debug.Log("Entering Idle State");
};
idleState.OnUpdate = (timeInState) => { /* Maybe play idle animation. */ };
movingState.OnEnter = () => Debug.Log("Entering Moving State");
movingState.OnUpdate = (timeInState) =>
{
Vector3 moveDir = GetMovementInput();
rb.AddForce(moveDir * 10f * Time.deltaTime);
};
jumpingState.OnEnter = () =>
{
Debug.Log("Entering Jumping State");
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
};
stunnedState.OnEnter = () => Debug.Log("Entering Stunned State");
stunnedState.OnUpdate = (timeInState) => { /* Maybe play stunned animation. */ };
// Define Transitions (using Fluent API)
(idleState > stunnedState).On<DamageTakenEvent>();
(movingState > stunnedState).On<DamageTakenEvent>();
(jumpingState > stunnedState).On<DamageTakenEvent>();
(stunnedState > idleState).After(_stunDuration);
(idleState > movingState).When(elapsedTime => GetMovementInput().magnitude > 0.1f);
(movingState > idleState).When(elapsedTime => GetMovementInput().magnitude <= 0.1f);
(idleState > jumpingState).When(elapsedTime => Input.GetButtonDown("Jump"));
(movingState > jumpingState).When(elapsedTime => Input.GetButtonDown("Jump"));
(jumpingState > idleState).After(1.0f);
// Set Initial State
movementGraph.InitialUnit = idleState;
});
// Lifecycle (Start, Update, Exit) is handled automatically.
// The StateMachine is already running and ready to process events!
}
Vector3 GetMovementInput()
{
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
return new Vector3(horizontal, 0, vertical).normalized;
}
}Important: This package depends on Nopnag.EventBusLib. You need to install both packages for StateMachineLib to work correctly.
You can install these packages using the Unity Package Manager:
- Open the Package Manager (
Window>Package Manager). - Click the
+button in the top-left corner and selectAdd package from git URL.... - Enter the repository URL for EventBusLib:
https://github.com/cihanozcelik/EventBusLib.gitand clickAdd. - Repeat step 2.
- Enter the repository URL for StateMachineLib:
https://github.com/cihanozcelik/StateMachineLib.gitand clickAdd.
Alternatively, you can add both directly to your Packages/manifest.json file:
{
"dependencies": {
"com.nopnag.eventbuslib": "https://github.com/cihanozcelik/EventBusLib.git",
"com.nopnag.statemachinelib": "https://github.com/cihanozcelik/StateMachineLib.git",
// ... other dependencies
}
}