Skip to content

State machine for Unity that helps to create a clear game architecture.

License

Notifications You must be signed in to change notification settings

natpuncher/states

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

48 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PRs Welcome

states

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.

Installation

  • In Package Manager press +, select Add package from git URL and paste https://github.com/natpuncher/states.git
  • Or find the manifest.json file in the Packages folder of your project and add the following line to dependencies section:
{
 "dependencies": {
    "com.npg.states": "https://github.com/natpuncher/states.git",
 },
}

Setup

Implement State Factory

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));
	}
}

Create a new state type

Than, the new type of state should be created by declaring an empty interface.

public interface IGameState
{
}

Implement StateMachine

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)
	{
	}
}

Create states

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()
	{
	}
}

Usage

Enter state

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()
	{
	}
}

Pass arguments

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

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>();
	}
}

Update

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()
	{
	}
}

Back

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)
	{
	}
}

State changed notifications

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();
	}
}

About

State machine for Unity that helps to create a clear game architecture.

Resources

License

Stars

Watchers

Forks

Languages