-
Notifications
You must be signed in to change notification settings - Fork 6
StateMachine
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.
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.
/// @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.
/// @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.
/// @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!).
/// @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.
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) {
// 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) {
// The second function is "step"
// IMPLEMENT ONLY WHEN YOU REALLY NEED IT
// This runs every frame
},
function(sdata) {
// 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
All method parameters are optional! You can see in the second added state in the example above that only the onEnter
function was specified.
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 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 data
is owned by the StateMachine
. It's by default an empty struct
where you can store anything you need in your StateMachine
.
Each StateMachine
puts one member into the data
struct it holds -- a pointer to itself in the data.state_machine
variable. This line runs in every StateMachine
constructor:
data.state_machine = self;
So, you can easily access the running StateMachine
from within any state function by using data.state_machine
variable in the function.
This data
struct is shared over all states and all function in the entire StateMachine
.
NOTE: If the StateMachine
is declared in a StatefulObject and the instance variable add_to_savegame
is true when the object is created, this data structure is also added to the savegame data
with the name state_data
.
That's the same scheme RACE (The Random Content Engine) uses to store its race_data
.
You do not need to care about saving your StateMachine
data as long as you don't set a new struct in the data
member of the StateMachine
. Just use the struct provided and you're good to go. If you change it to any other custom struct, you have to make sure to save that custom struct in the savegame.
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
|
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