Skip to content

Instantly share code, notes, and snippets.

@mandarinx
Last active October 21, 2022 23:54
Show Gist options
  • Save mandarinx/734ca9f02a8eccaa3c3e to your computer and use it in GitHub Desktop.
Save mandarinx/734ca9f02a8eccaa3c3e to your computer and use it in GitHub Desktop.
Unity3D Simple Finite State Machine
using MyGame;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace MyGame.FSM {
public abstract class FSMState {
protected Dictionary<Transition, StateID> map = new Dictionary<Transition, StateID>();
protected StateID stateID;
protected FSMSystem fsmSystem;
public FSMSystem FSMSystem {
set { this.fsmSystem = value; }
}
public StateID ID {
get { return stateID; }
}
public void AddTransition(Transition trans, StateID id) {
if (trans == Transition.NullTransition) {
Debug.LogError("FSMState ERROR: NullTransition is not allowed for a real transition");
return;
}
if (id == StateID.NullStateID) {
Debug.LogError("FSMState ERROR: NullStateID is not allowed for a real ID");
return;
}
if (map.ContainsKey(trans)) {
Debug.LogError("FSMState ERROR: State " + stateID.ToString() +
" already has transition " + trans.ToString() +
"Impossible to assign to another state");
return;
}
map.Add(trans, id);
}
public void DeleteTransition(Transition trans) {
if (trans == Transition.NullTransition) {
Debug.LogError("FSMState ERROR: NullTransition is not allowed");
return;
}
if (map.ContainsKey(trans)) {
map.Remove(trans);
return;
}
Debug.LogError("FSMState ERROR: Transition " + trans.ToString() +
" passed to " + stateID.ToString() +
" was not on the state's transition list");
}
public bool SetTransition(Transition trans, System.Object options = null) {
return fsmSystem.PerformTransition(trans, options);
}
public StateID GetOutputState(Transition trans) {
if (map.ContainsKey(trans)) {
return map[trans];
}
return StateID.NullStateID;
}
public virtual void DoBeforeEntering(System.Object options) { }
public virtual void DoBeforeLeaving() { }
public virtual bool Reason() {
return true;
}
public abstract void Act();
public override string ToString() {
return stateID.ToString();
}
}
}
namespace MyGame.FSM {
public enum Transition {
NullTransition = 0,
OneTwo = 10,
TwoOne = 20
}
public enum StateID {
NullStateID = 0,
One = 10,
Two = 20
}
}
using MyGame.FSM;
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
namespace MyGame.FSM {
public class FSMSystem {
private List<FSMState> states;
private StateID currentStateID;
private FSMState currentState;
public StateID CurrentStateID {
get { return currentStateID; }
}
public FSMState CurrentState {
get { return currentState; }
}
public FSMSystem() {
states = new List<FSMState>();
}
public void AddState(FSMState state) {
if (state == null) {
Debug.LogError("FSM ERROR: Null reference is not allowed");
}
foreach (FSMState st in states) {
if (st.ID == state.ID) {
Debug.LogError("FSM ERROR: Impossible to add state " +
state.ID.ToString() +
" because state has already been added");
return;
}
}
state.FSMSystem = this;
states.Add(state);
if (states.Count == 1) {
currentState = state;
currentStateID = state.ID;
}
}
public void DeleteState(StateID id) {
if (id == StateID.NullStateID) {
Debug.LogError("FSM ERROR: NullStateID is not allowed for a real state");
return;
}
foreach (FSMState state in states) {
if (state.ID == id) {
states.Remove(state);
return;
}
}
Debug.LogError("FSM ERROR: Impossible to delete state " +
id.ToString() +
". It was not on the list of states");
}
public bool CanPerformTransition(Transition trans) {
StateID id = currentState.GetOutputState(trans);
if (id == StateID.NullStateID) {
return false;
}
return true;
}
public bool PerformTransition(Transition trans, System.Object options = null) {
if (!CanPerformTransition(trans)) {
return false;
}
if (trans == Transition.NullTransition) {
Debug.LogError("FSM ERROR: NullTransition is not allowed for a real transition");
return false;
}
StateID id = currentState.GetOutputState(trans);
if (id == StateID.NullStateID) {
Debug.LogError("FSM ERROR: State " + currentStateID.ToString() +
" does not have a target state " +
" for transition " + trans.ToString());
return false;
}
foreach (FSMState state in states) {
if (state.ID == id) {
currentStateID = id;
currentState.DoBeforeLeaving();
currentState = state;
currentState.DoBeforeEntering(options);
break;
}
}
return true;
}
public void Update() {
if (currentState.Reason()) {
currentState.Act();
}
}
}
}
using UnityEngine;
namespace MyGame.FSM.States {
public class OneOptions : System.Object {
public float oneFloat;
}
public class TwoOptions : System.Object {
public bool twoBool;
}
}
@mandarinx
Copy link
Author

Based on the FSM at the Unity3D wiki.

I did a few improvements (from my point of view):

  • State and Transitions enums moved to a separate file. I find them easier to work with this way.
  • Each instance of FSMState gets a reference to the FSMSystem instance. That way states can transition out of themselves on their own.
  • FSMState has a SetTransition method which they can use to transition out of the state.
  • FSMState.Reason returns a bool. When Reason returns false, Act will not run.
  • An options object can be passed to FSMSystem.PerformTransition. A state can pick up the options object in FSMState.DoBeforeEntering. All options object are stored in StateObjects.cs.

@mandarinx
Copy link
Author

Here's an example of how to use the files above:

using UnityEngine;
using MyGame.FSM;
using MyGame.FSM.States;
using System.Collections;

namespace MyGame.Characters {

public class Player : MonoBehaviour {

    private FSMSystem fsm;

    void Awake() {
        fsm = new FSMSystem();

        One one = new One();
        one.AddTransition(Transition.OneTwo, StateID.Two);
        fsm.AddState(one);

        Two two = new Two();
        two.AddTransition(Transition.TwoOne, StateID.One);
        fsm.AddState(two);
    }

    void Update() {
        fsm.Update();
    }

    public void OneTwo( {
        TwoOptions opts = new TwoOptions();
        opts.twoBool = true;
        fsm.PerformTransition(Transition.OneTwo, opts);
    }
}
}

And here are the states:

using UnityEngine;
using MyGame.FSM;
using System.Collections;

namespace MyGame.FSM.States {

public class One : FSMState {

    public One() {
        stateID = StateID.One;
    }

    public override bool Reason() {
        return false;
    }

    public override void Act() {}

    public override void DoBeforeEntering(System.Object options) {}

    public override void DoBeforeLeaving() {}

}

public class Two : FSMState {

    private TwoOptions opts;

    public Two() {
        stateID = StateID.Two;
    }

    public override bool Reason() {
        return opts.twoBool;
    }

    public override void Act() {
        SetTransition(Transition.TwoOne);
    }

    public override void DoBeforeEntering(System.Object options) {
        TwoOptions opts = (TwoOptions)options;
        Debug.Log("TwoOptions.twoBool: "+opts.twoBool);
    }

    public override void DoBeforeLeaving() {}

}

}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment