-
Notifications
You must be signed in to change notification settings - Fork 29
/
state_machine.lua
181 lines (148 loc) · 4.49 KB
/
state_machine.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
--[[
state machine
a finite state machine implementation
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
- a function
which gets passed the current event name, the machine, and varargs as arguments
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
on update, the current state's update callback is called
the return value can trigger a transition
on draw, the current state's draw callback is called
the return value is discarded
TODO: consider coroutine friendliness
]]
local path = (...):gsub("state_machine", "")
local class = require(path .. "class")
local state_machine = class()
function state_machine:new(states, start)
self = self:init({
states = states or {},
current_state = "",
start_state = "",
})
self:reset()
return self
end
-------------------------------------------------------------------------------
--internal helpers
function state_machine:_get_state()
return self.states[self.current_state]
end
--make an internal call
function state_machine:_call(name, ...)
local state = self:_get_state()
if state then
if type(state[name]) == "function" then
return state[name](state, ...)
elseif type(state) == "function" then
return state(name, self, ...)
end
end
return nil
end
--make an internal call
-- return the call result if it isn't a valid state
-- transition if the return value is a valid state - and return nil if so
function state_machine:_call_and_transition(name, ...)
local r = self:_call(name, ...)
if type(r) == "string" and self:has_state(r) then
self:set_state(r, r == self.current_state)
return nil
end
return r
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 management
--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", self)
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_and_transition("enter", self)
end
return self
end
--ensure a state doesn't exist; transition out of it if we're currently in it
function state_machine:clear_state(name)
return self:replace_state(name, nil, true)
end
-------------------------------------------------------------------------------
--transitions and updates
--reset the machine to whatever the start state was defined at at creation
function state_machine:reset()
if self.start_state then
self:set_state(self.start_state, true)
end
end
--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)
if self.current_state ~= state or reset then
self:_call("exit")
self.current_state = state
self:_call_and_transition("enter", self)
end
return self
end
--perform an update
--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)
return self:_call_and_transition("update", dt)
end
--draw the current state
function state_machine:draw()
self:_call("draw")
end
--for compatibility when used as a state
function state_machine:enter(parent)
self.parent = parent
self:reset()
end
return state_machine