RapidGameFramework
Reusable Godot managers for data-driven small games
Loading...
Searching...
No Matches
arcade_physics_manager.gd
Go to the documentation of this file.
1extends RefCounted
2
3
10
11const DEFAULT_BALL := {
12 "position": Vector2.ZERO,
13 "velocity": Vector2(220, -120),
14 "radius": 8.0
15}
16
17const DEFAULT_PADDLE := {
18 "position": Vector2.ZERO,
19 "size": Vector2(14, 72),
20 "speed": 260.0
21}
22
23
24
25func create_ball(position:Vector2 = Vector2.ZERO, velocity:Vector2 = Vector2(220, -120), radius:float = 8.0) -> Dictionary:
26 var ball = DEFAULT_BALL.duplicate(true)
27 ball["position"] = position
28 ball["velocity"] = velocity
29 ball["radius"] = radius
30 return ball
31
32
33
34func create_paddle(position:Vector2 = Vector2.ZERO, size:Vector2 = Vector2(14, 72), speed:float = 260.0) -> Dictionary:
35 var paddle = DEFAULT_PADDLE.duplicate(true)
36 paddle["position"] = position
37 paddle["size"] = size
38 paddle["speed"] = speed
39 return paddle
40
41
42
43func step_ball(ball:Dictionary, delta:float, playfield:Rect2) -> Dictionary:
44 var next = ball.duplicate(true)
45 var radius = float(next.get("radius", 8.0))
46 var previous_position:Vector2 = _vector(next.get("position", Vector2.ZERO))
47 var position = previous_position + next.get("velocity", Vector2.ZERO) * delta
48 var velocity = next.get("velocity", Vector2.ZERO)
49 if position.y - radius < playfield.position.y:
50 position.y = playfield.position.y + radius
51 velocity.y = abs(velocity.y)
52 elif position.y + radius > playfield.end.y:
53 position.y = playfield.end.y - radius
54 velocity.y = -abs(velocity.y)
55 next["previous_position"] = previous_position
56 next["position"] = position
57 next["velocity"] = velocity
58 return next
59
60
61
64func step_balls(balls:Array, delta:float, playfield:Rect2, post_step:Callable = Callable()) -> Array:
65 var stepped:Array = []
66 for index in range(balls.size()):
67 if not balls[index] is Dictionary:
68 continue
69 var next_ball:Dictionary = step_ball(balls[index], delta, playfield)
70 if post_step.is_valid():
71 next_ball = post_step.call(index, next_ball)
72 stepped.append(next_ball)
73 return stepped
74
75
76
79func step_ball_group(primary_ball:Dictionary, extra_ball_rows:Array, delta:float, playfield:Rect2, post_step:Callable = Callable()) -> Dictionary:
80 var next_primary:Dictionary = {}
81 if not primary_ball.is_empty():
82 next_primary = step_ball(primary_ball, delta, playfield)
83 if post_step.is_valid():
84 next_primary = post_step.call(-1, next_primary)
85 var next_extras:Array = step_balls(extra_ball_rows, delta, playfield, post_step)
86 return {
87 "ball": next_primary,
88 "extra_balls": next_extras,
89 "count": (1 if not next_primary.is_empty() else 0) + next_extras.size()
90 }
91
92
93
96func collect_scoring_balls(balls:Array, playfield:Rect2) -> Dictionary:
97 var scoring_sides:Array = []
98 var active_balls:Array = []
99 for ball_value in balls:
100 if not ball_value is Dictionary:
101 continue
102 var scoring_side:String = get_score_side(ball_value, playfield)
103 if scoring_side == "":
104 active_balls.append(ball_value)
105 else:
106 scoring_sides.append(scoring_side)
107 return {
108 "scoring_sides": scoring_sides,
109 "active_balls": active_balls
110 }
111
112
113
116func collect_ball_group_scoring(primary_ball:Dictionary, extra_ball_rows:Array, playfield:Rect2) -> Dictionary:
117 var primary_scoring_side:String = ""
118 if not primary_ball.is_empty():
119 primary_scoring_side = get_score_side(primary_ball, playfield)
120 var scoring_result:Dictionary = collect_scoring_balls(extra_ball_rows, playfield)
121 var scoring_sides:Array = scoring_result.get("scoring_sides", [])
122 if primary_scoring_side != "":
123 scoring_sides.push_front(primary_scoring_side)
124 var active_balls:Array = scoring_result.get("active_balls", [])
125 var next_ball:Dictionary = primary_ball
126 var next_extra_balls:Array = active_balls
127 if primary_scoring_side != "":
128 if not active_balls.is_empty():
129 next_ball = active_balls.pop_front()
130 next_extra_balls = active_balls
131 else:
132 next_ball = {}
133 return {
134 "primary_scoring_side": primary_scoring_side,
135 "scoring_sides": scoring_sides,
136 "next_ball": next_ball,
137 "active_balls": next_extra_balls,
138 "primary_promoted": primary_scoring_side != "" and not next_ball.is_empty()
139 }
140
141
142
143func move_paddle(paddle:Dictionary, direction:float, delta:float, playfield:Rect2) -> Dictionary:
144 var next:Dictionary = paddle.duplicate(true)
145 var position:Vector2 = _vector(next.get("position", Vector2.ZERO))
146 var size:Vector2 = _vector(next.get("size", Vector2(14, 72)))
147 position.y += clamp(direction, -1.0, 1.0) * float(next.get("speed", 260.0)) * delta
148 position.y = clamp(position.y, playfield.position.y + size.y * 0.5, playfield.end.y - size.y * 0.5)
149 next["position"] = position
150 return next
151
152
153
154func create_projectile(owner:String, position:Vector2, velocity:Vector2, radius:float = 5.0, data:Dictionary = {}) -> Dictionary:
155 var projectile:Dictionary = data.duplicate(true)
156 projectile["owner"] = owner
157 projectile["position"] = position
158 projectile["velocity"] = velocity
159 projectile["radius"] = radius
160 return projectile
161
162
163
164func create_horizontal_projectile(source:Dictionary, owner:String, speed:float, playfield:Rect2, options:Dictionary = {}) -> Dictionary:
165 if source.is_empty():
166 return {}
167 var direction:float = float(options.get("direction", 1.0 if owner == "player" else -1.0))
168 var offset:Vector2 = _vector(options.get("offset", Vector2(direction * 16.0, 0.0)))
169 var radius:float = float(options.get("radius", 5.0))
170 var position:Vector2 = _vector(source.get("position", playfield.get_center())) + offset
171 return create_projectile(owner, position, Vector2(direction * speed, 0.0), radius, options.get("data", {}))
172
173
174
175func step_projectile(projectile:Dictionary, delta:float) -> Dictionary:
176 var next:Dictionary = projectile.duplicate(true)
177 next["position"] = _vector(next.get("position", Vector2.ZERO)) + _vector(next.get("velocity", Vector2.ZERO)) * delta
178 return next
179
180
181
190func step_projectiles(projectiles:Array, delta:float, playfield:Rect2, targets:Array = [], options:Dictionary = {}) -> Dictionary:
191 var active_projectiles:Array = []
192 var hits:Array = []
193 var out_of_bounds:Array = []
194 var margin:float = float(options.get("margin", 24.0))
195 for projectile_value in projectiles:
196 if not projectile_value is Dictionary:
197 continue
198 var stepped_projectile:Dictionary = step_projectile(projectile_value, delta)
199 var hit_target:Dictionary = _projectile_hit_target(stepped_projectile, targets)
200 if not hit_target.is_empty():
201 hits.append({
202 "projectile": stepped_projectile,
203 "target_id": str(hit_target.get("id", "")),
204 "target": hit_target.duplicate(true),
205 "owner": str(stepped_projectile.get("owner", ""))
206 })
207 continue
208 if projectile_out_of_bounds(stepped_projectile, playfield, margin):
209 out_of_bounds.append(stepped_projectile)
210 continue
211 active_projectiles.append(stepped_projectile)
212 return {
213 "active_projectiles": active_projectiles,
214 "hits": hits,
215 "out_of_bounds": out_of_bounds
216 }
217
218
219
220func projectile_out_of_bounds(projectile:Dictionary, playfield:Rect2, margin:float = 24.0) -> bool:
221 var position:Vector2 = _vector(projectile.get("position", Vector2.ZERO))
222 return position.x < playfield.position.x - margin or position.x > playfield.end.x + margin or position.y < playfield.position.y - margin or position.y > playfield.end.y + margin
223
224
225
226func projectile_intersects_paddle(projectile:Dictionary, paddle:Dictionary) -> bool:
227 if paddle.is_empty():
228 return false
229 var paddle_size:Vector2 = _vector(paddle.get("size", Vector2(12, 70)))
230 var paddle_rect:Rect2 = Rect2(_vector(paddle.get("position", Vector2.ZERO)) - paddle_size * 0.5, paddle_size)
231 return paddle_rect.grow(float(projectile.get("radius", 5.0))).has_point(_vector(projectile.get("position", Vector2.ZERO)))
232
233
234
235func create_split_ball(source_ball:Dictionary, options:Dictionary = {}) -> Dictionary:
236 if source_ball.is_empty():
237 return {}
238 var base_velocity:Vector2 = _vector(source_ball.get("velocity", Vector2(220, 0)))
239 var split_velocity:Vector2 = Vector2(base_velocity.x, -base_velocity.y)
240 var min_vertical:float = float(options.get("min_vertical_speed", 130.0))
241 if abs(split_velocity.y) < float(options.get("vertical_threshold", 90.0)):
242 split_velocity.y = min_vertical if base_velocity.y <= 0.0 else -min_vertical
243 var min_speed:float = float(options.get("min_speed", 150.0))
244 var speed_ratio:float = float(options.get("speed_ratio", 0.72))
245 var split_speed:float = maxf(min_speed, base_velocity.length() * speed_ratio)
246 if split_velocity.length() > 0.001:
247 split_velocity = split_velocity.normalized() * split_speed
248 var split_ball:Dictionary = create_ball(_vector(source_ball.get("position", Vector2.ZERO)), split_velocity, float(source_ball.get("radius", 8.0)))
249 if source_ball.has("last_hitter"):
250 split_ball["last_hitter"] = str(source_ball.get("last_hitter", ""))
251 return split_ball
252
253
254
256func ball_intersects_paddle(ball:Dictionary, paddle:Dictionary, inset:Vector2 = Vector2.ZERO) -> bool:
257 var position = ball.get("position", Vector2.ZERO)
258 var radius = float(ball.get("radius", 8.0))
259 var size = paddle.get("size", Vector2(14, 72))
260 var paddle_position = paddle.get("position", Vector2.ZERO)
261 var rect = Rect2(paddle_position - size * 0.5, size).grow_individual(-inset.x, -inset.y, -inset.x, -inset.y)
262 var closest = Vector2(
263 clamp(position.x, rect.position.x, rect.end.x),
264 clamp(position.y, rect.position.y, rect.end.y)
265 )
266 return position.distance_to(closest) <= radius
267
268
269
271func ball_swept_intersects_paddle(ball:Dictionary, paddle:Dictionary, inset:Vector2 = Vector2.ZERO) -> bool:
272 if paddle.is_empty():
273 return false
274 var current_position:Vector2 = _vector(ball.get("position", Vector2.ZERO))
275 var previous_position:Vector2 = _vector(ball.get("previous_position", current_position))
276 if previous_position == current_position:
277 return ball_intersects_paddle(ball, paddle, inset)
278 var radius:float = float(ball.get("radius", 8.0))
279 var size:Vector2 = _vector(paddle.get("size", Vector2(14, 72)))
280 var paddle_position:Vector2 = _vector(paddle.get("position", Vector2.ZERO))
281 var rect:Rect2 = Rect2(paddle_position - size * 0.5, size).grow_individual(-inset.x, -inset.y, -inset.x, -inset.y).grow(radius)
282 return _segment_intersects_rect(previous_position, current_position, rect)
283
284
285
286func bounce_from_paddle(ball:Dictionary, paddle:Dictionary, horizontal_sign:float, speedup:float = 1.04) -> Dictionary:
287 var next = ball.duplicate(true)
288 var velocity = next.get("velocity", Vector2.ZERO)
289 var paddle_position = paddle.get("position", Vector2.ZERO)
290 var paddle_size = paddle.get("size", Vector2(14, 72))
291 var ball_position = next.get("position", Vector2.ZERO)
292 var offset = clamp((ball_position.y - paddle_position.y) / max(1.0, paddle_size.y * 0.5), -1.0, 1.0)
293 var speed = max(80.0, velocity.length() * speedup)
294 var direction = Vector2(sign(horizontal_sign), offset * 0.75).normalized()
295 ball_position.x = paddle_position.x + sign(horizontal_sign) * (paddle_size.x * 0.5 + float(next.get("radius", 8.0)) + 0.5)
296 next["position"] = ball_position
297 next["previous_position"] = ball_position
298 next["velocity"] = direction * speed
299 return next
300
301
302
303func get_score_side(ball:Dictionary, playfield:Rect2) -> String:
304 var position = ball.get("position", Vector2.ZERO)
305 var radius = float(ball.get("radius", 8.0))
306 if position.x + radius < playfield.position.x:
307 return "right"
308 if position.x - radius > playfield.end.x:
309 return "left"
310 return ""
311
312
313func _vector(value) -> Vector2:
314 if value is Vector2:
315 return value
316 if value is Vector2i:
317 return Vector2(value.x, value.y)
318 if value is Array and value.size() >= 2:
319 return Vector2(float(value[0]), float(value[1]))
320 if value is Dictionary and value.has("x") and value.has("y"):
321 return Vector2(float(value.get("x", 0.0)), float(value.get("y", 0.0)))
322 return Vector2.ZERO
323
324
325func _projectile_hit_target(projectile:Dictionary, targets:Array) -> Dictionary:
326 for target_value in targets:
327 if not target_value is Dictionary:
328 continue
329 var target:Dictionary = target_value
330 if not _projectile_matches_target(projectile, target):
331 continue
332 var paddle:Dictionary = _dictionary(target.get("paddle", {}))
333 if projectile_intersects_paddle(projectile, paddle):
334 return target
335 return {}
336
337
338func _projectile_matches_target(projectile:Dictionary, target:Dictionary) -> bool:
339 var owner:String = str(projectile.get("owner", ""))
340 var owner_filters = target.get("owners", [])
341 if owner_filters is Array:
342 var owners:Array = owner_filters
343 if not owners.is_empty() and not owners.has(owner):
344 return false
345 var excluded_filters = target.get("exclude_owners", [])
346 if excluded_filters is Array:
347 var excluded_owners:Array = excluded_filters
348 if excluded_owners.has(owner):
349 return false
350 return true
351
352
353func _dictionary(value) -> Dictionary:
354 if value is Dictionary:
355 return value
356 return {}
357
358
359func _segment_intersects_rect(start:Vector2, end:Vector2, rect:Rect2) -> bool:
360 if rect.has_point(start) or rect.has_point(end):
361 return true
362 var delta:Vector2 = end - start
363 var t_min:float = 0.0
364 var t_max:float = 1.0
365 if abs(delta.x) < 0.00001:
366 if start.x < rect.position.x or start.x > rect.end.x:
367 return false
368 else:
369 var inv_x:float = 1.0 / delta.x
370 var tx1:float = (rect.position.x - start.x) * inv_x
371 var tx2:float = (rect.end.x - start.x) * inv_x
372 if tx1 > tx2:
373 var temp_x:float = tx1
374 tx1 = tx2
375 tx2 = temp_x
376 t_min = maxf(t_min, tx1)
377 t_max = minf(t_max, tx2)
378 if t_min > t_max:
379 return false
380 if abs(delta.y) < 0.00001:
381 return start.y >= rect.position.y and start.y <= rect.end.y
382 var inv_y:float = 1.0 / delta.y
383 var ty1:float = (rect.position.y - start.y) * inv_y
384 var ty2:float = (rect.end.y - start.y) * inv_y
385 if ty1 > ty2:
386 var temp_y:float = ty1
387 ty1 = ty2
388 ty2 = temp_y
389 t_min = maxf(t_min, ty1)
390 t_max = minf(t_max, ty2)
391 return t_min <= t_max
Callable
Advance a list of ball dictionaries with the same bounds step.
float
Create a reusable projectile state dictionary for arcade/action games.
Vector2
Create a reusable ball state dictionary.