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
@@ -1,5 +1,7 @@
1
1
  #include "graphics/clip.h"
2
2
 
3
+ #include <stdint.h>
4
+
3
5
  /* The canonical empty rect. Every path that produces "nothing" produces this
4
6
  * exact value, so two empties compare equal and cannot split a batch. */
5
7
  static const rgame_rect RGAME_EMPTY_RECT = { 0, 0, 0, 0 };
@@ -30,20 +32,27 @@ rgame_rect rgame_rect_intersect(rgame_rect a, rgame_rect b) {
30
32
  }
31
33
 
32
34
  /*
33
- * Edges are computed in `long` rather than `int`. A caller can legitimately
34
- * pass a very large width to mean "as wide as possible", and x + w would
35
- * then overflow — which is undefined behaviour, not merely a wrong answer.
36
- * The result always fits back in an int because it is bounded by the
37
- * smaller of the two inputs.
35
+ * Edges are computed in `int64_t` rather than `int`. A caller can
36
+ * legitimately pass a very large width to mean "as wide as possible", and
37
+ * x + w would then overflow — which is undefined behaviour, not merely a
38
+ * wrong answer. The result always fits back in an int because it is
39
+ * bounded by the smaller of the two inputs.
40
+ *
41
+ * `int64_t` rather than `long`: `long` is only 32 bits on Windows (LLP64),
42
+ * the same width as `int` there, so it does not actually buy the extra
43
+ * headroom this needs — found by UBSan under clang64, which is the only
44
+ * configuration that caught it (the intermediate overflow this guards
45
+ * against never occurs in Check's own test values on a 64-bit `long`
46
+ * platform, so plain Linux/macOS runs never exercise the bug either).
38
47
  */
39
- long left = a.x > b.x ? a.x : b.x;
40
- long top = a.y > b.y ? a.y : b.y;
41
- long a_right = (long)a.x + a.w;
42
- long b_right = (long)b.x + b.w;
43
- long a_bottom = (long)a.y + a.h;
44
- long b_bottom = (long)b.y + b.h;
45
- long right = a_right < b_right ? a_right : b_right;
46
- long bottom = a_bottom < b_bottom ? a_bottom : b_bottom;
48
+ int64_t left = a.x > b.x ? a.x : b.x;
49
+ int64_t top = a.y > b.y ? a.y : b.y;
50
+ int64_t a_right = (int64_t)a.x + a.w;
51
+ int64_t b_right = (int64_t)b.x + b.w;
52
+ int64_t a_bottom = (int64_t)a.y + a.h;
53
+ int64_t b_bottom = (int64_t)b.y + b.h;
54
+ int64_t right = a_right < b_right ? a_right : b_right;
55
+ int64_t bottom = a_bottom < b_bottom ? a_bottom : b_bottom;
47
56
 
