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
@@ -10,7 +10,8 @@ read input from a per-frame snapshot, never naming a graphics library at all.
10
10
  `RGame::Engine::Node2D` (`engine/node2d`) is the basic building block. (The `2D` in the
11
11
  name leaves room for a future 3D node; today everything is 2D.) A node carries:
12
12
 
13
- - a **transform** — relative `x`, `y`, `z` plus `width`/`height`;
13
+ - a **transform** — relative `x`, `y`, `angle` plus `width`/`height`, and a `z`
14
+ that orders it among its siblings;
14
15
  - **children** — other nodes nested under it (`add_node`);
15
16
  - **components** — reusable pieces of behaviour attached to it (`add_component`);
16
17
  - a **parent** — the node it hangs off (set automatically when it is added).
@@ -25,7 +26,8 @@ A node is driven in three phases, run in this order every frame:
25
26
  1. `control(actions)` — read intent, both from the player (the `actions`
26
27
  snapshot) and from AI/scripted controllers.
27
28
  2. `update(dt)` — advance game logic and physics over the timestep `dt`.
28
- 3. `draw(renderer)` — render the current visual state.
29
+ 3. `draw(renderer, view)` — render the current visual state into `view`, the
30
+ viewport being drawn.
29
31
 
30
32
  Each phase **settles the node itself first — its components, then its own hook —
31
33
  and only then descends into the children**. So you override the hook, not the
@@ -33,7 +35,14 @@ phase itself:
33
35
 
34
36
  - `on_control(actions)`
35
37
  - `on_update(dt)`
