A flexible, modular, and easy-to-use state machine system for Unity that works with any enum-based state structure.
- ๐ Fully Generic - Works with any
enumtype - ๐ฏ Type-Safe - Compile-time type checking for states
- ๐ Fluent API - Chain methods for easy setup
- โก Three Callback Types - OnEnter, OnExit, and OnUpdate
- ๐ก Event System - Subscribe to state transitions
- ๐งฉ Modular Design - Reusable across any game object or system
- ๐ ๏ธ Unity Integration - Seamless MonoBehaviour integration
- ๐งช Test Tools Included - Ready-to-use testing utilities
-
Copy the following files to your Unity project:
StateMachine.csโ Core state machine logicCharacterStateManager.csโ Example implementationCharacterStateTester.csโ Testing utility
-
Place them in your project's scripts folder (e.g.,
Assets/_Project/Scripts/)
public enum CharacterState
{
Idle,
Walking,
Running,
Jumping
}using UnityEngine;
public class CharacterController : MonoBehaviour
{
private StateMachine<CharacterState> stateMachine;
private void Awake()
{
// Initialize with default state
stateMachine = new StateMachine<CharacterState>(CharacterState.Idle);
// Register states with callbacks
stateMachine.RegisterState(CharacterState.Idle)
.OnEnter(() => Debug.Log("Character is now idle"))
.OnExit(() => Debug.Log("Character stopped being idle"))
.OnUpdate(() => CheckForInput());
stateMachine.RegisterState(CharacterState.Walking)
.OnEnter(() => StartWalkAnimation())
.OnUpdate(() => MoveCharacter(walkSpeed));
stateMachine.RegisterState(CharacterState.Running)
.OnEnter(() => StartRunAnimation())
.OnUpdate(() => MoveCharacter(runSpeed));
}
private void Update()
{
// Update current state
stateMachine.UpdateState();
}
// Change state from anywhere
public void StartWalking()
{
stateMachine.ChangeState(CharacterState.Walking);
}
}// Check single state
if (stateMachine.IsInState(CharacterState.Idle))
{
// Do something
}
// Check multiple states
if (stateMachine.IsInAnyState(CharacterState.Walking, CharacterState.Running))
{
// Character is moving
}
// Access current state
CharacterState current = stateMachine.CurrentState;
CharacterState previous = stateMachine.PreviousState;StateMachine<TState>(TState initialState)CurrentState- Get the current active statePreviousState- Get the last active state
RegisterState(TState state)- Register a state and return a StateNode for configurationChangeState(TState newState, bool force = false)- Transition to a new stateUpdateState()- Update the current state (call in Update/FixedUpdate)IsInState(TState state)- Check if currently in a specific stateIsInAnyState(params TState[] states)- Check if in any of the provided states
OnStateChanged- Triggered when state changes (provides previous and new state)
Fluent API methods for configuring state callbacks:
RegisterState(MyState.Example)
.OnEnter(() => { /* Called once when entering state */ })
.OnExit(() => { /* Called once when exiting state */ })
.OnUpdate(() => { /* Called every frame while in state */ });public enum EnemyState { Idle, Patrol, Chase, Attack, Dead }
public class EnemyAI : MonoBehaviour
{
private StateMachine<EnemyState> ai;
private Transform player;
private void Awake()
{
ai = new StateMachine<EnemyState>(EnemyState.Idle);
ai.RegisterState(EnemyState.Patrol)
.OnEnter(() => SetRandomPatrolPoint())
.OnUpdate(() => PatrolBehavior());
ai.RegisterState(EnemyState.Chase)
.OnEnter(() => audioSource.Play(chaseSound))
.OnUpdate(() => ChasePlayer())
.OnExit(() => audioSource.Stop());
ai.RegisterState(EnemyState.Attack)
.OnEnter(() => animator.SetTrigger("Attack"))
.OnUpdate(() => AttackBehavior());
}
private void Update()
{
ai.UpdateState();
// State transition logic
if (ai.IsInState(EnemyState.Patrol) && PlayerInRange())
{
ai.ChangeState(EnemyState.Chase);
}
}
}public enum MenuState { MainMenu, Settings, Gameplay, Paused }
public class MenuManager : MonoBehaviour
{
private StateMachine<MenuState> menu;
private void Awake()
{
menu = new StateMachine<MenuState>(MenuState.MainMenu);
menu.RegisterState(MenuState.MainMenu)
.OnEnter(() => ShowPanel(mainMenuPanel))
.OnExit(() => HidePanel(mainMenuPanel));
menu.RegisterState(MenuState.Settings)
.OnEnter(() => ShowPanel(settingsPanel))
.OnExit(() => HidePanel(settingsPanel));
menu.RegisterState(MenuState.Gameplay)
.OnEnter(() => Time.timeScale = 1f)
.OnUpdate(() => CheckForPause());
menu.RegisterState(MenuState.Paused)
.OnEnter(() => {
Time.timeScale = 0f;
ShowPanel(pausePanel);
})
.OnExit(() => Time.timeScale = 1f);
}
}public enum VehicleState { Parked, Driving, Crashed, Repairing }
public class VehicleController : MonoBehaviour
{
private StateMachine<VehicleState> vehicle;
private void Awake()
{
vehicle = new StateMachine<VehicleState>(VehicleState.Parked);
vehicle.OnStateChanged += (from, to) =>
{
Debug.Log($"Vehicle: {from} โ {to}");
};
vehicle.RegisterState(VehicleState.Driving)
.OnEnter(() => StartEngine())
.OnUpdate(() => HandleDriving())
.OnExit(() => StopEngine());
vehicle.RegisterState(VehicleState.Crashed)
.OnEnter(() => {
DisableControls();
SpawnSmokeEffect();
});
}
}The system includes a comprehensive testing utility:
- Add
CharacterStateTestercomponent to your GameObject - Reference your state machine manager
- Use multiple test methods:
- ๐ฎ In-Game UI - Click buttons during play mode
- โจ๏ธ Keyboard Shortcuts -
1-4for states,Spaceto cycle - ๐ Inspector Context Menu - Right-click component for manual controls
- โฐ Auto Test Mode - Enable automatic state cycling
1 - Go to first state
2 - Go to second state
3 - Go to third state
4 - Go to fourth state
Space - Cycle through states
StateMachine<TState>
โโโ StateNode (per state)
โ โโโ OnEnter (Action)
โ โโโ OnExit (Action)
โ โโโ OnUpdate (Action)
โโโ CurrentState (TState)
โโโ PreviousState (TState)
โโโ OnStateChanged (Event)
- Initialize in Awake - Set up state machine before other components need it
- Call UpdateState() - Don't forget to call this in Update/FixedUpdate
- Use Events - Subscribe to
OnStateChangedfor global state tracking - Validate Transitions - Check current state before changing
- Clean Callbacks - Keep OnEnter/OnExit/OnUpdate methods focused and small
- State changes during
OnEnterorOnExitcallbacks are supported but use carefully UpdateState()must be called manually in your Update loop- Changing to the same state does nothing (unless
force = true) - All callbacks are optional - register only what you need
Contributions are welcome! Please feel free to submit a Pull Request.
This project is available under the MIT License.
Designed for modular and scalable Unity game development.
If you encounter any issues or have questions, please open an issue on GitHub.
Made with โค๏ธ for the Unity community