RapidGameFramework
Reusable Godot managers for data-driven small games
Loading...
Searching...
No Matches
onscreen_control_manager.gd
Go to the documentation of this file.
1extends RefCounted
2
3
6
7const DEFAULT_OPTIONS := {
8 "visible": true,
9 "side": "left",
10 "anchor_y": 0.5,
11 "margin": 14.0,
12 "button_size": Vector2(72, 64),
13 "gap": 10.0,
14 "opacity": 0.34,
15 "fill_parent_height": false,
16 "layout": "vertical",
17 "controls": [
18 {"id": "up", "label": "Up", "action": "move_up"},
19 {"id": "down", "label": "Down", "action": "move_down"}
20 ]
21}
22
23var overlay:Control
24var options := DEFAULT_OPTIONS.duplicate(true)
25var pressed := {}
26var buttons := {}
27var queued_presses := []
28var overlays_created:int = 0
29var visibility_changes:int = 0
30
31
32func attach(parent:Control, next_options:Dictionary = {}) -> Control:
33 if overlay != null and is_instance_valid(overlay) and overlay.get_parent() != null:
34 overlay.get_parent().remove_child(overlay)
35 overlay = create_overlay(next_options)
36 parent.add_child(overlay)
37 return overlay
38
39
40func create_overlay(next_options:Dictionary = {}) -> Control:
41 options = DEFAULT_OPTIONS.duplicate(true)
42 for key in next_options.keys():
43 options[key] = next_options[key]
44 pressed.clear()
45 buttons.clear()
46 queued_presses.clear()
47 var root = Control.new()
48 overlays_created += 1
49 root.name = "OnscreenControls"
50 root.mouse_filter = Control.MOUSE_FILTER_PASS
51 root.set_anchors_preset(Control.PRESET_FULL_RECT)
52 root.visible = bool(options.get("visible", true))
53
54 var pad:Control
55 if str(options.get("layout", "vertical")) == "dpad":
56 pad = GridContainer.new()
57 pad.set("columns", 3)
58 else:
59 pad = VBoxContainer.new()
60 pad.name = "Pad"
61 pad.mouse_filter = Control.MOUSE_FILTER_PASS
62 pad.add_theme_constant_override("separation", int(options.get("gap", 10.0)))
63 root.add_child(pad)
64 _apply_pad_layout(pad)
65
66 if str(options.get("layout", "vertical")) == "dpad":
67 _build_dpad(pad)
68 else:
69 for control in options.get("controls", []):
70 var button = _make_button(control)
71 pad.add_child(button)
72 return root
73
74
75func set_visible(enabled:bool) -> void:
76 if is_visible() == enabled and bool(options.get("visible", true)) == enabled:
77 return
78 options["visible"] = enabled
79 visibility_changes += 1
80 if overlay != null and is_instance_valid(overlay):
81 overlay.visible = enabled
82 if not enabled:
83 pressed.clear()
84 queued_presses.clear()
85
86
87func is_visible() -> bool:
88 if overlay == null or not is_instance_valid(overlay):
89 return bool(options.get("visible", true))
90 return overlay.visible
91
92
93func get_axis(negative_id:String = "up", positive_id:String = "down") -> float:
94 var axis := 0.0
95 if bool(pressed.get(negative_id, false)):
96 axis -= 1.0
97 if bool(pressed.get(positive_id, false)):
98 axis += 1.0
99 return axis
100
101
102func get_vector(left_id:String = "left", right_id:String = "right", up_id:String = "up", down_id:String = "down") -> Vector2:
103 return Vector2(
104 get_axis(left_id, right_id),
105 get_axis(up_id, down_id)
106 )
107
108
109
111func consume_vector(left_id:String = "left", right_id:String = "right", up_id:String = "up", down_id:String = "down") -> Vector2:
112 while not queued_presses.is_empty():
113 var control_id = str(queued_presses.pop_front())
114 if control_id == left_id:
115 return Vector2.LEFT
116 if control_id == right_id:
117 return Vector2.RIGHT
118 if control_id == up_id:
119 return Vector2.UP
120 if control_id == down_id:
121 return Vector2.DOWN
122 return Vector2.ZERO
123
124
125func get_state() -> Dictionary:
126 return {
127 "visible": bool(options.get("visible", true)),
128 "side": str(options.get("side", "left")),
129 "anchor_y": float(options.get("anchor_y", 0.5)),
130 "margin": float(options.get("margin", 14.0)),
131 "button_size": options.get("button_size", Vector2(72, 64)),
132 "gap": float(options.get("gap", 10.0)),
133 "opacity": float(options.get("opacity", 0.34)),
134 "fill_parent_height": bool(options.get("fill_parent_height", false)),
135 "layout": str(options.get("layout", "vertical")),
136 "controls": options.get("controls", [])
137 }
138
139
140
141func get_stats() -> Dictionary:
142 return {
143 "overlays_created": overlays_created,
144 "buttons": buttons.size(),
145 "queued_presses": queued_presses.size(),
146 "visibility_changes": visibility_changes,
147 "visible": is_visible()
148 }
149
150
151func get_cache_stats() -> Dictionary:
152 return get_stats()
153
154
155func apply_state(state:Dictionary) -> void:
156 for key in state.keys():
157 options[key] = state[key]
158 if overlay != null and is_instance_valid(overlay):
159 overlay.visible = bool(options.get("visible", true))
160 var pad = overlay.get_node_or_null("Pad")
161 if pad != null:
162 _apply_pad_layout(pad)
163 for child in pad.get_children():
164 if child is Button:
165 _style_button(child)
166
167
168func set_layout(next_options:Dictionary) -> void:
169 apply_state(next_options)
170
171
172func _make_button(control:Dictionary) -> Button:
173 var control_id = str(control.get("id", "control"))
174 pressed[control_id] = false
175 var button = Button.new()
176 button.name = "%sButton" % control_id.capitalize()
177 button.text = str(control.get("label", control_id.capitalize()))
178 button.focus_mode = Control.FOCUS_NONE
179 button.custom_minimum_size = options.get("button_size", Vector2(72, 64))
180 if bool(options.get("fill_parent_height", false)):
181 button.custom_minimum_size.y = 0
182 button.size_flags_vertical = Control.SIZE_EXPAND_FILL
183 button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
184 button.mouse_filter = Control.MOUSE_FILTER_STOP
185 buttons[control_id] = button
186 button.button_down.connect(func():
187 pressed[control_id] = true
188 queued_presses.append(control_id)
189 )
190 button.button_up.connect(func(): pressed[control_id] = false)
191 button.visibility_changed.connect(func():
192 if not button.visible:
193 pressed[control_id] = false
194 )
195 _style_button(button)
196 return button
197
198
199func _build_dpad(pad:Control) -> void:
200 var controls_by_id := {}
201 for control in options.get("controls", []):
202 controls_by_id[str(control.get("id", ""))] = control
203 var cells = [
204 {}, controls_by_id.get("up", {"id": "up", "label": "Up"}), {},
205 controls_by_id.get("left", {"id": "left", "label": "Left"}), {}, controls_by_id.get("right", {"id": "right", "label": "Right"}),
206 {}, controls_by_id.get("down", {"id": "down", "label": "Down"}), {}
207 ]
208 for cell in cells:
209 if cell.is_empty():
210 var spacer = Control.new()
211 spacer.custom_minimum_size = options.get("button_size", Vector2(72, 64))
212 pad.add_child(spacer)
213 else:
214 pad.add_child(_make_button(cell))
215
216
217func _apply_pad_layout(pad:Control) -> void:
218 var button_size = options.get("button_size", Vector2(72, 64))
219 var controls_count = max(1, options.get("controls", []).size())
220 var is_dpad = str(options.get("layout", "vertical")) == "dpad"
221 var width = float(button_size.x)
222 var height = float(button_size.y) * controls_count + float(options.get("gap", 10.0)) * max(0, controls_count - 1)
223 if is_dpad:
224 width = float(button_size.x) * 3.0 + float(options.get("gap", 10.0)) * 2.0
225 height = float(button_size.y) * 3.0 + float(options.get("gap", 10.0)) * 2.0
226 var margin = float(options.get("margin", 14.0))
227 var anchor_y = clamp(float(options.get("anchor_y", 0.5)), 0.0, 1.0)
228 if bool(options.get("fill_parent_height", false)):
229 pad.custom_minimum_size = Vector2(width, 0)
230 pad.anchor_top = 0.0
231 pad.anchor_bottom = 1.0
232 pad.offset_top = margin
233 pad.offset_bottom = -margin
234 else:
235 pad.custom_minimum_size = Vector2(width, height)
236 pad.anchor_top = anchor_y
237 pad.anchor_bottom = anchor_y
238 pad.offset_top = -height * 0.5
239 pad.offset_bottom = height * 0.5
240 if str(options.get("side", "left")) == "right":
241 pad.anchor_left = 1.0
242 pad.anchor_right = 1.0
243 pad.offset_left = -width - margin
244 pad.offset_right = -margin
245 else:
246 pad.anchor_left = 0.0
247 pad.anchor_right = 0.0
248 pad.offset_left = margin
249 pad.offset_right = margin + width
250
251
252func _style_button(button:Button) -> void:
253 var opacity = clamp(float(options.get("opacity", 0.34)), 0.05, 0.85)
254 button.self_modulate = Color(1, 1, 1, opacity)
255 var normal = _make_style(Color(0.08, 0.13, 0.18, 1.0), Color(1.0, 1.0, 1.0, 0.55))
256 var hover = _make_style(Color(0.12, 0.20, 0.28, 1.0), Color(1.0, 1.0, 1.0, 0.7))
257 var pressed_style = _make_style(Color(0.22, 0.42, 0.58, 1.0), Color(1.0, 1.0, 1.0, 0.85))
258 button.add_theme_stylebox_override("normal", normal)
259 button.add_theme_stylebox_override("hover", hover)
260 button.add_theme_stylebox_override("pressed", pressed_style)
261 button.add_theme_stylebox_override("focus", hover)
262 button.add_theme_stylebox_override("disabled", normal)
263 button.add_theme_color_override("font_color", Color(1, 1, 1, 0.92))
264 button.add_theme_color_override("font_hover_color", Color.WHITE)
265 button.add_theme_color_override("font_pressed_color", Color.WHITE)
266 button.add_theme_font_size_override("font_size", 18)
267
268
269func _make_style(fill:Color, border:Color) -> StyleBoxFlat:
270 var style = StyleBoxFlat.new()
271 style.bg_color = fill
272 style.border_color = border
273 style.draw_center = true
274 style.set_border_width_all(1)
275 style.set_corner_radius_all(8)
276 style.content_margin_left = 4
277 style.content_margin_right = 4
278 style.content_margin_top = 4
279 style.content_margin_bottom = 4
280 return style