Legacy GM Question: Statemachines and how to use them

L

LordScrat

Guest
So, I wasn't sure whether I should post this thread here or in game design, but I decided for programming because in the end it is simply a way to clean and sector code.

First things first, what are state machines? By my understanding (which might not be universally agreed upon) a state machine is simply a construct where you can switch around states. For example from state moving to sate jumping, to state crouching, etc.

My problem now is, how the heck should I implement this obviously well received mechanic in my code? As it stands right now, all my code for my player is in one script and that gets called in the step event. But as I've tried to put everything in different states I encountered more and more problems separating my code in digestible bits. So now I'm kinda relying on you guys to maybe help me out and give me a push in the right direction.

For you to have a better understanding of what my player does, I'll post everything from him.

Create:
Code:
/// Creating Player

// Initiliazing globals
// scr_globals();

// Handling sprite properties
image_speed = 0.05;   

// Initializing states
enum STATE {
    NORMAL
}

state = STATE.NORMAL;

// Initializing variables
grav = global.DEF_GRAV;                 // Gravity, duh
fric = global.DEF_FRIC;                 // Friction, duh
j_spd = global.DEF_J_SPD;               // Jump speed
j_vel = global.DEF_J_VEL;               // Maximal jump velocity to be achieved with v_spd and grav
j_smt_height = global.DEF_J_SMT_HEIGHT  // Smooth jump height (Minimum height you jump in a controlled jump)
airjump = global.DEF_AIRJUMP;         
max_airjump = global.DEF_AIRJUMP;       // Amount of times player can jump while airborne
spd = global.DEF_SPD;                   // Movement speed
h_spd = 0;                              // Horizontal speed, duh
v_spd = 0;                              // Vertical speed, duh

shot_delay = global.DEF_SHOT_DELAY;     // Delay between projectiles
firing = false;                         // Firing boolean for shot delay

immune = false;                         // Immunity after hit


max_hp = global.DEF_HP;
hp = global.DEF_HP;

scr_load_player_stats();                // When savegame exists, load those stats
Step:
Code:
/// Step Calculation

// Statemachine
switch(state) {
    default: scr_state_normal();
}
and the script scr_state_normal:
Code:
/// scr_state_normal

// Movement keys
var r_key = keyboard_check(ord('D'));
var l_key = keyboard_check(ord('A'));
var j_key = keyboard_check_pressed(ord('W'));
var j_key_released = keyboard_check_released(ord('W'));

// Shooting keys
var s_key = keyboard_check(vk_space);
var s_up_key = keyboard_check(vk_up);
var s_down_key = keyboard_check(vk_down);

// No movement when hit
if(!immune) {
    // Check for ground
    if(place_meeting(x, y + 1, par_solid) &&  v_spd == 0) {
        airjump = global.DEF_AIRJUMP;
        v_spd = 0;
      
        // Jumping
        if(j_key)
            v_spd = -j_spd;
          
    // Else is airborne
    } else {
        // Gravity
        if(v_spd < j_vel)
            v_spd += grav; 
          
        // Check for airjump
        if(j_key && 0 < airjump) {
            v_spd = -j_spd;
            airjump--;
        }
      
        // Variable jump height
        if(j_key_released && v_spd < j_smt_height)
           v_spd = j_smt_height;
    }
  
    // Checking right
    if(r_key) {
        if(h_spd < spd)
            h_spd += fric;
        else
            h_spd = spd;
          
        // Left wall jump
        // if(place_meeting(x - 1, y, par_solid) && !place_meeting(x, y + 1, par_solid) && !l_key)
        //     v_spd = -j_spd;
    }
  
    // Checking left
    if(l_key) {
        if(-spd < h_spd)
            h_spd -= fric;
        else
            h_spd = -spd;
      
        // Right wall jump
        // if(place_meeting(x + 1, y, par_solid) && !place_meeting(x, y + 1, par_solid) && !r_key)
        //     v_spd = -j_spd;   
    }
  
    // Check for not moving
    if((!r_key && !l_key) || (r_key && l_key))
        if(h_spd != 0)
            if(h_spd < 0)
                h_spd += fric;
            else
                h_spd -= fric;
              
    // Move and collision
    scr_move_and_coll();
              
    // Animate running without shooting
    if(h_spd != 0 && place_meeting(x, y + 1, par_solid))
        sprite_index = spr_player_run;
    // Animate jumping without shooting
    else
        sprite_index = spr_player_jump;
      
    // Shooting
    if(s_key) {
        // Animate on the ground
        if(place_meeting(x, y + 1, par_solid))
            if(h_spd == 0)
                if(s_up_key)
                    // Animate standing with shooting up
                    sprite_index = spr_player_stand_shoot_up;
                else if(s_down_key)
                    // Animate standing with shooting down
                    sprite_index = spr_player_stand_shoot_down;
                else
                    // Animate standing with shooting
                    sprite_index = spr_player_stand_shoot;
            else
                if(s_up_key)
                    // Animate running with shooting up
                    sprite_index = spr_player_run_shoot_up;
                else if(s_down_key)
                    // Animate running with shooting down
                    sprite_index = spr_player_run_shoot_down;
                else
                    // Animate running with shooting
                    sprite_index = spr_player_run_shoot;
        // Animate in the air
        else
            if(s_up_key)
                // Animate jumping with shooting up
                sprite_index = spr_player_jump_shoot_up;
            else if(s_down_key)
                // Animate jumping with shooting down
                sprite_index = spr_player_jump_shoot_down;
            else
                // Animate jumping with shooting
                sprite_index = spr_player_jump_shoot;
              
        // Weapon direction (angle)
        var angle = 0;
        // tip x - x center * image facing
        // y center 33
        // Fist straight: 51 | 22
        // Fist up: 51 | 9
        // Fist down: 51 | 34
        var x_offset = (51 - sprite_get_xoffset(sprite_index)) * image_xscale;
        var y_offset = 22 - sprite_get_yoffset(sprite_index);
        if(s_up_key) {
            y_offset = 9 - sprite_get_yoffset(sprite_index);
          
            if(image_xscale < 0)
                angle = 315;
            else
                angle = 45;
        } else if(s_down_key) {
            y_offset = 33 - sprite_get_yoffset(sprite_index);
          
            if(image_xscale < 0)
                angle = 405;       
            else
                angle = 315;       
        }
          
        if(!firing) {
            firing = true;
            alarm[0] = shot_delay;
          
            var x_tip = x + x_offset;
            var y_tip = y + y_offset;
          
            var projectile = instance_create(x_tip, y_tip, obj_player_projectile);
          
            projectile.h_spd *= image_xscale;
          
            if(s_up_key) {
                projectile.h_spd *= 0.6;
                projectile.v_spd = -projectile.bullet_spd * 0.6;   
            }
          
            if(s_down_key) {
                projectile.h_spd *= 0.6;
                projectile.v_spd = projectile.bullet_spd * 0.6;   
            }
              
            projectile.image_xscale = image_xscale;
            projectile.image_angle = angle;
        }
    // Animate standing without shooting (standing still)
    } else if(h_spd == 0 && place_meeting(x, y + 1, par_solid))
        sprite_index = spr_player_stand;
  
    // Animate
    if(sign(h_spd) != 0)
        image_xscale = sign(h_spd);
} else {
    sprite_index = spr_player_damage;
  
    if(x <= x_en)
        h_spd = -kbck;
    else
        h_spd = kbck;
      
    v_spd = -kbck / 4;
  
    // Move and collision
    scr_move_and_coll(); 
}
and the move and collision script scr_move_and_coll:
Code:
/// scr_collision_detection

