rgame 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +94 -0
  3. data/README.md +130 -233
  4. data/docs/api/README.md +116 -69
  5. data/docs/api/assets.md +11 -12
  6. data/docs/api/components.md +58 -34
  7. data/docs/api/drawing.md +77 -9
  8. data/docs/api/game.md +34 -13
  9. data/docs/api/input.md +232 -51
  10. data/docs/api/scene_graph.md +242 -15
  11. data/docs/api/systems.md +20 -0
  12. data/docs/api/toolbox.md +19 -15
  13. data/docs/api/ui.md +98 -0
  14. data/docs/api/values.md +32 -0
  15. data/ext/README.md +6 -5
  16. data/ext/rgame_core/app/app.c +182 -8
  17. data/ext/rgame_core/audio/audio.c +74 -0
  18. data/ext/rgame_core/example.rb +17 -6
  19. data/ext/rgame_core/extconf.rb +52 -24
  20. data/ext/rgame_core/graphics/canvas.c +45 -4
  21. data/ext/rgame_core/graphics/canvas.h +65 -10
  22. data/ext/rgame_core/graphics/clip.c +22 -13
  23. data/ext/rgame_core/include/rgame/core.h +113 -3
  24. data/ext/rgame_core/input/gamepad.c +57 -3
  25. data/ext/rgame_core/ruby/core_ext.c +16 -0
  26. data/ext/rgame_core/ruby/renderer_ext.c +23 -0
  27. data/ext/rgame_util/color_ext.c +12 -3
  28. data/lib/rgame/core/app.rb +2 -0
  29. data/lib/rgame/core/input.rb +35 -41
  30. data/lib/rgame/core/recording.rb +3 -1
  31. data/lib/rgame/core/renderer.rb +76 -28
  32. data/lib/rgame/core/tile_map_renderer.rb +84 -55
  33. data/lib/rgame/engine/camera.rb +55 -10
  34. data/lib/rgame/engine/component.rb +11 -1
  35. data/lib/rgame/engine/components/animated_sprite.rb +9 -3
  36. data/lib/rgame/engine/components/camera_follow.rb +44 -0
  37. data/lib/rgame/engine/components/character_body.rb +25 -4
  38. data/lib/rgame/engine/components/sprite.rb +11 -1
  39. data/lib/rgame/engine/components/tile_world.rb +31 -18
  40. data/lib/rgame/engine/culling.rb +47 -0
  41. data/lib/rgame/engine/debug_overlay.rb +20 -9
  42. data/lib/rgame/engine/input/action_mapper.rb +101 -21
  43. data/lib/rgame/engine/input/actions.rb +69 -12
  44. data/lib/rgame/engine/input/input_map.rb +178 -0
  45. data/lib/rgame/engine/layout.rb +82 -0
  46. data/lib/rgame/engine/node2d.rb +205 -36
  47. data/lib/rgame/engine/player.rb +69 -0
  48. data/lib/rgame/engine/player_layer.rb +70 -0
  49. data/lib/rgame/engine/players.rb +212 -0
  50. data/lib/rgame/engine/scene/scene_stack.rb +25 -3
  51. data/lib/rgame/engine/spatial_hash.rb +17 -4
  52. data/lib/rgame/engine/tile_map_layer.rb +84 -0
  53. data/lib/rgame/engine/ui/menu.rb +115 -0
  54. data/lib/rgame/engine/ui/menu_item.rb +84 -0
  55. data/lib/rgame/engine/view.rb +76 -0
  56. data/lib/rgame/engine/viewports.rb +174 -0
  57. data/lib/rgame/engine/world_view.rb +70 -0
  58. data/lib/rgame/engine.rb +13 -1
  59. data/lib/rgame/game.rb +81 -11
  60. data/lib/rgame/util/controls.rb +117 -41
  61. data/lib/rgame/util/z.rb +133 -0
  62. data/lib/rgame/util.rb +1 -0
  63. data/lib/rgame/version.rb +1 -1
  64. metadata +26 -11
  65. data/lib/rgame/engine/camera_view.rb +0 -28
@@ -227,14 +227,168 @@ void rgame_app_gl_restore(const rgame_gl_context_save *saved) {
227
227
  * the compiler checks — it must not include SDL. These assertions close that
228
228
  * gap at compile time, so the two can never drift apart silently.
229
229
  */
230
- _Static_assert(RGAME_KEY_RETURN == SDL_SCANCODE_RETURN, "key id must match SDL scancode");
231
- _Static_assert(RGAME_KEY_ESCAPE == SDL_SCANCODE_ESCAPE, "key id must match SDL scancode");
232
- _Static_assert(RGAME_KEY_SPACE == SDL_SCANCODE_SPACE, "key id must match SDL scancode");
233
- _Static_assert(RGAME_KEY_F1 == SDL_SCANCODE_F1, "key id must match SDL scancode");
234
- _Static_assert(RGAME_KEY_RIGHT == SDL_SCANCODE_RIGHT, "key id must match SDL scancode");
235
- _Static_assert(RGAME_KEY_LEFT == SDL_SCANCODE_LEFT, "key id must match SDL scancode");
236
- _Static_assert(RGAME_KEY_DOWN == SDL_SCANCODE_DOWN, "key id must match SDL scancode");
237
- _Static_assert(RGAME_KEY_UP == SDL_SCANCODE_UP, "key id must match SDL scancode");
230
+ _Static_assert(RGAME_KEY_A == SDL_SCANCODE_A,
231
+ "key id must match SDL scancode");
232
+ _Static_assert(RGAME_KEY_B == SDL_SCANCODE_B,
233
+ "key id must match SDL scancode");
234
+ _Static_assert(RGAME_KEY_C == SDL_SCANCODE_C,
235
+ "key id must match SDL scancode");
236
+ _Static_assert(RGAME_KEY_D == SDL_SCANCODE_D,
237
+ "key id must match SDL scancode");
238
+ _Static_assert(RGAME_KEY_E == SDL_SCANCODE_E,
239
+ "key id must match SDL scancode");
240
+ _Static_assert(RGAME_KEY_F == SDL_SCANCODE_F,
241
+ "key id must match SDL scancode");
242
+ _Static_assert(RGAME_KEY_G == SDL_SCANCODE_G,
243
+ "key id must match SDL scancode");
244
+ _Static_assert(RGAME_KEY_H == SDL_SCANCODE_H,
245
+ "key id must match SDL scancode");
246
+ _Static_assert(RGAME_KEY_I == SDL_SCANCODE_I,
247
+ "key id must match SDL scancode");
248
+ _Static_assert(RGAME_KEY_J == SDL_SCANCODE_J,
249
+ "key id must match SDL scancode");
250
+ _Static_assert(RGAME_KEY_K == SDL_SCANCODE_K,
251
+ "key id must match SDL scancode");
252
+ _Static_assert(RGAME_KEY_L == SDL_SCANCODE_L,
253
+ "key id must match SDL scancode");
254
+ _Static_assert(RGAME_KEY_M == SDL_SCANCODE_M,
255
+ "key id must match SDL scancode");
256
+ _Static_assert(RGAME_KEY_N == SDL_SCANCODE_N,
257
+ "key id must match SDL scancode");
258
+ _Static_assert(RGAME_KEY_O == SDL_SCANCODE_O,
259
+ "key id must match SDL scancode");
260
+ _Static_assert(RGAME_KEY_P == SDL_SCANCODE_P,
261
+ "key id must match SDL scancode");
262
+ _Static_assert(RGAME_KEY_Q == SDL_SCANCODE_Q,
263
+ "key id must match SDL scancode");
264
+ _Static_assert(RGAME_KEY_R == SDL_SCANCODE_R,
265
+ "key id must match SDL scancode");
266
+ _Static_assert(RGAME_KEY_S == SDL_SCANCODE_S,
267
+ "key id must match SDL scancode");
268
+ _Static_assert(RGAME_KEY_T == SDL_SCANCODE_T,
269
+ "key id must match SDL scancode");
270
+ _Static_assert(RGAME_KEY_U == SDL_SCANCODE_U,
271
+ "key id must match SDL scancode");
272
+ _Static_assert(RGAME_KEY_V == SDL_SCANCODE_V,
273
+ "key id must match SDL scancode");
274
+ _Static_assert(RGAME_KEY_W == SDL_SCANCODE_W,
275
+ "key id must match SDL scancode");
276
+ _Static_assert(RGAME_KEY_X == SDL_SCANCODE_X,
277
+ "key id must match SDL scancode");
278
+ _Static_assert(RGAME_KEY_Y == SDL_SCANCODE_Y,
279
+ "key id must match SDL scancode");
280
+ _Static_assert(RGAME_KEY_Z == SDL_SCANCODE_Z,
281
+ "key id must match SDL scancode");
282
+ _Static_assert(RGAME_KEY_1 == SDL_SCANCODE_1,
283
+ "key id must match SDL scancode");
284
+ _Static_assert(RGAME_KEY_2 == SDL_SCANCODE_2,
285
+ "key id must match SDL scancode");
286
+ _Static_assert(RGAME_KEY_3 == SDL_SCANCODE_3,
287
+ "key id must match SDL scancode");
288
+ _Static_assert(RGAME_KEY_4 == SDL_SCANCODE_4,
289
+ "key id must match SDL scancode");
290
+ _Static_assert(RGAME_KEY_5 == SDL_SCANCODE_5,
291
+ "key id must match SDL scancode");
292
+ _Static_assert(RGAME_KEY_6 == SDL_SCANCODE_6,
293
+ "key id must match SDL scancode");
294
+ _Static_assert(RGAME_KEY_7 == SDL_SCANCODE_7,
295
+ "key id must match SDL scancode");
296
+ _Static_assert(RGAME_KEY_8 == SDL_SCANCODE_8,
297
+ "key id must match SDL scancode");
298
+ _Static_assert(RGAME_KEY_9 == SDL_SCANCODE_9,
299
+ "key id must match SDL scancode");
300
+ _Static_assert(RGAME_KEY_0 == SDL_SCANCODE_0,
301
+ "key id must match SDL scancode");
302
+ _Static_assert(RGAME_KEY_RETURN == SDL_SCANCODE_RETURN,
303
+ "key id must match SDL scancode");
304
+ _Static_assert(RGAME_KEY_ESCAPE == SDL_SCANCODE_ESCAPE,
305
+ "key id must match SDL scancode");
306
+ _Static_assert(RGAME_KEY_BACKSPACE == SDL_SCANCODE_BACKSPACE,
307
+ "key id must match SDL scancode");
308
+ _Static_assert(RGAME_KEY_TAB == SDL_SCANCODE_TAB,
309
+ "key id must match SDL scancode");
310
+ _Static_assert(RGAME_KEY_SPACE == SDL_SCANCODE_SPACE,
311
+ "key id must match SDL scancode");
312
+ _Static_assert(RGAME_KEY_MINUS == SDL_SCANCODE_MINUS,
313
+ "key id must match SDL scancode");
314
+ _Static_assert(RGAME_KEY_EQUALS == SDL_SCANCODE_EQUALS,
315
+ "key id must match SDL scancode");
316
+ _Static_assert(RGAME_KEY_LEFTBRACKET == SDL_SCANCODE_LEFTBRACKET,
317
+ "key id must match SDL scancode");
318
+ _Static_assert(RGAME_KEY_RIGHTBRACKET == SDL_SCANCODE_RIGHTBRACKET,
319
+ "key id must match SDL scancode");
320
+ _Static_assert(RGAME_KEY_BACKSLASH == SDL_SCANCODE_BACKSLASH,
321
+ "key id must match SDL scancode");
322
+ _Static_assert(RGAME_KEY_SEMICOLON == SDL_SCANCODE_SEMICOLON,
323
+ "key id must match SDL scancode");
324
+ _Static_assert(RGAME_KEY_APOSTROPHE == SDL_SCANCODE_APOSTROPHE,
325
+ "key id must match SDL scancode");
326
+ _Static_assert(RGAME_KEY_GRAVE == SDL_SCANCODE_GRAVE,
327
+ "key id must match SDL scancode");
328
+ _Static_assert(RGAME_KEY_COMMA == SDL_SCANCODE_COMMA,
329
+ "key id must match SDL scancode");
330
+ _Static_assert(RGAME_KEY_PERIOD == SDL_SCANCODE_PERIOD,
331
+ "key id must match SDL scancode");
332
+ _Static_assert(RGAME_KEY_SLASH == SDL_SCANCODE_SLASH,
333
+ "key id must match SDL scancode");
334
+ _Static_assert(RGAME_KEY_CAPSLOCK == SDL_SCANCODE_CAPSLOCK,
335
+ "key id must match SDL scancode");
336
+ _Static_assert(RGAME_KEY_F1 == SDL_SCANCODE_F1,
337
+ "key id must match SDL scancode");
338
+ _Static_assert(RGAME_KEY_F2 == SDL_SCANCODE_F2,
339
+ "key id must match SDL scancode");
340
+ _Static_assert(RGAME_KEY_F3 == SDL_SCANCODE_F3,
341
+ "key id must match SDL scancode");
342
+ _Static_assert(RGAME_KEY_F4 == SDL_SCANCODE_F4,
343
+ "key id must match SDL scancode");
344
+ _Static_assert(RGAME_KEY_F5 == SDL_SCANCODE_F5,
345
+ "key id must match SDL scancode");
346
+ _Static_assert(RGAME_KEY_F6 == SDL_SCANCODE_F6,
347
+ "key id must match SDL scancode");
348
+ _Static_assert(RGAME_KEY_F7 == SDL_SCANCODE_F7,
349
+ "key id must match SDL scancode");
350
+ _Static_assert(RGAME_KEY_F8 == SDL_SCANCODE_F8,
351
+ "key id must match SDL scancode");
352
+ _Static_assert(RGAME_KEY_F9 == SDL_SCANCODE_F9,
353
+ "key id must match SDL scancode");
354
+ _Static_assert(RGAME_KEY_F10 == SDL_SCANCODE_F10,
355
+ "key id must match SDL scancode");
356
+ _Static_assert(RGAME_KEY_F11 == SDL_SCANCODE_F11,
357
+ "key id must match SDL scancode");
358
+ _Static_assert(RGAME_KEY_F12 == SDL_SCANCODE_F12,
359
+ "key id must match SDL scancode");
360
+ _Static_assert(RGAME_KEY_INSERT == SDL_SCANCODE_INSERT,
361
+ "key id must match SDL scancode");
362
+ _Static_assert(RGAME_KEY_HOME == SDL_SCANCODE_HOME,
363
+ "key id must match SDL scancode");
364
+ _Static_assert(RGAME_KEY_PAGEUP == SDL_SCANCODE_PAGEUP,
365
+ "key id must match SDL scancode");
366
+ _Static_assert(RGAME_KEY_DELETE == SDL_SCANCODE_DELETE,
367
+ "key id must match SDL scancode");
368
+ _Static_assert(RGAME_KEY_END == SDL_SCANCODE_END,
369
+ "key id must match SDL scancode");
370
+ _Static_assert(RGAME_KEY_PAGEDOWN == SDL_SCANCODE_PAGEDOWN,
371
+ "key id must match SDL scancode");
372
+ _Static_assert(RGAME_KEY_RIGHT == SDL_SCANCODE_RIGHT,
373
+ "key id must match SDL scancode");
374
+ _Static_assert(RGAME_KEY_LEFT == SDL_SCANCODE_LEFT,
375
+ "key id must match SDL scancode");
376
+ _Static_assert(RGAME_KEY_DOWN == SDL_SCANCODE_DOWN,
377
+ "key id must match SDL scancode");
378
+ _Static_assert(RGAME_KEY_UP == SDL_SCANCODE_UP,
379
+ "key id must match SDL scancode");
380
+ _Static_assert(RGAME_KEY_LCTRL == SDL_SCANCODE_LCTRL,
381
+ "key id must match SDL scancode");
382
+ _Static_assert(RGAME_KEY_LSHIFT == SDL_SCANCODE_LSHIFT,
383
+ "key id must match SDL scancode");
384
+ _Static_assert(RGAME_KEY_LALT == SDL_SCANCODE_LALT,
385
+ "key id must match SDL scancode");
386
+ _Static_assert(RGAME_KEY_RCTRL == SDL_SCANCODE_RCTRL,
387
+ "key id must match SDL scancode");
388
+ _Static_assert(RGAME_KEY_RSHIFT == SDL_SCANCODE_RSHIFT,
389
+ "key id must match SDL scancode");
390
+ _Static_assert(RGAME_KEY_RALT == SDL_SCANCODE_RALT,
391
+ "key id must match SDL scancode");
238
392
 
239
393
  /* The snapshot array must be exactly as long as SDL's keyboard state array,
240
394
  * since filling it is a straight copy. */
@@ -400,6 +554,9 @@ void rgame_app_run(rgame_app *app, const rgame_app_callbacks *cb) {
400
554
 
401
555
  rgame_draw_backend backend = rgame_gl_backend_table(&app->gl);
402
556
  rgame_canvas_submit(&app->canvas, &backend);
557
+ if (cb->frame_end) {
558
+ cb->frame_end(cb->userdata);
559
+ }
403
560
  SDL_GL_SwapWindow(app->window);
404
561
  }
405
562
  }
