In my last article about Finite State Machines (FSM) for Unity, I showed a “pure code” way to create a state machine, states, and transitions between those states. It worked, but I wanted to create a simpler system. I’ll show you it today!

Today’s FSM design isn’t a library like the last one. Instead, it’s more of a pattern that you can use in your apps. The reason is that it’s so simple that there wouldn’t be much a library if it were one.

The FSM is essentially just a coroutine, similar to the library from the previous article. The current state is just an iterator function, which is represented by an IEnumerable. The state machine looks like this:

public class TestScript : MonoBehaviour
{
	private IEnumerable state;
 
	private void Start()
	{
		// TODO set initial state here
 
		// Run the state machine
		StartCoroutine(RunStateMachine());
	}
 
	public IEnumerator RunStateMachine()
	{
		while (state != null)
		{
			foreach (var cur in state)
			{
				yield return cur;
			}
		}
	}
}

This one coroutine is designed to loop forever since there should always be a valid state. It simply runs that state and yields whatever it yielded. To make a state, just make an iterator function! For example, here’s a really simple state that just prints a debug log every second:

private IEnumerable DebugLogState(string message)
{
	while (true)
	{
		yield return new WaitForSeconds(1);
		Debug.Log(message);
	}
}

Then you could plug this into the Start function as your initial state:

private void Start()
{
	// Start in the debug log state
	state = DebugLogState("hello");
 
	// Run the state machine
	StartCoroutine(RunStateMachine());
}

Now you need to switch states at some point, so let’s introduce another state that displays the high score for a multiplayer game every minute. If there’s ever a problem getting the high score, we switch back to the debug log state and display the error.

private IEnumerable HighScoreState()
{
	while (true)
	{
		// Fetch the high score
		var before = Time.time;
		var www = new WWW("http://my.server.com/api/highscore");
		yield return www;
		var elapsed = Time.time - before;
 
		// If successful, display on UI and wait until the next minute
		if (string.IsNullOrEmpty(www.error))
		{
			highscore.text = "High Score: " + www.text;
			yield return new WaitForSeconds(60 - elapsed);
		}
		// If failure, show the error on the debug log state
		else
		{
			state = DebugLogState(www.error);
		}
	}
}

Note how each state is self-contained in its own iterator function. We can pass parameters to other states just by passing parameters to that state’s iterator function. To set a state, we just set the state field to the return value of the next state’s iterator function.

So how about transitions? Those too are simply an iterator function. Here’s a really simple one that just inserts a delay:

private IEnumerable DelayTransition(float seconds)
{
	yield return new WaitForSeconds(seconds);
}

To use it, just loop over it yielding whatever it yielded. We’ve already seen that code in RunStateMachine, so it should look familiar:

// "Transition" going to the new state
foreach (var cur in DelayTransition(2))
{
	yield return cur;
}
 
// Go to the new state
state = DebugLogState(www.error);

That’s basically all there is to the state machine. States and transitions are just iterator functions and switching states is just setting the state field. It’s vastly simpler than the previous version!

To demonstrate, I’ve recreated the example from the previous article using this new FSM approach. It has the same “main menu” and “play” states as before and uses the same “fade” transition.

using System.Collections;
using System.Collections.Generic;
 
using UnityEngine;
using UnityEngine.UI;
 
public class TestScript : MonoBehaviour
{
	private IEnumerable state;
 
	private void Start()
	{
		// Start in the main menu state
		IEnumerable fadeOut;
		IEnumerable fadeIn;
		CreateFades(2, out fadeOut, out fadeIn);
		state = MainMenuState(fadeIn);
 
		// Run the state machine
		StartCoroutine(RunStateMachine());
	}
 
	public IEnumerator RunStateMachine()
	{
		while (state != null)
		{
			foreach (var cur in state)
			{
				yield return cur;
			}
		}
	}
 
