GMS 2.3+ Struct Based State Machines

samspade

Member
I'm working on creating a struct based state machine system. My primary goals are for it to be:
  1. Simple to use
  2. Handle entering and exiting a state separately from running a state
  3. Use structs
  4. be flexible and expandable
My current set up seems to be working well, but I haven't tried to do this seriously before. I'm wondering if anyone has comments on this system (suggestions for improvements, flaws, etc.) or a different system they would be willing to share?

Current 'global' functions (i.e. in a script asset):

GML:
function state(_start, _run, _stop) constructor {

    start = function() {};
    run = function() {};
    stop = function() {};
   
    if (_start != undefined) {
        start = _start;
    }
    if (_run != undefined) {
        run = _run;
    }
    if (_stop != undefined) {
        stop = _stop;
    }

}


function run_state() {
    active_state.run();
}

function change_state(_state) {
    active_state.stop();
    active_state = _state;
    active_state.start();
}

function init_state(_state) {
    active_state = _state;
    active_state.start();
}

Example Use Create Event

GML:
move_state = new state(undefined, function(){
   
    hsp = (right - left) * movement_speed;
    if (hsp != 0) {
        image_xscale = sign(hsp);
    }

    if (hsp != 0) {
        sprite_index = herochar_run_anim;
    } else {
        sprite_index = herochar_idle_anim;
    }
       
    if (jump_pressed) {
        vsp = jump_speed;
        change_state(fall_state);
        return;
    }  
   
    if (!place_meeting(x, y + 1, solid_parent)) {
        change_state(fall_state);
        return;
    }
   
    if (attack_pressed) {
        change_state(attack_state);
        return;
    }
   
    vsp += grav;

}, undefined);

//...more states

init_state(move_state);
Example Use Step Event
GML:
run_state();
My only current issue with this format is that initializing the states is a little clunky—you must include the undefineds for unused states and you could be passing in some pretty long anonymous functions and perhaps three of them for a single state. I thought about setting it up so you can't assign states on create and instead would assign them after creating the struct like this:

GML:
move_state = new state();
move_state.run = function() {
    //...add all the move state code here
}
This would mean the main struct state would just be:

GML:
function state() constructor {

    start = function() {};
    run = function() {};
    stop = function() {};

}
Edited: In trying this out, I think I like this version a little bit better. The state constructor is simpler, as is the actual initializing of the state and state functions (although it now happens over multiple lines. Additionally, it is a little easier to use across multiple obectsions as you can simple do something like move_state.run = method(id, basic_enemy_move_state_run); (which you could do in the above system as well, I just think it is easier to read and understand when you have this on its own line rather than one of possible three arguments in a constructor function).
 
Last edited:

kburkhart84

Firehammer Games
The way I did the state machine structs in my Jam entry was similar, yet different from yours.

I made each actual state be its own struct. Walking was a struct, jumping another, falling another.

I made the "enter state" stuff happen as part of the struct's constructor. Then the exit state was just another function you called before deleting the old state.

In my case, the states weren't really re-usable everywhere, but I see no reason I couldn't have made them reusable, just adding some variables to the constructors for things(or having the state check instance variables) for things like what sprite to use for different things.

As long as I personally remembered to put in the step event the calls to the state's step function, it worked fine.

Another thing I did was to put the owning instance id into the state. This made it much easier to control the instance pretty much directly(using with() ).

I didn't do this, but I'm pretty sure that the above detail would also let me access the previous state variable to call its exit function before the new state gets assigned to the variable since the variable doesn't change to the new struct until the constructor returns, and the constructor won't return until it handles calling the exit function. I didn't test this, but since you can always access a variable within the assignment(like myVar = myVar + 10; I see no reason why this wouldn't work.
 

samspade

Member
The way I did the state machine structs in my Jam entry was similar, yet different from yours.

I made each actual state be its own struct. Walking was a struct, jumping another, falling another.

I made the "enter state" stuff happen as part of the struct's constructor. Then the exit state was just another function you called before deleting the old state.

In my case, the states weren't really re-usable everywhere, but I see no reason I couldn't have made them reusable, just adding some variables to the constructors for things(or having the state check instance variables) for things like what sprite to use for different things.

As long as I personally remembered to put in the step event the calls to the state's step function, it worked fine.

Another thing I did was to put the owning instance id into the state. This made it much easier to control the instance pretty much directly(using with() ).

I didn't do this, but I'm pretty sure that the above detail would also let me access the previous state variable to call its exit function before the new state gets assigned to the variable since the variable doesn't change to the new struct until the constructor returns, and the constructor won't return until it handles calling the exit function. I didn't test this, but since you can always access a variable within the assignment(like myVar = myVar + 10; I see no reason why this wouldn't work.
You mean something like this?

GML:
///somewhere relevant

function walk_state(_id) constructor {
    my_owner = _id;
    //do whatever you want when switching
    static step = function() {
        with (my_owner) {
            //code for walk state here
            if (jump) {
                my_state = new jump_state(id);
                other.destroy();
            }
        }
    }
    static destroy = function() {
        //code thath hapens when you delete this struct
    }
}


///object create event
my_state = new walk_state(id);


///object step event
my_state.step();
 

kburkhart84

Firehammer Games
You mean something like this?
That is pretty much the idea. You might want to do the destroy before you initiate the new one though, not because of an error or anything, but just because it makes logical sense to exit one state before entering the next, and just in case you ever code something that depends on that ordering(not likely but who knows?).
 
Top