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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +94 -0
- data/README.md +130 -233
- data/docs/api/README.md +116 -69
- data/docs/api/assets.md +11 -12
- data/docs/api/components.md +58 -34
- data/docs/api/drawing.md +77 -9
- data/docs/api/game.md +34 -13
- data/docs/api/input.md +232 -51
- data/docs/api/scene_graph.md +242 -15
- data/docs/api/systems.md +20 -0
- data/docs/api/toolbox.md +19 -15
- data/docs/api/ui.md +98 -0
- data/docs/api/values.md +32 -0
- data/ext/README.md +6 -5
- data/ext/rgame_core/app/app.c +182 -8
- data/ext/rgame_core/audio/audio.c +74 -0
- data/ext/rgame_core/example.rb +17 -6
- data/ext/rgame_core/extconf.rb +52 -24
- data/ext/rgame_core/graphics/canvas.c +45 -4
- data/ext/rgame_core/graphics/canvas.h +65 -10
- data/ext/rgame_core/graphics/clip.c +22 -13
- data/ext/rgame_core/include/rgame/core.h +113 -3
- data/ext/rgame_core/input/gamepad.c +57 -3
- data/ext/rgame_core/ruby/core_ext.c +16 -0
- data/ext/rgame_core/ruby/renderer_ext.c +23 -0
- data/ext/rgame_util/color_ext.c +12 -3
- data/lib/rgame/core/app.rb +2 -0
- data/lib/rgame/core/input.rb +35 -41
- data/lib/rgame/core/recording.rb +3 -1
- data/lib/rgame/core/renderer.rb +76 -28
- data/lib/rgame/core/tile_map_renderer.rb +84 -55
- data/lib/rgame/engine/camera.rb +55 -10
- data/lib/rgame/engine/component.rb +11 -1
- data/lib/rgame/engine/components/animated_sprite.rb +9 -3
- data/lib/rgame/engine/components/camera_follow.rb +44 -0
- data/lib/rgame/engine/components/character_body.rb +25 -4
- data/lib/rgame/engine/components/sprite.rb +11 -1
- data/lib/rgame/engine/components/tile_world.rb +31 -18
- data/lib/rgame/engine/culling.rb +47 -0
- data/lib/rgame/engine/debug_overlay.rb +20 -9
- data/lib/rgame/engine/input/action_mapper.rb +101 -21
- data/lib/rgame/engine/input/actions.rb +69 -12
- data/lib/rgame/engine/input/input_map.rb +178 -0
- data/lib/rgame/engine/layout.rb +82 -0
- data/lib/rgame/engine/node2d.rb +205 -36
- data/lib/rgame/engine/player.rb +69 -0
- data/lib/rgame/engine/player_layer.rb +70 -0
- data/lib/rgame/engine/players.rb +212 -0
- data/lib/rgame/engine/scene/scene_stack.rb +25 -3
- data/lib/rgame/engine/spatial_hash.rb +17 -4
- data/lib/rgame/engine/tile_map_layer.rb +84 -0
- data/lib/rgame/engine/ui/menu.rb +115 -0
- data/lib/rgame/engine/ui/menu_item.rb +84 -0
- data/lib/rgame/engine/view.rb +76 -0
- data/lib/rgame/engine/viewports.rb +174 -0
- data/lib/rgame/engine/world_view.rb +70 -0
- data/lib/rgame/engine.rb +13 -1
- data/lib/rgame/game.rb +81 -11
- data/lib/rgame/util/controls.rb +117 -41
- data/lib/rgame/util/z.rb +133 -0
- data/lib/rgame/util.rb +1 -0
- data/lib/rgame/version.rb +1 -1
- metadata +26 -11
- data/lib/rgame/engine/camera_view.rb +0 -28
data/docs/api/scene_graph.md
CHANGED
|
@@ -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`, `
|
|
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
|
|
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
|
|
50
|
-
sits at the origin. Moving
|
|
51
|
-
its whole subtree. (
|
|
52
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
133
|
-
view
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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?(
|
|
181
|
-
input.axis(
|
|
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
|
-
#
|
|
184
|
-
RGame::
|
|
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)
|