2020-01-29 03:26:28 +00:00
|
|
|
--[[
|
|
|
|
state machine
|
|
|
|
|
2020-12-01 04:33:22 +00:00
|
|
|
a finite state machine implementation
|
2020-01-29 03:26:28 +00:00
|
|
|
|
2020-12-01 04:33:22 +00:00
|
|
|
each state is either:
|
|
|
|
|
|
|
|
- a table with enter, exit, update and draw callbacks (all optional)
|
|
|
|
which each take the state table and varargs as arguments
|
2021-07-05 04:53:55 +00:00
|
|
|
- a plain function
|
|
|
|
which gets passed the current event name, the machine table, and varargs as arguments
|
2020-12-01 04:33:22 +00:00
|
|
|
|
|
|
|
on changing state, the outgoing state's exit callback is called
|
|
|
|
then the incoming state's enter callback is called
|
|
|
|
enter can trigger another transition by returning a string
|
2020-01-29 03:26:28 +00:00
|
|
|
|
|
|
|
on update, the current state's update callback is called
|
2020-12-01 04:33:22 +00:00
|
|
|
the return value can trigger a transition
|
|
|
|
|
2020-01-29 03:26:28 +00:00
|
|
|
on draw, the current state's draw callback is called
|
2020-12-01 04:33:22 +00:00
|
|
|
the return value is discarded
|
2020-01-29 03:26:28 +00:00
|
|
|
|
|
|
|
TODO: consider coroutine friendliness
|
|
|
|
]]
|
|
|
|
|
2020-04-07 03:53:28 +00:00
|
|
|
local path = (...):gsub("state_machine", "")
|
|
|
|
local class = require(path .. "class")
|
2020-01-29 03:26:28 +00:00
|
|
|
|
2020-04-07 03:53:28 +00:00
|
|
|
local state_machine = class()
|
2021-07-05 04:53:55 +00:00
|
|
|
function state_machine:new(states, start_in_state)
|
2020-04-07 03:53:28 +00:00
|
|
|
self = self:init({
|
2020-01-29 03:26:28 +00:00
|
|
|
states = states or {},
|
2021-07-05 04:53:55 +00:00
|
|
|
current_state_name = "",
|
|
|
|
reset_state_name = start_in_state or "",
|
2020-04-07 03:53:28 +00:00
|
|
|
})
|
2020-01-29 03:26:28 +00:00
|
|
|
|
2020-12-01 04:33:22 +00:00
|
|
|
self:reset()
|
2020-01-29 03:26:28 +00:00
|
|
|
|
2020-04-07 03:53:28 +00:00
|
|
|
return self
|
2020-01-29 03:26:28 +00:00
|
|
|
end
|
|
|
|
|
2021-07-05 04:53:55 +00:00
|
|
|
--get the current state table (or nil if it doesn't exist)
|
|
|
|
function state_machine:current_state()
|
|
|
|
return self.states[self.current_state_name]
|
|
|
|
end
|
|
|
|
|
2020-01-29 03:26:28 +00:00
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
--internal helpers
|
|
|
|
|
2020-04-07 03:56:00 +00:00
|
|
|
--make an internal call
|
|
|
|
function state_machine:_call(name, ...)
|
2021-07-05 04:53:55 +00:00
|
|
|
local state = self:current_state()
|
2020-06-02 05:02:20 +00:00
|
|
|
if state then
|
|
|
|
if type(state[name]) == "function" then
|
2020-12-01 04:33:22 +00:00
|
|
|
return state[name](state, ...)
|
2020-06-02 05:02:20 +00:00
|
|
|
elseif type(state) == "function" then
|
2020-12-01 04:33:22 +00:00
|
|
|
return state(name, self, ...)
|
2020-06-02 05:02:20 +00:00
|
|
|
end
|
2020-01-29 03:26:28 +00:00
|
|
|
end
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
|
2020-12-01 04:33:22 +00:00
|
|
|
--make an internal call
|
2021-07-05 04:53:55 +00:00
|
|
|
-- transition if the return value is a valid state name - and return nil if so
|
|
|
|
-- return the call result if it isn't a valid state name
|
2020-06-02 05:02:20 +00:00
|
|
|
function state_machine:_call_and_transition(name, ...)
|
|
|
|
local r = self:_call(name, ...)
|
2020-12-01 04:33:22 +00:00
|
|
|
if type(r) == "string" and self:has_state(r) then
|
2021-07-05 04:53:55 +00:00
|
|
|
self:set_state(r, r == self.current_state_name)
|
2020-06-02 05:02:20 +00:00
|
|
|
return nil
|
|
|
|
end
|
|
|
|
return r
|
|
|
|
end
|
|
|
|
|
2020-01-29 03:26:28 +00:00
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
--various checks
|
|
|
|
|
|
|
|
function state_machine:in_state(name)
|
2021-07-05 04:53:55 +00:00
|
|
|
return self.current_state_name == name
|
2020-01-29 03:26:28 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function state_machine:has_state(name)
|
|
|
|
return self.states[name] ~= nil
|
|
|
|
end
|
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
2020-12-01 04:33:22 +00:00
|
|
|
--state management
|
2020-01-29 03:26:28 +00:00
|
|
|
|
|
|
|
--add a state
|
2021-07-05 04:53:55 +00:00
|
|
|
function state_machine:add_state(name, state)
|
2020-06-02 05:02:20 +00:00
|
|
|
if self:has_state(name) then
|
2020-01-29 03:26:28 +00:00
|
|
|
error("error: added duplicate state "..name)
|
|
|
|
else
|
2021-07-05 04:53:55 +00:00
|
|
|
self.states[name] = state
|
2020-01-29 03:26:28 +00:00
|
|
|
if self:in_state(name) then
|
2021-07-05 04:53:55 +00:00
|
|
|
self:_call_and_transition("enter")
|
2020-01-29 03:26:28 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return self
|
|
|
|
end
|
|
|
|
|
|
|
|
--remove a state
|
|
|
|
function state_machine:remove_state(name)
|
2020-06-02 05:02:20 +00:00
|
|
|
if not self:has_state(name) then
|
2021-07-05 04:53:55 +00:00
|
|
|
error("error: removed missing state "..name)
|
2020-01-29 03:26:28 +00:00
|
|
|
else
|
|
|
|
if self:in_state(name) then
|
|
|
|
self:_call("exit")
|
|
|
|
end
|
|
|
|
self.states[name] = nil
|
|
|
|
end
|
|
|
|
|
|
|
|
return self
|
|
|
|
end
|
|
|
|
|
|
|
|
--hard-replace a state table
|
2021-07-05 04:53:55 +00:00
|
|
|
-- if we're replacing the current state,
|
|
|
|
-- exit is called on the old state and enter is called on the new state
|
|
|
|
-- mask_transitions can be used to prevent this if you need to
|
|
|
|
function state_machine:replace_state(name, state, mask_transitions)
|
|
|
|
local do_transitions = not mask_transitions and self:in_state(name)
|
|
|
|
if do_transitions then
|
2020-01-29 03:26:28 +00:00
|
|
|
self:_call("exit")
|
|
|
|
end
|
2021-07-05 04:53:55 +00:00
|
|
|
self.states[name] = state
|
|
|
|
if do_transitions then
|
2020-12-01 04:33:22 +00:00
|
|
|
self:_call_and_transition("enter", self)
|
2020-01-29 03:26:28 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
return self
|
|
|
|
end
|
|
|
|
|
2020-06-02 05:02:20 +00:00
|
|
|
--ensure a state doesn't exist; transition out of it if we're currently in it
|
2020-01-29 03:26:28 +00:00
|
|
|
function state_machine:clear_state(name)
|
2021-07-05 04:53:55 +00:00
|
|
|
return self:replace_state(name, nil)
|
2020-01-29 03:26:28 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
--transitions and updates
|
|
|
|
|
2021-07-05 04:53:55 +00:00
|
|
|
--reset the machine state to whatever state was specified at creation
|
2020-12-01 04:33:22 +00:00
|
|
|
function state_machine:reset()
|
2021-07-05 04:53:55 +00:00
|
|
|
if self.reset_state_name then
|
|
|
|
self:set_state(self.reset_state_name, true)
|
2020-12-01 04:33:22 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-06-02 05:02:20 +00:00
|
|
|
--set the current state
|
2020-12-01 04:33:22 +00:00
|
|
|
-- 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
|
2021-07-05 04:53:55 +00:00
|
|
|
function state_machine:set_state(name, reset)
|
|
|
|
if self.current_state_name ~= name or reset then
|
2020-01-29 03:26:28 +00:00
|
|
|
self:_call("exit")
|
2021-07-05 04:53:55 +00:00
|
|
|
self.current_state_name = name
|
2020-12-01 04:33:22 +00:00
|
|
|
self:_call_and_transition("enter", self)
|
2020-01-29 03:26:28 +00:00
|
|
|
end
|
|
|
|
return self
|
|
|
|
end
|
|
|
|
|
|
|
|
--perform an update
|
2020-12-01 04:33:22 +00:00
|
|
|
--pass in an optional delta time, which is passed as an arg to the state functions
|
2020-06-02 05:02:20 +00:00
|
|
|
--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
|
2020-01-29 03:26:28 +00:00
|
|
|
function state_machine:update(dt)
|
2020-06-02 05:02:20 +00:00
|
|
|
return self:_call_and_transition("update", dt)
|
2020-01-29 03:26:28 +00:00
|
|
|
end
|
|
|
|
|
2020-06-02 05:02:20 +00:00
|
|
|
--draw the current state
|
2020-01-29 03:26:28 +00:00
|
|
|
function state_machine:draw()
|
|
|
|
self:_call("draw")
|
|
|
|
end
|
|
|
|
|
2021-07-05 04:53:55 +00:00
|
|
|
--for compatibility when a state machine is nested as a state in another machine
|
2020-12-01 04:33:22 +00:00
|
|
|
function state_machine:enter(parent)
|
|
|
|
self.parent = parent
|
|
|
|
self:reset()
|
2020-06-02 05:02:20 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
return state_machine
|