State machine for Unity that helps to create a clear game architecture. The conditions for transition are embedded within states to improve understanding of the state logic. Supports nested state machines. There are only one active state for each state machine, that means whenever a new state is entered, the previous one is exited.
- In Package Manager press
+
, selectAdd package from git URL
and pastehttps://github.com/natpuncher/states.git
- Or find the
manifest.json
file in thePackages
folder of your project and add the following line to dependencies section:
{
"dependencies": {
"com.npg.states": "https://github.com/natpuncher/states.git",
},
}
First, IStateFactory
interface should be implemented. This implementation will provide instances of your states to the StateMachine.
It could use a dependency injection or manage instances manually.
DI, recommended
public class StateFactory : IStateFactory
{
private readonly IFactory<IExitable> _factory;
public StateFactory(IFactory<IExitable> factory)
{
_factory = factory;
}
public TState GetState<TState>() where TState : class, IExitable
{
return _factory.Resolve<TState>();
}
}
Manual
public class StateFactory : IStateFactory
{
private readonly Dictionary<Type, IExitable> _states = new Dictionary<Type, IExitable>
{
{typeof(MyState1), new MyState1()},
{typeof(MyState2), new MyState2()},
};
public TState GetState<TState>() where TState : class, IExitable
{
return (TState)GetState(typeof(TState));
}
}
Than, the new type of state should be created by declaring an empty interface.
public interface IGameState
{
}
Next, create an implementation of StateMachine
with newly created state type,
so this StateMachine will only work with this type of states.
public class GameStateMachine : StateMachine<IGameState>
{
public GameStateMachine(StateFactory stateFactory) : base(stateFactory)
{
}
}
Now, create a new state by implementing IState
for a state life cycle
and your IGameState
to bind this state to the GameStateMachine
.
public class MyFirstGameState : IGameState, IState
{
public void Enter()
{
}
public void Exit()
{
}
}
To enter a new state call stateMachine.Enter<TState>()
. The previous active state will receive Exit
.
stateMachine.Enter<InitializeGameState>();
public class InitializeGameState : IGameState, IState
{
private readonly GameStateMachine _gameStateMachine;
public InitializeGameState(GameStateMachine gameStateMachine)
{
_gameStateMachine = gameStateMachine;
}
public void Enter()
{
// do initialization
_gameStateMachine.Enter<MetaGameState>();
}
public void Exit()
{
}
}
To pass arguments to a state on enter implement IPayloadedState<TPayloadType>
interface instead of IState
.
public class CoreGameState : IGameState, IPayloadedState<string>
{
public void Enter(string levelName)
{
Debug.Log(levelName);
}
public void Exit()
{
}
}
stateMachine.Enter<CoreGameState, string>(levelName);
Nested state machines can be used to better control on a different layers of your game.
public interface IMetaGameState
{
}
public class MetaGameStateMachine : StateMachine<IMetaGameState>
{
public MetaGameStateMachine(StateFactory stateFactory) : base(stateFactory)
{
}
}
public class MetaGameState : IGameState, IState
{
private readonly MetaGameStateMachine _metaGameStateMachine;
public MetaGameState(MetaGameStateMachine metaGameStateMachine)
{
_metaGameStateMachine = metaGameStateMachine;
}
public void Enter()
{
_metaGameStateMachine.Enter<HudMetaState>();
}
public void Exit()
{
_metaGameStateMachine.Dispose();
}
}
public class HudMetaState : IMetaGameState, IState
{
private readonly MetaGameStateMachine _metaGameStateMachine;
private readonly HudWindow _hudWindow;
public HudMetaState(MetaGameStateMachine metaGameStateMachine, HudWindow hudWindow)
{
_metaGameStateMachine = metaGameStateMachine;
_hudWindow = hudWindow;
}
public void Enter()
{
_hudWindow.OnInventoryButtonPressed -= GoToInventoryState;
_hudWindow.OnInventoryButtonPressed += GoToInventoryState;
_hudWindow.Show();
}
public void Exit()
{
_hudWindow.OnInventoryButtonPressed -= GoToInventoryState;
_hudWindow.Hide();
}
private void GoToInventoryState()
{
_metaGameStateMachine.Enter<InventoryMetaState>();
}
}
Calls on active state only.
IUpdatable -> stateMachine.Update()
ILateUpdatable -> stateMachine.LateUpdate()
IFixedUpdatable -> stateMachine.FixedUpdate()
public class CoreGameState : IGameState, IPayloadedState<string>, IFixedUpdatable
{
private readonly InputController _inputController;
public CoreGameState(InputController inputController)
{
_inputController = inputController;
}
public void Enter(string levelName)
{
}
public void FixedUpdate()
{
_inputController.PollInput();
}
public void Exit()
{
}
}
It is possible to enter previous state by calling stateMachine.Back()
.
It also provides right payloads for payloaded states.
Default back history buffer size is 1, that means it can do only one back transition.
public class InfoDialogMetaState : IMetaGameState, IPayloadedState<string>
{
private readonly MetaGameStateMachine _metaGameStateMachine;
private readonly InfoDialogWindow _infoDialogWindow;
public InfoDialogMetaState(MetaGameStateMachine metaGameStateMachine, InfoDialogWindow infoDialogWindow)
{
_metaGameStateMachine = metaGameStateMachine;
_infoDialogWindow = infoDialogWindow;
}
public void Enter(string dialogText)
{
_infoDialogWindow.OnAccept -= GoBack;
_infoDialogWindow.OnAccept += GoBack;
_infoDialogWindow.Show();
}
public void Exit()
{
_infoDialogWindow.OnAccept -= GoBack;
_infoDialogWindow.Hide();
}
private void GoBack()
{
_metaGameStateMachine.Back();
}
}
Back history buffer size could be increased.
public class GameStateMachine : StateMachine<IGameState>
{
public override int BackHistorySize => 5;
public GameStateMachine(StateFactory stateFactory) : base(stateFactory)
{
}
}
By default, StateMachine will logs every state transition.
[GameStateMachine] -> InitializeGameState
[GameStateMachine] InitializeGameState -> MetaGameState
[MetaGameStateMachine] -> HudMetaState
It can be changed or turned off by overriding StateChanged
method in the StateMachine implementation.
public class GameStateMachine : StateMachine<IGameState>
{
public GameStateMachine(StateFactory stateFactory) : base(stateFactory)
{
}
protected override void StateChanged(Type oldStateType, Type newStateType)
{
}
}
State changed notifications could be also received from stateMachine.OnStateChanged
event.
public class MetaGameState : IGameState, IState
{
private readonly MetaGameStateMachine _metaGameStateMachine;
private readonly FireworkEmitter _fireworkEmitter;
public MetaGameState(MetaGameStateMachine metaGameStateMachine, FireworkEmitter fireworkEmitter)
{
_metaGameStateMachine = metaGameStateMachine;
_fireworkEmitter = fireworkEmitter;
}
public void Enter()
{
_metaGameStateMachine.OnStateChanged -= MetaGameStateChanged;
_metaGameStateMachine.OnStateChanged += MetaGameStateChanged;
_metaGameStateMachine.Enter<HudMetaState>();
}
public void Exit()
{
_metaGameStateMachine.Dispose();
}
private void MetaGameStateChanged(Type previousStateType, Type nextStateType)
{
_fireworkEmitter.Emit();
}
}