36
- - `on_draw(renderer)`
38
+ - `on_draw(renderer, view)`
39
+
40
+ `view` is the viewport this node is being drawn into — its rectangle, and the camera (if
41
+ any) it is seen through. Most nodes ignore it and just draw. Two things need it: laying
42
+ out against the edges of *this* region rather than the whole window
43
+ (`view.x`, `view.width`), and culling (`view.visible?(x, y, w, h)`), which stops being an
44
+ optimisation once the world is drawn once per player. See
45
+ [Viewports](#viewports-and-views).
37
46
 
38
47
  Self-before-subtree keeps the transform flowing downward: a component or hook
39
48
  that moves the node does so before its children resolve their origin from it (see
@@ -44,12 +53,81 @@ iteration** — add children with `add_node` and let the tree drive them.
44
53
 
45
54
  ### Absolute position
46
55
 
47
- `x`/`y`/`z` are **relative to the parent**. At the start of each phase a node
56
+ `x`/`y` are **relative to the parent**. At the start of each phase a node
48
57
  resolves its absolute position by accumulating onto the parent's origin
49
- (`abs_x = parent.abs_x + x`, and likewise for `y`/`z`); a node with no parent
50
- sits at the origin. Moving or re-layering a node therefore moves and re-layers
51
- its whole subtree. (Rotation, dirty-flag caching and smarter `z`/depth handling
52
- are noted as future work in the source.)
58
+ (`abs_x = parent.abs_x + x`, and likewise for `y`, with the parent's rotation
59
+ applied); a node with no parent sits at the origin. Moving a node therefore
60
+ moves its whole subtree. (Dirty-flag caching is noted as future work in the
61
+ source.)
62
+
63
+ **`z` is not among them, and there is no `abs_z`.** Depth is decided by where
64
+ the traversal reaches a node, not by summing what its ancestors picked — see
65
+ "Draw order" below.
66
+
67
+ ### Draw order
68
+
69
+ A node's `z` says where it sits among its **siblings**, and nowhere else. The
70
+ tree is drawn depth-first with siblings in `z` order, so:
71
+
72
+ ```ruby
73
+ sky.add_node(Clouds.new(z: 2))
74
+ sky.add_node(Birds.new(z: 1))
75
+ sky.add_node(People.new(z: 0))
76
+ ```
77
+
78
+ draws people, then birds, then clouds. Each of them may be built out of as many
79
+ child nodes as it likes: **a subtree is atomic**, so no part of `clouds` can end
80
+ up behind `birds`, and no part of `birds` in front of `clouds`.
81
+
82
+ Only the comparison matters. `z` is never added to anything and never reaches
83
+ the renderer, so its magnitude means nothing — `1` and `1_000_000` behave
84
+ identically if they are the only two children — and negatives are ordinary.
85
+ Equal `z` keeps the order the nodes were added in.
86
+
87
+ A **band** overrules all of it. `band:` is `:world` (the default), `:hud`,
88
+ `:overlay` or `:debug`, and it is inherited down the tree like `input_owner`:
89
+
90
+ ```ruby
91
+ scene.add_node(RGame::Engine::PlayerLayer.new(player: player)) # :hud
92
+ scene.add_node(Cutscene.new(band: :overlay))
93
+ ```
94
+
95
+ Everything in `:world` draws under everything in `:hud`, whatever either asked
96
+ for, and nothing a node passes as `z:` can cross the gap. `WorldView` declares
97
+ `:world` and `PlayerLayer` declares `:hud`, so most games never name a band at
98
+ all; a node that must escape the band it inherits says so with `band:`, which is
99
+ the one way out and is explicit.
100
+
101
+ The engine turns all of this into the single number the renderer sorts on:
102
+ `Node2D#draw` opens a layer per node, taking the next slot in its band. See
103
+ [Drawing](drawing.md#draw-order) and `RGame::Util::Z`.
104
+
105
+ ### Who a node answers to
106
+
107
+ `control` is handed an input **source**, not one player's snapshot — a
108
+ [`RGame::Engine::Players`](input.md) registry, or a bare `Actions` when there is
109
+ only ever one answer. Each node asks the source for the actions of whichever
110
+ player owns it, and hands its components and its own `on_control` that plain
111
+ `Actions`.
112
+
113
+ Ownership is `input_owner`, and it is **inherited down the tree exactly like the
114
+ transform**, resolved onto `abs_input_owner` alongside `abs_x`/`abs_y`:
115
+
116
+ ```ruby
117
+ ship.input_owner = game.players[1] # the ship and everything under it
118
+ ```
119
+
120
+ A node that names nobody inherits its parent's; a tree that names nobody
121
+ anywhere reads the primary player. That is what keeps single-player free of
122
+ ceremony — no game that has one player ever mentions this.
123
+
124
+ Because the *source* descends rather than the resolved snapshot, two subtrees in
125
+ one traversal can read two different controllers, while a component still sees
126
+ the `control(actions)` it always did.
127
+
128
+ > It is `input_owner` rather than `player` because `@player` is what a game's own
129
+ > scene usually calls its hero node, and rather than `controller` because
130
+ > `Actor#controller` already means the thing producing movement intent.
53
131
 
54
132
  ### View transforms and the camera
55
133
 
@@ -58,13 +136,138 @@ maps that world onto the screen (a camera), and it must wrap a whole subtree's d
58
136
  without being baked into any node's position. So `draw` calls a `draw_children` step a
59
137
  subclass can override to wrap the subtree in a renderer transform.
60
138
 
61
- `RGame::Engine::CameraView` is that subclass: built with an `RGame::Engine::Camera`, it wraps its
62
- children's draw in `renderer.translated(-camera.x, -camera.y)`. Its children draw at
63
- their own world origin (they never know about the camera); the translate maps them to
64
- the screen. Because the offset is a draw-time transform rather than a node position, the
65
- same world can later be drawn through several cameras — split-screen is repeating the
66
- pass under different offsets/clips. The owning scene drives the camera (e.g. centring it
67
- on the player); `CameraView` only applies it. See `examples/15_tiled_world`.
139
+ ### Two words that are easy to confuse
140
+
141
+ **Space** is structural and the tree enforces it: a node is either inside a
142
+ `WorldView` or it is not, and that decides what its coordinates mean and how
143
+ many times it is drawn.
144
+
145
+ **Band** is an ordering partition: `:world`, `:hud`, `:overlay`, `:debug`. It is
146
+ structural too — inherited down the tree, declared by `WorldView` and
147
+ `PlayerLayer` — but it decides *what covers what* rather than what coordinates
148
+ mean. See [Drawing](drawing.md#draw-order).
149
+
150
+ They are not the same partition. All screen-space content is one *space* and is
151
+ drawn once; the bands subdivide it by what should cover what.
152
+
153
+ `RGame::Engine::WorldView` is that subclass, and it is where **world space begins**.
154
+ Its children draw at their own world origin and never know about a camera; the node
155
+ draws them **once per active viewport**, clipping to that viewport's rectangle and
156
+ translating by its camera:
157
+
158
+ ```ruby
159
+ view = scene.add_node(RGame::Engine::WorldView.new)
160
+ view.add_node(player) # world coordinates
161
+ ```
162
+
163
+ Everything *outside* a `WorldView` is screen space and draws once. That one distinction
164
+ is what separates a HUD from the world, and where it goes is the game's choice — nothing
165
+ is imposed above the game's own root.
166
+
167
+ A `WorldView` takes no camera. Cameras belong to players
168
+ (`RGame::Engine::Player#camera`), and the node asks
169
+ `node.system(RGame::Engine::Viewports)` which viewports exist, so the same subtree serves
170
+ one player or four with nothing below it changing. A camera owned by a node *inside* the
171
+ world could not do that — it would force the world to know how many times it is drawn.
172
+
173
+ **Only `draw` multiplies.** `control` and `update` still run once per node per tick
174
+ however many players are watching, which is what keeps simulation cost independent of
175
+ player count — and what makes the standing "draw renders state" rule load-bearing rather
176
+ than stylistic: a `draw` with a side effect now runs once per player.
177
+
178
+ See `examples/15_tiled_world`.
179
+
180
+ ## Viewports and views
181
+
182
+ `RGame::Engine::Viewports` is a root-scoped system holding how the screen is divided;
183
+ `RGame::Engine::Layout` is the pure arithmetic behind it, and a `RGame::Engine::View` is
184
+ one viewport being drawn.
185
+
186
+ ```ruby
187
+ viewports = node.system(RGame::Engine::Viewports)
188
+ viewports.views # one View per active player — what a WorldView draws through
189
+ viewports.screen # the whole window, no camera — screen space
190
+ viewports.screen_for(player) # that player's own region, no camera — their HUD and menus
191
+ ```
192
+
193
+ `screen_for` is the same rectangle that player's world view is drawn into, so a
194
+ HUD laid out at (10, 10) lands ten pixels inside the region the world beneath it
195
+ occupies. It is **nil** when they have nowhere to draw: an empty seat has no
196
+ viewport, and while the split is collapsed nobody owns a half of the screen —
197
+ a cutscene is everyone looking at one thing, so something that must stay on
198
+ screen through it belongs in the global overlay band instead.
199
+
200
+ A **`View`** carries `x`, `y`, `width`, `height`, its `camera` (nil in screen
201
+ space) and its `player`, plus two things nodes actually use:
202
+
203
+ | | |
204
+ |---|---|
205
+ | `view.visible?(x, y, w, h)` | is this worth drawing at all |
206
+ | `view.offset_x` / `offset_y` | the translate that maps its contents onto the screen |
207
+
208
+ **Views are reused, not rebuilt.** `Viewports` mutates one per viewport each frame, the
209
+ way `ActionMapper` reuses its `Actions` — building fresh ones would allocate every frame.
210
+ Hold the player or the viewports, never a `View`.
211
+
212
+ **`Layout`** answers only "given a count and a window, where does each one go", with no
213
+ state and no anchors: one viewport gets the window, two get a row each, three or four
214
+ share a 2x2 grid. Edges are computed as `(i * total) / count`, so the rects tile exactly
215
+ and no seam is left down the middle of an odd-sized window.
216
+
217
+ ### A player's own screen
218
+
219
+ `RGame::Engine::PlayerLayer` is the node for it: its subtree is drawn **once**,
220
+ clipped to that player's viewport and translated to its corner, in screen space.
221
+
222
+ ```ruby
223
+ layer = scene.add_node(RGame::Engine::PlayerLayer.new(player: game.players[1]))
224
+ layer.add_node(inventory)
225
+ ```
226
+
227
+ That is the third kind of content a frame holds. The world is drawn once per
228
+ viewport under a camera (`WorldView`), a global overlay once across the whole
229
+ window (anything else in the tree), and this once per player inside their own
230
+ region.
231
+
232
+ It is also where the `:hud` band comes from: `PlayerLayer` declares it, so
233
+ everything under here draws over everything in the world without any of it
234
+ saying so.
235
+
236
+ **Children are positioned relative to the layer**, so a node at (10, 10) is ten
237
+ pixels inside *that player's* region wherever the layout put it, and the same
238
+ HUD class serves either player unchanged. Lay out against the far edge with the
239
+ view's **size** — `view.width - margin`. `view.x` and `view.y` are where the
240
+ region sits on the window and are the clip's business, not a layout origin;
241
+ adding them would offset a second time.
242
+
243
+ **It sets `input_owner`**, and ownership is inherited, so a menu anywhere under
244
+ it reads that player's controller and nobody else's. Two players with a menu
245
+ open at once are independent without either knowing the other exists — see
246
+ [Who a node answers to](#who-a-node-answers-to).
247
+
248
+ It draws nothing when `screen_for` has no region for that player: an empty seat,
249
+ or anybody while the split is collapsed.
250
+
251
+ ### Collapsing the split
252
+
253
+ ```ruby
254
+ node.system(RGame::Engine::Viewports).solo!(cutscene_camera)
255
+ node.system(RGame::Engine::Viewports).split!
256
+ ```
257
+
258
+ `solo!` collapses to one screen-wide view — for a cutscene, or anywhere the world should
259
+ be seen through a single camera. **The camera is required**: promoting one player's would
260
+ silently give everyone else their view, and choosing what is on screen is what a cutscene
261
+ is for. Point an ordinary `Camera` however you like (a `CameraFollow` on a cutscene actor
262
+ works) and hand it over.
263
+
264
+ Both are **deferred**, like `queue_free`: they record a request and it takes effect on the
265
+ next tick. This system is reachable from anywhere including a `draw`, and a `draw` runs
266
+ once per view, so applying immediately would tear the frame it was requested in.
267
+
268
+ A full-screen UI — a results screen, a pause panel — usually wants no collapse at all:
269
+ draw it in screen space, outside any `WorldView`, with `band: :overlay` so it covers
270
+ the whole window over whatever the players are seeing, HUDs included.
68
271
 
69
272
  ## Components
70
273
 
@@ -137,6 +340,30 @@ A *system* is just a `Component` living on one of those anchor nodes; nodes find
137
340
  with `node.system(SomeSystem)` (scene scope first, then the global root). See
138
341
  [Systems & shared resources](systems.md) for the scoping model and worked examples.
139
342
 
343
+ ## Pausing a subtree
344
+
345
+ ```ruby
346
+ world_view.paused = true # the world stops; an overlay above it does not
347
+ walker.paused = true # or just one node, while its owner is in a menu
348
+ ```
349
+
350
+ A paused node skips `control` and `update` — and so does everything under it,
351
+ because a subtree is only ever reached through its parent. **It still draws.**
352
+ Pausing is about time, not visibility, which is what lets a frozen world sit
353
+ under a cutscene that keeps animating.
354
+
355
+ It is a property of a *node* rather than of the world on purpose. "Pause the
356
+ world" is `world_view.paused = true` with no new concept, and the same flag
357
+ stops one player's character while they browse a menu without touching the
358
+ simulation everyone else is in.
359
+
360
+ There is no `abs_paused` to go with `abs_input_owner`: ownership has to be
361
+ resolved because a node needs to know whose input it reads even when its parent
362
+ claims nobody, while a paused node simply never descends.
363
+
364
+ `draw` still resolves the transform, so a paused node under an ancestor that is
365
+ still moving is drawn where it now is rather than where it was when it stopped.
366
+
140
367
  ## Deferred free
141
368
 
142
369
  A node that detaches itself or a sibling mid-tick would mutate a parent's `children`
data/docs/api/systems.md CHANGED
@@ -73,6 +73,26 @@ class CircleCollider < RGame::Engine::Component
73
73
  end
74
74
  ```
75
75
 
76
+ ## The two the platform mounts for you
77
+
78
+ `RGame::Game` puts two systems on the root before the tree comes alive, so any
79
+ node can reach them without a game wiring anything:
80
+
81
+ | | |
82
+ |---|---|
83
+ | `node.system(RGame::Engine::Players)` | who is playing — devices, bindings, cameras, and who a newly used controller belongs to |
84
+ | `node.system(RGame::Engine::Viewports)` | how the screen is divided — one `View` per active player, and collapsing the split |
85
+
86
+ They are ordinary root-scoped systems, mounted the same way a game would mount
87
+ its own. A scene that needs a camera to follow asks the first
88
+ (`players.primary.camera`); a cutscene that needs to collapse the split asks the
89
+ second (`viewports.solo!(camera)`), from wherever in the tree it happens to be
90
+ and with nothing threaded into it. That reachability is the whole reason they
91
+ are systems rather than something `Game` hands down.
92
+
93
+ See [Input](input.md#players-seats-and-joining) and
94
+ [Scene graph](scene_graph.md#viewports-and-views).
95
+
76
96
  ## Systems that index their clients (the tag-registry pattern)
77
97
 
78
98
  A many-to-many system (broadphase collision) lives on the scene node and keeps its
data/docs/api/toolbox.md CHANGED
@@ -40,7 +40,7 @@ path (e.g. in `on_add`), then read it by value in `on_draw`:
40
40
  ```ruby
41
41
  @score_label = RGame::Engine::CachedLabel.new { |score| "Score: #{score}" } # built once
42
42
 
43
- def on_draw(renderer)
43
+ def on_draw(renderer, _view)
44
44
  renderer.text(@score_label[@score], 12, 10) # cached; rebuilds only when @score changes
45
45
  end
46
46
  ```
@@ -129,24 +129,28 @@ you poll `ready?`/`consume`.
129
129
 
130
130
  ## `Camera` — follow a point, clamp to the world
131
131
 
132
- `RGame::Engine::Camera` (`rgame/engine/camera`) is the pure follow-and-clamp maths for a scrolling
133
- view: `center_on(world_x, world_y)` parks its top-left so the point is centred, but
134
- clamped so it never shows past the map edges (near a corner the target drifts off-centre
135
- instead). A scene constructs one, sizes it with the viewport and the world, and centres
136
- it on the player each frame.
132
+ `RGame::Engine::Camera` (`rgame/engine/camera`) is the pure follow-and-clamp maths for a
133
+ scrolling view. It splits into two calls, and the split is the whole design:
137
134
 
138
135
  ```ruby
139
- camera = RGame::Engine::Camera.new(
140
- viewport_width: 640, viewport_height: 480,
141
- world_width: map.pixel_width, world_height: map.pixel_height
142
- )
143
- camera.center_on(player_x, player_y) # camera.x / camera.y now hold the clamped offset
136
+ camera = RGame::Engine::Camera.new(world_width: map.pixel_width, world_height: map.pixel_height)
137
+ camera.center_on(player_x, player_y) # in update: what to look at
138
+ camera.resolve(view_width, view_height) # at draw: the offset for *this* viewport
139
+ camera.x, camera.y # the resolved offset
144
140
  ```
145
141
 
146
- It only computes the offset; applying it is a *view transform* —
147
- [`CameraView`](scene_graph.md#view-transforms-and-the-camera) wraps the world subtree in
148
- `renderer.translated(-camera.x, -camera.y)`, and a [`TileWorld`](components.md#tileworld)
149
- draws the map at the same offset. See `examples/15_tiled_world`.
142
+ `center_on` records the target; `resolve` works out the offset, clamped so the view never
143
+ shows past the world's edges (near a corner the target drifts off-centre instead).
144
+ **The viewport size is an argument rather than state** because the same camera is drawn
145
+ through viewports of different sizes a half-width one clamps differently from a
146
+ full-width one, and the difference is visible near a world edge.
147
+
148
+ **A camera belongs to a player** ([`RGame::Engine::Player#camera`](input.md#players-seats-and-joining)),
149
+ not to a scene: a scene may have any number of viewers. Nothing calls `resolve` by hand —
150
+ the platform resolves each camera against the viewport it is about to draw. Pointing one
151
+ is a [`CameraFollow`](components.md#camerafollow) component on the node being followed,
152
+ and applying it is a [`WorldView`](scene_graph.md#view-transforms-and-the-camera). See
153
+ `examples/15_tiled_world`.
150
154
 
151
155
  ## `CollisionBox` — an actor's feet box
152
156
 
data/docs/api/ui.md ADDED
@@ -0,0 +1,98 @@
1
+ # UI
2
+
3
+ A menu you navigate with a keyboard or a controller, and the region one player's
4
+ UI lives in.
5
+
6
+ **There is no pointer**, deliberately — `RGame::Core::Input` has no mouse and the
7
+ id range one would occupy is left unused. So there is no hover, and the thing a
8
+ mouse-driven control takes from the cursor being over it, a control here takes
9
+ from being the **focused** one. That is the whole design; the rest follows.
10
+
11
+ ## A player's own screen
12
+
13
+ `RGame::Engine::PlayerLayer` is the region. Its subtree is drawn once, clipped
14
+ to that player's viewport, translated to its corner, and driven by their
15
+ controller:
16
+
17
+ ```ruby
18
+ layer = scene.add_node(RGame::Engine::PlayerLayer.new(player: game.players[1]))
19
+ layer.add_node(inventory)
20
+ ```
21
+
22
+ See [Scene graph](scene_graph.md#a-players-own-screen) for what it does and how
23
+ it decides there is nothing to draw.
24
+
25
+ ## `RGame::Engine::UI::Menu`
26
+
27
+ A vertical list of things to choose from.
28
+
29
+ ```ruby
30
+ menu = layer.add_node(RGame::Engine::UI::Menu.new(item_width: 220, item_height: 44))
31
+ menu.add_item('Resume').on_activated { close }
32
+ menu.add_item('Save').on_activated { save }
33
+ menu.add_item('Quit', enabled: false)
34
+ ```
35
+
36
+ | | |
37
+ |---|---|
38
+ | `ui_up` / `ui_down` | move focus, wrapping at the ends |
39
+ | `ui_confirm` | activate the focused item |
40
+ | `add_item(label, enabled: true)` | append an item and return it |
41
+ | `items`, `focused`, `focused_index` | what it holds and where focus is |
42
+ | `focus(index)`, `focus_by(delta)` | move focus directly |
43
+
44
+ Those three actions come from the [universal set](input.md#the-universal-ui-set)
45
+ that every `InputMap` is merged over, so a menu works without a game declaring
46
+ anything.
47
+
48
+ ### Focus is per player, and it costs nothing
49
+
50
+ A menu inside a `PlayerLayer` inherits that player as its `input_owner`, and
51
+ ownership is inherited down the tree — so the `actions` its `on_control`
52
+ receives are already that player's. **Two players with a menu open at once are
53
+ independent, and neither menu mentions players at all.**
54
+
55
+ That is not a feature of the menu; it is [ownership
56
+ routing](scene_graph.md#who-a-node-answers-to) doing its job one layer down.
57
+
58
+ ### `RGame::Engine::UI::MenuItem`
59
+
60
+ One entry: a label on a nine-slice, and an `on_activated` signal. It draws
61
+ itself from its **state**, which is why the shipped atlas has an element for
62
+ each:
63
+
64
+ | State | Element |
65
+ |---|---|
66
+ | focused | `button_focus` |
67
+ | focused, confirm held | `button_pressed` |
68
+ | not focused | `button_idle` |
69
+ | `enabled: false` | `button_disabled` |
70
+
71
+ A disabled item is skipped by focus movement and cannot be activated by any
72
+ route, so a caller never has to check first.
73
+
74
+ The element names are `MenuItem::STYLE`, and a menu can be built with a
75
+ different hash — a game with its own art is not obliged to name it the way the
76
+ shipped atlas does.
77
+
78
+ ### Getting the art on screen
79
+
80
+ Nine-slice ids name an *element of an atlas*, not a file, so there is nothing
81
+ for the asset manager to resolve on demand. Register the atlas once:
82
+
83
+ ```ruby
84
+ game.renderer.register_ui_atlas(game.assets.ui_atlas('ui/ui_atlas.json'))
85
+ ```
86
+
87
+ `media/ui/ui_atlas.json` ships with `panel` and the four button elements above.
88
+ See [Sheets, atlases and maps](assets.md).
89
+
90
+ ## What this is not
91
+
92
+ It is a menu, not a widget library. Items are stacked vertically at a fixed
93
+ size, and that is the whole of its layout — no nesting, no scrolling lists, no
94
+ text entry, and no general answer to how UI should be laid out.
95
+
96
+ The package this replaces positioned everything absolutely and hit-tested a
97
+ mouse cursor. It was deleted with the mouse, none of it is a reference, and its
98
+ API is deliberately not preserved.
data/docs/api/values.md CHANGED
@@ -99,3 +99,35 @@ grid.depth.times do |z|
99
99
  end
100
100
  end
101
101
  ```
102
+
103
+ ## `RGame::Util::Z`
104
+
105
+ The vocabulary of draw order: which band a thing is drawn in, and the arithmetic
106
+ that turns a band plus a position in the tree into the single number the renderer
107
+ sorts a frame by.
108
+
109
+ ```ruby
110
+ RGame::Util::Z::BANDS # => [:world, :hud, :overlay, :debug]
111
+ RGame::Util::Z::DEFAULT # => :world
112
+ RGame::Util::Z::Z_MIN # => -512, the smallest `z:` a drawing call may pass
113
+ RGame::Util::Z::Z_MAX # => 511
114
+ ```
115
+
116
+ It lives here for the same reason [`Controls`](input.md) does: both the scene
117
+ graph (which decides a node's band) and the renderer (which turns one into a z)
118
+ have to name it, and neither may name the other's layer.
119
+
120
+ Games rarely touch it. What a game writes is a node's `z` and, occasionally, a
121
+ `band:` — see [the scene graph](scene_graph.md#draw-order). What it buys is that
122
+ `z` numbers cannot leak between nodes and bands cannot leak into each other:
123
+
124
+ | | |
125
+ |---|---|
126
+ | `SLOT` | 1024 — the room one node has for ordering its own drawing |
127
+ | `Z_MIN`…`Z_MAX` | what a `z:` on a drawing call may be; anything else raises |
128
+ | `STRIDE` | `2**40` — the gap between bands, which no `z:` can cross |
129
+
130
+ Every value is an integer below `2**42`, and the `double` the draw queue sorts on
131
+ is exact below `2**53`, so two different slots can never compare equal by
132
+ rounding — which would show up as two sprites swapping places between frames, and
133
+ would be very hard to recognise as a precision problem.
data/ext/README.md CHANGED
@@ -29,7 +29,7 @@ ext/rgame_core/
29
29
  transform.c/.h # pure 2D affine transform stack (unit-tested)
30
30
  clip.c/.h # pure rects + intersecting clip stack (unit-tested)
31
31
  draw_queue.c/.h # pure z-sort + batching of draw commands (unit-tested)
32
- canvas.c/.h # pure transform+clip+queue composition (unit-tested)
32
+ canvas.c/.h # pure transform+clip+layer+queue composition (unit-tested)
33
33
  backend.h/.c # the GL seam: function-pointer table + submit loop
34
34
  texture.c/.h # pure texture sheets, sub-rects and UVs (unit-tested)
35
35
  primitives.c/.h # pure rects/lines/circles/sprites -> canvas (unit-tested)
@@ -177,11 +177,11 @@ app.fps # => Float, most recent FPS reading
177
177
  controls = RGame::Util::Controls
178
178
  input = RGame::Core::Input.new(app)
179
179
  input.down?(:fire) # keyboard (the default)
180
- input.down?(:fire, device: controls.gamepad(0))
181
- input.axis(:move_x, device: controls.gamepad(0)) # => Float
180
+ input.down?(controls::PAD_A, device: controls.gamepad(0))
181
+ input.axis(controls::AXIS_LEFT_X, device: controls.gamepad(0)) # => Float
182
182
 
183
- # Rebinding is just a different table:
184
- RGame::Core::Input.new(app, bindings: controls::DEFAULT_KEYBOARD.merge(fire: controls::KEY_RETURN))
183
+ # Input is the raw query: physical ids, per device. What an id *means* to a game
184
+ # is RGame::Engine::InputMap, one table per player, a layer up.
185
185
 
186
186
  # Which controllers are plugged in — a readout for menus, not the frame path:
187
187
  pads = RGame::Core::Gamepad.new(app)
@@ -203,6 +203,7 @@ renderer.line(0, 0, 100, 100, thickness: 4)
203
203
  renderer.image(sheet, 400, 300, angle: 45, scale: 2) # centred, clockwise
204
204
  renderer.rotated(30, 400, 300) { renderer.rect(380, 280, 40, 40) }
205
205
  renderer.clipped(0, 0, 400, 600) { renderer.background(sheet) }
206
+ renderer.layered(:hud) { renderer.text("Score: 1200", 10, 10) } # over the world
206
207
 
207
208
  # Text. The renderer has a font already; Font.new(app, size) makes another.
208
209
  renderer.text("Score: 1200", 10, 10)