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
data/docs/api/README.md CHANGED
@@ -1,12 +1,13 @@
1
1
  # rgame API guide
2
2
 
3
3
  Reference documentation for using rgame from Ruby. The engine is written in C
4
- and exposed as two Ruby extensions; nothing here assumes you will read or write
5
- any C.
4
+ and exposed as two Ruby extensions, with the scene graph a game is actually
5
+ written in sitting on top of them in pure Ruby; nothing here assumes you will
6
+ read or write any C.
6
7
 
7
8
  | Page | Covers |
8
9
  |---|---|
9
- | This page | Loading the library, the two namespaces, a working program, testing |
10
+ | This page | Loading the library, the three namespaces, a working program, testing |
10
11
  | [App](app.md) | `RGame::Core::App` — the window and the frame loop |
11
12
  | [Game](game.md) | `RGame::Game` — the entry point that wires both halves together |
12
13
  | [Input](input.md) | `RGame::Core::Input`, `RGame::Util::Controls`, `RGame::Core::Gamepad` |
@@ -15,7 +16,7 @@ any C.
15
16
  | [Text](text.md) | `RGame::Core::Font` and `Renderer#text` |
16
17
  | [Audio](audio.md) | `RGame::Core::Audio`, `Sample`, `Song` — samples and streamed music |
17
18
  | [Sheets, atlases and maps](assets.md) | `RGame::Core::SpriteSheet` and the rest of the asset layer |
18
- | [Values](values.md) | `RGame::Util::Color`, `RGame::Util::Tensor` |
19
+ | [Values](values.md) | `RGame::Util::Color`, `RGame::Util::Tensor`, `RGame::Util::Z` |
19
20
 
20
21
  The scene graph — `RGame::Engine`, the layer a game is actually written in:
21
22
 
@@ -24,15 +25,18 @@ The scene graph — `RGame::Engine`, the layer a game is actually written in:
24
25
  | [Scene graph](scene_graph.md) | `Node2D`, the tree, the lifecycle, transforms and the camera |
25
26
  | [Components](components.md) | Reusable behaviour attached to a node |
26
27
  | [Systems](systems.md) | Services a subtree shares — collision worlds, tile worlds |
28
+ | [UI](ui.md) | `PlayerLayer` and `UI::Menu` — a player's own screen, navigated by focus |
27
29
  | [Signals](signals.md) | The typed observer pattern nodes talk through |
28
30
  | [Toolbox](toolbox.md) | What a game author reaches for directly: pooling, timers, camera, i18n, the audio bus |
29
31
  | [Internal building blocks](internals.md) | What components are built from: collision maths, the spatial index, animation playback |
30
32
 
31
33
  **The engine is a work in progress.** A window opens, the loop runs, input
32
- works, shapes, images and text can be drawn, sound plays, and a scene graph runs
33
- on top of it — the two games under `examples/` are built on exactly what is
34
- documented here. What is missing is a UI toolkit and split-screen; pages here
35
- describe what exists today and grow as more lands.
34
+ works, shapes, images and text can be drawn, sound plays, and a scene graph with
35
+ split-screen players runs on top of it — the games under `examples/` are built
36
+ on exactly what is documented here. What is missing is a UI *toolkit*: [UI](ui.md)
37
+ gives each player a region of the screen, focus and activation, and stops there —
38
+ no layout, no scrolling lists, no text entry. Pages here describe what exists
39
+ today and grow as more lands.
36
40
 
37
41
  ## Loading it
38
42
 
@@ -66,102 +70,145 @@ Both extensions must be compiled before they can be required:
66
70
  make ext # builds both, copies them into lib/rgame/
