-
Notifications
You must be signed in to change notification settings - Fork 6
StateMachine
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 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.
/// @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.
If you create your game object oriented (what 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. Those objects have children. Like the "Enemy" could have "Beast", "Demon" and "Angel" children, which all do their thing.
Now, what should happen, if the base object declares a state, say "attack", but the attack shall look different on each child? You will likely say "Well, when I declare the same state a second time, the latter shall replace the former".
Well, this is true, in some, maybe in most cases. But what if the generic base object also does damage calculation, apply buffs, send broadcasts to the achievement-receiver and does lots of other stuff and you "just want to play a specific animation", without copying the base code over to your object?
And in a good object oriented design, this is exactly, what you will face. Your parent objects all "do their bit of work", but you don't want to copy that code, you don't want to destroy all of your parent object's actions! This would not 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.
And this is the reason for the third parameter, the previous_result
. In this you receive 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 |
Raptor core: Macros ● Logger ● Controllers ● LG Localization ● RACE (The Random Content Engine) ● Savegame System
Game modules: UI Subsystem ● Animation ● StateMachine ● Shaders ● Particle Effects ● Tools, other Objects and Helpers
Back to Repo ● Wiki Home ● Copyright © coldrock.games