StateMachine - Grisgram/gml-raptor GitHub Wiki

The StateMachine is an easy-to-use but very powerful class that will help to control both, objects and the entire game. It is paired with the Animation class. Using these together, most of your game will be managed by the state machines, and you won't need to worry much about your sprites, animations, and many other things related to the rendering of your game.

Before we dive deeper into the StateMachine, please take a moment to read about the ListPool class. This will help you understand how it all works.

I assume that you already know what a finite state machine is. If I am wrong, you can read the theory of state machines here at wikipedia.

The StateMachine class

All functions of the StateMachine are chainable, so you can declare all your states in one go in the Create event of your objects.

Let's start with the basic concepts and options you have with the StateMachine.
Put simply, you just declare a list of named states your object can have. Each of those states supports three methods:

  • on_enter
  • step
  • on_leave

Each of these allows you different things. All of them are optional.

To set a new state in an object, you invoke the .set_state(...) method.

set_state

/// @function 		set_state(name, enter_override = undefined, leave_override = undefined)
/// @description	Set a new state in the object. You may override the defined state events
///			for this one state change if you need to.
///			NOTE: If you override a state, the original state is NOT lost!
///			It is even still available as additional parameter when on_enter
///			is invoked. See on_enter/on_leave docs!
/// @param {func} enter_override Override the on_enter of the new state for this change.
/// @param {func} leave_override Override the on_leave of the current state for this change.

It allows you to override the on_enter and on_leave events for this one particular state change. There are several scenarios where this is useful. An example would be that you want to force the state change to the new state and therefore need to disable the rules you have defined in the on_leave of the current state. In this case, you would provide an empty function() {} as leave_override, causing the rules to not be executed. Another example would be if the new state will begin some Animation or particle effects when entered, but in this case, the state change will be animated differently. You can provide this special implementation as the enter_override, and it will get a new look.


Important notice about set_state: By default you cannot enter or set the same state that is currently active. If an object is in its "Idle" state, a call to set_state("Idle"); will be silently ignored.
This default behavior has been chosen to allow setting a state without consequences (by re-launching the on_enter method) if it is already active.

However, you can change this default behavior by invoking

states.set_allow_re_enter_state(true);

Like most other methods of the StateMachine this one is chainable too.


on_enter

/// @function 		on_enter(state_data, previous_state, original_on_enter)
/// @description	The object enters a new state.
/// @param {struct} state_data		The data object of the state machine. Shared amongst all states.
/// @param {string} previous_state 	The state the object had before.
/// @param {func}   original_on_enter 	If this is an override of on_enter, this parameter contains the 
/// 					original on_enter event, so you can invoke it, if you need to.

Invoked when the object enters a state.
You may return a string from this function which must be the name of another state. If you do, the object will immediately leave the current state and enter the state you returned.
If you do not return a string from this method, the object will stay in this state and process the step beginning with the next frame.

step

/// @function 		step(state_data, frame)
/// @description	Invoked onve per frame.
/// @param {struct} state_data		The data object of the state machine. Shared amongst all states.
/// @param {int}    frame		The number of frames this state has been active. First frame receives 0.

Invoked every frame.
In most cases, you will leave this function undefined, but there are states (like Monsters looking for a player or wandering around randomly) where a new decision might be available each frame. This method also allows you to return a string the same way the on_enter method does. If you do, the object will leave its current state immediately and enter the new state immediately (in the same frame!).

on_leave

/// @function 		on_leave(state_data, new_state, original_on_leave)
/// @description	The object is about to leave its current state.
/// @param {struct} state_data		The data object of the state machine. Shared amongst all states.
/// @param {string} new_state 		The state the object wants to go to.
/// @param {func}   original_on_leave 	If this is an override of on_leave, this parameter contains the 
/// 					original on_leave event, so you can invoke it, if you need to.

Invoked when the object wants to leave a state.
This method allows you to return a boolean value (or nothing - if you don't return anything, true is assumed) indicating whether it is ok to leave the state. If you want to forbid the state change, return false, otherwise return true or don't return anything.

With this return value you can implement your transition rules by accepting or cancelling a state change away from the current state.

A real example

Theory is good, but hands-on is better, so let's look at how such a StateMachine is declared in code.

// This is the Create event of your object, which is a child of StatefulObject

states
.add_state("idle",
    function(sdata, prev_state) {
        // The first function is "on_enter"
        // Do things, when this state starts. Mostly this is some Animation
        // and/or sound effect or a sprite change...
    },
    function(sdata, frame) {
        // The second function is "step"
        // IMPLEMENT ONLY WHEN YOU REALLY NEED IT
        // This runs every frame
    },
    function(sdata, new_state) {
        // The third function is "on_leave"
        // Is it ok to leave the state now?
        // return false, if not.
    }
)
.add_state("another_state",
    function(sdata) {
        // on_enter
    }
)
.set_state("idle");

Lets go through this line by line.
The most important thing here is the .add_state method, which takes 4 parameters:

  • The name of the state
  • The on_enter method
  • The step method
  • The on_leave method

Tip

All method parameters are optional! You can see in the second added state in the example above that only the onEnter function was specified, and even the on_enter function only looks at the first argument, "sdata" and doesn't even declare the prev_state second argument!

Callbacks and Parameters

Another thing I'd like to mention is that this is one of the moments where the design of GML really shines. You only have to declare as many parameters as you want to get. From the function docs above, you know that on_enter and on_leave could receive three parameters. You are not forced to declare all three if they're not useful to you!
Each of these declarations is ok and will compile fine:

  • function() {}
  • function(sdata) {}
  • function(sdata, prev_state) {}
  • function(sdata, prev_state, original) {}

The almighty data. Again!

The first parameter, each of these callbacks receives, is a data struct. I normally name it sdata (means "state_data") to avoid confusion with the data member of my objects, because all game objects are Saveable and therefore own a data variable on object level.

This sdata is owned by the StateMachine. It's by default an empty struct where you can store anything you need in your StateMachine. The sdata struct is shared over all states and all function in the entire StateMachine.


Tip

You do not need to care about saving your StateMachine data. Just use the struct provided and you're good to go.
In the same way, the data of RACE (The Random Content Engine) is automatically saved with the Savegame System, your state data will be saved also.
raptor handles this for you.


More or less, that's it for the creation of states. I highly recommend that you look at the source code of the Example Project to see how a real running StateMachine works.

The Spawner and the Monsters demonstrate quite simple StateMachines, and the Player object shows a fully fledged StateMachine in a StatefulObject that even handles input signals and reacts through state changes.


Now read on in

StateMachine Functions All available functions of the StateMachine
StatefulObject See how this power is brought to your game objects and is even enhanced by event processing!
Shared States Even more architectural options by sharing states among multiple StateMachines
Functions in the StatefulObject The object declares some convenience functions. Read all about them here
⚠️ **GitHub.com Fallback** ⚠️