48
57
  if (right <= left || bottom <= top) {
49
58
  return RGAME_EMPTY_RECT;
@@ -52,16 +52,93 @@ void rgame_app_destroy(rgame_app *app);
52
52
  #define RGAME_BUTTON_GAMEPAD_FIRST 0x1000
53
53
  #define RGAME_BUTTON_GAMEPAD_LAST 0x10FF
54
54
 
55
- /* Keyboard ids == SDL scancodes. Only the keys the engine actually binds are
56
- * named; adding one is a #define plus a _Static_assert in app.c. */
55
+ /* Keyboard ids == SDL scancodes, which are physical *positions* rather than
56
+ * letters: RGAME_KEY_A is the key marked A on a QWERTY board and Q on AZERTY.
57
+ * The set is what a Western keyboard can be relied on to have. No numpad (most
58
+ * laptops have none), no GUI/Windows/Command key, no print-screen cluster, and
59
+ * nothing whose position depends on the layout. Those can be added when
60
+ * something needs them; each is a #define plus a _Static_assert in app.c. */
61
+ #define RGAME_KEY_A 4
62
+ #define RGAME_KEY_B 5
63
+ #define RGAME_KEY_C 6
64
+ #define RGAME_KEY_D 7
65
+ #define RGAME_KEY_E 8
66
+ #define RGAME_KEY_F 9
67
+ #define RGAME_KEY_G 10
68
+ #define RGAME_KEY_H 11
69
+ #define RGAME_KEY_I 12
70
+ #define RGAME_KEY_J 13
71
+ #define RGAME_KEY_K 14
72
+ #define RGAME_KEY_L 15
73
+ #define RGAME_KEY_M 16
74
+ #define RGAME_KEY_N 17
75
+ #define RGAME_KEY_O 18
76
+ #define RGAME_KEY_P 19
77
+ #define RGAME_KEY_Q 20
78
+ #define RGAME_KEY_R 21
79
+ #define RGAME_KEY_S 22
80
+ #define RGAME_KEY_T 23
81
+ #define RGAME_KEY_U 24
82
+ #define RGAME_KEY_V 25
83
+ #define RGAME_KEY_W 26
84
+ #define RGAME_KEY_X 27
85
+ #define RGAME_KEY_Y 28
86
+ #define RGAME_KEY_Z 29
87
+ #define RGAME_KEY_1 30
88
+ #define RGAME_KEY_2 31
89
+ #define RGAME_KEY_3 32
90
+ #define RGAME_KEY_4 33
91
+ #define RGAME_KEY_5 34
92
+ #define RGAME_KEY_6 35
93
+ #define RGAME_KEY_7 36
94
+ #define RGAME_KEY_8 37
95
+ #define RGAME_KEY_9 38
96
+ #define RGAME_KEY_0 39
57
97
  #define RGAME_KEY_RETURN 40
58
98
  #define RGAME_KEY_ESCAPE 41
99
+ #define RGAME_KEY_BACKSPACE 42
100
+ #define RGAME_KEY_TAB 43
59
101
  #define RGAME_KEY_SPACE 44
102
+ #define RGAME_KEY_MINUS 45
103
+ #define RGAME_KEY_EQUALS 46
104
+ #define RGAME_KEY_LEFTBRACKET 47
105
+ #define RGAME_KEY_RIGHTBRACKET 48
106
+ #define RGAME_KEY_BACKSLASH 49
107
+ #define RGAME_KEY_SEMICOLON 51
108
+ #define RGAME_KEY_APOSTROPHE 52
109
+ #define RGAME_KEY_GRAVE 53
110
+ #define RGAME_KEY_COMMA 54
111
+ #define RGAME_KEY_PERIOD 55
112
+ #define RGAME_KEY_SLASH 56
113
+ #define RGAME_KEY_CAPSLOCK 57
60
114
  #define RGAME_KEY_F1 58
115
+ #define RGAME_KEY_F2 59
116
+ #define RGAME_KEY_F3 60
117
+ #define RGAME_KEY_F4 61
118
+ #define RGAME_KEY_F5 62
119
+ #define RGAME_KEY_F6 63
120
+ #define RGAME_KEY_F7 64
121
+ #define RGAME_KEY_F8 65
122
+ #define RGAME_KEY_F9 66
123
+ #define RGAME_KEY_F10 67
124
+ #define RGAME_KEY_F11 68
125
+ #define RGAME_KEY_F12 69
126
+ #define RGAME_KEY_INSERT 73
127
+ #define RGAME_KEY_HOME 74
128
+ #define RGAME_KEY_PAGEUP 75
129
+ #define RGAME_KEY_DELETE 76
130
+ #define RGAME_KEY_END 77
131
+ #define RGAME_KEY_PAGEDOWN 78
61
132
  #define RGAME_KEY_RIGHT 79
62
133
  #define RGAME_KEY_LEFT 80
63
134
  #define RGAME_KEY_DOWN 81
64
135
  #define RGAME_KEY_UP 82
136
+ #define RGAME_KEY_LCTRL 224
137
+ #define RGAME_KEY_LSHIFT 225
138
+ #define RGAME_KEY_LALT 226
139
+ #define RGAME_KEY_RCTRL 228
140
+ #define RGAME_KEY_RSHIFT 229
141
+ #define RGAME_KEY_RALT 230
65
142
 
66
143
  /* Gamepad button ids: the gamepad range plus SDL's controller button number.
67
144
  * app.c asserts each against the SDL constant it mirrors, as with the keys. */
@@ -80,6 +157,12 @@ void rgame_app_destroy(rgame_app *app);
80
157
  #define RGAME_PAD_DPAD_DOWN (RGAME_BUTTON_GAMEPAD_FIRST + 12)
81
158
  #define RGAME_PAD_DPAD_LEFT (RGAME_BUTTON_GAMEPAD_FIRST + 13)
82
159
  #define RGAME_PAD_DPAD_RIGHT (RGAME_BUTTON_GAMEPAD_FIRST + 14)
160
+ #define RGAME_PAD_MISC1 (RGAME_BUTTON_GAMEPAD_FIRST + 15)
161
+ #define RGAME_PAD_PADDLE1 (RGAME_BUTTON_GAMEPAD_FIRST + 16)
162
+ #define RGAME_PAD_PADDLE2 (RGAME_BUTTON_GAMEPAD_FIRST + 17)
163
+ #define RGAME_PAD_PADDLE3 (RGAME_BUTTON_GAMEPAD_FIRST + 18)
164
+ #define RGAME_PAD_PADDLE4 (RGAME_BUTTON_GAMEPAD_FIRST + 19)
165
+ #define RGAME_PAD_TOUCHPAD (RGAME_BUTTON_GAMEPAD_FIRST + 20)
83
166
 
84
167
  /*
85
168
  * Analog axes are their own small id space rather than part of the button
@@ -121,6 +204,7 @@ void rgame_app_destroy(rgame_app *app);
121
204
  typedef void (*rgame_frame_begin_fn)(void *userdata);
122
205
  typedef void (*rgame_update_fn)(void *userdata, double dt_seconds);
123
206
  typedef void (*rgame_draw_fn)(void *userdata);
207
+ typedef void (*rgame_frame_end_fn)(void *userdata);
124
208
  typedef int (*rgame_needs_redraw_fn)(void *userdata);
125
209
  typedef void (*rgame_button_fn)(void *userdata, int button_id);
126
210
  typedef void (*rgame_resize_fn)(void *userdata, int width, int height);
@@ -141,6 +225,12 @@ typedef void (*rgame_gamepad_fn)(void *userdata, int slot);
141
225
  * - `needs_redraw` is polled before drawing; returning 0 skips the draw for
142
226
  * that frame (simulation still advances). NULL means always redraw.
143
227
  * - `draw` renders one frame.
228
+ * - `frame_end` runs once per rendered frame, after that frame's drawing has
229
+ * been submitted to the GPU but *before* the buffer swap — the only point
230
+ * at which the frame just drawn can be read back deterministically. Never
231
+ * called when `needs_redraw` skipped the draw. Rarely needed: it exists for
232
+ * reading pixels back for a test, not for game code, since a real driver is
233
+ * free to swap however it likes once `draw` returns.
144
234
  * - `button_down`/`button_up` report discrete key presses and releases. Key
145
235
  * repeats are filtered out, so holding a key reports exactly one press.
146
236
  * - `resize` reports a new window size; the GL viewport is already updated.
@@ -154,6 +244,7 @@ typedef struct {
154
244
  rgame_update_fn update;
155
245
  rgame_needs_redraw_fn needs_redraw;
156
246
  rgame_draw_fn draw;
247
+ rgame_frame_end_fn frame_end;
157
248
  rgame_button_fn button_down;
158
249
  rgame_button_fn button_up;
159
250
  rgame_resize_fn resize;
@@ -360,7 +451,7 @@ int rgame_app_draw_image_rot(rgame_app *app, const rgame_image *image, float cx,
360
451
  float angle_degrees, float scale, unsigned int color, double z);
361
452
 
362
453
  /*
363
- * The transform and clip stacks. Every push is undone by the same
454
+ * The transform, clip and layer stacks. Every push is undone by the same
364
455
  * `rgame_app_pop`, so a caller can never pop the wrong one; a push that cannot
365
456
  * be honoured (a full stack) is still counted, so pops stay balanced and the
366
457
  * drawing comes out untransformed rather than desynchronised.
@@ -378,6 +469,25 @@ void rgame_app_push_scale(rgame_app *app, float sx, float sy);
378
469
  * always succeed as far as the caller is concerned.
379
470
  */
380
471
  int rgame_app_push_clip(rgame_app *app, int x, int y, int width, int height);
472
+
473
+ /*
474
+ * The layer stack: what every subsequent `z` is measured from, until the
475
+ * matching pop. It *replaces* rather than accumulates, and starts at 0, so a
476
+ * caller that never pushes one gets exactly the z it passes.
477
+ *
478
+ * `rgame_app_next_layer_slot` is the other half: one counter per band, reset at
479
+ * the start of each frame, handing out increasing indices. It counts and
480
+ * nothing more — what a band means, and how a slot index becomes a base, is
481
+ * decided a layer up (RGame::Util::Z). Bands outside 0..7 answer 0.
482
+ *
483
+ * Together they are how a scene graph gives each node its own narrow window of
484
+ * z values: the traversal takes the next slot as it reaches a node, so draw
485
+ * order is tree order, and a node's `z:` can only reorder its own drawing.
486
+ */
487
+ void rgame_app_push_layer(rgame_app *app, double base);
488
+ double rgame_app_layer(rgame_app *app);
489
+ unsigned int rgame_app_next_layer_slot(rgame_app *app, int band);
490
+
381
491
  void rgame_app_pop(rgame_app *app);
382
492
 
383
493
  /*
@@ -11,15 +11,69 @@ _Static_assert(RGAME_GAMEPAD_AXIS_COUNT == SDL_CONTROLLER_AXIS_MAX,
11
11
 
12
12
  /* Gamepad button ids are the gamepad range plus SDL's own button number, so
13
13
  * each named id must line up with the SDL constant it mirrors. */
14
- _Static_assert(RGAME_PAD_A == RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_A,
14
+ _Static_assert(RGAME_PAD_A ==
15
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_A,
15
16
  "pad id must match SDL controller button");
16
- _Static_assert(RGAME_PAD_START == RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_START,
17
+ _Static_assert(RGAME_PAD_B ==
18
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_B,
17
19
  "pad id must match SDL controller button");
18
- _Static_assert(RGAME_PAD_DPAD_UP == RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_DPAD_UP,
20
+ _Static_assert(RGAME_PAD_X ==
21
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_X,
22
+ "pad id must match SDL controller button");
23
+ _Static_assert(RGAME_PAD_Y ==
24
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_Y,
25
+ "pad id must match SDL controller button");
26
+ _Static_assert(RGAME_PAD_BACK ==
27
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_BACK,
28
+ "pad id must match SDL controller button");
29
+ _Static_assert(RGAME_PAD_GUIDE ==
30
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_GUIDE,
31
+ "pad id must match SDL controller button");
32
+ _Static_assert(RGAME_PAD_START ==
33
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_START,
34
+ "pad id must match SDL controller button");
35
+ _Static_assert(RGAME_PAD_LEFT_STICK ==
36
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_LEFTSTICK,
37
+ "pad id must match SDL controller button");
38
+ _Static_assert(RGAME_PAD_RIGHT_STICK ==
39
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_RIGHTSTICK,
40
+ "pad id must match SDL controller button");
41
+ _Static_assert(RGAME_PAD_LEFT_SHOULDER ==
42
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
43
+ "pad id must match SDL controller button");
44
+ _Static_assert(RGAME_PAD_RIGHT_SHOULDER ==
45
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
46
+ "pad id must match SDL controller button");
47
+ _Static_assert(RGAME_PAD_DPAD_UP ==
48
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_DPAD_UP,
49
+ "pad id must match SDL controller button");
50
+ _Static_assert(RGAME_PAD_DPAD_DOWN ==
51
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_DPAD_DOWN,
52
+ "pad id must match SDL controller button");
53
+ _Static_assert(RGAME_PAD_DPAD_LEFT ==
54
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_DPAD_LEFT,
19
55
  "pad id must match SDL controller button");
20
56
  _Static_assert(RGAME_PAD_DPAD_RIGHT ==
21
57
  RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_DPAD_RIGHT,
22
58
  "pad id must match SDL controller button");
59
+ _Static_assert(RGAME_PAD_MISC1 ==
60
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_MISC1,
61
+ "pad id must match SDL controller button");
62
+ _Static_assert(RGAME_PAD_PADDLE1 ==
63
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_PADDLE1,
64
+ "pad id must match SDL controller button");
65
+ _Static_assert(RGAME_PAD_PADDLE2 ==
66
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_PADDLE2,
67
+ "pad id must match SDL controller button");
68
+ _Static_assert(RGAME_PAD_PADDLE3 ==
69
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_PADDLE3,
70
+ "pad id must match SDL controller button");
71
+ _Static_assert(RGAME_PAD_PADDLE4 ==
72
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_PADDLE4,
73
+ "pad id must match SDL controller button");
74
+ _Static_assert(RGAME_PAD_TOUCHPAD ==
75
+ RGAME_BUTTON_GAMEPAD_FIRST + SDL_CONTROLLER_BUTTON_TOUCHPAD,
76
+ "pad id must match SDL controller button");
23
77
  _Static_assert(RGAME_AXIS_LEFT_X == SDL_CONTROLLER_AXIS_LEFTX, "axis id must match SDL axis");
24
78
  _Static_assert(RGAME_AXIS_TRIGGER_RIGHT == SDL_CONTROLLER_AXIS_TRIGGERRIGHT,
25
79
  "axis id must match SDL axis");
@@ -23,6 +23,9 @@
23
23
  * def button_down(id); end # discrete key press
24
24
  * def button_up(id); end
25
25
  * def frame_begin; end # once per frame, before the tick batch
26
+ * def frame_end; end # after draw is submitted, before the swap —
27
+ * # rarely needed; it exists for reading pixels
28
+ * # back in a test, not for game code
26
29
  * def resize(w, h); end
27
30
  * def gamepad_connected(slot); end
28
31
  * def gamepad_disconnected(slot); end
@@ -47,6 +50,7 @@
47
50
  static ID id_frame_begin;
48
51
  static ID id_update;
49
52
  static ID id_draw;
53
+ static ID id_frame_end;
50
54
  static ID id_needs_redraw;
51
55
  static ID id_button_down;
52
56
  static ID id_button_up;
@@ -265,6 +269,10 @@ static void tramp_draw(void *userdata) {
265
269
  protected_call((run_state *)userdata, id_draw, 0, NULL);
266
270
  }
267
271
 
272
+ static void tramp_frame_end(void *userdata) {
273
+ protected_call((run_state *)userdata, id_frame_end, 0, NULL);
274
+ }
275
+
268
276
  static int tramp_needs_redraw(void *userdata) {
269
277
  VALUE result = protected_call((run_state *)userdata, id_needs_redraw, 0, NULL);
270
278
  return RTEST(result) ? 1 : 0;
@@ -308,6 +316,7 @@ static VALUE app_run(VALUE self) {
308
316
  .update = tramp_update,
309
317
  .needs_redraw = tramp_needs_redraw,
310
318
  .draw = tramp_draw,
319
+ .frame_end = tramp_frame_end,
311
320
  .button_down = tramp_button_down,
312
321
  .button_up = tramp_button_up,
313
322
  .resize = tramp_resize,
@@ -421,6 +430,11 @@ static VALUE app_default_draw(VALUE self) {
421
430
  return Qnil;
422
431
  }
423
432
 
433
+ static VALUE app_default_frame_end(VALUE self) {
434
+ (void)self;
435
+ return Qnil;
436
+ }
437
+
424
438
  /* Default is "always redraw", matching a NULL needs_redraw in the C API. */
425
439
  static VALUE app_default_needs_redraw(VALUE self) {
426
440
  (void)self;
@@ -457,6 +471,7 @@ void Init_core_ext(void) {
457
471
  id_frame_begin = rb_intern("frame_begin");
458
472
  id_update = rb_intern("update");
459
473
  id_draw = rb_intern("draw");
474
+ id_frame_end = rb_intern("frame_end");
460
475
  id_needs_redraw = rb_intern("needs_redraw?");
461
476
  id_button_down = rb_intern("button_down");
462
477
  id_button_up = rb_intern("button_up");
@@ -498,6 +513,7 @@ void Init_core_ext(void) {
498
513
  rb_define_method(cApp, "frame_begin", app_default_frame_begin, 0);
499
514
  rb_define_method(cApp, "update", app_default_update, 1);
500
515
  rb_define_method(cApp, "draw", app_default_draw, 0);
516
+ rb_define_method(cApp, "frame_end", app_default_frame_end, 0);
501
517
  rb_define_method(cApp, "needs_redraw?", app_default_needs_redraw, 0);
502
518
  rb_define_method(cApp, "button_down", app_default_button, 1);
503
519
  rb_define_method(cApp, "button_up", app_default_button, 1);
@@ -297,6 +297,26 @@ static VALUE renderer_push_clip(VALUE self, VALUE x, VALUE y, VALUE width, VALUE
297
297
  return self;
298
298
  }
299
299
 
300
+ /*
301
+ * #push_layer(base) / #layer — the base every subsequent z is measured from.
302
+ *
303
+ * `base` is a whole number well inside a double's exact range (see
304
+ * RGame::Util::Z), so it crosses as a double and comes back as a Float.
305
+ */
306
+ static VALUE renderer_push_layer(VALUE self, VALUE base) {
307
+ rgame_app_push_layer(drawing_app(self), NUM2DBL(base));
308
+ return self;
309
+ }
310
+
311
+ static VALUE renderer_layer(VALUE self) {
312
+ return DBL2NUM(rgame_app_layer(drawing_app(self)));
313
+ }
314
+
315
+ /* #next_layer_slot(band) -> the next slot index in that band this frame. */
316
+ static VALUE renderer_next_layer_slot(VALUE self, VALUE band) {
317
+ return UINT2NUM(rgame_app_next_layer_slot(drawing_app(self), NUM2INT(band)));
318
+ }
319
+
300
320
  /* ------------------------------------------------------------------------- *
301
321
  * Recording
302
322
  *
@@ -368,6 +388,9 @@ void rgame_init_renderer(VALUE mCore) {
368
388
  rb_define_method(cRenderer, "push_rotate", renderer_push_rotate, 3);
369
389
  rb_define_method(cRenderer, "push_scale", renderer_push_scale, 2);
370
390
  rb_define_method(cRenderer, "push_clip", renderer_push_clip, 4);
391
+ rb_define_method(cRenderer, "push_layer", renderer_push_layer, 1);
392
+ rb_define_method(cRenderer, "layer", renderer_layer, 0);
393
+ rb_define_method(cRenderer, "next_layer_slot", renderer_next_layer_slot, 1);
371
394
  rb_define_method(cRenderer, "pop", renderer_pop, 0);
372
395
 
373
396
  rb_define_method(cRenderer, "begin_record", renderer_begin_record, 0);
@@ -63,10 +63,19 @@ static VALUE color_s_new(int argc, VALUE *argv, VALUE klass) {
63
63
  NIL_P(a) ? 255 : component(a, "alpha")));
64
64
  }
65
65
 
66
- /* Color.from_packed(0xRRGGBBAA) */
66
+ /* Color.from_packed(0xRRGGBBAA)
67
+ *
68
+ * The bounds check below needs a C type wider than 32 bits so a
69
+ * one-bit-too-wide value can be caught rather than silently truncated.
70
+ * `unsigned long` is that on Linux/macOS (LP64: 64 bits) but not on Windows
71
+ * (LLP64: `long` stays 32 bits even in a 64-bit build), where NUM2ULONG would
72
+ * itself raise RangeError on the very value this function means to turn into
73
+ * a clean ArgumentError. NUM2ULL is unsigned long long, 64 bits on every
74
+ * platform this project builds for, so the check below runs before either
75
+ * platform's integer width can get in the way. */
67
76
  static VALUE color_s_from_packed(VALUE klass, VALUE packed) {
68
- unsigned long value = NUM2ULONG(packed);
69
- if (value > 0xFFFFFFFFul) {
77
+ unsigned long long value = NUM2ULL(packed);
78
+ if (value > 0xFFFFFFFFull) {
70
79
  rb_raise(rb_eArgError, "packed colour must fit in 32 bits");
71
80
  }
72
81
  return color_wrap(klass, (rgame_color)value);
@@ -16,6 +16,8 @@
16
16
  # # fixed step, never wall-clock frame time
17
17
  # def needs_redraw?; end # false skips the draw (simulation still runs)
18
18
  # def draw; end # render one frame
19
+ # def frame_end; end # after draw is submitted, before the swap —
20
+ # # rarely needed outside a test that reads pixels
19
21
  # def button_down(id); end # discrete key press (no repeats)
20
22
  # def button_up(id); end
21
23
  # def resize(w, h); end
@@ -5,72 +5,66 @@ require_relative '../util/controls'
5
5
 
6
6
  module RGame
7
7
  module Core
8
- # Translates the game's symbolic actions (:fire, :confirm) into the physical
9
- # button ids the engine understands, and asks the app whether they're held.
8
+ # Asks the engine whether a physical input is active, on a given device.
10
9
  #
11
10
  # input = RGame::Core::Input.new(app)
12
- # input.down?(:fire) # keyboard, the single-player default
13
- # input.down?(:fire, device: 1) # the pad in player slot 0
14
- # input.axis(:move_x, device: 1) # => Float, -1.0..1.0
11
+ # input.down?(Controls::KEY_SPACE) # keyboard
12
+ # input.down?(Controls::PAD_A, device: Controls.gamepad(0)) # player 1's pad
13
+ # input.axis(Controls::AXIS_LEFT_X, device: Controls.gamepad(0))
15
14
  #
16
- # The id vocabulary itself lives in RGame::Util::Controls, not here: ids are
17
- # values, and the engine layer must be able to name one without touching
18
- # Core. That is also what makes rebinding possible a game builds its own
19
- # table from those constants and passes it in:
15
+ # **This is the raw query, and deliberately nothing more.** It used to carry
16
+ # binding tables of its own `down?(:fire)` resolved `:fire` to a scancode
17
+ # through one of three tables passed to the constructor. Those are gone, and
18
+ # binding now lives one layer up in RGame::Engine::InputMap, for two
19
+ # reasons. A game's rebinding screen has to be able to edit the table, and
20
+ # the engine layer may not name RGame::Core at all; and with a player per
21
+ # device, the table is a per-player value rather than a property of the one
22
+ # object that talks to the hardware.
20
23
  #
21
- # controls = RGame::Util::Controls
22
- # input = RGame::Core::Input.new(
23
- # app, bindings: controls::DEFAULT_KEYBOARD.merge(fire: controls::KEY_J)
24
- # )
24
+ # What is left is an argument-order adapter over the app's own
25
+ # `input_down?(device, id)` / `input_axis(device, axis_id)`. It stays a named
26
+ # class rather than collapsing into those, because it is what gets handed to
27
+ # the engine layer as an input backend — and handing it a whole App, which
28
+ # can also close the window and rename it, would be a worse seam.
25
29
  #
26
- # Devices are numbered keyboard-first: Controls::KEYBOARD is 0, and
27
- # Controls.gamepad(slot) is the pad in that player slot. Defaulting
28
- # `device:` to the keyboard is what lets single-player call sites stay
29
- # `input.down?(:fire)` with no ceremony.
30
+ # Ids come from RGame::Util::Controls. They are values with nothing behind
31
+ # them, so both layers can name one.
30
32
  #
31
33
  # **There is no pointer or mouse support, by design.** The layer this
32
34
  # replaced had a cursor position and a click button riding the same "is
33
35
  # held" path as keys, and none of it was carried over: this engine's input
34
- # is keyboard and controllers. A game wanting click-based UI has to build
35
- # hit-testing on top rather than find it here, and the intended answer for
36
- # menus is keyboard and controller navigation instead.
36
+ # is keyboard and controllers. The intended answer for menus is keyboard and
37
+ # controller navigation instead.
37
38
  class Input
38
39
  Controls = RGame::Util::Controls
39
40
 
40
- # `bindings` covers the keyboard, `pad_bindings` the controllers, and
41
- # `axis_bindings` the analog axes. All default to the standard tables.
42
- def initialize(app,
43
- bindings: Controls::DEFAULT_KEYBOARD,
44
- pad_bindings: Controls::DEFAULT_PAD,
45
- axis_bindings: Controls::DEFAULT_AXES)
41
+ def initialize(app)
46
42
  @app = app
47
- @bindings = bindings
48
- @pad_bindings = pad_bindings
49
- @axis_bindings = axis_bindings
50
43
  end
51
44
 
52
- # Is the action's physical button held on `device`?
45
+ # Is `id` held on `device`?
53
46
  #
54
47
  # Reads the engine's per-frame input snapshot, so the answer is identical
55
48
  # for every simulation tick within one frame — a key held for a single
56
49
  # frame behaves the same whether that frame ran one catch-up tick or five.
50
+ #
51
+ # A device only answers for its own kind of input: asking a gamepad about
52
+ # a keyboard scancode is `false`, never the keyboard's answer. That is
53
+ # what lets one binding table list a key and a pad button for the same
54
+ # action and still keep player two's pad from echoing player one.
57
55
  # hot-path
58
- def down?(action, device: Controls::KEYBOARD)
59
- @app.input_down?(device, bindings_for(device).fetch(action))
56
+ def down?(id, device: Controls::KEYBOARD)
57
+ @app.input_down?(device, id)
60
58
  end
61
59
 
62
60
  # Current value of an analog axis: sticks -1.0..1.0, triggers 0.0..1.0.
63
61
  # The keyboard has no axes, so it always reads 0.0.
62
+ #
63
+ # No dead zone is applied here — this is the hardware's answer. Ignoring
64
+ # a resting stick's jitter is RGame::Engine::ActionMapper's job.
64
65
  # hot-path
65
- def axis(action, device: Controls::KEYBOARD)
66
- @app.input_axis(device, @axis_bindings.fetch(action))
67
- end
68
-
69
- private
70
-
71
- # hot-path
72
- def bindings_for(device)
73
- device == Controls::KEYBOARD ? @bindings : @pad_bindings
66
+ def axis(axis_id, device: Controls::KEYBOARD)
67
+ @app.input_axis(device, axis_id)
74
68
  end
75
69
  end
76
70
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'rgame/core_ext'
4
4
  require_relative '../util/color'
5
+ require_relative '../util/z'
5
6
 
6
7
  module RGame
7
8
  module Core
@@ -37,6 +38,7 @@ module RGame
37
38
  # renderer.clipped(0, 0, 400, 600) { ground.draw(0, 0) }
38
39
  class Recording
39
40
  Color = RGame::Util::Color
41
+ Z = RGame::Util::Z
40
42
 
41
43
  # Replays everything that was recorded, with the recording's origin at
42
44
  # (x, y).
@@ -45,7 +47,7 @@ module RGame
45
47
  # so white (the default) draws the recording unchanged and a colour with
46
48
  # alpha fades the whole layer out at once.
47
49
  def draw(x = 0, y = 0, z: 0, color: nil)
48
- draw_at(x, y, z, Color.coerce(color).packed)
50
+ draw_at(x, y, Z.offset(z), Color.coerce(color).packed)
49
51
  end
50
52
  end
51
53
  end