	private IEnumerable MainMenuState(IEnumerable fadeIn)
	{
		// Create the main menu UI
		var canvasPrefab = Resources.Load<Canvas>("MainMenu");
		var canvas = UnityEngine.Object.Instantiate(canvasPrefab);
		var playButtonGO = canvas.transform.Find("PlayButton");
		var playButton = playButtonGO.GetComponent<Button>();
		var frameCountGO = canvas.transform.Find("FrameCount");
		var frameCount = frameCountGO.GetComponent<Text>();
 
		// Fade in
		foreach (var cur in fadeIn)
		{
			yield return cur;
		}
 
		// Update the frame count until we transition
		var initialFrame = Time.frameCount;
		var running = true;
		playButton.onClick.AddListener(() => running = false);
		while (running)
		{
			var numFrames = Time.frameCount - initialFrame;
			frameCount.text = "Frames spent on menu: " + numFrames;
			yield return null;
		}
 
		// Fade out
		IEnumerable fadeOut;
		IEnumerable nextFadeIn;
		CreateFades(2, out fadeOut, out nextFadeIn);
		foreach (var cur in fadeOut)
		{
			yield return cur;
		}
 
		// Clean up the UI
		UnityEngine.Object.Destroy(canvas.gameObject);
 
		// Go to the play state
		state = PlayState(nextFadeIn);
	}
 
	private IEnumerable PlayState(IEnumerable fadeIn)
	{
		// Set up the targets
		var targetsContainer = new GameObject("TargetsContainer");
		var targetPrefab = Resources.Load<GameObject>("Target");
		var targets = new List<GameObject>(3);
		for (var i = 0; i < targets.Capacity; ++i)
		{
			var target = UnityEngine.Object.Instantiate(targetPrefab);
			target.transform.parent = targetsContainer.transform;
			target.transform.position += new Vector3(i*2, 0, 0);
			targets.Add(target);
		}
 
		// Fade in
		foreach (var cur in fadeIn)
		{
			yield return cur;
		}
 
		// Turn the targets green to indicate that they're ready to be clicked
		foreach (var target in targets)
		{
			SetTargetColor(target, Color.green);
		}
 
		// Handle clicks until the player has clicked on all three targets
		var running = true;
		while (running)
		{
			if (Input.GetMouseButtonDown(0))
			{
				var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
				RaycastHit hitInfo;
				if (Physics.Raycast(ray, out hitInfo))
				{
					foreach (var target in targets)
					{
						if (target != null && hitInfo.transform == target.transform)
						{
							SetTargetColor(target, Color.red);
							targets.Remove(target);
							running = targets.Count > 0;
							break;
						}
					}
				}
			}
			yield return null;
		}
 
		// Fade out
		IEnumerable fadeOut;
		IEnumerable nextFadeIn;
		CreateFades(2, out fadeOut, out nextFadeIn);
		foreach (var cur in fadeOut)
		{
			yield return cur;
		}
 
		// Clean up the targets
		UnityEngine.Object.Destroy(targetsContainer);
 
		// Go to the main menu state
		state = MainMenuState(nextFadeIn);
	}
 
	private static void SetTargetColor(GameObject target, Color color)
	{
		var renderer = target.GetComponent<Renderer>();
		renderer.material.color = color;
	}
 
	public static void CreateFades(float fadeTime, out IEnumerable fadeOut, out IEnumerable fadeIn)
	{
		var screenFadePrefab = Resources.Load<Canvas>("ScreenFade");
		var canvas = UnityEngine.Object.Instantiate(screenFadePrefab);
		var coverGO = canvas.transform.Find("Cover");
		var cover = coverGO.GetComponent<Image>();
		fadeOut = FadeOut(cover, fadeTime);
		fadeIn = FadeIn(cover, fadeTime, canvas);
	}
 
	private static IEnumerable FadeOut(Image cover, float fadeTime)
	{
		foreach (var cur in TweenAlpha(cover, 0, 1, fadeTime / 2))
		{
			yield return cur;
		}
	}
 
	private static IEnumerable FadeIn(Image cover, float fadeTime, Canvas canvas)
	{
		foreach (var cur in TweenAlpha(cover, 1, 0, fadeTime / 2))
		{
			yield return cur;
		}
		UnityEngine.Object.Destroy(canvas.gameObject);
	}
 
	private static IEnumerable TweenAlpha(
		Image image,
		float fromAlpha,
		float toAlpha,
		float duration
	)
	{
		var startTime = Time.time;
		var endTime = startTime + duration;
		while (Time.time < endTime)
		{
			var sinceStart = Time.time - startTime;
			var percent = sinceStart / duration;
			var color = image.color;
			color.a = Mathf.Lerp(fromAlpha, toAlpha, percent);
			image.color = color;
			yield return null;
		}
	}
}

What do you think of this FSM versus the previous article’s system? Let me know in the comments!