67
71
  ```
68
72
 
69
- ## The two namespaces
73
+ ## The three namespaces
70
74
 
71
- Everything lives under `RGame`, split in two by what it depends on:
75
+ Everything lives under `RGame`, split three ways — two of them by what they
76
+ depend on, the third by what it is *for*:
72
77
 
73
- | | `RGame::Util` | `RGame::Core` |
74
- |---|---|---|
75
- | Contains | shareable *values* — no window, no GPU, nothing to release | things owning a window, GPU or OS handle |
76
- | Today | `Color`, `Tensor`, `Controls` | `App`, `Input`, `Gamepad`, `Image`, `Renderer`, `Recording`, `Font` |
77
- | Loading it costs | nothing | SDL2 + OpenGL in your process |
78
+ | | `RGame::Util` | `RGame::Core` | `RGame::Engine` |
79
+ |---|---|---|---|
80
+ | Contains | shareable *values* — no window, no GPU, nothing to release | things owning a window, GPU or OS handle | game concepts: the scene graph a game is written in |
81
+ | Today | `Color`, `Tensor`, `Controls`, `Z` | `App`, `Input`, `Gamepad`, `Image`, `Renderer`, `Recording`, `Font`, `Audio`, `SpriteSheet`, `AssetManager` | `Node2D`, components, systems, signals, `TileMap`, `Player`, `InputMap`, `UI::Menu` |
82
+ | Loading it costs | nothing | SDL2 + OpenGL in your process | nothing |
78
83
 
79
- The rule for deciding where something belongs: **a value goes in `Util`; only a
84
+ The rule for splitting the bottom two: **a value goes in `Util`; only a
80
85
  handle-owner goes in `Core`.** A colour is a value. A window is not.
81
86
 
82
- This is not tidiness. Game logic is expected to hold `Util` types freely as
83
- attributes, and to reach `Core` only through objects handed to it — a node's
84
- `draw` receives a renderer and calls methods on it, rather than naming a class.
85
- That is what keeps game code runnable with no window, which the testing section
86
- below relies on.
87
+ `RGame::Engine` sits above both, and its rule is what makes the split worth
88
+ having:
89
+
90
+ - it may hold `Util` values freely as attributes a `Color`, a `Tensor`;
91
+ - it may **not name `Core` at all** — no require, no constant, no attribute;
92
+ - it reaches `Core` only through objects handed to it. A node's `on_draw`
93
+ receives a renderer and calls methods on it by name, never storing it and
94
+ never asking what class it is.
95
+
96
+ This is not tidiness. It is what keeps a whole game — its rules, its scenes, its
97
+ collisions — runnable and testable with no window, which the testing section
98
+ below relies on. Two RuboCop cops enforce it in both directions, so a stray
99
+ reference is a failing lint rather than a discovery made later.
100
+
101
+ `RGame::Game` is the single exception, and the only class directly under
102
+ `RGame`: introducing the two halves to each other is exactly what it is for, and
103
+ confining that to one file is what keeps the rule checkable everywhere else.
87
104
 
88
105
  ## A complete program
89
106
 
107
+ A game is a tree of nodes plus `RGame::Game` to run it.
108
+
90
109
  ```ruby
91
- require 'rgame'
92
- require 'rgame/core'
110
+ require 'rgame/game'
93
111
 
94
- class MyGame < RGame::Core::App
95
- Controls = RGame::Util::Controls
112
+ # One game object: a square the player walks around. Pure Engine — it names no
113
+ # graphics class, so it runs just as happily in a spec with no window.
114
+ class Hero < RGame::Engine::Node2D
115
+ SPEED = 200.0
96
116
 
97
117
  def initialize
98
- super(width: 800, height: 600, caption: 'My Game')
99
- @input = RGame::Core::Input.new(self)
100
- @renderer = RGame::Core::Renderer.new(self)
101
- @x = 400.0
102
- @y = 300.0
118
+ super(x: 400, y: 300, width: 16, height: 16)
119
+ @vx = 0.0
120
+ @vy = 0.0
103
121
  end
104
122
 
105
- # One fixed simulation tick. `dt` is always the same fixed step, never
106
- # wall-clock frame time, so movement is deterministic.
107
- def update(dt)
108
- speed = 200.0 * dt
109
- @x -= speed if @input.down?(:left)
110
- @x += speed if @input.down?(:right)
111
- @y -= speed if @input.down?(:up)
112
- @y += speed if @input.down?(:down)
123
+ # Intent, read once per simulation tick. Never a key: `move_x` is whatever
124
+ # this player's input map binds it to — arrows, WASD or a stick.
125
+ def on_control(actions)
126
+ @vx = actions.axis(:move_x) * SPEED
127
+ @vy = actions.axis(:move_y) * SPEED
113
128
  end
