Created
May 27, 2019 23:49
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
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