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.
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.
/// @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.
/// @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.
/// @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!).
/// @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.
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_result
parameter. 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.
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.
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!
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 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 |