114
129
 
115
- # Everything is drawn here, through a renderer built in initialize. See
116
- # docs/api/drawing.md.
117
- def draw
118
- @renderer.rect(@x - 8, @y - 8, 16, 16)
130
+ # `dt` is always the same fixed step, never wall-clock frame time, so
131
+ # movement is deterministic. `x`/`y` are relative to the parent.
132
+ def on_update(dt)
133
+ self.x += @vx * dt
134
+ self.y += @vy * dt
119
135
  end
120
136
 
121
- def button_down(id)
122
- close if id == Controls::KEY_ESCAPE
137
+ # The renderer is handed in and never stored; `view` is the viewport being
138
+ # drawn into, which most nodes ignore. Draw from the resolved absolute
139
+ # position, not from `x`/`y`. See docs/api/drawing.md.
140
+ def on_draw(renderer, _view)
141
+ renderer.rect(abs_x, abs_y, width, height)
123
142
  end
124
143
  end
125
144
 
126
- MyGame.new.run
145
+ # The root of the tree. Children are added in `on_add`, once the node is in a
146
+ # tree and can reach the game around it.
147
+ class Scene < RGame::Engine::Node2D
148
+ def on_add
149
+ add_node(Hero.new)
150
+ end
151
+ end
152
+
153
+ RGame::Game.new(root: Scene.new, width: 800, height: 600, caption: 'My Game').start
127
154
  ```
128
155
 
129
- You subclass `App` and override the hooks you care about. Everything you do not
130
- override is an inherited no-op there is no `super` to remember, and no way to
131
- break the loop by forgetting one. See [App](app.md) for the full list.
156
+ You subclass `Node2D` and override the hooks you care about `on_control`,
157
+ `on_update`, `on_draw`, and the lifecycle hooks around them. Everything you do
158
+ not override is an inherited no-op, and the phase methods that do the
159
+ bookkeeping (resolving the transform, driving components, descending into
160
+ children) are not the ones you override — so there is no `super` to remember and
161
+ no way to break the tree by forgetting one. See [Scene graph](scene_graph.md)
162
+ for the full list, and [Game](game.md) for what `Game` assembles around it: the
163
+ window, the renderer, the asset manager, the sound device, the input mapper and
164
+ the players.
165
+
166
+ Pass no `input_map:` and you get the default one used above: eight-way `move_x`
167
+ / `move_y` on the arrows, WASD, the d-pad or the left stick, plus `fire`. See
168
+ [Input](input.md).
132
169
 
133
170
  ## Testing a game built on this
134
171
 
135
172
  The namespace split exists so that game logic can be tested without opening a
136
- window. Drive `update` directly and it runs as fast as the CPU allows:
173
+ window. `require 'rgame'` gives you `Util` and the whole scene graph with no SDL
174
+ and no OpenGL in the process, and the nodes from the program above run there
175
+ unchanged — drive their phases directly and a simulated hour takes milliseconds:
137
176
 
138
177
  ```ruby
139
- # A plain object holding your game's rules — no RGame::Core anywhere.
140
- class Player
141
- attr_reader :x
142
-
143
- def initialize = @x = 0.0
144
-
145
- def update(dt, moving_right:)
146
- @x += 200.0 * dt if moving_right
147
- end
148
- end
178
+ require 'rgame'
149
179
 
150
- RSpec.describe Player do
180
+ RSpec.describe Hero do
151
181
  it 'walks right at 200 units a second' do
152
- player = Player.new
182
+ hero = Hero.new
183
+ # The same snapshot object the input mapper hands a node at runtime, built
184
+ # by hand with the stick pushed fully right.
185
+ actions = RGame::Engine::Actions.new(axes: { move_x: 1.0, move_y: 0.0 })
186
+
153
187
  # One simulated second, sixty ticks, no window and no clock.
154
- 60.times { player.update(1.0 / 60.0, moving_right: true) }
188
+ 60.times do
189
+ hero.control(actions)
190
+ hero.update(1.0 / 60.0)
191
+ end
155
192
 
