Giter VIP home page Giter VIP logo

susmachine's Introduction

SUSMachine - a Simple Unity State Machine

ci GitHub version

A state machine implementation with a quick, easy and nice to use API - no reflection or string names, no need to inherit anything.

Supports Unity 2020.2 and up.

A Quick Glance

enum States { Normal, Blocking }
enum Events { Attacked }

int lives = 3;

[SerializeField]
float blockTime = 2;

StateMachine<States, Events> stateMachine;

private void Awake()
{
    stateMachine = new StateMachine<States, Events>
    {
        AnyState =
        {
            OnEnter = fsm => Debug.Log($"Entering state {fsm.CurrentState}"),
            
            OnExit = fsm => Debug.Log($"Exiting state {fsm.CurrentState} to {fsm.NextState}")
        },

        [States.Normal] =
        {
            OnUpdate = fsm => Debug.Log($"Time idling: {fsm.TimeInState}"),

            [Events.Attacked] = _ => lives--,

            Transitions = {
                // in this state, check in update for space press. enter blocking state if true
                {_ => Input.GetKeyDown(KeyCode.Space), States.Blocking}
            }
        },

        [States.Blocking] =
        {
            [Events.Attacked] = _ => Debug.Log("Attack blocked"),

            Transitions = {
                {fsm => fsm.TimeInState >= blockTime, States.Normal}
            }
        }
    };

    /// Initialize can be called at any time when you're ready -- not just in Awake().
    /// By supplying this gameObject into the second parameter,
    /// the state machine will autmatically Close() when the gameObject is destroyed.
    /// Otherwise, you may want to call Close() in OnDestroy(), OnDisable() etc.
    /// Note: Initialization calls Enter() and Closing calls Exit().
    stateMachine.Initialize(States.Normal, this.gameObject);
}

void OnCollisionEnter(Collision collision)
{
    stateMachine.TriggerEvent(Events.Attacked);
}

Installing

via OpenUPM

  • openupm add com.kdmagic.susmachine

via Unity Package Manager

  • In the unity package manager window, click add package from git URL... and paste the following:
  • https://github.com/JamesYFC/SUSMachine.git?path=/Packages/com.kdmagical.susmachine#release

After installation, just import the namespace and you're good to go.

using KDMagical.SUSMachine;

Updating

To update to the latest version, go to the Simple Unity State Machine package in the Package Manager and hit the Update button.

Note: Unity currently doesn't notify you that a new version is available for packages installed from git.

Unity 2020

As the update button isn't available in 2020, reinstall the package by following the installation step above again.

Noteworthy Changes

  • v0.9.0 - In transitions with events, the event parameter is now first in the order. e.g. {States.State1, Events.Event1} is now {Events.Event1, States.State1}

State Objects

A state object contains all the actions to call when certain events happen, as well as the transitions.

The state machine is provided as the sole parameter when calling these actions.

The available actions are:

  • OnEnter
  • OnExit
  • OnUpdate
  • OnFixedUpdate
  • OnLateUpdate
  • [EventEnum.EventName] (when events are enabled)

A StateMachine can contain a state object for each member of the states enum, plus AnyState, whose actions are called before any specific state's. AnyState's transitions take lower priority than specific state object's transitions.

var fsm = new StateMachine<States>
{
    AnyState =
    {
        //...
    },

    [States.State1] =
    {
        // ...
    },

    [States.State2] =
    {
        // ...
    },

    [States.State3] =
    {
        // ...
    },
}

Events

State objects also support events for actions and transitions.

Simply add another generic parameter to StateMachine to enable this support.

// without events
var fsm = new StateMachine<States> { };

// with events
var fsm = new StateMachine<States, Events>
{
    [States.State1] =
    {
        [Events.Event1] = _ => Debug.Log("Event1 Triggered!")
    }
};

You can then trigger these events with fsm.TriggerEvent(TEvents eventToTrigger).

Stateful Data

To keep track of and modify variables within a state object, use the Stateful<TStates, TData> or Stateful<TStates, TEvents, TData> classes.

TData must be a struct type.

When using this class, your actions and transition functions get more parameters related to the data being tracked.

var fsm =
{
    [States.State1] = new Stateful<States, (string name, int count)>
    {
        // names are optional here -- ("Hello", 0) does the same thing.
        InitialData = (name: "Hello", count: 0),

        OnEnter = (_, data, modify) =>
        {
            // deconstruct syntax
            var (name, count) = data;

            Debug.Log($"Name: {name}, Count: {count}");

            data.count++;

            modify(data);
        }

        Transitions =
        {
            { // transitions to State2 on the first frame where count reaches 2
                (_, data) => data.count >= 2,
                States.State2
            }
        }
    }
};

