A Simpler Finite State Machine (FSM)
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!
#1 by mirko on January 23rd, 2017 ·
I am using prime31 state machine and its really easy to use and very flexible
#2 by jackson on January 23rd, 2017 ·
I hadn’t checked it out until just now, but it does look easy to use and flexible. Thanks for sharing!
#3 by Myrtanim on January 23rd, 2017 ·
Looks simple to implement and use.
But I have an issue with using possibly infinitely running Coroutines. You need to keep in mind, that the memory garbage created within them is only disposed once the coroutine ends. So if you have long running states in here, you pile up memory garbage the GC is not able to collect and dispose.
I think the garbage would be cleaned up by switching states, but I’m not sure because of the approach of nesting coroutines. Testing this would be really nice.
Greetings
Myrtanim
#4 by jackson on January 23rd, 2017 ·
Garbage created by code in the coroutine should be treated just like any other garbage. However, if your coroutine has a local variable that keeps a reference to some object then that object isn’t garbage and won’t get collected until the coroutine ends. For example,
The
big
variable will hold a reference to theBigObject
instance for the whole minute that the coroutine runs. Then the coroutine will end and the object will become garbage and eligible for collection.This is definitely something to keep in mind when using any coroutines, including this state machine.
#5 by Valerie on January 23rd, 2017 ·
Thanks for this new simpler FSM JD, this definitely looks promising. I currently have an app in beta which utilizes the previous FSM. Would you be able to explain what the pros and cons of using the two different versions are?
Also using the previous FSM, I definitely have cases where code is split between the BeginEnter and EndEnter phases. This maybe due to my poor design skills, but I have cases where I can only have one instance of an object (a map in this case), so when transitioning to a new state the previous state’s map instance hasn’t been garbage collected, so I must wait until the EndEnter phase of the new state before trying to access the map. I’m curious how I would handle something like this using the new FSM, since it lacks begin/end enter/exit phases.
Finally, and this might be a lame/inaccurate thought, but it looks like this simpler FSM could get quite messy with all of the states contained in one class.
Thanks for your weekly article’s!!
#6 by jackson on January 23rd, 2017 ·
The FSM in this article would give you more control over the transition between states. There’s no
StateMachine
with predefined steps that happen in a particular order. TheBeginEnter
,EndEnter
, andEndExit
steps can operate over multiple frames, too. So in your case where you need to wait for your map to get garbage collected, you’d just insert a new step with this FSM:Basically you get to define your own transition phases. You can have as many or as few as you want and they don’t need to be consistent across all of your transitions. Of course that’s a “con”, too. It’s not quite as “cookie cutter” as the previous FSM since you aren’t forced to implement
IState
orIStateTransition
and “fill in the blanks” for functions likeBeginEnter
.As for putting everything in the same class, I really just did that for simplicity in the article. You certainly wouldn’t want to put the whole game in one class so that’s not the intention here. It should be pretty simple to lift the “screen fade” functions out into their own class, which could even be
static
. Just take the last four functions of the class and move them to another class.The state functions could be moved to another class too. All you’d need to do is pass a
ref IEnumerable state
parameter to them so they can set the state. Those classes could bestatic
as well if you want. Then you’re just left withTestScript
having the state machine part. It’s small, but you could split it out into its ownStateMachine
class by moving thestate
field andRunStateMachine
function out.Best of luck with your game! It’s so cool that you’re using my FSM and that it’s worked out for you. Send me a link when it’s available to the public and I’ll check it out. :)
#7 by Stephen Hodgson on February 13th, 2017 ·
You could make this a lot faster if you cache some of your references that never change during Start or Awake.
// Create the main menu UI
var canvasPrefab = Resources.Load(“MainMenu”);
var canvas = UnityEngine.Object.Instantiate(canvasPrefab);
var playButtonGO = canvas.transform.Find(“PlayButton”);
var playButton = playButtonGO.GetComponent();
var frameCountGO = canvas.transform.Find(“FrameCount”);
var frameCount = frameCountGO.GetComponent();
#8 by jackson on February 13th, 2017 ·
You’re right! I only skipped this in the article for simplicity. By all means cache expensive operations in your production code.
#9 by EMebane on April 5th, 2017 ·
This FSM is great! To truly appreciate the magic, first digest this article on IEnumerable: http://jacksondunstan.com/articles/3036. Another excellent description of the difference between IEnumerable and IEnumerator is this Stack Overflow response:
http://stackoverflow.com/a/558428/6292664.
It’s not quite obvious that each loop over an IEnumerable with a foreach loop will result in an IEnumerator (not an IEnumerable) object that you can return with yield return. This is what allows you to wrap the foreach loop in a method that returns IEnumerator so you can pass the method to StartCoroutine (Coroutine requires IEnumerator not IEnumerable)
#10 by EMebane on April 5th, 2017 ·
Oh, that first link is also important to understanding why the call to CreateFades doesn’t run the first pass over the fadeOut and fadeIn it creates – calling an IEnumerable doesn’t work like a traditional method call. It just returns an IEnumerable without running the body of the method.
i.e., fadeOut = FadeOut(cover, fadeTime); doesn’t immediately run the foreach loop in the FadeOut method. It returns an IEnumerable which you can run a foreach loop over.