Complicated fighting game state machines?

There’s a very small thread on finite state machines already, but it’s a little old, and my topic is less broad I think. I was hoping to get more information on what britzl mentioned in that thread (https://github.com/kyleconroy/lua-state-machine) if anyone can chime in.

I’m trying to make a game sort of like Super Smash Bros. Melee, which I think utilizes a very complicated finite state machine to achieve its behavior. I’m in the process of trying to figure out all of the different states and how to transition between them, but I’m getting bogged down with not knowing how to integrate the specific FSM mentioned in Defold.

I’m not an experienced programmer so I hope you can bear with me.
I have tons of questions along the lines of:

  • How do I deal with getting controller input to the FSM?
  • How do I deal with governing the state transitions?
  • How can I keep track of the time between states for frame-perfect state execution?

I’m pretty lost. If anyone can provide even broad clues it might help.

Here is what my state machine is looking like so far (the spacing doesn’t seem to want to copy right in here, sorry):

local state_machine = machine.create(
{
    initial = "standing",
    events =
    {
        { name = "stop",            from = "crouching",           to = "standing" },
        { name = "stop",            from = "walking",             to = "standing" },
        { name = "stop",            from = "dashing",             to = "standing" },
        { name = "stop",            from = "running",             to = "standing" },
        { name = "stop",            from = "landing",             to = "standing" },
        { name = "stop",            from = "ground_attacking",    to = "standing" },
        { name = "stop",            from = "shielding",           to = "standing" },
        { name = "stop",            from = "rolling",             to = "standing" },
        { name = "stop",            from = "spot_dodging",        to = "standing" },
        { name = "stop",            from = "getting_up",          to = "standing" },
        { name = "stop",            from = "teching",             to = "standing" },

        { name = "crouch",          from = "standing",            to = "crouching" },
        { name = "crouch",          from = "running",             to = "crouching" },

        { name = "walk",            from = "standing",            to = "walking" },

        { name = "dash",            from = "standing",            to = "dashing" },
        { name = "dash",            from = "walking",             to = "dashing" },

        { name = "run",             from = "dashing",             to = "running" },

        { name = "jump",            from = "standing",            to = "jump_squatting" },
        { name = "jump",            from = "walking",             to = "jump_squatting" },
        { name = "jump",            from = "dashing",             to = "jump_squatting" },
        { name = "jump",            from = "running",             to = "jump_squatting" },
        { name = "jump",            from = "shielding",           to = "jump_squatting" },

        { name = "get_hurt",        from = "*",                   to = "hurt" },

        { name = "fall",            from = "standing",            to = "falling" },
        { name = "fall",            from = "walking",             to = "falling" },
        { name = "fall",            from = "dashing",             to = "falling" },
        { name = "fall",            from = "running",             to = "falling" },
        { name = "fall",            from = "jump_squatting",      to = "falling" },
        { name = "fall",            from = "shielding",           to = "falling" },

        { name = "fast_fall",       from = "falling",             to = "fast_falling" },

        { name = "tumble",          from = "hurt",                to = "tumbling" },

        { name = "helplessly_fall", from = "air_dodging",         to = "helplessly_falling" },

        { name = "land",            from = "falling",             to = "landing" },
        { name = "land",            from = "fast_falling",        to = "landing" },
        { name = "land",            from = "helplessly_falling",  to = "landing" },
        { name = "land",            from = "air_attacking",       to = "landing" },

        { name = "ground_attack",   from = "crouching",           to = "ground_attacking" },
        { name = "ground_attack",   from = "walking",             to = "ground_attacking" },
        { name = "ground_attack",   from = "dashing",             to = "ground_attacking" },
        { name = "ground_attack",   from = "running",             to = "ground_attacking" },
        { name = "ground_attack",   from = "lying_down",          to = "ground_attacking" },

        { name = "air_attack",      from = "falling",             to = "air_attacking" },
        { name = "air_attack",      from = "fast_falling",        to = "air_attacking" },
        { name = "air_attack",      from = "tumbling",            to = "air_attacking" },

        { name = "shield",          from = "crouching",           to = "shielding" },
        { name = "shield",          from = "walking",             to = "shielding" },
        { name = "shield",          from = "dashing",             to = "shielding" },
        { name = "shield",          from = "running",             to = "shielding" },

        { name = "get_stunned",     from = "crouching",           to = "stunned" },
        { name = "get_stunned",     from = "walking",             to = "stunned" },
        { name = "get_stunned",     from = "dashing",             to = "stunned" },
        { name = "get_stunned",     from = "running",             to = "stunned" },
        { name = "get_stunned",     from = "shielding",           to = "stunned" },

        { name = "roll",            from = "shielding",           to = "rolling" },

        { name = "spot_dodge",      from = "shielding",           to = "spot_dodging" },

        { name = "air_dodge",       from = "falling",             to = "air_dodging" },
        { name = "air_dodge",       from = "fast_falling",        to = "air_dodging" },

        { name = "lie_down",        from = "hurt",                to = "lying_down" },
        { name = "lie_down",        from = "tumbling",            to = "lying_down" },

        { name = "get_up",          from = "lying_down",          to = "getting_up" },

        { name = "tech",            from = "hurt",                to = "teching" },
        { name = "tech",            from = "tumbling",            to = "teching" },

        { name = "grab_ledge",      from = "falling",             to = "on_ledge" },
        { name = "grab_ledge",      from = "fast_falling",        to = "on_ledge" },
        { name = "grab_ledge",      from = "tumbling",            to = "on_ledge" },
        { name = "grab_ledge",      from = "helplessly_falling",  to = "on_ledge" },

        { name = "ledge_roll",      from = "on_ledge",            to = "ledge_rolling" },

        { name = "ledge_jump",      from = "on_ledge",            to = "ledge_jumping" },

        { name = "ledge_get_up",    from = "on_ledge",            to = "ledge_getting_up" },

        { name = "ledge_attack",    from = "on_ledge",            to = "ledge_attacking" },

    },
    callbacks =
    {
        onstop = function(self, event, from, to)
        end,

        oncrouch = function(self, event, from, to)
        end,

        onwalk = function(self, event, from, to)
        end,

        ondash = function(self, event, from, to)
        end,

        onrun = function(self, event, from, to)
        end,

        onjump = function(self, event, from, to)
        end,

        onget_hurt = function(self, event, from, to)
        end,

        onfall = function(self, event, from, to)
        end,

        onfast_fall = function(self, event, from, to)
        end,

        ontumble = function(self, event, from, to)
        end,

        onhelplessly_fall = function(self, event, from, to)
        end,

        onland = function(self, event, from, to)
        end,

        onground_attack = function(self, event, from, to)
        end,

        onair_attack = function(self, event, from, to)
        end,

        onshield = function(self, event, from, to)
        end,

        onget_stunned = function(self, event, from, to)
        end,

        onroll = function(self, event, from, to)
        end,

        onspot_dodge = function(self, event, from, to)
        end,

        onair_dodge = function(self, event, from, to)
        end,

        onlie_down = function(self, event, from, to)
        end,

        onget_up = function(self, event, from, to)
        end,

        ontech = function(self, event, from, to)
        end,

        ongrab_ledge = function(self, event, from, to)
        end,

        onledge_roll = function(self, event, from, to)
        end,

        onledge_jump = function(self, event, from, to)
        end,

        onledge_get_up = function(self, event, from, to)
        end,

        onledge_attack = function(self, event, from, to)
        end,
    }
})

First the theory

State machines themselves are just an efficient way to manage code branching/forking (like in if .. then .. else. They have been around for about 5 decades, and didn’t change much since then. So don’t worry about the thread being “a bit old” - anything on state machines in Lua since 2007 is modern enough.

Now, a bit more practical advice

If you are not an experienced programmer start with following:

  • in your game object declare a local variable local state = "idle"

  • in your game object’s methods define whatever behavior that you want for idle:
    if state == "idle" then ... end

  • the state should change on certain events in the game. Perhaps in your on_input you will add something like:

                 state = "moving_right" end ```

- now you need to define behavior for the "moving_right" state. 

This is the simplest "state machine" - no magic. Check out the Defold tutorials once again: this approach is described in them. Only when (and if) you realize that going through a dozen of "if"s is too tedious, it's time to think about using a more abstract implementation. But by that time, hopefully, you'll get enough knowledge, just as a by-product of working with code, to understand the code on Github.
4 Likes

Thanks for the help!

I tried what you said and it is much simpler than trying to use that Lua module I linked.

I’m able to start working on the states themselves now, unfortunately what I am trying to do is far more complicated than any example I can find online.

I suppose now I am searching for the best practices for making a really complicated state machine manageable and easy to read.

Like there has to be a line you draw somewhere between breaking things down into states and just using flags and if statements. For example, in Super Smash Bros. Melee, you can become invincible for small periods of time. Obviously it would be extremely annoying to have duplicate states for everything that mean you are invincible. It would be better to just be a flag. The states a character can be in in Super Smash Bros. Melee could probably be broken down to hundreds if you get really specific.

And in your example (I know it’s just an example) you have a state for moving right, I would opt to have states be direction independent as to not double my number of states.

I haven’t even gotten started yet with hitboxes, attacks, and timings, but that’s all something I could use help with too. There is a huge number of moves you can do in Super Smash Bros. Melee, probably in the mid 20s depending on the character. Should those all be different states or should there be an attacking state that handles it?

If anyone can help provide some information or ideas I would be grateful!

Yes, a flag seems reasonable. Or maybe disable hit boxes while invincible and start a timer that enables them again.

It depends. Is there some difference between the attacks besides the animation? Is it possible to go from one attack to any other attack or can certain attacks open up for special attacks or combos? Can you do any kind of attack while on the ground or in the air?

I’m guessing this will be the hard part. Getting the hitboxes and timings just right. There will likely be a lot of juggling of collision object enable/disable at the correct times. Not sure how I’d solve it to be honest.

Thanks for the reply!

Most attacks in the game are singular attacks, however, most characters have a ground jab that leads in to different secondary jabs if you push the button again within a certain time. You can only do special air attacks in the air, one for every direction and one neutral. Also, if you are doing an air attack and you land during it, there is a certain amount of landing lag that is dependent on the air attack you were doing. There are several ground attacks that can only be done on the ground.

Right now my idea is to have a ground_attack state and an air_attack state, and somehow take care of which attack it is with a flag or something. Maybe another state machine in parallel? I don’t know if that’s a thing people do in this situation.

And yeah I am completely lost with the hitbox part, perhaps I will start a different thread about it since this one is about state machines.

Now that I am starting to code the states, it is starting to become a pretty large mess. It’s only going to get worse considering what I am trying to do.

Right now I am ending up with tons of if statements everywhere. I don’t mind that too much, the logic is simple enough so far, the problem is that I can’t figure out a way to break the states up into a manageable format. Right now I have to have them all in line in a huge if elseif chain.

Is there any way to break this up? It would be nice to be able to find the state I want to work with easily. I can’t even imagine working with this when I get up to 30-40 states.

Here is what I have so far:

[CODE]local function handle_states(self)
if self.state ~= self.last_state then
self.state_timer = 0.0
end

self.last_state = self.state

if self.state == “standing” then
play_animation(self, anim_idle)

change_velocity_x(self, 0.0, ground_friction)
    
if math.abs(game_inputs[player_number].lstick.x_axis.negative.deadzoned_value) > 0 then
  self.direction = -1
elseif math.abs(game_inputs[player_number].lstick.x_axis.positive.deadzoned_value) > 0 then
  self.direction = 1
end

if game_inputs[player_number].lstick.x_axis.smashed == false and
  math.abs(game_inputs[player_number].lstick.x_axis.deadzoned_value) > 0 and
  game_inputs[player_number].lstick.x_axis.smash_timer > turn_around_hold_time then
  self.state = "walking"
end

if game_inputs[player_number].lstick.x_axis.smashed == true then
  self.state = "dashing"
end

if game_inputs[player_number].y.pressed then
  self.state = "jumping"
end

if self.is_on_ground == false then
  self.state = "falling"
end

elseif self.state == “walking” then
play_animation(self, anim_dash)

if game_inputs[player_number].lstick.x_axis.negative.deadzoned_value > 0 then
  change_velocity_x(self, walk_speed * game_inputs[player_number].lstick.x_axis.deadzoned_value, walk_acceleration)
  self.direction = -1
elseif game_inputs[player_number].lstick.x_axis.positive.deadzoned_value > 0 then
  change_velocity_x(self, walk_speed * game_inputs[player_number].lstick.x_axis.deadzoned_value, walk_acceleration)
  self.direction = 1
end

if game_inputs[player_number].lstick.x_axis.deadzoned_value == 0 then
  self.state = "standing"
  change_velocity_x(self, 0.0)
end

if game_inputs[player_number].lstick.x_axis.smashed == true then
  self.state = "dashing"
end

if game_inputs[player_number].y.pressed then
  self.state = "jumping"
end

if self.is_on_ground == false then
  self.state = "falling"
end

elseif self.state == “dashing” then
play_animation(self, anim_dash)

if game_inputs[player_number].lstick.x_axis.negative.deadzoned_value > 0 then
  change_velocity_x(self, -dash_speed)
  self.direction = -1
elseif game_inputs[player_number].lstick.x_axis.positive.deadzoned_value > 0 then       
  change_velocity_x(self, dash_speed)
  self.direction = 1
end

if game_inputs[player_number].lstick.x_axis.deadzoned_value == 0 then
  self.state = "standing"
  change_velocity_x(self, 0.0)
end

if game_inputs[player_number].y.pressed then
  self.state = "jumping"
end

if self.is_on_ground == false then
  self.state = "falling"
end

elseif self.state == “falling” then
play_animation(self, anim_fall)

change_velocity_y(self, -fall_speed, fall_acceleration)
change_velocity_x(self, 0.0, air_friction)

if math.abs(game_inputs[player_number].lstick.x_axis.deadzoned_value) > 0 then
  change_velocity_x(self, air_speed * game_inputs[player_number].lstick.x_axis.deadzoned_value, air_acceleration)
end

if self.is_on_ground == true then
  self.state = "landing"
end

if game_inputs[player_number].y.pressed then
  self.state = "jumping"
end

elseif self.state == “landing” then
play_animation(self, anim_idle)

change_velocity_x(self, 0.0, ground_friction)

if self.state_timer > normal_landing_lag then
  self.state = "standing"
end

if self.is_on_ground == false then
  self.state = "falling"
end

elseif self.state == “jumping” then
play_animation(self, anim_jump)

change_velocity_y(self, full_hop_speed)

if self.is_on_ground == false then
  self.state = "falling"
end

end
end[/CODE]

1 Like

Since you posted your question I’ve been playing around with a state machine implementation in Lua and Defold. It’s not a trivial thing to get right, especially if it’s supposed to handle all of the different states of a fighting game such as Super Smash Bros. I’ll share something once I’m happy with it.

1 Like

Well I look forward to seeing what you come up with!

Alright then, so I will chip in here with my implementation of a state machine in Lua that I came up with in PICO-8 and have been using in Defold.

I am a novice myself (been coding for just a few months now) so I’m totally confident there are far better ways of doing this sort of thing, and I’m certain @britzl will come up with something much more capable and professional … but you might get some mileage out of this :slight_smile:

First up, just in case you haven’t read it, this is a superb introduction to state machines that helped me get my head around all this. (The whole book is brilliant and well worth checking out)

Now, I represent state machines as tables, taking advantage of the fact that Lua lets me define functions as entries in a table. Let’s say I have a game object with an accompanying script and I want to define and control its behaviour in a state machine. Inside the object’s script I define one main table for the whole state machine, which contains a series of sub-tables for each state. Each state sub-table contains a variable for the name of the state and three functions: init(), update() and exit().

Then outside of this main table I have a variable in the script which I use to record the current state. In the main update function of the object script I then call the update function of the current state sub-table. The individual state’s update functions contain all the functionality that is available to the game object when it is in that current state, and the code which switches it to other states according to user input, collisions etc…

To do this the object’s script contains a function called set_state() which first calls the exit() function of the old state, then determines the index for the sub-table of the requested state, adjusts the current state variable accordingly, and then calls the init() function of the new state. The exit() and init() functions allow me to perform actions that I only want to occur once whenever the state in question is entered or left which is useful for setting animations, playing sound effects and resetting timers etc…

So, for example, the following state machine has just two states which will cycle between each other frame by frame:

local state_machine = {
  {
    name = "state_1",
    init = function(self)
      print("Entered STATE 1")
    end,
    update = function(self, dt)
        set_state(self, "state_2")
    end,
    exit = function(self)
      print("Leaving STATE 1")
    end,
  },
  {
    name = "state_2",
    init = function(self)
      print("Entered STATE 2")
    end,
    update = function(self, dt)
        set_state(self, "state_1")
    end,
    exit = function(self)
      print("Leaving STATE 2")
    end,
  },
}

Here is the variable and set_state() function:

local state = 1 -- variable to track the current state

function set_state(self, new_state)
  state_machine[state].exit(self)   -- run the exit function of the current state
  -- now determine the table index to set from the "new_state" string ...
  local state_to_set = 0
  for i, v in ipairs(state_machine) do -- loop through states in state_machine table
    if v.name == new_state then -- if state.name is the requested new state
      state_to_set = i -- record the requested state's index (i)
      break -- stop looping as we have found the next state
    end
  end
  state = state_to_set  -- adjust the current state variable
  state_machine[state].init(self)  -- call the init() script of the new current state
end

The main update function for the script calls the current state’s update function like so:

function update(self, dt)
	state_machine[state].update(self,dt)
end

I also call the starting state’s init() funciton in the object script init() funtion:

function init(self)
	state_machine[state].init(self,dt)
end

I hope that makes some kind of sense, not so sure I explained it particularly well :slight_smile:

However, for a more fleshed out example you can have a look here at a quick mock-up I wrote of your state machine using this method. You’ll see there are 5 states in the example (standing, walking, jumping, falling, landing) and they are easy to find and work with individually using this system. I also included a basic example of accessing a module which can be used to communicate between an input handler object and the state machine in an object script.

I use this method to not only control individual objects behaviour but also to control overall game states so I can move between main menu, gameplay, scoreboard functionality etc. or other types of game loop control. You can also fairly simply have multiple state machines running concurrently this way for the same object.

Anyway, I’d be interested to know what you or anyone else thinks of this method and I hope it may help you out a little at least :wink:

4 Likes

Thanks for the detailed response!

Your method is nice, certainly better than the one I’m using currently. I need entry and exit actions.

I think it would still get hard to work with when it gets more complicated though. When finished breaking down the state machine of Super Smash Bros. Melee, you would end up with thousands upon thousands of lines of state code, all right next to each other in the main character file. I suppose you can fold the states down in a text editor, but you still would have to scroll through and look for the proper one to work on.

It would be nice to be able to split each state up to a different file. Or maybe I just need to get used to dealing with large files.

1 Like

Lua modules allow you to break up large script and put some logic into reusable scripts that you can require and use from multiple locations. I’m sure you could break up the state logic into multiple Lua modules.

That sounds like a nice way to do things. Thanks for the advice!