var fsm = new Stateful<States, int>
{
    [States.State1] = new Stateful<States, Events, int>
    {
        // as data is a struct, omitting InitialData will mean it is set to its default value.
        OnEnter = (_, data, __) => Debug.Log("data = " + data) // data = 0
    }
};

Transitions

Automatic

Automatic transitions are functions that run on a specified update loop (Update, FixedUpdate or LateUpdate) after behaviour actions trigger, checking for if the state machine should automatically switch to another state.

You can setup as many automatic transitions as you want on a specific state object.

There are two ways to setup automatic transitions:

Simple Syntax

{PredicateCondition, State, TransitionType}

var stateMachine = new StateMachine<States>
{
    [States.Idle] =
    {
        Transitions =
        {
            {
                // if this returns true, enter the state specified.
                _ => Input.GetKeyDown(KeyCode.Space),
                // the state to enter if the predicate returned true.
                States.Jumping,
                // the update loop to run on. Can be omitted as Update is the default.
                TransitionType.Update
            }
        }
    }
}

When using Stateful, the PredicateCondition can also be written in the form (stateMachine, data) => bool.

Complex Syntax

{TransitionFunction, TransitionType}

Allows you to define a function where you can return any state or null, allowing you to set up several conditions in the same place.

The following example is a simple switch case, but the logic in this function can be as complex as you need.

Transitions =
{
    {
        _ => {
            switch (someNum)
            {
                case 1:
                    return States.State1;
                case 2:
                    return States.State2;
                case 3:
                    return States.State3;
                default:
                    return null;
            }
        }
    },
    // this last parameter is not necessary as it will default to TransitionType.Update if omitted
    TransitionType.Update
}

When using Stateful, the TransitionFunction can also be written in the form (stateMachine, data) => States?.

In some cases, such as below, the compiler can't figure out that the returned type is States?.

If this happens you simply need to cast one of the returns like so:

[States.Idle] =
{
    Transitions =
    {
        {
            _ => someNum < 0
                ? States.State1
                : (States?)null
        }
    }
}

Event Transitions

There are several ways to write event transitions.

These are only run when the event is triggered, directly after any event actions are called.

Direct

{Event, State}

When the event is called, enters the specified state.

Transitions =
{
    { Events.Event1, States.State1 }
}

Simple

{Event, PredicateCondition, State}

Transitions =
{
    // when the event is triggered, the condition is called. Enters State1 if the condition is true.
    { Events.Event1, _ => someNum > 0, States.State1 }
}

When using Stateful, the PredicateCondition can also be written in the form (stateMachine, data) => bool.

Complex

{Event, TransitionFunction}

Allows you to define a function where you can return any state or null, allowing you to set up several conditions in the same place.

Transitions =
{
    Events.Event1,
    _ => someNum switch
    {
        1 => States.State1,
        2 => States.State2,
        var x when x >= 3 => States.State3,
        _ => null
    }
}

When using Stateful, the TransitionFunction can also be written in the form (stateMachine, data) => States?.

Order

As soon as the first automatic transition in a particular update loop returns a positive result, the state machine will switch its state to that result.

Therefore, the order in which you set your transitions matters.

Consider the following:

var stateMachine = new StateMachine<States>
{
    [States.Jumping] =
    {
        Transitions =
        {
            {
                _ => {
                    Debug.Log("first check");
                    return GetJumpHeight() > limit;
                },
                States.State1
            },
            {
                fsm => {
                    Debug.Log("second check");
                    return fsm.TimeInState > jumpTime;
                },
                States.State2
            }
        }
    }
}

If the return value of GetJumpHeight() rises above the limit before, or in the same frame, as jumpTime is surpassed, then the second check will not occur and the state will be set to States.State1.

Manual

The built-in event and transitions support should cover most use cases, but a manual SetState method is also provided.

void StateCheck()
{
    if (someCondition)
    {
        stateMachine.SetState(States.State1);
    }
}

Be careful when using this in OnEnter and OnExit, as improper use can cause a stack overflow!

var fsm = new StateMachine<States>
{
    AnyState =
    {
        OnEnter = fsm => fsm.SetState(States.State2)
    }
}

fsm.Initialize(States.State1); // stack overflow! AnyState.OnEnter called infinitely!

