StateMachine - Grisgram/gml-raptor GitHub Wiki

The StateMachine class is a simple yet powerful tool designed to help manage both individual objects and the entire game. Paired with the Animation class, these tools allow you to efficiently handle most aspects of game management, from controlling sprites to animations and rendering.

Before diving into the details, it's worth reading about the ListPool class. Understanding this will give you insights into how the StateMachine works in conjunction with other elements.

I’ll assume you’re familiar with finite state machines (FSMs). If not, you can learn more about FSM theory on wikipedia.

The StateMachine class

All functions in the StateMachine are chainable, meaning you can declare all your states in a single call within the Create event of your objects.

The concept is straightforward: you define a list of named states for your object. Each state supports three optional methods:

  • on_enter: Called when entering the state.
  • step: Invoked once per frame while the object is in this state.
  • on_leave: Called when the state is about to change.

You can change an object’s state by calling 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.
/// @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.


Note

Important Note about set_state: By default calling set_state with the same state name does nothing if that state is already active. For example, if an object is in the "Idle" state and you call set_state("Idle"), it will be ignored. This prevents unnecessary state re-entry and re-triggering of on_enter.

However, if needed, you can allow re-entry by calling:

states.set_allow_re_enter_state(true);

Like most methods in StateMachine, this is chainable.


on_enter

/// @function 		on_enter(state_data, previous_state, previous_result)
/// @description	The object enters a new state.
/// @param {struct} state_data		The data object of the state machine. Shared between all states.
/// @param {string} previous_state 	The state the object had before.
/// @param {string} previous_result 	The return value of the base- (parent-) on_enter function.

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, previous_result)
/// @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.
/// @param {string} previous_result 	The return value of the base- (parent-) on_enter function.

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, previous_result)
/// @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 {string} previous_result 	The return value of the base- (parent-) on_enter function.

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.

State Inheritance

In the function descriptions above you have seen a previous_result parameter and the description was something like "... the return value of the parent-on_enter function". Let me explain this.

When designing your game using object-oriented principles (as you should!), you will likely have some base objects, like a generic "Card" object in a card game or some "Enemy" and "Player" base objects. These base objects can have child objects. For example, an "Enemy" might have child objects like "Beast," "Demon," and "Angel", each with their own behaviors. Now, imagine the base object defines a state, say "attack," but each child object needs to handle the "attack" differently. You might think, “If I redefine the state in the child object, it should override the one in the base class.”

That’s often true, but what if the base object also handles things like damage calculation, applying buffs, sending achievement notifications, and more? You may just want to add a specific animation without copying all the base code into the child object.

In a well-designed object-oriented system, this is exactly what you'll encounter. The parent objects take care of their responsibilities, but you don’t want to duplicate or destroy their functionality. That wouldn’t be object-oriented.

For this reason, StateMachine does not delete/overwrite a state, if you declare it a second or even third time. Instead, it puts it on a stack, together with all the parent states with the same name. When you now enter this state, all the on_enter functions will run in order, from the very base object through the inheritance chain and your child's on_enter will run last.

This explains the purpose of the previous_resultparameter. It contains the string, your parent would have returned, if it were the last in the chain. You can use this to return it, you can examine it and decide differently or you can simply ignore it and return whatever you like.

How to replace a state

But of course, there might be situations, even in object oriented design, where you really want to replace the base state with the new one. Where the inheritance chain shall not survive.

For this, there's the (chainable) .delete_state method available.

Instead of

states
    .add_state("yourstate",
        function(sdata, prev_state) { ... do your thing ...}
    );

simply write

states
    .delete_state("yourstate")
    .add_state("yourstate",
        function(sdata, prev_state) { ... do your thing ...}
    );

and delete the existing before you create yours. Your state will then be the first in an entirely new chain.

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, previous_result) {}

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** ⚠️