Skip to content

A Fully Featured State Machine for your Unity Projects!

License

Notifications You must be signed in to change notification settings

Macawls/OceanFSM

Repository files navigation

Ocean Finite State Machine

apu

A code-only, simple and easy to use Finite State Machine for your Unity Projects!

GitHub package.json version GitHub license GitHub last commit OpenUPM

player-fsm-example.mp4

Getting Started

Installation

OpenUPM (Recommended)

openupm add com.macawls.oceanfsm

Git URL (Recommended)

  1. Open the package manager window
  2. Click the plus icon
  3. Choose "Add package from git URL..."
  4. Use the link below.
https://github.com/Macawls/OceanFsm.git

Manual (Not Recommended)

Add the following to your manifest.json.

{
  "scopedRegistries": [
    {
      "name": "OpenUPM",
      "url": "https://package.openupm.com",
      "scopes": [
        "com.macawls.oceanfsm"
      ]
    }
  ],
  "dependencies": { 
    // Replace with latest version or version of your choice
    "com.macawls.oceanfsm": "{version}" 
  }
}

Samples

After adding the package, you can use the import button from the package manager window to inspect the samples.

The samples were created from the Default 3D Unity template.

How to Use

States

1. Functionality

To add functionality to a state, you can override the following methods.

virtual void OnInitialize() { }  // called once
virtual void OnEnter() { } 
virtual void OnExit() { } 
virtual void OnUpdate(float deltaTime) { } // depends on how you decide to update the state machine

2. Creating a State

All states have to inherit from the State<T> class.

Generic Usage

The generic reference type T is used to associate your states and state machines.

You can use whatever you want, but I recommend using an interface to keep things tidy. Lets define IDoor as our reference type.

public interface IDoor
{
    void Close();
    void Open();
}
public class Door : MonoBehaviour, IDoor 
{
    private IPollingMachine<IDoor> _mFsm;
    
    // Instance of IDoor passed to constructor of the builder, because Door implements IDoor
    _mFsm = new PollingBuilder<IDoor>(this) 
    ...
 }

Here, all the states will have access to instance which implements IDoor using the Runner property. Like so.

class Closed : State<IDoor>
{
    public override void OnEnter()
    {
	Runner.Close();
    }
}

States also have a Machine property. If the machine is not castable to IAutonomousStateMachine<T>, it will be null.

class Closed : State<IDoor>
{
    public override void OnUpdate()
    {
        if (PlayerIsNearby() && Input.GetKeyDown(unlockKey))
        {
            Machine.ChangeState<Open>(() => {
                Debug.Log("Player has opened the door");
            });
        }
    }
}

4. Inheritance Example

For a simple use case for inheritance, suppose we have a base class called PlayerStateBase where when we enter a new state, we'd like to play a specific animation. It would look something like this.

[Serializable]
public abstract class PlayerStateBase : State<IPlayer>
{
    [SerializeField] private AnimationClip stateAnimation;

    public override void Enter()
    {
        Runner.PlayAnimation(stateAnimation);
    }
}
[Serializable]
public class Jump : PlayerStateBase
{
    // whatever fields you need
    
    public override void Enter()
    {
        base.Enter(); // play the animation
        // other logic
    }
}

5. Utilities

Since all states have to inherit from State<T> class, you can use the State Generator tool to generate a state boilerplate class for you.

You'll find it under the Tools dropdown menu.

State generation tool

State Machines

1. State Machine Types

There are two types at the moment :)

Autonomous State Machine

  • States are responsible for transitioning to other states.
  • The machine can receive commands to transition to a specific state.
  • Can freely transition to any state.

Polling State Machine

  • The machine holds an internal dictionary of states and their transitions.
  • Transitions are triggered by a condition/predicate.
  • States cannot transition by themselves.

2. Creating the State Machine

There are two builders to aid in creating state machines.

Polling State Machine

private IPollingMachine<IDoor> _mFsm;

private void Awake()
{
    var closed = new Closed();
    var open = new Open();

    _mFsm = new PollingBuilder<IDoor>(this)
        .SetStartingState(nameof(Closed))
        .AddTransition(closed, open, () => {
            return PlayerIsNearby && Input.GetKeyDown(KeyCode.Space) && !_mIsLocked)
        })
        .AddTransition(open, closed, () => {
            return PlayerIsNearby() && Input.GetKeyDown(KeyCode.Space)
        }, onTransition: () => {
            Debug.Log("Closing the door"); // optional
        })
        .Build();
}

Autonomous State Machine

private IAutonomousMachine<IPlayer> _mFsm;

private void Awake()
{
    _mFsm = new AutonomousBuilder<IDoor>(this)
        .SetStartingState(nameof(Idle))
        .AddState(new Idle())
        .AddState(new Walk())
        .AddState(new Jump())
        .Build();
}

3. Command Usage (WIP)

Currently, only the AutonomousStateMachine supports commands. Commands are useful for triggering actions or responding to events from outside the state machine. Conditions and the actions are optional.

private void Awake()
{
    _mFsm = new AutonomousBuilder<IPlayer>(this)
        .SetInitialState(nameof(Idle)) 
        .AddStates(idle, run, jump)
        .Build();
    
    _mFsm.AddCommand("Jump")
        .SetTargetState<Jump>()
        .SetCondition(() => _mFsm.CurrentState is Run && IsGrounded)
        .OnSuccess(() => Debug.Log("Hi mom")) // visual fx for example
        .OnFailure(() => Debug.Log("depression")); // negative sound effect for example
}

private void OnJump(InputAction.CallbackContext ctx)
{
    if (ctx.performed)
    {
        _mFsm.ExecuteCommand("Jump");
    }
}

4. Running the State Machine

It is completely up to you how you want to run the state machine. The key methods are:

void Start();
void Stop();
void Update(float deltaTime);
void Evaluate(); // only for Polling State machines

Important to note that the state machine will not run until you call Start()

if you're using the Polling state machine, I would recommend calling Evaluate() in Late Update.

Evaluate() will continuously check all transitions of the current state. If a transition is met, it will change to the new state.

Using Monobehaviour Hooks

private void OnEnable()
{
    _mFsm.Start();
}

private void OnDisable()
{
    _mFsm.Stop();
}

private void Update()
{
    _mFsm.Update(Time.deltaTime);
}

private void LateUpdate()
{
    _mFsm.Evaluate();
}

5. Which one should I use?

I've found that if it's a simple entity with a few states and transitions, the Polling state machine is good. For example a door, checkpoint, traffic light, treasure chest etc.

If it's an entity that is fairly complex and or reacts to external input , the Autonomous state machine is the way to go. Something like a player, enemy, NPC, UI system etc.

The Autonomous one is easier to use and more flexible. Most of the time I recommend using it.