forked from farfalk/gd-YAFSM
-
Notifications
You must be signed in to change notification settings - Fork 0
/
StateMachinePlayer.gd
366 lines (309 loc) · 11.1 KB
/
StateMachinePlayer.gd
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
tool
extends "StackPlayer.gd"
const State = preload("states/State.gd")
signal transited(from, to) # Transition of state
signal entered(to) # Entry of state machine(including nested), empty string equals to root
signal exited(from) # Exit of state machine(including nested, empty string equals to root
signal updated(state, delta) # Time to update(based on process_mode), up to user to handle any logic, for example, update movement of KinematicBody
# Enum to define how state machine should be updated
enum ProcessMode {
PHYSICS,
IDLE,
MANUAL
}
export(Resource) var state_machine # StateMachine being played
export(bool) var active = true setget set_active # Activeness of player
export(bool) var autostart = true # Automatically enter Entry state on ready if true
export(ProcessMode) var process_mode = ProcessMode.IDLE setget set_process_mode # ProcessMode of player
var _is_started = false
var _parameters # Parameters to be passed to condition
var _local_parameters
var _is_update_locked = true
var _was_transited = false # If last transition was successful
var _is_param_edited = false
func _init():
if Engine.editor_hint:
return
_parameters = {}
_local_parameters = {}
_was_transited = true # Trigger _transit on _ready
func _get_configuration_warning():
if state_machine:
if not state_machine.has_entry():
return "State Machine will not function properly without Entry node"
else:
return "State Machine Player is not going anywhere without default State Machine"
return ""
func _ready():
if Engine.editor_hint:
return
set_process(false)
set_physics_process(false)
call_deferred("_initiate") # Make sure connection of signals can be done in _ready to receive all signal callback
func _initiate():
if autostart:
start()
_on_active_changed()
_on_process_mode_changed()
func _process(delta):
if Engine.editor_hint:
return
_update_start()
update(delta)
_update_end()
func _physics_process(delta):
if Engine.editor_hint:
return
_update_start()
update(delta)
_update_end()
# Only get called in 2 condition, _parameters edited or last transition was successful
func _transit():
if not active:
return
# Attempt to transit if parameter edited or last transition was successful
if not _is_param_edited and not _was_transited:
return
var from = get_current()
var local_params = _local_parameters.get(path_backward(from), {})
var next_state = state_machine.transit(get_current(), _parameters, local_params)
if next_state:
if stack.has(next_state):
reset(stack.find(next_state))
else:
push(next_state)
var to = next_state
_was_transited = !!next_state
_is_param_edited = false
_flush_trigger(_parameters)
_flush_trigger(_local_parameters, true)
if _was_transited:
_on_state_changed(from, to)
func _on_state_changed(from, to):
match to:
State.ENTRY_STATE:
emit_signal("entered", "")
State.EXIT_STATE:
set_active(false) # Disable on exit
emit_signal("exited", "")
if to.ends_with(State.ENTRY_STATE) and to.length() > State.ENTRY_STATE.length():
# Nexted Entry state
var state = path_backward(get_current())
emit_signal("entered", state)
elif to.ends_with(State.EXIT_STATE) and to.length() > State.EXIT_STATE.length():
# Nested Exit state, clear "local" params
var state = path_backward(get_current())
clear_param(state, false) # Clearing params internally, do not update
emit_signal("exited", state)
emit_signal("transited", from, to)
# Called internally if process_mode is PHYSICS/IDLE to unlock update()
func _update_start():
_is_update_locked = false
# Called internally if process_mode is PHYSICS/IDLE to lock update() from external call
func _update_end():
_is_update_locked = true
# Called after update() which is dependant on process_mode, override to process current state
func _on_updated(delta, state):
pass
func _on_process_mode_changed():
if not active:
return
match process_mode:
ProcessMode.PHYSICS:
set_physics_process(true)
set_process(false)
ProcessMode.IDLE:
set_physics_process(false)
set_process(true)
ProcessMode.MANUAL:
set_physics_process(false)
set_process(false)
func _on_active_changed():
if Engine.editor_hint:
return
if active:
_on_process_mode_changed()
_transit()
else:
set_physics_process(false)
set_process(false)
# Remove all trigger(param with null value) from provided params, only get called after _transit
# Trigger another call of _flush_trigger on first layer of dictionary if nested is true
func _flush_trigger(params, nested=false):
for param_key in params.keys():
var value = params[param_key]
if nested and value is Dictionary:
_flush_trigger(value)
if value == null: # Param with null as value is treated as trigger
params.erase(param_key)
func reset(to=-1, event=ResetEventTrigger.LAST_TO_DEST):
.reset(to, event)
_was_transited = true # Make sure to call _transit on next update
# Manually start the player, automatically called if autostart is true
func start():
push(State.ENTRY_STATE)
emit_signal("entered", "")
_was_transited = true
_is_started = true
# Restart player
func restart(is_active=true, preserve_params=false):
reset()
set_active(is_active)
if not preserve_params:
clear_param("", false)
start()
# Update player to, first initiate transition, then call _on_updated, finally emit "update" signal, delta will be given based on process_mode.
# Can only be called manually if process_mode is MANUAL, otherwise, assertion error will be raised.
# *delta provided will be reflected in signal updated(state, delta)
func update(delta=get_physics_process_delta_time()):
if not active:
return
if process_mode != ProcessMode.MANUAL:
assert(not _is_update_locked, "Attempting to update manually with ProcessMode.%s" % ProcessMode.keys()[process_mode])
_transit()
var current_state = get_current()
_on_updated(current_state, delta)
emit_signal("updated", current_state, delta)
if process_mode == ProcessMode.MANUAL:
# Make sure to auto advance even in MANUAL mode
if _was_transited:
call_deferred("update")
# Set trigger to be tested with condition, then trigger _transit on next update,
# automatically call update() if process_mode set to MANUAL and auto_update true
# Nested trigger can be accessed through path "path/to/param_name", for example, "App/Game/is_playing"
func set_trigger(name, auto_update=true):
set_param(name, null, auto_update)
func set_nested_trigger(path, name, auto_update=true):
set_nested_param(path, name, null, auto_update)
# Set param(null value treated as trigger) to be tested with condition, then trigger _transit on next update,
# automatically call update() if process_mode set to MANUAL and auto_update true
# Nested param can be accessed through path "path/to/param_name", for example, "App/Game/is_playing"
func set_param(name, value, auto_update=true):
var path = ""
if "/" in name:
path = path_backward(name)
name = path_end_dir(name)
set_nested_param(path, name, value, auto_update)
func set_nested_param(path, name, value, auto_update=true):
if path.empty():
_parameters[name] = value
else:
var local_params = _local_parameters.get(path)
if local_params is Dictionary:
local_params[name] = value
else:
local_params = {}
local_params[name] = value
_local_parameters[path] = local_params
_on_param_edited(auto_update)
# Remove param, then trigger _transit on next update,
# automatically call update() if process_mode set to MANUAL and auto_update true
# Nested param can be accessed through path "path/to/param_name", for example, "App/Game/is_playing"
func erase_param(name, auto_update=true):
var path = ""
if "/" in name:
path = path_backward(name)
name = path_end_dir(name)
return erase_nested_param(path, name, auto_update)
func erase_nested_param(path, name, auto_update=true):
var result = false
if path.empty():
result = _parameters.erase(name)
else:
result = _local_parameters.get(path, {}).erase(name)
_on_param_edited(auto_update)
return result
# Clear params from specified path, empty string to clear all, then trigger _transit on next update,
# automatically call update() if process_mode set to MANUAL and auto_update true
# Nested param can be accessed through path "path/to/param_name", for example, "App/Game/is_playing"
func clear_param(path="", auto_update=true):
if path.empty():
_parameters.clear()
else:
_local_parameters.get(path, {}).clear()
# Clear nested params
for param_key in _local_parameters.keys():
if param_key.begins_with(path):
_local_parameters.erase(param_key)
# Called when param edited, automatically call update() if process_mode set to MANUAL and auto_update true
func _on_param_edited(auto_update=true):
_is_param_edited = true
if process_mode == ProcessMode.MANUAL and auto_update and _is_started:
update()
# Get value of param
# Nested param can be accessed through path "path/to/param_name", for example, "App/Game/is_playing"
func get_param(name, default=null):
var path = ""
if "/" in name:
path = path_backward(name)
name = path_end_dir(name)
return get_nested_param(path, name, default)
func get_nested_param(path, name, default=null):
if path.empty():
return _parameters.get(name, default)
else:
var local_params = _local_parameters.get(path, {})
return local_params.get(name, default)
# Get duplicate of whole parameter dictionary
func get_params():
return _parameters.duplicate()
# Return true if param exists
# Nested param can be accessed through path "path/to/param_name", for example, "App/Game/is_playing"
func has_param(name):
var path = ""
if "/" in name:
path = path_backward(name)
name = path_end_dir(name)
return has_nested_param(path, name)
func has_nested_param(path, name):
if path.empty():
return name in _parameters
else:
var local_params = _local_parameters.get(path, {})
return name in local_params
# Return if player started
func is_entered():
return State.ENTRY_STATE in stack
# Return if player ended
func is_exited():
return get_current() == State.EXIT_STATE
func set_active(v):
if active != v:
if v:
if is_exited():
push_warning("Attempting to make exited StateMachinePlayer active, call reset() then set_active() instead")
return
active = v
_on_active_changed()
func set_process_mode(mode):
if process_mode != mode:
process_mode = mode
_on_process_mode_changed()
func get_current():
var v = .get_current()
return v if v else ""
func get_previous():
var v = .get_previous()
return v if v else ""
# Convert node path to state path that can be used to query state with StateMachine.get_state.
# Node path, "root/path/to/state", equals to State path, "path/to/state"
static func node_path_to_state_path(node_path):
var p = node_path.replace("root", "")
if p.begins_with("/"):
p = p.substr(1)
return p
# Convert state path to node path that can be used for query node in scene tree.
# State path, "path/to/state", equals to Node path, "root/path/to/state"
static func state_path_to_node_path(state_path):
var path = state_path
if path.empty():
path = "root"
else:
path = str("root/", path)
return path
# Return parent path, "path/to/state" return "path/to"
static func path_backward(path):
return path.substr(0, path.rfind("/"))
# Return end directory of path, "path/to/state" returns "state"
static func path_end_dir(path):
return path.right(path.rfind("/") + 1)