Finite State Machines for Game Development

By Eric Lathrop on

This is a lightning talk I gave at Louisville Makes Games on Thursday, November 14th.

Finite State Machines

What?

A way of structuring code that limits the behaviours it has.

Why?

To reduce bugs, enhance consistency, and make it easier to think about complicated behaviour.

When?

When you have an thing that changes behaviour depending on what has happened.

Simple 2D Platformer Flowchart

Simple 2D Platformer Flowchart

Simple 2D Platformer State Machine

This is a representation of the above flowchart as a Finite State Machine inside a Unity MonoBehaviour. The code compiles, but was not tested.

using UnityEngine;
using System.Collections.Generic;
using TMPro;
using UnityEngine.Tilemaps;

[RequireComponent(typeof(BoxCollider2D))]
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(SpriteRenderer))]
public class PlatformerStateMachineController : MonoBehaviour {

  enum State {
    IDLE,
    WALKING,
    JUMPING,
  }

  private State state = State.IDLE;

  private new Collider2D collider;
  private Rigidbody2D rigidbody2d;
  private Animator animator;
  private SpriteRenderer spriteRenderer;
  private int foregroundMask;

  private static readonly int pixelsPerUnit = 32;
  private static readonly float onePixelInUnits = 1.0f / pixelsPerUnit;

  public AudioClipGroup jumpSound;

  void Start() {
    collider = GetComponent<Collider2D>();
    rigidbody2d = GetComponent<Rigidbody2D>();
    animator = GetComponent<Animator>();
    spriteRenderer = GetComponent<SpriteRenderer>();
    foregroundMask = LayerMask.GetMask("Foreground");
  }

  void Update() {
    state = NextState(state);
  }

  private State NextState(State state) {
    switch(state) {

      case State.IDLE:
        if (Input.GetButton("Left")) {
          return TransitionToWalking(new Vector2(-1, 0));
        }
        if (Input.GetButton("Right")) {
          return TransitionToWalking(new Vector2(1, 0));
        }
        if (Input.GetButton("Space")) {
          return TransitionToJumping();
        }
        break;

      case State.WALKING:
        if (Input.GetButton("Left")) {
          rigidbody2d.velocity = new Vector2(-1, 0);
          return state;
        }
        if (Input.GetButton("Right")) {
          rigidbody2d.velocity = new Vector2(1, 0);
          return state;
        }
        if (Input.GetButton("Space")) {
          return TransitionToJumping();
        }
        return TransitionToIdle();

      case State.JUMPING:
        var bottomHits = Physics2D.BoxCast(transform.position, collider.bounds.size, 0, Vector2.down, onePixelInUnits * 2, foregroundMask);
        if (bottomHits.collider != null) {
          if (Input.GetButton("Left")) {
            return TransitionToWalking(new Vector2(-1, 0));
          }
          if (Input.GetButton("Right")) {
            return TransitionToWalking(new Vector2(1, 0));
          }
          return TransitionToIdle();
        }
        break;
    }
    return state;
  }

  private State TransitionToIdle() {
    animator.Play("Idle");
    return State.IDLE;
  }

  private State TransitionToWalking(Vector2 direction) {
    rigidbody2d.velocity = direction;
    animator.Play("Walking");
    if (direction.x < 0) {
      FaceLeft(transform);
    } else {
      FaceRight(transform);
    }
    return State.WALKING;
  }
  private State TransitionToJumping() {
    rigidbody2d.velocity += new Vector2(0, 1);
    animator.Play("Jumping");
    jumpSound.Play();
    return State.JUMPING;
  }

  private void FaceLeft(Transform t) {
    FlipX(t, true);
  }
  private void FaceRight(Transform t) {
    FlipX(t, false);
  }

  private void FlipX(Transform t, bool shouldFlip) {
    var scale = t.localScale;
    var sx = System.Math.Abs(scale.x);
    scale.x = shouldFlip ? -sx : sx;
    t.localScale = scale;
  }
}