156
- expect(player.x).to be_within(0.01).of(200.0)
193
+ expect(hero.x).to be_within(0.01).of(600.0)
157
194
  end
158
195
  end
159
196
  ```
160
197
 
161
- The engine deliberately makes this easy: `update` takes `dt` as an argument
162
- rather than reading a clock, so a test can pass whatever timestep it likes and
163
- simulate an hour in milliseconds.
198
+ Two things make that work:
199
+
200
+ - **`update` takes `dt` as an argument rather than reading a clock**, so a test
201
+ passes whatever timestep it likes. That means tests are not dependent on real time and can simulate game behavior based on time in miliseconds.
202
+ - **A node never holds a renderer.** `on_draw` is given one, so drawing can be
203
+ checked by passing a recording double and asserting on what the node asked
204
+ for — see the renderer contract in `spec/support/shared_examples/`. All tests can be run completely headless.
205
+
206
+ `hero.x` is asserted rather than `abs_x` because this node has no parent here.
207
+ Absolute position accumulates from the parent, and a node with no parent resolves
208
+ to the origin. Put it under a root and `abs_x` is what the rest of the engine
209
+ reads. See [Scene graph](scene_graph.md#absolute-position).
164
210
 
165
- Keep the parts of your game that decide *what happens* free of `RGame::Core`,
166
- and hand them a renderer at draw time rather than storing one. Then the only
167
- code that needs a window is the thin layer that puts pixels on screen.
211
+ Keep the parts of your game that decide *what happens* in `RGame::Engine` — the
212
+ layer cannot name `RGame::Core`, so it cannot accidentally acquire a dependency
213
+ on a window. Then the only code that needs one is the thin layer that puts
214
+ pixels on screen.
data/docs/api/assets.md CHANGED
@@ -361,25 +361,24 @@ frame and culled to the viewport.
361
361
  ```ruby
362
362
  tiles = app.assets.tilemap('map/island.tmx')
363
363
 
364
- renderer.tilemap('map/island.tmx', camera_x, camera_y, view_w, view_h, elapsed: seconds)
364
+ renderer.tilemap('map/island.tmx', 0, camera_x, camera_y, view_w, view_h, elapsed: seconds)
365
365
  # ... the scene draws its actors here ...
366
- renderer.tilemap_overlay('map/island.tmx', camera_x, camera_y, view_w, view_h,
367
- z: 20, elapsed: seconds)
366
+ renderer.tilemap('map/island.tmx', 1, camera_x, camera_y, view_w, view_h, elapsed: seconds)
368
367
  ```
369
368
 
370
- ### Two bands, with the actors between them
369
+ ### One call per layer, because the actors go between them
371
370
 
372
- Layers split by Tiled's `above` custom property. The **below** bandground and
373
- same-level detail is drawn under the actors; the **above** band tree
374
- canopies, roofs over them, at a `z` the scene picks. Two calls rather than
375
- one, because the scene draws its actors in between; collapsing them would put
376
- every canopy behind every character.
371
+ A layer is drawn on its own, in the order the caller asks for which is what
372
+ lets a scene put its actors between two of them, trunks under and canopies over.
373
+ Which layers those are is a question about the scene, not about the map, so no
374
+ `z` is passed: in a game it is [`TileMapLayer`](components.md#tileworld) mounting
375
+ a node per layer, and the scene tree deciding the rest.
377
376
 
378
377
  ### What it costs
379
378
 
380
- Within each band, every tile that is **not** animated is baked into a
379
+ Within each layer, every tile that is **not** animated is baked into a
381
380
  [recording](drawing.md#recordings-bake-once-replay-cheaply) the first time that
382
- band is drawn. Scrolling it afterwards is one call per texture, however many
381
+ layer is drawn. Scrolling it afterwards is one call per texture, however many
383
382
  thousand tiles went into it. The handful that *are* animated are drawn
384
383
  individually, **culled to the viewport** — so a map far larger than the screen
385
384
  costs only what is on screen.
@@ -394,7 +393,7 @@ the asset manager rather than being loaded by the map.
394
393
  ```ruby
395
394
  def update(dt) = @elapsed += dt
396
395
  def draw(renderer)
