Skip to content

Instantly share code, notes, and snippets.

@1bardesign
Created May 27, 2019 23:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 1bardesign/952c77a6acdee277f0e37ec4fb54c233 to your computer and use it in GitHub Desktop.
Save 1bardesign/952c77a6acdee277f0e37ec4fb54c233 to your computer and use it in GitHub Desktop.
state machine example code - to help understand how to define states, and how to use them to keep related logic all in one place
--require the state machine module
local state_machine = require("state_machine")
--variable for the example state machine - set up in love.load
local example_machine
--(simple "last 10" history of what states we've been to)
local state_history = {}
function add_history(s)
table.insert(state_history, s)
if #state_history > 10 then
table.remove(state_history, 1)
end
end
function draw_history()
love.graphics.print("path we took: "..table.concat(state_history, " -> "), 10, 100)
end
function love.load()
-- this is some shared update/draw functionality for the states
-- (it saves us copy-pasting as much code below)
local function transition_state_update(m, s, dt)
-- we store our possible transitions in the state table as {button, state} pairs
for i,v in ipairs(s.transitions) do
local button, state = unpack(v)
if love.keyboard.isDown(button) then
m:set_state(state)
break
end
end
end
local function transition_state_draw(name, m, s)
love.graphics.print("we are in "..name, 10, 10)
for i,v in ipairs(s.transitions) do
local button, state = unpack(v)
love.graphics.print("press "..button.." to go to "..state, 10, 10 + i * 20)
end
end
--set up the example state machine
example_machine = state_machine:new(
--the first argument is a table of named states
--each state can have enter, exit, update and draw callbacks
--these all receive:
-- - the machine as their first argument (can be used to share data between all states, and to change state)
-- - the current state as their second argument (can be used for convenient state-local data)
--update also recieves a dt argument indicating the time passed since last frame
--enter and exit are useful for setting up/tearing down required conditions for update/draw and triggering sound effects etc
{
state_a = {
transitions = {
{"b", "state_b"},
{"c", "state_c"},
},
enter = function(m, s)
add_history("a")
end,
update = transition_state_update,
draw = function(m, s)
transition_state_draw("state_a", m, s)
end,
exit = function(m, s) end,
},
state_b = {
transitions = {
{"a", "state_a"},
{"c", "state_c"},
},
enter = function(m, s)
add_history("b")
end,
update = transition_state_update,
draw = function(m, s)
transition_state_draw("state_b", m, s)
end,
exit = function(m, s) end,
},
--this state does some special logic to wiggle the graphics a bit
--note that all the spinning functionality is completely contained here
--so you dont need to refer anywhere else when thinking about this state
state_c = {
transitions = {
{"a", "state_a"},
{"b", "state_b"},
},
enter = function(m, s)
add_history("c")
s.spin = 0
end,
update = function(m, s, dt)
transition_state_update(m, s)
s.spin = s.spin + dt
end,
draw = function(m, s)
love.graphics.push()
local spin_x, spin_y = 100, 50
love.graphics.translate(spin_x, spin_y)
love.graphics.rotate(math.sin(s.spin * math.pi) * 0.1)
love.graphics.translate(-spin_x, -spin_y)
transition_state_draw("state_c (definitely the coolest state)", m, s)
love.graphics.pop()
end,
exit = function(m, s) end,
},
},
--the second argument is the state to start in
--if the starting state is not provided it will start in no state at all and can be started later by setting a state
"state_a")
end
--update called each frame to do logic
function love.update(dt)
--just update the machine
example_machine:update(dt)
end
--draw called each frame to display
function love.draw()
--draw the current machine state
example_machine:draw()
--draw the history up til this point
draw_history()
end
--[[
state machine
a finite state machine implementation;
each state is a table with optional enter, exit, update and draw callbacks
which each optionally take the machine, and the state table as arguments
on changing state, the outgoing state's exit callback is called, then the incoming state's
enter callback is called.
on update, the current state's update callback is called
on draw, the current state's draw callback is called
]]
local state_machine = {}
state_machine._mt = {
__index = state_machine,
}
function state_machine:new(states, start)
local ret = setmetatable({
states = states or {},
current_state = "",
}, state_machine._mt)
if type(start) == "string" then
ret:set_state(start)
end
return ret
end
-------------------------------------------------------------------------------
--internal helpers
function state_machine:_get_state()
return self.states[self.current_state]
end
--make an internal call, with up to 4 arguments
function state_machine:_call(name, a, b, c, d)
local state = self:_get_state()
if state and type(state[name]) == "function" then
return state[name](self, state, a, b, c, d)
end
return nil
end
-------------------------------------------------------------------------------
--various checks
function state_machine:in_state(name)
return self.current_state == name
end
function state_machine:has_state(name)
return self.states[name] ~= nil
end
-------------------------------------------------------------------------------
--state adding/removing
--add a state
function state_machine:add_state(name, data)
if self.has_state(name) then
error("error: added duplicate state "..name)
else
self.states[name] = data
if self:in_state(name) then
self:_call("enter")
end
end
return self
end
--remove a state
function state_machine:remove_state(name)
if not self.has_state(name) then
error("error: removed missed state "..name)
else
if self:in_state(name) then
self:_call("exit")
end
self.states[name] = nil
end
return self
end
--hard-replace a state table
--if do_transitions is truthy and we're replacing the current state,
--exit is called on the old state and enter is called on the new state
function state_machine:replace_state(name, data, do_transitions)
local current = self:in_state(name)
if do_transitions and current then
self:_call("exit")
end
self.states[name] = data
if do_transitions and current then
self:_call("enter")
end
return self
end
--ensure a state doesn't exist
function state_machine:clear_state(name)
return self:replace_state(name, nil, true)
end
-------------------------------------------------------------------------------
--transitions and updates
function state_machine:set_state(state, reset)
if self.current_state ~= state or reset then
self:_call("exit")
self.current_state = state
self:_call("enter")
end
return self
end
--perform an update
--pass in an optional delta time which is passed as an arg to the state functions
function state_machine:update(dt)
return self:_call("update", dt)
end
function state_machine:draw()
self:_call("draw")
end
return state_machine
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment