Blazor GameDev – part 9: introducing the Finite State Machine

Hi everyone! Welcome back to part 8 of our Blazor 2d Gamedev series. Today we’ll keep refactoring our last example, cleaning up the code using a Finite State Machine.

As usual, you can check out the results in your browser before moving on. Use left/right arrows to move the player and the space bar to attack.

So, what’s a Finite State Machine? It’s a computational machine, built from a finite list of states and the list of transitions between them.

An FSM can be exactly in only one of the states and can (possibly) transition into another one after receiving some particular input. This input can be state-specific or global. For more details, check out the excellent article on Wikipedia, it will give for sure a lot of food for thoughts.

Now, how can an FSM help us with our game? In a lot of ways actually. The first scenario is getting rid of all those nasty if/else blocks:

public override async ValueTask Update(GameContext game)
{
	var right = InputSystem.Instance.GetKeyState(Keys.Right);
	var left = InputSystem.Instance.GetKeyState(Keys.Left);

	if (right.State == ButtonState.States.Down)
	{
		_transform.Direction = Vector2.UnitX;
		_animationComponent.Animation = _animationsSet.GetAnimation("Run");
	}
	else if (left.State == ButtonState.States.Down)
	{
		_transform.Direction = -Vector2.UnitX;
		_animationComponent.Animation = _animationsSet.GetAnimation("Run");
	}
	else if (right.State == ButtonState.States.Up)
		_animationComponent.Animation = _animationsSet.GetAnimation("Idle");
	else if (left.State == ButtonState.States.Up)
		_animationComponent.Animation = _animationsSet.GetAnimation("Idle");
}

If you remember from our last article, that’s the code handling the animation switching. We’re going to replace that entirely with an FSM.

The idea is to have a State for each possible animation of our character (“idle”, “running”, “attack” and so on). Then we’ll define the possible transitions between them. A transition occurs when a specific condition is detected: for example, if we’re idle and our speed is > 0.1 then we transition to “running”.

At every update cycle, the current State will loop the list of its transactions and check the conditions for each one. If one of the predicates is satisfied, the transition occurs. We might even decide to get fancy and assign a weight to the transitions: this way if multiple predicates are true, we’ll pick the one with the highest weight.

Let’s take a look at the code now. This is the AnimationState class:

public class AnimationState
{
	private readonly List<Transition> _transitions;
	private readonly AnimationCollection.Animation _animation;

	public async ValueTask Update(AnimationController controller)
	{
		var transition = _transitions.FirstOrDefault(t => t.Check(controller));
		if(null != transition)
			controller.SetCurrentState(transition.To);
	}

	public void Enter(AnimatedSpriteRenderComponent animationComponent) =>
		animationComponent.Animation = _animation;
}

The Transition class instead looks more or less like this:

public class Transition
{
	private readonly IEnumerable<Func<AnimationController, bool>> _conditions;

	public Transition(AnimationState to, IEnumerable<Func<AnimationController, bool>> conditions)
	{
		To = to;
		_conditions = conditions ?? Enumerable.Empty<Func<AnimationController, bool>>();
	}

	public bool Check(AnimationController controller)
	{
		return _conditions.Any(c => c(controller));
	}

	public AnimationState To { get; }
}

As you can see, the list of conditions is a collection of predicates, accepting as input an instance of the AnimationController class, which represents our FSM:

public class AnimationController : BaseComponent
{
	private readonly IList<AnimationState> _states;
	private AnimationState _defaultState;
	private AnimationState _currentState;
	private readonly AnimatedSpriteRenderComponent _animationComponent;
	private readonly IDictionary<string, float> _floatParams;
	private readonly IDictionary<string, bool> _boolParams;

	public AnimationController(GameObject owner) : base(owner)
	{
		_states = new List<AnimationState>();
		_animationComponent = owner.Components.Get<AnimatedSpriteRenderComponent>() ??
							  throw new ComponentNotFoundException<AnimatedSpriteRenderComponent>();

		_floatParams = new Dictionary<string, float>();
		_boolParams = new Dictionary<string, bool>();
	}


	public void AddState(AnimationState state)
	{
		if (!_states.Any())
			_defaultState = state;
		_states.Add(state);
	}

	public override async ValueTask Update(GameContext game)
	{
		if (null == _currentState)
		{
			_currentState = _defaultState;
			_currentState.Enter(_animationComponent);
		}

		await _currentState.Update(this);
	}

	public void SetCurrentState(AnimationState state)
	{
		_currentState = state;
		_currentState?.Enter(_animationComponent);
	}

	public float GetFloat(string name) => _floatParams[name];

	public void SetFloat(string name, float value)
	{
		if(!_floatParams.ContainsKey(name))
			_floatParams.Add(name, 0f);
		_floatParams[name] = value;
	}

	public void SetBool(string name, in bool value)
	{
		if (!_boolParams.ContainsKey(name))
			_boolParams.Add(name, false);
		_boolParams[name] = value;
	}

	public bool GetBool(string name) => _boolParams[name];
}

The AnimationController holds the list of States, plus some custom parameters. We can use those to store some relevant information about the current state of the player. Later on, we can leverage them when creating the transition predicates:

var animationController = new AnimationController(warrior);
animationController.SetFloat("speed", 0f);

var idle = new AnimationState(animationCollection.GetAnimation("Idle"));
animationController.AddState(idle);

var run = new AnimationState(animationCollection.GetAnimation("Run"));
animationController.AddState(run);

idle.AddTransition(run,new Func<AnimationController, bool>[]
{
	ctrl => ctrl.GetFloat("speed") > .1f
});

run.AddTransition(idle, new Func<AnimationController, bool>[]
{
	ctrl => ctrl.GetFloat("speed") < .1f
});

That’s all for today. Next time we’ll see what a Scene Graph is and how we can use it to handle our Game Objects.

Blazor GameDev – part 9: introducing the Finite State Machine
Scroll to top