397
- renderer.tilemap(@id, camera.x, camera.y, w, h, elapsed: @elapsed)
396
+ renderer.tilemap(@id, @layer, camera.x, camera.y, w, h, elapsed: @elapsed)
398
397
  end
399
398
  ```
400
399
 
@@ -16,9 +16,13 @@ nodes, it extends the signal DSL, so a component can declare and emit signals.
16
16
  Per-tick hooks (a node runs its components in each phase, before its own hook and
17
17
  before its children):
18
18
 
19
- - `control(actions)` — read intent from the per-frame action snapshot.
19
+ - `control(actions)` — read intent from the per-frame action snapshot. It is the
20
+ actions of whoever [owns the node](scene_graph.md#who-a-node-answers-to), so a
21
+ component never learns there is more than one player.
20
22
  - `update(dt)` — advance state over the timestep.
21
- - `draw(renderer)` — render against the renderer interface.
23
+ - `draw(renderer, view)` — render against the renderer interface, into the
24
+ [viewport being drawn](scene_graph.md#viewports-and-views). Most components ignore the
25
+ view; it is there for laying out against the region's edges and for culling.
22
26
 
23
27
  Tree-lifecycle hooks (fired by the engine when the node enters/leaves the live tree —
24
28
  this is where anchors and sibling systems are reachable, so do cross-node wiring here,
@@ -102,21 +106,6 @@ draws them; this component only manages their pool membership.
102
106
  still attached. So despawning is just `node.queue_free` anywhere; the pool recycles it with
103
107
  no game-side wiring. Allocation-free in steady state.
104
108
 
105
- ### `Clickable`
106
-
107
- Makes the owning node a world-space click target: on the click-down edge it hit-tests the
108
- pointer against a circle of `radius` about the node's absolute origin and emits `on_clicked`
109
- (no payload — the node identifies the click, like a button). It reads the pointer from the
110
- Actions snapshot, so it assumes screen == world (a fixed, unscrolled board; a scrolling
111
- camera would need the pointer unprojected first).
112
-
113
- - **Construct:** `Clickable.new(radius:, action: :ui_click)`. The action must be bound to the
114
- pointer button — `action_map: { ui_click: { button: %i[pointer] } }`.
115
- - **Signal:** `on_clicked` fires once per press inside the radius —
116
- `spot.on_clicked { build_tower }`.
117
- - **Phase:** `control(actions)` hit-tests and emits; allocation-free. Add it named (`as:`)
118
- when a node needs more than one click region.
119
-
120
109
  ### `ScreenWrap`
121
110
 
122
111
  Wraps the node's position toroidally within a rectangle, so an entity leaving one edge
@@ -208,11 +197,30 @@ acts. Because enemies already register with the broadphase through their
208
197
  Draws a single registered image centered on the node's absolute origin.
209
198
 
210
199
  - **Construct:** `Sprite.new(id:, scale: 1.0, z: 0)` — `id` is a renderer image id; `z`
211
- is the render layer (distinct from the node's transform `z`/`abs_z`).
200
+ orders this component against the node's *other* drawing (a shadow under a sprite),
201
+ inside the node's own slot. It is not the node's `z`, which orders the node against
202
+ its siblings. See [Drawing](drawing.md#draw-order).
212
203
  - **State:** `scale` is a read/write accessor (a pooled entity can retune it).
213
- - **Phase:** `draw(renderer)` draws the image with **no angle** — `Node2D#draw` already
214
- wraps a node's own draws in `renderer.rotated(abs_angle, …)`, so the node's rotation
215
- orients the sprite; passing an angle here would rotate it twice.
204
+ - **Phase:** `draw(renderer, view)` draws the image with **no angle** — `Node2D#draw`
205
+ already wraps a node's own draws in `renderer.rotated(abs_angle, …)`, so the node's
206
+ rotation orients the sprite; passing an angle here would rotate it twice. It skips the
207
+ draw entirely when the view cannot show it, measuring the node's box scaled — a node
208
+ that never set a size is never culled.
209
+
210
+ ### `CameraFollow`
211
+
212
+ Points a camera at the node it is attached to.
213
+
214
+ - **Construct:** `CameraFollow.new(camera:, offset_x: 0.0, offset_y: 0.0)` — the offsets
215
+ shift the point being centred on, for a node whose origin is not what should be in the
216
+ middle of the screen (a bottom-anchored sprite usually wants its feet).
217
+ - **Phase:** `update(dt)` calls `camera.center_on` with the node's resolved absolute
218
+ origin. The camera trails the node's own movement by one step, uniformly.
219
+
220
+ The camera belongs to a [player](input.md#players-seats-and-joining), not to this
221
+ component or to the scene — a scene may have any number of viewers. Ownership and
222
+ behaviour are different questions: the player owns the camera, and this moves it. So
223
+ "player two's camera follows player two" is this component with their camera in it.
216
224
 
217
225
  ### `ThrustController`
218
226
 
@@ -247,17 +255,19 @@ sibling's movement: `walk_left`/`walk_right`/`walk_up`/`walk_down` while moving
247
255
  on a diagonal), `stand` when still. Owns an `RGame::Engine::Animator` over the pure `AnimationSet` built
