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