// Horizontal collision
if(place_meeting(x + h_spd, y, par_solid)) {
    while(!place_meeting(x + sign(h_spd), y, par_solid))
        x += sign(h_spd);
    h_spd = 0;
}

// Move horizontally
x += h_spd;

// Vertical collision
if(place_meeting(x, y + v_spd, par_solid)) {
    while(!place_meeting(x, y + sign(v_spd), par_solid))
        y += sign(v_spd);
    v_spd = 0;
}

// Move vertically
y += v_spd;
Alarm 0:
Code:
/// Firing delay
firing = false;
Alarm 1:
Code:
/// Immunity after hit
immune = false;
Collision with par_enemy (Parent object for every enemy)
Code:
/// Collision with any enemy
if(!immune) {
    immune = true;
  
    x_en = other.x;
    kbck = other.kbck;
  
    alarm[1] = room_speed * 0.33;
  
    hp--;
  
    if(hp <= 0)
        room_restart();
}
and last but not least, the probably least interesting part:
The draw GUI:
Code:
/// Draw GUI
for(i = 1; i <= max_hp; i++)
    if(i <= hp)
        draw_sprite(spr_gui_hp, 0, 20 + (i - 1) * 75, 20);
    else
        draw_sprite(spr_gui_hp, 1, 20 + (i - 1) * 75, 20);
I already have a pause menu, which works by deactivating everything but a few items, so that shouldn't mess with the alarms.

I wanted to also to note that I DO NOT ask for a complete solution like someone that makes this for me (Although I wouldn't say no if someone made that with good explanations as to why), but for a general idea what exactly I could do, like what states I should consider and what I have to pay attention to to not get bugs
 

Jakylgamer

Member
simply put state machines are a finite number of actions you can perform per state.

a simple example would be
Code:
switch(state) {
    case "state1":
        //actions you can perform in this state
       if keyboard_check(vk_space) {
          state = "state2"
        }
    break;

    case "state2":
        //actions you can perform in this state
       if !keyboard_check(vk_space) {
          state = "state1"
        }
    break;
    ///etc...
}
in the example it changes to state 2 as long as space is held down else it reverts back to state1
 

TheouAegis

Member
State machines are a lot easier to write from the ground up, not from the ceiling down.

there is no one way to write a state machine. some people like putting their idle and walking and jumping states in one, others separate out every little thing so that you have the idle state in one, the accelerating into a walk State, the walking state, the running state, the stopping state, the initial jumping State, the jumping up into the air State, the falling through the air State, The Landing State, the attacking standing State, the attacking Crouch State, the attacking jump state, etc etc. like I said, it is a lot easier to write a state machine from the ground up.
 

Neptune

Member
A good state machine is one that works for your needs. If each part of your object (player, enemy, npc etc) has a lot of complex things that could happen, break it up! Otherwise try to clump simple things together.

I'd recommend creating an enum, and a "main/home" state, and expand out from it.

Good luck!
 
Top