@@ -599,6 +756,23 @@ int rgame_app_push_clip(rgame_app *app, int x, int y, int width, int height) {
599
756
  return 1;
600
757
  }
601
758
 
759
+ void rgame_app_push_layer(rgame_app *app, double base) {
760
+ rgame_canvas *canvas = drawing_canvas(app);
761
+ if (canvas) {
762
+ rgame_canvas_push_layer(canvas, base);
763
+ }
764
+ }
765
+
766
+ double rgame_app_layer(rgame_app *app) {
767
+ rgame_canvas *canvas = drawing_canvas(app);
768
+ return canvas ? rgame_canvas_layer(canvas) : 0.0;
769
+ }
770
+
771
+ unsigned int rgame_app_next_layer_slot(rgame_app *app, int band) {
772
+ rgame_canvas *canvas = drawing_canvas(app);
773
+ return canvas ? rgame_canvas_next_slot(canvas, band) : 0;
774
+ }
775
+
602
776
  void rgame_app_pop(rgame_app *app) {
603
777
  rgame_canvas *canvas = drawing_canvas(app);
604
778
  if (canvas) {
@@ -44,6 +44,15 @@
44
44
  struct rgame_audio {
45
45
  ma_resource_manager resources;
46
46
  ma_engine engine;
47
+ /*
48
+ * Set only on Windows, only when construction fell back to `noDevice`
49
+ * because there was nothing to open — see create_audio. Exists so
50
+ * rgame_audio_backend can still answer "Null" for this engine, matching
51
+ * what every other no-hardware case on every platform already reports,
52
+ * even though this engine has no `ma_device` at all for the normal
53
+ * lookup to find.
54
+ */
55
+ int forced_no_device;
47
56
  };
48
57
 
49
58
  struct rgame_sample {
@@ -131,6 +140,43 @@ static rgame_audio *create_audio(int offline, unsigned int sample_rate, char *er
131
140
  * no sound card — a build server, a container — gets a working engine that
132
141
  * plays silence rather than an error a game has to handle. Verified: with
133
142
  * ALSA and PulseAudio unavailable, the chosen backend is "Null".
143
+ *
144
+ * That fallback is *inside* the real backend's own device-open attempt —
145
+ * it only helps if opening fails cleanly. Verified on Linux, which is the
146
+ * one platform where it demonstrably does, and where an earlier attempt to
147
+ * generalise the workaround below broke a leg that had been working.
148
+ *
149
+ * On Windows, a machine with zero playback devices at all (every GitHub
150
+ * Actions Windows runner, among others: confirmed via
151
+ * `Get-CimInstance Win32_SoundDevice`) crashes instead of failing inside
152
+ * WASAPI's default-device lookup, so the fallback above never gets a
153
+ * chance to run.
154
+ *
155
+ * Windows alone gets an extra step first: enumerate devices (read-only,
156
+ * does not attempt to open anything, so it does not hit whatever WASAPI's
157
+ * own open path gets wrong with no endpoint to find) and, when there are
158
+ * none, fall back to `noDevice` — the same mode the offline constructor
159
+ * below uses — rather than the auto-detect list's own real-backend open
160
+ * attempt.
161
+ *
162
+ * macOS looks like the same bug and is not, which is worth stating because
163
+ * extending this branch to it does nothing. A device-less Mac still
164
+ * enumerates a playback device, so a zero-device test never fires there;
165
+ * its equivalent crash is CoreAudio being initialised in a **forked
166
+ * child**, which macOS forbids outright. That belongs to the test harness
167
+ * rather than here — see test/test_main.c.
168
+ *
169
+ * An *explicit* null-backend device was tried first and rejected: it still
170
+ * spins up a real background device thread, and that thread crashed inside
171
+ * a Ruby process in a way the plain-C Check build of the same code never
172
+ * did. `noDevice` sidesteps the question entirely: with no device and no
173
+ * background thread, there is nothing left that could crash the way a
174
+ * thread can.
175
+ *
176
+ * Linux is deliberately excluded rather than merely untested. Generalising
177
+ * this to every platform is what broke its leg once already, and its own
178
+ * fallback is proven — so the rule here is that a platform opts in on the
179
+ * strength of a measured crash, not on the logic looking harmless.
134
180
  */
135
181
  ma_engine_config engine = ma_engine_config_init();
136
182
  engine.pResourceManager = &audio->resources;
@@ -141,6 +187,24 @@ static rgame_audio *create_audio(int offline, unsigned int sample_rate, char *er
141
187
  engine.channels = 2;
142
188
  engine.sampleRate = sample_rate;
143
189
  }
190
+ #ifdef _WIN32
191
+ else {
192
+ ma_context probe;
193
+ if (ma_context_init(NULL, 0, NULL, &probe) == MA_SUCCESS) {
194
+ ma_uint32 playback_count = 0;
195
+ ma_result enum_result =
196
+ ma_context_get_devices(&probe, NULL, &playback_count, NULL, NULL);
197
+ ma_context_uninit(&probe);
198
+
199
+ if (enum_result == MA_SUCCESS && playback_count == 0) {
200
+ engine.noDevice = MA_TRUE;
201
+ engine.channels = 2;
202
+ engine.sampleRate = 44100;
203
+ audio->forced_no_device = 1;
204
+ }
205
+ }
206
+ }
207
+ #endif
144
208
 
145
209
  if (ma_engine_init(&engine, &audio->engine) != MA_SUCCESS) {
146
210
  ma_resource_manager_uninit(&audio->resources);
@@ -199,6 +263,16 @@ const char *rgame_audio_backend(const rgame_audio *audio) {
199
263
  return "none";
200
264
  }
201
265
 
266
+ /* The forced-noDevice fallback (see create_audio) has no ma_device for
267
+ * the normal lookup below to find, but it is standing in for exactly what
268
+ * the Null backend already means elsewhere — no hardware, silence — so it
269
+ * answers the same way rather than the more literal but less useful
270
+ * "none", which every other caller of this function reserves for "there
271
+ * is no audio object at all". */
272
+ if (audio->forced_no_device) {
273
+ return "Null";
274
+ }
275
+
202
276
  ma_device *device = ma_engine_get_device((ma_engine *)&audio->engine);
203
277
  return device ? ma_get_backend_name(device->pContext->backend) : "none";
204
278
  }
@@ -88,16 +88,27 @@ class Example < RGame::Core::App
88
88
  @ticks += 1
89
89
  @spin += dt * 45.0
90
90
  speed = 200.0 * dt
91
- @cursor_x -= speed if @input.down?(:left, device: @device)
92
- @cursor_x += speed if @input.down?(:right, device: @device)
93
- @cursor_y -= speed if @input.down?(:up, device: @device)
94
- @cursor_y += speed if @input.down?(:down, device: @device)
91
+ @cursor_x -= speed if held?(Controls::KEY_LEFT, Controls::PAD_DPAD_LEFT)
92
+ @cursor_x += speed if held?(Controls::KEY_RIGHT, Controls::PAD_DPAD_RIGHT)
93
+ @cursor_y -= speed if held?(Controls::KEY_UP, Controls::PAD_DPAD_UP)
94
+ @cursor_y += speed if held?(Controls::KEY_DOWN, Controls::PAD_DPAD_DOWN)
95
95
 
96
96
  # Analog sticks are additive on top of the dpad; the keyboard reads 0.0.
97
- @cursor_x += @input.axis(:move_x, device: @device) * speed
98
- @cursor_y += @input.axis(:move_y, device: @device) * speed
97
+ @cursor_x += @input.axis(Controls::AXIS_LEFT_X, device: @device) * speed
98
+ @cursor_y += @input.axis(Controls::AXIS_LEFT_Y, device: @device) * speed
99
99
  end
100
100
 
101
+ # Core is the raw layer: Input answers "is this scancode down on this device",
102
+ # and naming the key *and* the pad button for one intent is the caller's job.
103
+ # Asking both costs nothing, because a device only answers for its own kind of
104
+ # input — a gamepad reads false for a scancode.
105
+ #
106
+ # A game does not write this. RGame::Engine::InputMap is a table of exactly
107
+ # these pairs, one entry per action, and the scene graph reads named actions
108
+ # instead. This example is here to exercise Core on its own, so it does
109
+ # without that and shows what the layer above is for.
110
+ def held?(key, pad) = @input.down?(key, device: @device) || @input.down?(pad, device: @device)
111
+
101
112
  # One of everything, so a look at the window checks the whole drawing path.
102
113
  # Colours are RGame::Util::Color values rather than arrays: a Color is frozen
103
114
  # and allocates nothing when it is drawn, which is what per-frame code wants.
@@ -68,38 +68,66 @@ $INCFLAGS << ' -I$(srcdir)/../rgame_util'
68
68
  # later compile error.
69
69
  abort 'SDL2 not found (pkg-config --exists sdl2 failed). Install libsdl2-dev.' unless pkg_config('sdl2')
70
70
 
71
- # OpenGL + libm, matching the link line in the root Makefile. append_library
72
- # adds `-lGL`/`-lm` in the right spot on the link command.
71
+ # The GL header, probed as the sources actually spell it: SDL's own
72
+ # <SDL2/SDL_opengl.h>, which resolves to GL/gl.h, OpenGL/gl.h or the Windows SDK's
73
+ # per platform. Probing GL/gl.h directly would ask the wrong question off Linux.
73
74
  #
74
- # Checked rather than assumed, unlike in the root Makefile. This script is what
75
- # runs on someone else's machine during `gem install`, and a missing OpenGL
76
- # development package is a routine thing to hit there. Without a check it
77
- # surfaces as a linker error about an undefined `glClear` at the end of a long
78
- # build; with one, the install stops on a sentence naming the package to
79
- # install. Same reasoning as the SDL2 abort above.
80
- #
81
- # The header probed is the one the sources actually include — SDL's own
82
- # <SDL2/SDL_opengl.h>, which resolves to GL/gl.h or OpenGL/gl.h per platform, so
83
- # probing GL/gl.h directly would ask the wrong question off Linux. It needs
84
- # SDL's cflags, which pkg_config folded into $CFLAGS just above.
75
+ # It needs SDL's cflags, which pkg_config folded into $CFLAGS just above this
76
+ # is also what makes the probe a real check of the include path rather than of
77
+ # the filesystem.
85
78
  abort 'SDL2 OpenGL header not found (SDL2/SDL_opengl.h). Install libsdl2-dev.' unless have_header('SDL2/SDL_opengl.h')
86
79
 
87
- # `-lGL` is how OpenGL is linked on Linux and the BSDs; macOS wants
88
- # `-framework OpenGL` instead. Only the first is implemented as in the root
89
- # Makefile, which hardcodes -lGL so this aborts on a platform it cannot link
90
- # rather than emitting undefined symbols for every gl* call. have_library
91
- # appends -lGL to $libs itself when the probe succeeds.
92
- unless have_library('GL', 'glClear')
93
- abort 'OpenGL library not found (-lGL). Install libgl1-mesa-dev. ' \
94
- '(macOS needs -framework OpenGL, which is not implemented yet.)'
80
+ # Three platforms, three different ways to link the same OpenGL — the one place
81
+ # in this file that has to branch, and the mirror of the root Makefile's own
82
+ # branch (see its GL_LIBS). The engine calls only GL 1.1 entry points, which all
83
+ # three expose directly, so no extension loader (GLAD/GLEW) is involved anywhere;
84
+ # see CLAUDE.md's Conventions on why the legacy profile is deliberate.
85
+ #
86
+ # Probed rather than assumed, because this script is what runs on someone else's
87
+ # machine during `gem install` and a missing OpenGL is a routine thing to hit
88
+ # there. Without a probe it surfaces as a linker error about an undefined
89
+ # `glClear` at the end of a long build; with one, the install stops on a
90
+ # sentence naming what to install. Same reasoning as the SDL2 abort above — and
91
+ # the message is per-platform, since "install libgl1-mesa-dev" is useless advice
92
+ # on a Mac.
93
+ #
94
+ # `have_library` appends `-lGL`/`-lopengl32` to $libs on success; `have_framework`
95
+ # appends `-framework OpenGL` to $LDFLAGS. Both end up on the link line.
96
+ case RbConfig::CONFIG['host_os']
97
+ when /darwin/
98
+ # macOS ships OpenGL as a framework rather than a library, so there is no
99
+ # `-lGL` to find. It is part of the OS — a failure here means the toolchain
100
+ # is missing, not the graphics stack.
101
+ unless have_framework('OpenGL')
102
+ abort 'OpenGL framework not found. It ships with macOS, so this usually means ' \
103
+ 'the command line tools are missing: xcode-select --install'
104
+ end
105
+ when /mingw|mswin|cygwin/
106
+ # Windows' OpenGL is an OS component: opengl32.dll is in system32 on every
107
+ # installation, and the import library comes with the toolchain. Nothing to
108
+ # install from a graphics vendor.
109
+ unless have_library('opengl32', 'glClear')
110
+ abort 'opengl32 not found. It is part of Windows itself, so this means an ' \
111
+ 'incomplete MSYS2 toolchain: pacman -S mingw-w64-ucrt-x86_64-gcc'
112
+ end
113
+ else
114
+ # Linux and the BSDs.
115
+ unless have_library('GL', 'glClear')
116
+ abort 'OpenGL library not found (-lGL). Install the OpenGL development package: ' \
117
+ 'libgl1-mesa-dev on Debian/Ubuntu, mesa-libGL-devel on Fedora.'
118
+ end
95
119
  end
96
120
 
97
121
  $libs = append_library($libs, 'm')
98
122
 
99
123
  # miniaudio's threading, and the dlopen it uses to find ALSA or PulseAudio at
100
- # runtime — the property that makes audio cost no new system dependency. Both
101
- # live in glibc on Linux/BSD. Windows and macOS need neither, and appending them
102
- # unconditionally would fail the link there, so each is probed first.
124
+ # runtime — the property that makes audio cost no new system dependency.
125
+ #
126
+ # Probed rather than branched, because unlike OpenGL above the *answer* differs
127
+ # per platform but the question does not: link them where they exist. On
128
+ # glibc both are system libraries nobody installs; macOS resolves `-lpthread`
129
+ # into libSystem and needs no `-ldl`; MSYS2 supplies winpthreads. Appending
130
+ # either unconditionally would break the link wherever it is absent.
103
131
  $libs = append_library($libs, 'pthread') if have_library('pthread')
104
132
  $libs = append_library($libs, 'dl') if have_library('dl')
105
133
 
@@ -6,15 +6,26 @@
6
6
  enum {
7
7
  RGAME_PUSH_TRANSFORM,
8
8
  RGAME_PUSH_CLIP,
9
+ RGAME_PUSH_LAYER,
9
10
  /* The underlying stack was full. Nothing to undo, but it still occupies a
10
11
  * slot so that the caller's matching pop stays paired with this push. */
11
12
  RGAME_PUSH_NOTHING
12
13
  };
13
14
 
15
+ /* The layer stack and the per-band counters, both back to their frame start. */
16
+ static void reset_layers(rgame_canvas *canvas) {
17
+ canvas->layers[0] = 0.0;
18
+ canvas->layer_depth = 0;
19
+ for (int i = 0; i < RGAME_LAYER_BANDS; i++) {
20
+ canvas->slots[i] = 0;
21
+ }
22
+ }
23
+
14
24
  void rgame_canvas_init(rgame_canvas *canvas) {
15
25
  rgame_draw_queue_init(&canvas->queue);
16
26
  rgame_transform_stack_init(&canvas->transforms);
17
27
  rgame_clip_stack_init(&canvas->clips, 0, 0);
28
+ reset_layers(canvas);
18
29
  canvas->push_depth = 0;
19
30
  canvas->unrecorded_pushes = 0;
20
31
  canvas->width = 0;
@@ -29,6 +40,7 @@ void rgame_canvas_begin_frame(rgame_canvas *canvas, int width, int height) {
29
40
  rgame_draw_queue_reset(&canvas->queue);
30
41
  rgame_transform_stack_init(&canvas->transforms);
31
42
  rgame_clip_stack_init(&canvas->clips, width, height);
43
+ reset_layers(canvas);
32
44
  canvas->push_depth = 0;
33
45
  canvas->unrecorded_pushes = 0;
34
46
  canvas->width = width;
@@ -126,6 +138,26 @@ void rgame_canvas_push_clip(rgame_canvas *canvas, rgame_rect rect) {
126
138
  account_push(canvas, ok, RGAME_PUSH_CLIP);
127
139
  }
128
140
 
141
+ void rgame_canvas_push_layer(rgame_canvas *canvas, double base) {
142
+ int ok = canvas->layer_depth + 1 < RGAME_LAYER_STACK_DEPTH;
143
+ if (ok) {
144
+ /* Replaces rather than accumulates: see canvas.h. */
145
+ canvas->layers[++canvas->layer_depth] = base;
146
+ }
147
+ account_push(canvas, ok, RGAME_PUSH_LAYER);
148
+ }
149
+
150
+ double rgame_canvas_layer(const rgame_canvas *canvas) {
151
+ return canvas->layers[canvas->layer_depth];
152
+ }
153
+
154
+ unsigned int rgame_canvas_next_slot(rgame_canvas *canvas, int band) {
155
+ if (band < 0 || band >= RGAME_LAYER_BANDS) {
156
+ return 0;
157
+ }
158
+ return canvas->slots[band]++;
159
+ }
160
+
129
161
  void rgame_canvas_pop(rgame_canvas *canvas) {
130
162
  /* Unrecorded pushes unwind first: they are the most recent ones, since a
131
163
  * push is only left unrecorded once the record stack is already full. */
@@ -144,11 +176,20 @@ void rgame_canvas_pop(rgame_canvas *canvas) {
144
176
  case RGAME_PUSH_CLIP:
145
177
  rgame_clip_pop(&canvas->clips);
146
178
  break;
179
+ case RGAME_PUSH_LAYER:
180
+ canvas->layer_depth--;
181
+ break;
147
182
  default:
148
183
  break; /* RGAME_PUSH_NOTHING: the push never took effect */
149
184
  }
150
185
  }
151
186
 
187
+ /* Every z that reaches the queue is an offset from the current layer base. One
188
+ * addition, in the one place all four queueing paths pass through. */
189
+ static double layer_z(const rgame_canvas *canvas, double z) {
190
+ return canvas->layers[canvas->layer_depth] + z;
191
+ }
192
+
152
193
  /* Fills one vertex: map the point into screen space, copy through the texture
153
194
  * coordinate, and write the colour as the four bytes GL reads. */
154
195
  static void write_vertex(const rgame_canvas *canvas, rgame_vertex *vertex, float x, float y,
@@ -161,7 +202,7 @@ static void write_vertex(const rgame_canvas *canvas, rgame_vertex *vertex, float
161
202
 
162
203
  void rgame_canvas_triangle(rgame_canvas *canvas, const float *xy6, rgame_color color,
163
204
  double z) {
164
- rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 3, z, 0,
205
+ rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 3, layer_z(canvas, z), 0,
165
206
  rgame_clip_current(&canvas->clips));
166
207
  for (int i = 0; i < 3; i++) {
167
208
  write_vertex(canvas, &out[i], xy6[i * 2], xy6[(i * 2) + 1], 0.0f, 0.0f, color);
@@ -172,7 +213,7 @@ void rgame_canvas_triangle(rgame_canvas *canvas, const float *xy6, rgame_color c
172
213
  static const int RGAME_QUAD_TRIANGLES[6] = { 0, 1, 2, 0, 2, 3 };
173
214
 
174
215
  void rgame_canvas_quad(rgame_canvas *canvas, const float *xy8, rgame_color color, double z) {
175
- rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 6, z, 0,
216
+ rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 6, layer_z(canvas, z), 0,
176
217
  rgame_clip_current(&canvas->clips));
177
218
  for (int i = 0; i < 6; i++) {
178
219
  int corner = RGAME_QUAD_TRIANGLES[i];
@@ -183,7 +224,7 @@ void rgame_canvas_quad(rgame_canvas *canvas, const float *xy8, rgame_color color
183
224
 
184
225
  void rgame_canvas_textured_quad(rgame_canvas *canvas, unsigned int texture, const float *xy8,
185
226
  const float *uv8, rgame_color color, double z) {
186
- rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 6, z, texture,
227
+ rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 6, layer_z(canvas, z), texture,
187
228
  rgame_clip_current(&canvas->clips));
188
229
  for (int i = 0; i < 6; i++) {
189
230
  int corner = RGAME_QUAD_TRIANGLES[i];
@@ -213,7 +254,7 @@ void rgame_canvas_replay(rgame_canvas *canvas, const rgame_recording *recording,
213
254
  /* One command per baked batch, rather than one per original draw call:
214
255
  * the whole point of a recording is that the per-tile work happened
215
256
  * once, at bake time. */
216
- rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, batch->vertex_count, z,
257
+ rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, batch->vertex_count, layer_z(canvas, z),
217
258
  batch->texture,
218
259
  rgame_clip_current(&canvas->clips));
219
260
 
@@ -46,25 +46,67 @@
46
46
  * rotates a clip today.
47
47
  *
48
48
  * ---------------------------------------------------------------------------
49
+ * The layer stack: where a caller's z is measured from
50
+ * ---------------------------------------------------------------------------
51
+ *
52
+ * A `z` handed to a primitive is an *offset*, added to whatever base is on top
53
+ * of the layer stack. The stack starts at 0, so a caller that never pushes one
54
+ * gets exactly the z it passed.
55
+ *
56
+ * What it is for: the scene graph gives every node its own narrow window of z
57
+ * values, so a node's drawing can only ever be ordered against its own other
58
+ * drawing — never against another node's, and never out of the band it is in.
59
+ * Composing that is one addition here, and the alternative is every caller
60
+ * remembering to add the same number to every z it passes.
61
+ *
62
+ * `rgame_canvas_next_slot` is the other half: a small set of counters, one per
63
+ * band, reset at the start of each frame and handing out increasing indices. It
64
+ * is deliberately ignorant of what a band *is* — that vocabulary lives in
65
+ * RGame::Util::Z, a layer up — and does nothing but count, because "which node
66
+ * comes next" is already the order the draw traversal visits nodes in.
67
+ *
68
+ * A push *replaces* rather than adds. Nesting bases would re-introduce the
69
+ * additive relative z this design exists to remove: the point is that a node's
70
+ * window is decided by where the traversal reached it, not by summing what its
71
+ * ancestors happened to pick.
72
+ *
73
+ * ---------------------------------------------------------------------------
49
74
  * One pop for any push
50
75
  * ---------------------------------------------------------------------------
51
76
  *
52
- * `push_translate`, `push_rotate`, `push_scale` and `push_clip` are all undone
53
- * by the same `pop`. The canvas remembers which stack each push went to, so a
54
- * caller never has to — and cannot pop the wrong one. Pushes that could not be
55
- * honoured (a full stack) are still counted, so pop stays balanced and the
56
- * drawing comes out untransformed rather than desynchronised.
77
+ * `push_translate`, `push_rotate`, `push_scale`, `push_clip` and `push_layer`
78
+ * are all undone by the same `pop`. The canvas remembers which stack each push
79
+ * went to, so a caller never has to — and cannot pop the wrong one. Pushes that
80
+ * could not be honoured (a full stack) are still counted, so pop stays balanced
81
+ * and the drawing comes out untransformed rather than desynchronised.
57
82
  */
58
83
 
59
- /* Deep enough that it cannot fill before both underlying stacks have; pushes
60
- * beyond that are counted rather than recorded, so balance always holds. */
61
- #define RGAME_CANVAS_STACK_DEPTH (RGAME_TRANSFORM_STACK_DEPTH + RGAME_CLIP_STACK_DEPTH)
84
+ /* Deep enough for any sane scene graph, like the two stacks it sits beside. */
85
+ #define RGAME_LAYER_STACK_DEPTH 32
86
+
87
+ /* How many independent slot counters a frame has. RGame::Util::Z uses four;
88
+ * the spare room costs four unsigned ints. */
89
+ #define RGAME_LAYER_BANDS 8
90
+
91
+ /* Deep enough that it cannot fill before all three underlying stacks have;
92
+ * pushes beyond that are counted rather than recorded, so balance always
93
+ * holds. */
94
+ #define RGAME_CANVAS_STACK_DEPTH \
95
+ (RGAME_TRANSFORM_STACK_DEPTH + RGAME_CLIP_STACK_DEPTH + RGAME_LAYER_STACK_DEPTH)
62
96
 
63
97
  typedef struct {
64
98
  rgame_transform_stack transforms;
65
99
  rgame_clip_stack clips;
66
100
  rgame_draw_queue queue;
67
101
 
102
+ /* entries[0] is 0 — the base an un-layered draw sits on — so `layer_depth`
103
+ * is the number of pushes outstanding and entries[layer_depth] is current. */
104
+ double layers[RGAME_LAYER_STACK_DEPTH];
105
+ int layer_depth;
106
+
107
+ /* Slots handed out per band this frame. Reset by begin_frame. */
108
+ unsigned int slots[RGAME_LAYER_BANDS];
109
+
68
110
  /* Which stack each outstanding push went to, so pop can undo the right one. */
69
111
  unsigned char pushes[RGAME_CANVAS_STACK_DEPTH];
70
112
  int push_depth;
@@ -79,8 +121,8 @@ void rgame_canvas_init(rgame_canvas *canvas);
79
121
  void rgame_canvas_destroy(rgame_canvas *canvas);
80
122
 
81
123
  /*
82
- * Starts a frame: empties the queue (keeping its buffers), resets both stacks,
83
- * and sets the clip base to the window. Re-stating the size every frame is also
124
+ * Starts a frame: empties the queue (keeping its buffers), resets all three
125
+ * stacks and the slot counters, and sets the clip base to the window. Re-stating the size every frame is also
84
126
  * how a resize takes effect, so there is no separate path to forget.
85
127
  */
86
128
  void rgame_canvas_begin_frame(rgame_canvas *canvas, int width, int height);
@@ -97,6 +139,19 @@ void rgame_canvas_push_scale(rgame_canvas *canvas, float sx, float sy);
97
139
  * transform before narrowing the clip. */
98
140
  void rgame_canvas_push_clip(rgame_canvas *canvas, rgame_rect rect);
99
141
 
142
+ /* Sets the base every subsequent z is measured from, until the matching pop. */
143
+ void rgame_canvas_push_layer(rgame_canvas *canvas, double base);
144
+
145
+ /* The base currently in effect. */
146
+ double rgame_canvas_layer(const rgame_canvas *canvas);
147
+
148
+ /*
149
+ * The next slot index in `band`, counting from 0 each frame. Out-of-range bands
150
+ * answer 0 rather than reading past the array — a caller with a bad band gets
151
+ * everything piled in one slot, which is wrong but bounded.
152
+ */
153
+ unsigned int rgame_canvas_next_slot(rgame_canvas *canvas, int band);
154
+
100
155
  void rgame_canvas_pop(rgame_canvas *canvas);
101
156
 
102
157
  /*