Splitting Out

If you find that your functions get too large to be written in lambda syntax, or for any other reason, you can write them elsewhere -- as long as they have the matching signature.

// in state machine init...
[States.SomeState] = {
    OnEnter = SomeFunc
}

void SomeFunc(IStateMachine<States> stateMachine) { }
// in state machine init...
[States.SomeState] = Stateful<States, MyStruct>
{
    OnEnter = SomeFunc
}

void SomeFunc(IStateMachine<States> stateMachine, MyStruct data) { }

Manual Initialization & Closing

State machines need to call Close() when their use is finished.

This is done for you if when you specify the second parameter in Initialize().

A MonoBehaviour will be created that calls Close() on the state machine when the specified GameObject is destroyed:

Initialize(T initialState, GameObject closeOnDestroy)

However, you may omit the second parameter if you want to call Close() manually.

fsm.Initialize(States.State1);

// elsewhere, e.g. in OnDestroy
fsm.Close();

susmachine's People

Contributors

jamesyfc avatar mrwellmann avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

susmachine's Issues

ci

Mainly to run the tests.

Add runner mode choices

on stateMachine.Initialize(), add a runner mode enum that allows the user to choose where life cycle & update methods are called?

  • StateMachineRunner (default)
  • Manual
  • Automatic? (adds a monobehaviour? or requires another parameter that spells out which monobehaviour to check before giving it update callbacks?)

New language feature improvements

When Unity supports newer language features like covariant returns, I will be able to simplify the codebase and improve quite a few aspects of this library. Though that will likely bump up the supported Unity version as well. We'll leave the previous version in another branch.

After this, I will look into new features and improvements such as...

  • Supporting UniTask/task based delegates on some functions
    • (it is currently possible to provide async methods, the state machine will just run them and forget about them)
  • Make passing event args nicer.
  • Look into nested state machines
  • FSM-wide state
  • various other API improvements
    and more...

No promises that everything will be feasible but I will certainly be looking into them when it's time.

Check for component destroy instead of gameobject destroy

If you create a state machine with update functions in any of its states, then destroy that component, you'd probably expect that state machine to go away.

However, if I recall the details correctly, the manager will still hold onto it as the gameobject destruction component from Initialize(x, y) won't trigger if the gameobject is still alive.

event support

Something like this?

enum States {State1, State2};
enum Events {Event1, Event2};

fsm = new StateMachine<States, Events>
{
	[States.State1] = {
		// new
		OnEvents = {
			[Events.Event1] = _ => Debug.Log("event1 triggered for State1")
		}
		
		AutoTransitions = {
			// new overload for Add() super simple form
			{States.State2, Events.Event1}

			// new overload for Add() simple form
			{_ => true, States.State2, Events.Event1}

			// new overload for Add() complex form
			{_ => someCondition ? States.State2 : (States?)null, Events.Event1}

Somewhere down the line...

fsm.TriggerEvent(Events.Event1)
// debug "event1 triggered for state1"
// enters state2

Don't call OnUpdate / OnFixedUpdate methods when gameObject stateMachine is on is disabled

add Enable/Disable functions to AutoCloser (and rename it) that tell the state machine manager to stop or resume calling the relevant update functions

double check that the TimeInState counting functionality still works correctly (should it count or not while the gameObject is disabled?)

this might have to be togglable behaviour or something. sometimes there is value in being able to turn off the game object but still allowing these life cycle functions to run. Purists would hate it, but it does make certain scenarios more convenient.

if togglable, we'll use a modified version of AutoCloser instead, or a separate component that contains the new magic methods

pick a monobehaviour to update enable status?

due to the fact that these state machines are independent of MonoBehaviour, users may unwittingly expect SUSMachine update functions to only be active when the creator script is active and enabled.

E.G. we could have an option (in the constructor perhaps, seeing as we already have the game object param there?) to input a monobehaviour and check on update its enabled status to mirror update functions on/off in the StateMachineManager

Init only properties (C# 9)

If I'm reading the microsoft docs correctly...

This will make the fsm a little safer by not allowing modifications to be made after initialization, as public setters are required to use object initialization syntax

state data

First decent idea might be blocked by this

Maybe something like record structs in C# 9, but is there a way to have copy on write optimizations without the user having to do anything special?

Any state support

There is a chance this isn't actually that useful, we'll see.

var fsm = new StateMachine<States>{
	AnyState = {
		// ...
	},
	[States.State1] = {
		// ...
	}
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.