RapidGameFramework
Reusable Godot managers for data-driven small games
Loading...
Searching...
No Matches
input_manager.gd
Go to the documentation of this file.
1extends RefCounted
2
3
8
9const DataCacheManager = preload("res://scripts/systems/dataCacheManager/data_cache_manager.gd")
10
11var prompts := {}
12var actions := {}
13var controller_active := false
14var last_controller_active := false
15var keyboard_active := false
16var data_cache_manager = DataCacheManager.new()
17
18const JOY_BUTTON_NAMES := {
19 "A": JOY_BUTTON_A,
20 "B": JOY_BUTTON_B,
21 "X": JOY_BUTTON_X,
22 "Y": JOY_BUTTON_Y,
23 "Back": JOY_BUTTON_BACK,
24 "Start": JOY_BUTTON_START,
25 "Left Shoulder": JOY_BUTTON_LEFT_SHOULDER,
26 "Right Shoulder": JOY_BUTTON_RIGHT_SHOULDER,
27 "Left Stick": JOY_BUTTON_LEFT_STICK,
28 "Right Stick": JOY_BUTTON_RIGHT_STICK,
29 "Dpad Up": JOY_BUTTON_DPAD_UP,
30 "Dpad Down": JOY_BUTTON_DPAD_DOWN,
31 "Dpad Left": JOY_BUTTON_DPAD_LEFT,
32 "Dpad Right": JOY_BUTTON_DPAD_RIGHT
33}
34
35const JOY_AXIS_NAMES := {
36 "Left X": JOY_AXIS_LEFT_X,
37 "Left Y": JOY_AXIS_LEFT_Y,
38 "Right X": JOY_AXIS_RIGHT_X,
39 "Right Y": JOY_AXIS_RIGHT_Y,
40 "Trigger Left": JOY_AXIS_TRIGGER_LEFT,
41 "Trigger Right": JOY_AXIS_TRIGGER_RIGHT
42}
43
44
45
47func load_actions(file_path:String) -> void:
48 var data = data_cache_manager.load_json(file_path)
49 if not data is Dictionary:
50 return
51 actions.clear()
52 for action in data.get("actions", []):
53 if not action is Dictionary:
54 continue
55 var id = str(action.get("id", ""))
56 if id == "":
57 continue
58 actions[id] = action.duplicate(true)
59 ensure_action(id)
60 if action.has("keyboard"):
61 _add_key_binding(id, OS.find_keycode_from_string(str(action["keyboard"])))
62 for key_name in action.get("keyboard_alt", []):
63 _add_key_binding(id, OS.find_keycode_from_string(str(key_name)))
64 for button_name in action.get("joy_buttons", []):
65 bind_joy_button(id, _joy_button_from_value(button_name))
66 for axis_binding in action.get("joy_axes", []):
67 if axis_binding is Dictionary:
68 bind_joy_axis(id, _joy_axis_from_value(axis_binding.get("axis", 0)), float(axis_binding.get("value", 1.0)))
69 if action.has("prompt"):
70 set_prompt(id, "keyboard", str(action["prompt"]))
71
72
73
74func get_cache_stats() -> Dictionary:
75 return data_cache_manager.get_cache_stats()
76
77
78
79func get_actions(include_locked:bool = false) -> Array:
80 var list = []
81 for id in actions.keys():
82 if not include_locked and not is_action_remappable(str(id)):
83 continue
84 var action = actions[id].duplicate(true)
85 action["id"] = id
86 action["prompt"] = get_prompt(id)
87 list.append(action)
88 return list
89
90
91
92func is_action_remappable(action:String) -> bool:
93 var definition = actions.get(action, {})
94 if not definition is Dictionary:
95 return true
96 if str(action) == "game_pause" or str(action) == "game_settings":
97 return false
98 return bool(definition.get("remappable", true))
99
100
101
102func ensure_action(action:String, deadzone:float = 0.5) -> void:
103 if not InputMap.has_action(action):
104 InputMap.add_action(action, deadzone)
105
106
107
108func bind_key(action:String, keycode:Key) -> void:
109 ensure_action(action)
110 InputMap.action_erase_events(action)
111 _add_key_binding(action, keycode)
112 set_prompt(action, "keyboard", OS.get_keycode_string(keycode))
113
114
115
116func bind_from_event(action:String, event:InputEvent) -> bool:
117 note_input_event(event)
118 if not is_action_remappable(action):
119 return false
120 if event is InputEventKey and event.pressed and not event.echo:
121 bind_key(action, event.keycode)
122 return true
123 if event is InputEventMouseButton and event.pressed:
124 bind_mouse_button(action, event.button_index)
125 set_prompt(action, "keyboard", "Mouse %d" % int(event.button_index))
126 return true
127 if event is InputEventJoypadButton and event.pressed:
128 bind_joy_button(action, event.button_index)
129 set_prompt(action, "controller", "Button %d" % int(event.button_index))
130 return true
131 if event is InputEventJoypadMotion and abs(event.axis_value) >= 0.5:
132 bind_joy_axis(action, event.axis, sign(event.axis_value))
133 set_prompt(action, "controller", "Axis %d %s" % [int(event.axis), "+" if event.axis_value > 0.0 else "-"])
134 return true
135 return false
136
137
138
139func bind_joy_button(action:String, button_index:int) -> void:
140 ensure_action(action)
141 var event = InputEventJoypadButton.new()
142 event.button_index = button_index
143 _add_event_if_missing(action, event)
144
145
146
147func bind_joy_axis(action:String, axis:int, axis_value:float) -> void:
148 ensure_action(action)
149 var event = InputEventJoypadMotion.new()
150 event.axis = axis
151 event.axis_value = clamp(axis_value, -1.0, 1.0)
152 _add_event_if_missing(action, event)
153
154
155
156func bind_mouse_button(action:String, button_index:int) -> void:
157 ensure_action(action)
158 var event = InputEventMouseButton.new()
159 event.button_index = button_index
160 _add_event_if_missing(action, event)
161
162
163
164func is_pressed(action:String) -> bool:
165 return Input.is_action_pressed(action)
166
167
168
169func just_pressed(action:String) -> bool:
170 return Input.is_action_just_pressed(action)
171
172
173
174func note_input_event(event:InputEvent) -> void:
175 if event is InputEventJoypadButton and event.pressed:
176 controller_active = true
177 keyboard_active = false
178 elif event is InputEventJoypadMotion and abs(event.axis_value) >= 0.35:
179 controller_active = true
180 keyboard_active = false
181 elif event is InputEventKey and event.pressed:
182 controller_active = false
183 keyboard_active = true
184 elif event is InputEventMouseButton and event.pressed:
185 controller_active = false
186 keyboard_active = false
187 elif event is InputEventScreenTouch and event.pressed:
188 controller_active = false
189 keyboard_active = false
190
191
192
193func is_controller_active() -> bool:
194 return controller_active or not Input.get_connected_joypads().is_empty()
195
196
197
198func is_controller_label_active() -> bool:
199 return controller_active
200
201
202
203func is_keyboard_active() -> bool:
204 return keyboard_active
205
206
207
208func consume_controller_mode_changed() -> bool:
209 var active = is_controller_active()
210 if active == last_controller_active:
211 return false
212 last_controller_active = active
213 return true
214
215
216
217func get_slot_label(slot_index:int, controller_label_set:String = "xbox") -> String:
218 if is_controller_label_active():
219 var labels = _controller_slot_labels(controller_label_set)
220 if slot_index >= 0 and slot_index < labels.size():
221 return str(labels[slot_index])
222 return str(slot_index + 1)
223
224
225
226func get_slot_color(slot_index:int, controller_label_set:String = "xbox") -> Color:
227 if not is_controller_label_active():
228 return Color("#f2c86b")
229 var colors = _controller_slot_colors(controller_label_set)
230 if slot_index >= 0 and slot_index < colors.size():
231 return Color(str(colors[slot_index]))
232 return Color("#f2c86b")
233
234
235
236func apply_controller_scroll(root:Node, delta:float, pixels_per_second:float = 720.0) -> bool:
237 var amount = Input.get_action_strength("ui_scroll_down") - Input.get_action_strength("ui_scroll_up")
238 if abs(amount) < 0.12:
239 return false
240 var scroll = _find_scroll_target(root)
241 if scroll == null:
242 return false
243 scroll.scroll_vertical = clampi(
244 scroll.scroll_vertical + int(amount * pixels_per_second * delta),
245 0,
246 scroll.get_v_scroll_bar().max_value
247 )
248 return true
249
250
251
253func update_input_module(root:Node, delta:float, onscreen_controls = null, user_onscreen_enabled:bool = true, scroll_speed:float = 720.0) -> bool:
254 var mode_changed = consume_controller_mode_changed()
255 if onscreen_controls != null and onscreen_controls.has_method("set_visible"):
256 var desired_visible := user_onscreen_enabled and not is_controller_active() and not is_keyboard_active()
257 if not onscreen_controls.has_method("is_visible") or bool(onscreen_controls.is_visible()) != desired_visible:
258 onscreen_controls.set_visible(desired_visible)
259 apply_controller_scroll(root, delta, scroll_speed)
260 return mode_changed
261
262
263
264func set_prompt(action:String, device:String, text:String) -> void:
265 if not prompts.has(action):
266 prompts[action] = {}
267 prompts[action][device] = text
268
269
270
271func get_prompt(action:String, device:String = "keyboard") -> String:
272 return str(prompts.get(action, {}).get(device, action))
273
274
275
277func get_state() -> Dictionary:
278 return {
279 "prompts": prompts.duplicate(true),
280 "actions": actions.duplicate(true),
281 "bindings": _get_binding_state()
282 }
283
284
285
286func apply_state(state:Dictionary) -> void:
287 if state.is_empty():
288 return
289 prompts = state.get("prompts", prompts).duplicate(true)
290 if state.has("actions") and not state.get("actions", {}).is_empty():
291 var saved_actions = state.get("actions", {})
292 for action_id in saved_actions.keys():
293 if actions.has(action_id) and saved_actions[action_id] is Dictionary:
294 var merged = actions[action_id].duplicate(true)
295 for key in saved_actions[action_id].keys():
296 merged[key] = saved_actions[action_id][key]
297 actions[action_id] = merged
298 else:
299 actions[action_id] = saved_actions[action_id]
300 for action in state.get("bindings", {}).keys():
301 _apply_action_bindings(str(action), state["bindings"][action])
302
303
304func _get_binding_state() -> Dictionary:
305 var bindings = {}
306 for action in actions.keys():
307 if not is_action_remappable(str(action)):
308 continue
309 var action_bindings = []
310 if not InputMap.has_action(str(action)):
311 continue
312 for event in InputMap.action_get_events(str(action)):
313 if event is InputEventKey:
314 action_bindings.append({"type": "key", "keycode": event.keycode})
315 elif event is InputEventMouseButton:
316 action_bindings.append({"type": "mouse", "button_index": event.button_index})
317 elif event is InputEventJoypadButton:
318 action_bindings.append({"type": "joy_button", "button_index": event.button_index})
319 elif event is InputEventJoypadMotion:
320 action_bindings.append({"type": "joy_axis", "axis": event.axis, "axis_value": event.axis_value})
321 bindings[action] = action_bindings
322 return bindings
323
324
325func _apply_action_bindings(action:String, action_bindings:Array) -> void:
326 ensure_action(action)
327 if not is_action_remappable(action):
328 InputMap.action_erase_events(action)
329 _ensure_definition_defaults(action)
330 return
331 InputMap.action_erase_events(action)
332 for binding in action_bindings:
333 if not binding is Dictionary:
334 continue
335 match str(binding.get("type", "")):
336 "key":
337 var event = InputEventKey.new()
338 event.keycode = int(binding.get("keycode", 0))
339 InputMap.action_add_event(action, event)
340 "mouse":
341 var event = InputEventMouseButton.new()
342 event.button_index = int(binding.get("button_index", 0))
343 InputMap.action_add_event(action, event)
344 "joy_button":
345 var event = InputEventJoypadButton.new()
346 event.button_index = int(binding.get("button_index", 0))
347 InputMap.action_add_event(action, event)
348 "joy_axis":
349 var event = InputEventJoypadMotion.new()
350 event.axis = int(binding.get("axis", 0))
351 event.axis_value = float(binding.get("axis_value", 1.0))
352 InputMap.action_add_event(action, event)
353 _ensure_definition_defaults(action)
354
355
356func _add_key_binding(action:String, keycode:Key) -> void:
357 if keycode == KEY_NONE:
358 return
359 var event = InputEventKey.new()
360 event.keycode = keycode
361 _add_event_if_missing(action, event)
362
363
364func _add_event_if_missing(action:String, event:InputEvent) -> void:
365 for existing in InputMap.action_get_events(action):
366 if _events_match(existing, event):
367 return
368 InputMap.action_add_event(action, event)
369
370
371func _events_match(left:InputEvent, right:InputEvent) -> bool:
372 if left.get_class() != right.get_class():
373 return false
374 if left is InputEventKey and right is InputEventKey:
375 return left.keycode == right.keycode
376 if left is InputEventMouseButton and right is InputEventMouseButton:
377 return left.button_index == right.button_index
378 if left is InputEventJoypadButton and right is InputEventJoypadButton:
379 return left.button_index == right.button_index
380 if left is InputEventJoypadMotion and right is InputEventJoypadMotion:
381 return left.axis == right.axis and sign(left.axis_value) == sign(right.axis_value)
382 return false
383
384
385func _ensure_definition_defaults(action_id:String) -> void:
386 var definition = actions.get(action_id, {})
387 if not definition is Dictionary:
388 return
389 if definition.has("keyboard"):
390 _add_key_binding(action_id, OS.find_keycode_from_string(str(definition["keyboard"])))
391 for key_name in definition.get("keyboard_alt", []):
392 _add_key_binding(action_id, OS.find_keycode_from_string(str(key_name)))
393 for button_name in definition.get("joy_buttons", []):
394 bind_joy_button(action_id, _joy_button_from_value(button_name))
395 for axis_binding in definition.get("joy_axes", []):
396 if axis_binding is Dictionary:
397 bind_joy_axis(action_id, _joy_axis_from_value(axis_binding.get("axis", 0)), float(axis_binding.get("value", 1.0)))
398
399
400func _joy_button_from_value(value) -> int:
401 if value is int:
402 return int(value)
403 return int(JOY_BUTTON_NAMES.get(str(value), int(value) if str(value).is_valid_int() else JOY_BUTTON_A))
404
405
406func _joy_axis_from_value(value) -> int:
407 if value is int:
408 return int(value)
409 return int(JOY_AXIS_NAMES.get(str(value), int(value) if str(value).is_valid_int() else JOY_AXIS_LEFT_X))
410
411
412func _controller_slot_labels(controller_label_set:String) -> Array:
413 match controller_label_set:
414 "nintendo":
415 return ["B", "Y", "X", "A", "LS"]
416 "playstation":
417 return ["Cross", "Square", "Triangle", "Circle", "L3"]
418 return ["A", "X", "Y", "B", "LS"]
419
420
421func _controller_slot_colors(controller_label_set:String) -> Array:
422 match controller_label_set:
423 "nintendo":
424 return ["#f05f64", "#55d17a", "#4aa3ff", "#f2c86b", "#aab6c4"]
425 "playstation":
426 return ["#55d17a", "#4aa3ff", "#f2c86b", "#f05f64", "#aab6c4"]
427 return ["#55d17a", "#4aa3ff", "#f2c86b", "#f05f64", "#aab6c4"]
428
429
430func _find_scroll_target(root:Node) -> ScrollContainer:
431 if root == null:
432 return null
433 var viewport = root.get_viewport() if root is Control else null
434 if viewport != null:
435 var focus = viewport.gui_get_focus_owner()
436 var focused_scroll = _ancestor_scroll_container(focus)
437 if _scroll_can_move(focused_scroll):
438 return focused_scroll
439 var visible_scrolls = []
440 _collect_visible_scrolls(root, visible_scrolls)
441 for scroll in visible_scrolls:
442 if _scroll_can_move(scroll):
443 return scroll
444 return null
445
446
447func _ancestor_scroll_container(node:Node) -> ScrollContainer:
448 var current = node
449 while current != null:
450 if current is ScrollContainer:
451 return current
452 current = current.get_parent()
453 return null
454
455
456func _collect_visible_scrolls(node:Node, results:Array) -> void:
457 if node is Control and not node.is_visible_in_tree():
458 return
459 if node is ScrollContainer:
460 results.append(node)
461 for child in node.get_children():
462 _collect_visible_scrolls(child, results)
463
464
465func _scroll_can_move(scroll:ScrollContainer) -> bool:
466 if scroll == null or not scroll.is_visible_in_tree():
467 return false
468 var bar = scroll.get_v_scroll_bar()
469 return bar != null and bar.max_value > 0.0 and scroll.vertical_scroll_mode != ScrollContainer.SCROLL_MODE_DISABLED
float
Ensure an action exists.
bool
Return cache diagnostics for file-based convenience loads.