← View other assets
asset

State Machines, the struct-based approach

There's many ways to make a state machine. This approach let's you use structs to manage your states.
Mimpy
Lv. 26
· 6 min read · 1301 views

1
Introduction
State machines are a vital element of any gamemaker's toolset. You've probably seen state machines ranging from the very simple, like just a boolean variable, to something very complex, with support for substates, swap triggers and other advanced additions.

What if you're not interested in too many bells and whistles, but just want a state machine that is fairly robust without a ton of features? When I think of my needs for a state machine, I think that it needs to do three things.

To be able to change behavior immediately by swapping to a new state. To track the amount of time that I've been in a state. To run code when swapping into and out of a state.

A state machine with just a variable can do the first task, but not the other two. But I don't want to utilize some huge system I don't understand just to get the other two pieces of functionality I need.

2
The State Machine
So, I present a state machine that barely breaks 20 lines of code, that can handle these three tasks.

function StateMachine() constructor {
    static nullState = new State();

    state = nullState;
    time = 0;

    // Swap to a new state
    swap = function(_state = nullState) {
        state.destroy();

        state = _state;
        time = 0;

        state.create();
    }

    // Run current state
    run = function() {
        state.update();
        time++;
    }
}

This state machine has one static variable, nullState, which is used when you have no active state. You can also swap to undefined to return to this "do nothing" state.

This state machine contains two variables.
state - the state we are currently in.
time - the amount of frames we have been in the current state.

It also has two methods:
swap - called to swap to a new state.
run - called each frame by the object using the state machine, to make it do its job.

3
Creating a State
Next, we'll need to be able to create states to swap to. We'll make a second constructor that will be used to make states.

function State() constructor {
    static NOOP = function() { };

    create = NOOP;
    update = NOOP;
    destroy = NOOP;
}

Our state has a static NOOP variable just to declare a "do nothing" function, and three main variables:
create - stores a method to run when the state begins
update - stores a method to run each frame the state is active
destroy - stores a method to run when the state ends

You can see that our three variables mimic the Event system of a GameMaker object.

4
Using the State Machine
So, we've got all the code to implement our state machine, but how do we use it? Let's start by implementing the state machine, then we'll add some states.

Setting the state machine up is as simple as adding one in our create event:

/// Create Event
sm = new StateMachine();

And running it in our Step event:

/// Step Event
sm.run();

Next, let's add a new state to our Create event:

/// Create Event
myState = new State();

When a state is newly created, it won't do anything, because all of its methods start with the "do nothing" function. We can set these methods when we create our state to define what it does. For example, let's say that we want to show the message "Swapped to myState!" when we swap to this state:

myState.create = function() {
    show_message("Swapped to myState!");
}

By setting our create method, the state will now run this code as soon as we swap to it, but it won't run this code every frame that we stay in the state.
Now let's say we want our state to make us spin around in a circle while we're in the state.

myState.update = function() {
    image_angle += 10;
}

By setting the updatefunction, we'll run our spinning code every frame that we're in the state.
And now let's say that we want our state to show the message "Goodbye!" when we swap to a new state.

myState.destroy = function() {
    show_message("Goodbye!");
}

The destroy method will run when we swap away.
Finally, in order to actually use this state, we have to have our state machine swap to it. That means we'll want to call the swap method in our state machine. If we want to start in this state, we can swap in our create event.

Let's see what the whole thing looks like when it's all put together:

/// Create Event
sm = new StateMachine();

// Define myState
myState = new State();
myState.create = function(_sm) {
    show_message("Swapped to myState!");
}
myState.update = function(_sm) {
    image_angle += 10;
}
myState.destroy = function(_sm) {
    show_message("Goodbye!");
}

// Swap to myState!
sm.swap(myState);

Since we swap to myState immediately, our object should show the message "Swapped to myState!" once, the moment that it is created, and then it should start spinning around in a circle. If we made another state and swapped to it, by calling sm.swap(anotherState);, then we'd see the message "Goodbye!" appear before anotherState runs its own code.

5
Additional Example
So, what kind of things can we do with this system?

Well, let's imagine a scenario where we would want multiple states. Let's say we have a blue object that needs to walk right for one second then turn red. When it's red, it needs to wait for the user to press spacebar before changing back to blue and walking right again. Here we have two clear states. One of them moves us and checks the time to swap our state, the other one changes our color and checks for user input to swap our state. How would we code these?

/// Create Event
sm = new StateMachine();

// Blue State
blueState = new State();
blueState.update = function() {
    x += 1;
    if (sm.time >= room_speed) {
        sm.swap(redState);
    }
}

// Red State
redState = new State();
redState.create = function() {
    image_blend = c_red;
}
redState.update = function() {
    if (keyboard_check_pressed(vk_space)) {
        sm.swap(blueState);
    }
}
redState.destroy = function() {
    image_blend = c_blue;
}

// Start in the Blue State
sm.swap(blueState);

We can define our state code like so. Note a few things here. The blue state can check how long it has been running by reading our sm.time variable. The red state can swap the color to c_red when it starts, and back to c_blue when it ends. Putting code in our destroy method is useful, because if the red state had many different ways to swap out to another state, we can guarantee that the code in the destroy method will definitely run when the state is over.

Lastly we swap to the blueState to make it our starting state. We should be sure to also run our state machine in our object's Step event:

/// Step Event
sm.run();

And we now have a dynamic object that swaps color and behavior based on time and user input!