248
256
  from the sheet's animation table.
249
257
 
250
- - **Construct:** `AnimatedSprite.new(sheet:, z: 0)` — `sheet` is the asset's relative path; `z` the
251
- render layer.
258
+ - **Construct:** `AnimatedSprite.new(sheet:, z: 0)` — `sheet` is the asset's relative path; `z`
259
+ orders this component against the node's other drawing, inside the node's own slot (as
260
+ for [`Sprite`](#sprite)).
252
261
  - **Lifecycle:** `on_attach` resolves the sheet from the game's asset manager
253
262
  (`node.root.context.assets.sheet(sheet)`), builds its animation set, **sizes the node** to the
254
263
  sheet's frame (`node.width`/`height`, so a `CharacterBody` sibling can read them), and pulls that
255
264
  sibling (the facing source). The renderer resolves the same path when drawing, so nothing is
256
265
  registered or passed in by hand.
257
- - **Phase:** `update(dt)` selects + advances the animation; `draw` renders the current frame via
258
- `renderer.sprite` at the node's **world** origin (`abs_x`/`abs_y`) with no angle — a
259
- [`CameraView`](scene_graph.md) ancestor applies the camera offset, so the component never touches
260
- the camera. (`Sprite` above is the single-image counterpart.)
266
+ - **Phase:** `update(dt)` selects + advances the animation; `draw(renderer, view)` renders the
267
+ current frame via `renderer.sprite` at the node's **world** origin (`abs_x`/`abs_y`) with no
268
+ angle — a [`WorldView`](scene_graph.md#view-transforms-and-the-camera) ancestor applies the
269
+ camera offset, so the component never touches the camera. It skips the draw when the view
270
+ cannot show it, measuring the node's box. (`Sprite` above is the single-image counterpart.)
261
271
 
262
272
  ### `CharacterBody`
263
273
 
@@ -297,15 +307,29 @@ deterministic in tests.
297
307
 
298
308
  The scene-scoped tile **system** (see [Systems](systems.md)): it holds the parsed `RGame::Engine::TileMap`
299
309
  and answers everything an actor needs from it — collision against the solid tiles (reusing
300
- `RGame::Engine::CollisionSystem`), the world bounds, and drawing the map through the scene's camera. Found
301
- with `node.system(TileWorld)`.
310
+ `RGame::Engine::CollisionSystem`) and the world bounds. Found with `node.system(TileWorld)`.
311
+
312
+ **It does not draw.** `RGame::Engine::TileMapLayer` does — one node per Tiled layer, mounted
313
+ inside a `WorldView`, so the map is drawn once per viewport like the rest of world space.
314
+ This stays the thing actors ask questions of.
302
315
 
303
- - **Construct:** `TileWorld.new(map:, tilemap_id:, camera:)`.
316
+ - **Construct:** `TileWorld.new(map:, tilemap_id:, cameras: [])` — it clamps each camera it is given to
317
+ the map's edges, and `bound(camera)` does the same for one that arrives later (a player joining).
304
318
  - **Queries:** `move(actor, dx, dy)` slides an actor (anything responding to `x`/`y`/`collision_box`)
305
- along solids and clamps it to the world; `solid?(col, row)`; `world_width`/`world_height`.
306
- - **Phase:** `draw(renderer)` draws the below band at `GROUND_Z` and the above band at `OVERLAY_Z`
307
- (canopies/roofs). Actors draw at a z in between, so the renderer's z-sort composites ground < actors <
308
- canopy regardless of draw-call order.
319
+ along solids and clamps it to the world; `solid?(col, row)`; `world_width`/`world_height`;
320
+ `tilemap_id` and `elapsed`, which the layers read; `layer_count` and `first_above_layer`,
321
+ which `TileMapLayer.mount` reads to decide where the actors go.
322
+ - **Phase:** `update(dt)` advances the map's animation clock.
323
+
324
+ ```ruby
325
+ world = scene.add_node(RGame::Engine::WorldView.new)
326
+ actors = RGame::Engine::TileMapLayer.mount(world) # a node per Tiled layer
327
+ actors.add_node(player) # in the gap between them
328
+ ```
329
+
330
+ `mount` returns the node the actors go in. It sits below the first layer Tiled flags
331
+ `above` — trunks under the walker, canopies over — and `mount(world, under: index)`
332
+ overrides that for a map with a different arrangement. Nothing here picks a `z`.
309
333
 
310
334
  ```ruby
311
335
  # A node composing components, with collision meaning decided by the owner:
data/docs/api/drawing.md CHANGED
@@ -34,7 +34,7 @@ would be silently discarded — and an invisible failure is worse than a loud on
34
34
 
35
35
  **Nothing is drawn immediately.** Calls accumulate, and the frame is sorted and
36
36
  sent to the GPU once, after `draw` returns. So the order you make calls in does
37
- not decide what ends up on top — `z:` does.
37
+ not decide what ends up on top — the scene tree does. See "Draw order" below.
38
38
 
39
39
  ## Coordinates, colours and z
40
40
 
@@ -42,15 +42,77 @@ not decide what ends up on top — `z:` does.
42
42
  |---|---|
43
43
  | Origin | Top-left. x grows right, y grows **down**. |
44
44
  | Angles | Degrees. A **positive angle turns clockwise** on screen. |
45
- | `z:` | Higher is nearer the viewer. Equal z keeps call order. |
45
+ | `z:` | Where this call sits among **this node's own** drawing. −512…511. |
46
46
  | `color:` | `nil` (white), `[r, g, b]`, `[r, g, b, a]`, or a `RGame::Util::Color`. |
47
47
 
48
- `z` defaults to `50` for shapes and `0` for images, so a debug box or a health
49
- bar drawn without a `z:` lands on top of the scene rather than under it.
48
+ `z:` is an offset inside the current layer, not a global number. It orders a
49
+ node's panel under its label and its shadow under its sprite, and it can reach
50
+ nothing else — passing anything outside −512…511 raises.
50
51
 
51
- Equal-z stability matters more than it sounds: without it, two sprites on the
52
- same layer would swap places whenever the sort felt like it, which reads as
53
- flicker.
52
+ It defaults to `50` for shapes, `10` for text and `0` for images, so a debug box
53
+ or a health bar drawn without a `z:` lands on top of *that node's* sprite. Equal
54
+ z keeps call order, which matters more than it sounds: without it two sprites on
55
+ the same layer would swap places whenever the sort felt like it, and that reads
56
+ as flicker.
57
+
58
+ ## Draw order
59
+
60
+ Order is decided in three steps, coarsest first, and only the last of them is a
61
+ number a drawing call passes.
62
+
63
+ 1. **The band.** `:world` (the default), `:hud`, `:overlay`, `:debug`.
64
+ Everything in one band is under everything in the next, whatever either drew.
65
+ 2. **The slot.** The scene tree is walked depth-first with siblings in `z`
66
+ order, and each node takes the next slot in its band as it is reached. So
67
+ draw order is **tree order**, and a node's subtree is one contiguous run —
68
+ a subtree is atomic and cannot straddle a sibling.
69
+ 3. **The offset.** The `z:` above, inside one node's slot.
70
+
71
+ A scene graph arranges all of this for you: `RGame::Engine::Node2D#draw` opens a
72
+ layer per node, so a game writes `z` on nodes and a `band` on the handful that
73
+ mark one. See [scene_graph.md](scene_graph.md), "Draw order".
74
+
75
+ ### Opening a layer by hand
76
+
77
+ Anything drawing outside the scene tree — the debug overlay, a spec, a script —
78
+ opens its own:
79
+
80
+ ```ruby
81
+ renderer.layered(:hud) do
82
+ renderer.nine_slice(:panel, x, y, w, h)
83
+ renderer.text(score, x + 8, y + 6, z: 1) # above this layer's own panel
84
+ end
85
+ ```
86
+
87
+ `layered` takes the next slot in that band, makes it the base every `z:` inside
88
+ is measured from, and restores the previous base afterwards (including when the
89
+ block raises). Nesting *replaces* rather than accumulates — a node's slot is
90
+ decided by where the traversal reached it, not by summing what its ancestors
91
+ picked. Outside any block the base is 0, so a bare script gets exactly the z it
92
+ passes.
93
+
94
+ `renderer.layer` reports the base currently in effect.
95
+
96
+ ### Why bands exist at all
97
+
98
+ A frame holds three kinds of content:
99
+
100
+ - **World** — inside a `WorldView`, drawn once per viewport, under a camera.
101
+ - **A player's own screen space** — their HUD, their menu, drawn once and
102
+ clipped to their viewport (`PlayerLayer`).
103
+ - **Global screen space** — a cutscene, a results panel, drawn once across the
104
+ whole window.
105
+
106
+ The first is a different *space* from the other two, and the tree enforces that:
107
+ `WorldView` is what draws its subtree once per viewport. The last two share one
108
+ space, and the band is what tells them apart.
109
+
110
+ Two viewports interleaving in the sort is harmless — their commands carry
111
+ different clips and land on different pixels. Order *within* one viewport is
112
+ not, and nothing about drawing a HUD after the world puts it above the world.
113
+ Its band does. Bands are `2**40` apart and a `z:` spans 1024, so no arithmetic
114
+ below can carry one band into the next: containment is arithmetic rather than
115
+ convention. See `RGame::Util::Z`.
54
116
 
55
117
  ### Colours and allocation
56
118
 
@@ -146,8 +208,7 @@ An id is normally a **root-relative path**, resolved through the app's
146
208
  renderer.sprite('example 09/player.json', row, col, x, y, flip_x: false, z: 0)
147
209
  renderer.image('space.png', cx, cy, angle: 0, scale: 1)
148
210
  renderer.background('space.png')
149
- renderer.tilemap('map/island.tmx', camera_x, camera_y, viewport_w, viewport_h)
150
- renderer.tilemap_overlay('map/island.tmx', camera_x, camera_y, viewport_w, viewport_h, z: 20)
211
+ renderer.tilemap('map/island.tmx', layer, camera_x, camera_y, viewport_w, viewport_h)
151
212
  ```
152
213
 
153
214
  Nothing has to be set up for that: `Renderer.new(app)` takes the app's own
@@ -201,6 +262,7 @@ renderer.translated(dx, dy) { ... }
201
262
  renderer.rotated(angle, pivot_x, pivot_y) { ... }
202
263
  renderer.scaled(sx, sy = sx) { ... }
203
264
  renderer.clipped(x, y, width, height) { ... }
265
+ renderer.layered(band) { ... } # see "Draw order" above
204
266
  ```
205
267
 
206
268
  They nest, and they compose in the order they are opened:
@@ -238,6 +300,12 @@ def draw
238
300
  end
239
301
  ```
240
302
 
303
+ **A game does not write that.** It is what
304
+ [`RGame::Engine::WorldView`](scene_graph.md#view-transforms-and-the-camera) does for you,
305
+ once per active player, with the rectangles from the layout and each player's own camera.
306
+ Reach for `clipped` directly for a region of your own — a minimap, a scrolling list — and
307
+ let the world band handle the split.
308
+
241
309
  ## Recordings: bake once, replay cheaply
242
310
 
243
311
  A tile layer is a couple of thousand quads that have not changed since the level