Shared States - Grisgram/gml-raptor GitHub Wiki
Shared states are more of a concept than a physically existing class.
As States are just struct classes, they are references. And references can be linked to multiple locations. The problem with this is the data
member of the StateMachine
is shared with all other states that the StateMachine
knows.
If we assigned the same State
reference to multiple StateMachines
, we would create a chaotic mix of data
pointers, and you would never be sure whose data
you are currently working with.
StateMachine
is capable of managing this. It assigns its own data
member to a State
immediately before the on_enter
, on_leave
or step
callbacks are invoked. So, you may use the same State
in multiple StateMachines
!
With that said, let's look at the State
class.
/// @function State(_name, _on_enter = undefined, _on_step, _on_leave = undefined)
/// @description Defines a state for a StateMachine.
/// @param {string} _name The name of the state
/// @param {func} _on_enter Callback to be invoked when this state becomes the active state
/// @param {func} _on_step Callback to be invoked on every frame
/// @param {func} _on_leave Callback to be invoked when this state is no longer the active state
You see, the signature is quite similar to the add_state method of the StateMachine
. Behind the scenes, add_state
creates an instance of this class and assigns it internally to the StateMachines
' collection of known states.
Now, to create a shared state, you simply create one of these instances by yourself, store it in any variable, and then you can assign this one State
to multiple StateMachines
by invoking add_state_shared on the StateMachine
.
There are many situations where this might become handy.
Of course, you can create base objects and children of those base objects, and you can declare the common states in the base and add more custom states in each of the children, but this way of working has limitations: What if one standard state will be used by multiple base objects? GML
does not support multiple inheritance (and this is good!), so you would have to declare the same base-state in multiple places.
An example would be a state like ev:mouse_enter
that causes objects to make some sound effect whenever the mouse touches them. This state might be applied to all npc's, all monsters, and interactive furniture.
It doesn't matter how many "parents" share this state. You can declare it in the Create
event of the RoomController
or even globally in some script. The key point is:
You only need to write the code for this state once in your game!
No copy/paste sessions and no horrible "omg, no, I can't change this, I would need to replace that in 100 places" anymore.
// Somewhere in a script
global_mouse_enter = new State("ev:mouse_enter",
function(sdata, prev_state) {
// Play a sound effect
return prev_state; // And return to the state, the object had before
}
}
With such a simple construct you can declare a game-wide state for an event and then assign it to any StateMachine
you want.
Remember: To use events as states, your object must be a child of StatefulObject.
Most games offer at least a little configuration UI and it is quite common, to allow the player to redefine key bindings. While I have seen some games, where "WASD" seemed to be hardcoded and I could not redefine them, most other keys, like Inventory, Map, and several other actions could be rebound. Some players prefer I
for inventory, others prefer B
for Bag. We as developers should respect that.
So, now that we have shared states being used in some arbitrary number of StateMachines
and the player can redefine Inventory to be opened with B
instead of I
, how can we find all of these states in order to make changes?
The answer is simple: if you use a shared state, you only have one single instance! So, just set
// assuming, your globally shared state is held in a variable named global_mouse_enter
global_mouse_enter.name = "ev:key_press_B"
and you have renamed it everywhere. That's the biggest benefit of shared states!
Of course, there might be other situations where a state is not shared or where an unknown number of objects in your game owns a state that listens for a specific key (imagine a game like Diablo -- when you press the Alt
key, many many loot objects on the ground start rendering their name so you can see what has dropped).
To solve this, recall the ListPool class and the fact that all StateMachine
instances are globally managed by the RoomController
. We have a global macro available called STATEMACHINES
, and the ListPool offers a process_all
method.
Also, remember that the StateMachine
offers a rename_state
method.
Putting these ideas together, we have everything we need to create dynamically changed keyboard shortcuts!
function rebind_key(old_key, new_key) {
STATEMACHINES.process_all("rename_state", "ev:key_press_" + old_key, "ev:key_press_" + new_key);
}
Call this as rebind_key("I", "B");
and all states in all state machines that listened for the I
key will now listen for B
!
That's it for the StateMachine.
Again, I highly recommend you look at the source code of the Example Project to see how the StateMachine
is used!