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/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
|
|
5
|
-
|
|
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
|
|
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
|
|
33
|
-
on top of it — the
|
|
34
|
-
documented here. What is missing is a UI toolkit
|
|
35
|
-
|
|
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
|
|
73
|
+
## The three namespaces
|
|
70
74
|
|
|
71
|
-
Everything lives under `RGame`, split
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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(
|
|
99
|
-
@
|
|
100
|
-
@
|
|
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
|
-
#
|
|
106
|
-
#
|
|
107
|
-
def
|
|
108
|
-
|
|
109
|
-
@
|
|
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
|
-
#
|
|
116
|
-
#
|
|
117
|
-
def
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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 `
|
|
130
|
-
|
|
131
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
180
|
+
RSpec.describe Hero do
|
|
151
181
|
it 'walks right at 200 units a second' do
|
|
152
|
-
|
|
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
|
|
188
|
+
60.times do
|
|
189
|
+
hero.control(actions)
|
|
190
|
+
hero.update(1.0 / 60.0)
|
|
191
|
+
end
|
|
155
192
|
|
|
156
|
-
expect(
|
|
193
|
+
expect(hero.x).to be_within(0.01).of(600.0)
|
|
157
194
|
end
|
|
158
195
|
end
|
|
159
196
|
```
|
|
160
197
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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*
|
|
166
|
-
|
|
167
|
-
code that needs
|
|
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.
|
|
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
|
-
###
|
|
369
|
+
### One call per layer, because the actors go between them
|
|
371
370
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
data/docs/api/components.md
CHANGED
|
@@ -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
|
-
|
|
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`
|
|
214
|
-
wraps a node's own draws in `renderer.rotated(abs_angle, …)`, so the node's
|
|
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`
|
|
251
|
-
|
|
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
|
|
258
|
-
`renderer.sprite` at the node's **world** origin (`abs_x`/`abs_y`) with no
|
|
259
|
-
[`
|
|
260
|
-
the camera.
|
|
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`)
|
|
301
|
-
|
|
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:,
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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 —
|
|
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:` |
|
|
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
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|