[modified] minor semantics of state machines, added machinery to allow them to be nested more easily, added notes

This commit is contained in:
Max Cahill 2020-06-02 15:02:20 +10:00
parent 11483c52ad
commit 01713f766f

View File

@ -12,6 +12,10 @@
on draw, the current state's draw callback is called on draw, the current state's draw callback is called
TODO: consider coroutine friendliness TODO: consider coroutine friendliness
TODO: consider refactoring the callback signatures to allow using objects with methods
like update(dt)/draw() directly
current pattern means they need to be wrapped (as in :as_state())
]] ]]
local path = (...):gsub("state_machine", "") local path = (...):gsub("state_machine", "")
@ -41,12 +45,27 @@ end
--make an internal call --make an internal call
function state_machine:_call(name, ...) function state_machine:_call(name, ...)
local state = self:_get_state() local state = self:_get_state()
if state and type(state[name]) == "function" then if state then
return state[name](self, state, ...) if type(state[name]) == "function" then
return state[name](self, state, ...)
elseif type(state) == "function" then
return state(self, name, ...)
end
end end
return nil return nil
end end
--make an internal call and transition if the return value is a valid state
--return the value if it isn't a valid state
function state_machine:_call_and_transition(name, ...)
local r = self:_call(name, ...)
if self:has_state(r) then
self:set_state(r, r == self.current_state)
return nil
end
return r
end
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
--various checks --various checks
@ -63,7 +82,7 @@ end
--add a state --add a state
function state_machine:add_state(name, data) function state_machine:add_state(name, data)
if self.has_state(name) then if self:has_state(name) then
error("error: added duplicate state "..name) error("error: added duplicate state "..name)
else else
self.states[name] = data self.states[name] = data
@ -77,7 +96,7 @@ end
--remove a state --remove a state
function state_machine:remove_state(name) function state_machine:remove_state(name)
if not self.has_state(name) then if not self:has_state(name) then
error("error: removed missed state "..name) error("error: removed missed state "..name)
else else
if self:in_state(name) then if self:in_state(name) then
@ -99,13 +118,13 @@ function state_machine:replace_state(name, data, do_transitions)
end end
self.states[name] = data self.states[name] = data
if do_transitions and current then if do_transitions and current then
self:_call("enter") self:_call_and_transition("enter")
end end
return self return self
end end
--ensure a state doesn't exist --ensure a state doesn't exist; transition out of it if we're currently in it
function state_machine:clear_state(name) function state_machine:clear_state(name)
return self:replace_state(name, nil, true) return self:replace_state(name, nil, true)
end end
@ -113,23 +132,52 @@ end
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
--transitions and updates --transitions and updates
--set the current state
--if the enter callback of the target state returns a valid state name, then
-- it is transitioned to in turn, and so on until the machine is at rest
function state_machine:set_state(state, reset) function state_machine:set_state(state, reset)
if self.current_state ~= state or reset then if self.current_state ~= state or reset then
self:_call("exit") self:_call("exit")
self.current_state = state self.current_state = state
self:_call("enter") self:_call_and_transition("enter")
end end
return self return self
end end
--perform an update --perform an update
--pass in an optional delta time which is passed as an arg to the state functions --pass in an optional delta time which is passed as an arg to the state functions
--if the state update returns a string, and we have that state
-- then we change state (reset if it's the current state)
-- and return nil
--otherwise, the result is returned
function state_machine:update(dt) function state_machine:update(dt)
return self:_call("update", dt) return self:_call_and_transition("update", dt)
end end
--draw the current state
function state_machine:draw() function state_machine:draw()
self:_call("draw") self:_call("draw")
end end
return state_machine --wrap a state machine in a table suitable for use directly as a state in another state_machine
--upon entry, this machine will be forced into enter_state
--the parent will be accessible under m.parent
function state_machine:as_state(enter_state)
if not self._as_state then
self._as_state = {
enter = function(m, s)
self.parent = m
self:set_state(enter_state)
end,
update = function(m, s, dt)
return self:update(dt)
end,
draw = function(m, s)
return self:draw()
end,
}
end
return self._as_state
end
return state_machine