rgame 0.2.0 → 0.3.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 (253) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +135 -1
  3. data/README.md +67 -65
  4. data/docs/api/README.md +86 -82
  5. data/docs/api/app.md +125 -68
  6. data/docs/api/assets.md +203 -165
  7. data/docs/api/audio.md +130 -89
  8. data/docs/api/cli.md +259 -0
  9. data/docs/api/components.md +1032 -265
  10. data/docs/api/drawing.md +160 -160
  11. data/docs/api/examples.md +263 -0
  12. data/docs/api/game.md +149 -50
  13. data/docs/api/images.md +39 -39
  14. data/docs/api/input.md +226 -148
  15. data/docs/api/internals.md +241 -62
  16. data/docs/api/localization.md +285 -0
  17. data/docs/api/scene_graph.md +397 -244
  18. data/docs/api/signals.md +81 -76
  19. data/docs/api/systems.md +186 -73
  20. data/docs/api/text.md +58 -41
  21. data/docs/api/tile_maps.md +237 -0
  22. data/docs/api/toolbox.md +395 -146
  23. data/docs/api/ui.md +883 -54
  24. data/docs/api/values.md +216 -33
  25. data/examples/assets/README.md +322 -0
  26. data/examples/assets/blip.ogg +0 -0
  27. data/examples/assets/glyphs.json +5 -0
  28. data/examples/assets/glyphs.png +0 -0
  29. data/examples/assets/hero.json +12 -0
  30. data/examples/assets/hero.png +0 -0
  31. data/examples/assets/icons.json +13 -0
  32. data/examples/assets/icons.png +0 -0
  33. data/examples/assets/music.ogg +0 -0
  34. data/examples/assets/skills.json +10 -0
  35. data/examples/assets/skills.png +0 -0
  36. data/examples/assets/tileset.png +0 -0
  37. data/examples/assets/tileset.tsx +65 -0
  38. data/examples/assets/town.tmx +26 -0
  39. data/examples/assets/ui.json +11 -0
  40. data/examples/assets/ui.png +0 -0
  41. data/examples/collision/locales/en.yml +8 -0
  42. data/examples/collision/main.rb +316 -0
  43. data/examples/collision_tiles/locales/en.yml +9 -0
  44. data/examples/collision_tiles/main.rb +274 -0
  45. data/examples/fullscreen/locales/en.yml +10 -0
  46. data/examples/fullscreen/main.rb +216 -0
  47. data/examples/game_menu/locales/en.yml +8 -0
  48. data/examples/game_menu/main.rb +170 -0
  49. data/examples/input_glyphs/locales/en.yml +14 -0
  50. data/examples/input_glyphs/main.rb +213 -0
  51. data/examples/jump_topdown/locales/en.yml +9 -0
  52. data/examples/jump_topdown/main.rb +178 -0
  53. data/examples/localization/locales/de.yml +12 -0
  54. data/examples/localization/locales/en.yml +13 -0
  55. data/examples/localization/main.rb +158 -0
  56. data/examples/menu_navigation/locales/en.yml +23 -0
  57. data/examples/menu_navigation/main.rb +365 -0
  58. data/examples/music/locales/en.yml +7 -0
  59. data/examples/music/main.rb +134 -0
  60. data/examples/pathfinding/locales/en.yml +17 -0
  61. data/examples/pathfinding/main.rb +298 -0
  62. data/examples/pooling/locales/en.yml +7 -0
  63. data/examples/pooling/main.rb +259 -0
  64. data/examples/quick_wheel/locales/en.yml +16 -0
  65. data/examples/quick_wheel/main.rb +184 -0
  66. data/examples/radial_menu/locales/en.yml +16 -0
  67. data/examples/radial_menu/main.rb +184 -0
  68. data/examples/save_load/locales/en.yml +11 -0
  69. data/examples/save_load/main.rb +207 -0
  70. data/examples/save_load_ids/locales/en.yml +11 -0
  71. data/examples/save_load_ids/main.rb +322 -0
  72. data/examples/scroll_map/locales/en.yml +4 -0
  73. data/examples/scroll_map/main.rb +140 -0
  74. data/examples/signals/locales/en.yml +6 -0
  75. data/examples/signals/main.rb +278 -0
  76. data/examples/skill_bar/locales/en.yml +14 -0
  77. data/examples/skill_bar/main.rb +159 -0
  78. data/examples/sound/locales/en.yml +6 -0
  79. data/examples/sound/main.rb +122 -0
  80. data/examples/split_screen/locales/en.yml +9 -0
  81. data/examples/split_screen/main.rb +304 -0
  82. data/examples/sprite/locales/en.yml +8 -0
  83. data/examples/sprite/main.rb +180 -0
  84. data/examples/timer/locales/en.yml +12 -0
  85. data/examples/timer/main.rb +273 -0
  86. data/examples/velocity/locales/en.yml +6 -0
  87. data/examples/velocity/main.rb +196 -0
  88. data/examples/walk/locales/en.yml +4 -0
  89. data/examples/walk/main.rb +99 -0
  90. data/exe/rgame +9 -0
  91. data/ext/rgame_core/app/app.c +33 -3
  92. data/ext/rgame_core/app/locale.c +67 -0
  93. data/ext/rgame_core/app/locale.h +28 -0
  94. data/ext/rgame_core/audio/audio.c +39 -2
  95. data/ext/rgame_core/example.rb +0 -49
  96. data/ext/rgame_core/extconf.rb +0 -125
  97. data/ext/rgame_core/include/rgame/core.h +38 -2
  98. data/ext/rgame_core/ruby/audio_ext.c +10 -5
  99. data/ext/rgame_core/ruby/core_ext.c +30 -7
  100. data/ext/rgame_core/ruby/core_ext.h +3 -0
  101. data/ext/rgame_core/ruby/locale_ext.c +44 -0
  102. data/ext/rgame_core/ruby/recording_ext.c +1 -1
  103. data/ext/rgame_core/ruby/renderer_ext.c +20 -20
  104. data/ext/rgame_util/extconf.rb +2 -20
  105. data/ext/rgame_util/route_search.c +305 -0
  106. data/ext/rgame_util/route_search.h +86 -0
  107. data/ext/rgame_util/route_search_ext.c +150 -0
  108. data/ext/rgame_util/solid_grid.c +58 -0
  109. data/ext/rgame_util/solid_grid.h +49 -0
  110. data/ext/rgame_util/solid_grid_ext.c +161 -0
  111. data/ext/rgame_util/tile_sweep.c +164 -0
  112. data/ext/rgame_util/tile_sweep.h +62 -0
  113. data/ext/rgame_util/tile_sweep_ext.c +155 -0
  114. data/ext/rgame_util/util_ext.c +3 -0
  115. data/ext/rgame_util/util_ext.h +15 -0
  116. data/lib/rgame/boot.rb +0 -10
  117. data/lib/rgame/cli/new_project.rb +139 -0
  118. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  119. data/lib/rgame/cli/templates/README.md.tt +93 -0
  120. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  121. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  122. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  123. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  124. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  125. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  126. data/lib/rgame/cli/templates/rspec.tt +2 -0
  127. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  128. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  129. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  130. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  131. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  132. data/lib/rgame/cli.rb +66 -0
  133. data/lib/rgame/core/app.rb +6 -44
  134. data/lib/rgame/core/asset_manager.rb +13 -31
  135. data/lib/rgame/core/audio.rb +37 -16
  136. data/lib/rgame/core/font.rb +0 -3
  137. data/lib/rgame/core/locale.rb +22 -0
  138. data/lib/rgame/core/nine_slice.rb +0 -21
  139. data/lib/rgame/core/renderer.rb +6 -63
  140. data/lib/rgame/core/sprite_sheet.rb +0 -3
  141. data/lib/rgame/core/tile_map_renderer.rb +2 -19
  142. data/lib/rgame/core/ui_atlas.rb +28 -13
  143. data/lib/rgame/core.rb +1 -8
  144. data/lib/rgame/engine/actor_blockers.rb +131 -0
  145. data/lib/rgame/engine/animation_set.rb +1 -0
  146. data/lib/rgame/engine/audio_director.rb +36 -6
  147. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  148. data/lib/rgame/engine/camera.rb +3 -3
  149. data/lib/rgame/engine/circle_collider.rb +4 -2
  150. data/lib/rgame/engine/collision_box.rb +26 -1
  151. data/lib/rgame/engine/collision_system.rb +110 -22
  152. data/lib/rgame/engine/component.rb +34 -10
  153. data/lib/rgame/engine/components/action_trigger.rb +0 -1
  154. data/lib/rgame/engine/components/animated_sprite.rb +27 -25
  155. data/lib/rgame/engine/components/box_collider.rb +99 -0
  156. data/lib/rgame/engine/components/camera_follow.rb +6 -5
  157. data/lib/rgame/engine/components/character_body.rb +20 -61
  158. data/lib/rgame/engine/components/circle_collider.rb +47 -11
  159. data/lib/rgame/engine/components/collision_world.rb +159 -31
  160. data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
  161. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  162. data/lib/rgame/engine/components/hop.rb +76 -0
  163. data/lib/rgame/engine/components/identity.rb +73 -0
  164. data/lib/rgame/engine/components/mover.rb +285 -0
  165. data/lib/rgame/engine/components/navigator.rb +145 -0
  166. data/lib/rgame/engine/components/path_follow.rb +123 -31
  167. data/lib/rgame/engine/components/player_controller.rb +5 -2
  168. data/lib/rgame/engine/components/pool.rb +1 -1
  169. data/lib/rgame/engine/components/screen_wrap.rb +33 -11
  170. data/lib/rgame/engine/components/sprite.rb +12 -6
  171. data/lib/rgame/engine/components/targeting.rb +9 -11
  172. data/lib/rgame/engine/components/thrust_controller.rb +1 -1
  173. data/lib/rgame/engine/components/tile_world.rb +41 -13
  174. data/lib/rgame/engine/components/timer.rb +1 -1
  175. data/lib/rgame/engine/components/velocity.rb +23 -7
  176. data/lib/rgame/engine/components/wander_controller.rb +6 -2
  177. data/lib/rgame/engine/components/world.rb +133 -0
  178. data/lib/rgame/engine/contact_set.rb +74 -0
  179. data/lib/rgame/engine/culling.rb +4 -6
  180. data/lib/rgame/engine/debug_overlay.rb +5 -11
  181. data/lib/rgame/engine/i18n/plural.rb +45 -0
  182. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  183. data/lib/rgame/engine/i18n/template.rb +59 -0
  184. data/lib/rgame/engine/i18n.rb +276 -51
  185. data/lib/rgame/engine/input/action_mapper.rb +0 -26
  186. data/lib/rgame/engine/input/actions.rb +2 -8
  187. data/lib/rgame/engine/input/input_map.rb +39 -21
  188. data/lib/rgame/engine/nav_grid.rb +87 -0
  189. data/lib/rgame/engine/node2d.rb +248 -133
  190. data/lib/rgame/engine/path.rb +4 -6
  191. data/lib/rgame/engine/players.rb +6 -13
  192. data/lib/rgame/engine/presentation.rb +171 -0
  193. data/lib/rgame/engine/scene/scene_stack.rb +4 -4
  194. data/lib/rgame/engine/sealed_privates.rb +54 -0
  195. data/lib/rgame/engine/spatial_hash.rb +51 -19
  196. data/lib/rgame/engine/text.rb +194 -0
  197. data/lib/rgame/engine/tile_blockers.rb +63 -0
  198. data/lib/rgame/engine/tile_map.rb +2 -3
  199. data/lib/rgame/engine/tile_map_layer.rb +0 -2
  200. data/lib/rgame/engine/tileset.rb +2 -4
  201. data/lib/rgame/engine/timer.rb +2 -2
  202. data/lib/rgame/engine/ui/button.rb +248 -0
  203. data/lib/rgame/engine/ui/column.rb +20 -0
  204. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  205. data/lib/rgame/engine/ui/menu.rb +246 -71
  206. data/lib/rgame/engine/ui/navigation.rb +57 -0
  207. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  208. data/lib/rgame/engine/ui/option_button.rb +163 -0
  209. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  210. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  211. data/lib/rgame/engine/ui/pointing.rb +146 -0
  212. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  213. data/lib/rgame/engine/ui/ring.rb +55 -0
  214. data/lib/rgame/engine/ui/row.rb +21 -0
  215. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  216. data/lib/rgame/engine/ui/stack.rb +58 -0
  217. data/lib/rgame/engine/ui/stepping.rb +93 -0
  218. data/lib/rgame/engine/ui/text_button.rb +59 -0
  219. data/lib/rgame/engine/viewports.rb +2 -5
  220. data/lib/rgame/engine/world_view.rb +5 -4
  221. data/lib/rgame/engine.rb +31 -25
  222. data/lib/rgame/game.rb +99 -27
  223. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  224. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  225. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  226. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  227. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  228. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  229. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  230. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  231. data/lib/rgame/rubocop/default.yml +39 -0
  232. data/lib/rgame/rubocop/plugin.rb +45 -0
  233. data/lib/rgame/rubocop.rb +11 -0
  234. data/lib/rgame/util/color.rb +20 -24
  235. data/lib/rgame/util/controls.rb +14 -28
  236. data/lib/rgame/util/route_search.rb +27 -0
  237. data/lib/rgame/util/save_file.rb +107 -0
  238. data/lib/rgame/util/solid_grid.rb +37 -0
  239. data/lib/rgame/util/tensor.rb +0 -9
  240. data/lib/rgame/util/tile_sweep.rb +36 -0
  241. data/lib/rgame/util/z.rb +4 -14
  242. data/lib/rgame/util.rb +4 -3
  243. data/lib/rgame/version.rb +1 -1
  244. data/lib/rgame.rb +0 -15
  245. metadata +152 -11
  246. data/lib/rgame/engine/actor.rb +0 -53
  247. data/lib/rgame/engine/body.rb +0 -49
  248. data/lib/rgame/engine/cached_label.rb +0 -33
  249. data/lib/rgame/engine/input/player_controller.rb +0 -14
  250. data/lib/rgame/engine/matrix.rb +0 -32
  251. data/lib/rgame/engine/resettable.rb +0 -67
  252. data/lib/rgame/engine/tile_collision.rb +0 -78
  253. data/lib/rgame/engine/ui/menu_item.rb +0 -84
data/docs/api/drawing.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # Drawing
2
2
 
3
- `RGame::Core::Renderer` is what a game draws with. It is created from an app and
4
- used inside `draw`:
3
+ Everything on screen goes through `RGame::Core::Renderer`. **In an
4
+ `RGame::Game`, you never build one.** `Game` builds it and passes it to every
5
+ node's `on_draw(renderer, view)`. The examples on this page build one on a plain
6
+ `App` instead, to show the calls without a scene graph:
5
7
 
6
8
  ```ruby
7
9
  require 'rgame'
@@ -26,15 +28,16 @@ end
26
28
  MyGame.new.run
27
29
  ```
28
30
 
29
- Two things about that are worth knowing before anything else.
31
+ Know two rules before anything else.
30
32
 
31
- **Drawing is only legal inside `draw`.** Calling one of these from `update` or
32
- from a constructor raises. The frame is not open at those times, so the call
33
- would be silently discarded — and an invisible failure is worse than a loud one.
33
+ **Draw only inside `draw`**, or a node's `on_draw`. A drawing call from `update`
34
+ or from a constructor raises. The frame is not open then, so the call would vanish without a trace.
35
+ A loud failure beats an invisible one. `renderer.drawing?` returns whether a frame
36
+ is open.
34
37
 
35
- **Nothing is drawn immediately.** Calls accumulate, and the frame is sorted and
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 the scene tree does. See "Draw order" below.
38
+ **The renderer draws nothing immediately.** It collects calls, sorts the frame
39
+ and sends it to the GPU once, after `draw` returns. So call order does not decide
40
+ what ends up on top; the scene tree does. See "Draw order" below.
38
41
 
39
42
  ## Coordinates, colours and z
40
43
 
@@ -45,37 +48,36 @@ not decide what ends up on top — the scene tree does. See "Draw order" below.
45
48
  | `z:` | Where this call sits among **this node's own** drawing. −512…511. |
46
49
  | `color:` | `nil` (white), `[r, g, b]`, `[r, g, b, a]`, or a `RGame::Util::Color`. |
47
50
 
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.
51
+ **`z:` is an offset inside the current layer**, not a global number. It puts a
52
+ node's panel under its label and its shadow under its sprite. It cannot reach
53
+ anything else. A value outside −512…511 raises.
51
54
 
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.
55
+ Shapes default to `50`, text to `10` and images to `0`. A debug box or a health
56
+ bar without a `z:` therefore lands on top of *that node's* sprite. Calls with
57
+ equal z keep their call order. Without that rule, two sprites on one layer could
58
+ swap places from frame to frame, and players would see flicker.
57
59
 
58
60
  ## Draw order
59
61
 
60
- Order is decided in three steps, coarsest first, and only the last of them is a
61
- number a drawing call passes.
62
+ **The renderer orders a frame in three steps, coarsest first.** A drawing call
63
+ passes a number only for the last step.
62
64
 
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.
65
+ 1. **The band**: `:world` (the default), `:hud`, `:overlay` or `:debug`.
66
+ Everything in one band lies under everything in the next.
67
+ 2. **The slot.** The traversal walks the scene tree depth-first, siblings in `z`
68
+ order. Each node takes the next slot in its band when the walk reaches it.
69
+ Draw order is therefore **tree order**. A node's subtree forms one unbroken
70
+ run and cannot straddle a sibling.
71
+ 3. **The offset**: the `z:` above, inside one node's slot.
70
72
 
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".
73
+ A scene graph arranges all of this. `RGame::Engine::Node2D#draw` opens a layer per
74
+ node, so a game sets `z` on nodes and a `band` on the few that start one. See
75
+ [scene_graph.md](scene_graph.md), "Draw order".
74
76
 
75
77
  ### Opening a layer by hand
76
78
 
77
- Anything drawing outside the scene tree the debug overlay, a spec, a script —
78
- opens its own:
79
+ Code that draws outside the scene tree opens its own layer. The debug overlay, a
80
+ spec and a script all do:
79
81
 
80
82
  ```ruby
81
83
  renderer.layered(:hud) do
@@ -84,41 +86,39 @@ renderer.layered(:hud) do
84
86
  end
85
87
  ```
86
88
 
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.
89
+ `layered` takes the next slot in that band and measures every `z:` inside the
90
+ block from it. Afterwards it restores the previous base, even when the block
91
+ raises. Nesting *replaces* the base; it does not add to it. A node's slot depends
92
+ on where the traversal reached it, not on its ancestors' picks. Outside any block
93
+ the base is 0, so a bare script gets exactly the z it passes.
93
94
 
94
- `renderer.layer` reports the base currently in effect.
95
+ `renderer.layer` returns the base in effect.
95
96
 
96
- ### Why bands exist at all
97
+ ### Why bands exist
97
98
 
98
99
  A frame holds three kinds of content:
99
100
 
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
101
+ - **World**: inside a `WorldView`, drawn once per viewport, under a camera.
102
+ - **A player's own screen space**: their HUD and menu, drawn once and clipped to
103
+ their viewport (`PlayerLayer`).
104
+ - **Global screen space**: a cutscene or a results panel, drawn once across the
104
105
  whole window.
105
106
 
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.
107
+ The world is a different *space* from the other two, and the tree enforces that:
108
+ `WorldView` draws its subtree once per viewport. The other two share one space,
109
+ and the band tells them apart.
109
110
 
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`.
111
+ Two viewports may interleave in the sort without harm. Their commands carry
112
+ different clips and land on different pixels. Order *within* one viewport
113
+ matters, and drawing a HUD after the world does not put it on top. Its band
114
+ does. Bands lie `2**40` apart and a `z:` spans 1024, so no offset can carry a
115
+ call into the next band. See `RGame::Util::Z`.
116
116
 
117
117
  ### Colours and allocation
118
118
 
119
- Passing a `Color` allocates nothing it is a frozen value, and the same one can
120
- be shared by every sprite that uses it. Passing an array allocates a colour per
121
- call, which is fine at setup and wasteful sixty times a second:
119
+ **Passing a `Color` allocates nothing.** A `Color` is a frozen value, so every
120
+ sprite can share the same one. Passing an array allocates a colour on every
121
+ call. That is fine at setup and wasteful sixty times a second:
122
122
 
123
123
  ```ruby
124
124
  RED = RGame::Util::Color.new(224, 64, 64) # once
@@ -139,20 +139,17 @@ renderer.circle(cx, cy, radius, z: 50, color: nil, segments: 64)
139
139
  renderer.debug_box(x, y, width, height, z: 50)
140
140
  ```
141
141
 
142
- A **quad's** four points are taken in loop order top-left, top-right,
143
- bottom-right, bottom-left for a rectangle. Listing them in Z order gives an
144
- hourglass.
142
+ A **quad** takes its four points in loop order: top-left, top-right,
143
+ bottom-right, bottom-left for a rectangle. Points in Z order give an hourglass.
145
144
 
146
- A **line** has real thickness because it is drawn as a quad. OpenGL's own line
147
- width is a suggestion drivers are free to ignore above one pixel, so a line
148
- worth seeing has to be a shape.
145
+ A **line** has real thickness, because the renderer draws it as a quad. Drivers
146
+ may ignore OpenGL's own line width above one pixel.
149
147
 
150
- A **circle** is a fan of triangles, and the whole fan is one batch there is no
151
- cached circle texture to warm up and nothing to configure. `segments:` is there
152
- for the rare case where 64 is too many or too few.
148
+ A **circle** is a fan of triangles in one batch. It needs no cached texture and
149
+ no configuration. Adjust `segments:` if 64 is too many or too few.
153
150
 
154
- `debug_box` is a translucent red rectangle for visualising a collision box, so a
155
- scene can ask for one without deciding what colour "debug" is.
151
+ `debug_box` draws a translucent red rectangle to show a collision box. A scene
152
+ can ask for one without choosing a debug colour.
156
153
 
157
154
  ## Images
158
155
 
@@ -162,63 +159,65 @@ renderer.image_at(image, x, y, scale_x: 1, scale_y: 1, z: 0, color: nil)
162
159
  renderer.background(image, x = 0, y = 0, z: 0, color: nil)
163
160
  ```
164
161
 
165
- Three anchors for three jobs. `image` **centres** on the position given and
166
- rotates about that centre — the sprite case. `image_at` places the **top-left**
167
- corner and scales each axis on its own tiles, nine-slice corners, sheet
168
- frames. `background` is `image_at` at natural size, named for its usual job.
162
+ Each method anchors the image differently:
163
+
164
+ - `image` **centres** the image on the position and rotates it about that
165
+ centre. Use it for sprites.
166
+ - `image_at` places the **top-left** corner and scales each axis separately. Use
167
+ it for tiles, nine-slice corners and sheet frames.
168
+ - `background` is `image_at` at natural size.
169
169
 
170
170
  ### Mirroring
171
171
 
172
- A negative scale on `image_at` mirrors the image **inside the same rectangle**.
173
- It does not move it:
172
+ **A negative scale on `image_at` mirrors the image inside the same rectangle.**
173
+ It does not move the image:
174
174
 
175
175
  ```ruby
176
176
  renderer.image_at(frame, x, y, scale_x: facing_left ? -1 : 1)
177
177
  ```
178
178
 
179
- Both calls cover the same pixels; only the picture is reversed. That is worth
180
- knowing if you are coming from Gosu, where a negative scale mirrors *about* the
181
- anchor and the caller adds a width back to compensate. Here `(x, y)` is the
182
- top-left corner whatever the sign, so there is nothing to compensate for and
183
- nothing to forget.
179
+ Both calls cover the same pixels; only the picture is reversed. `(x, y)` stays
180
+ the top-left corner whatever the scale's sign, so a mirrored sprite stays put.
181
+ Mirroring about the anchor would shift the image one width to the left. Every
182
+ flipped draw would then have to add that width back.
184
183
 
185
184
  A scale of `0` draws nothing.
186
185
 
187
- `color:` tints: the image's pixels are multiplied by it, so white leaves the
188
- image alone and a colour with alpha fades it.
186
+ `color:` tints the image by multiplying its pixels. White leaves the image
187
+ unchanged, and a colour with alpha fades it.
189
188
 
190
- See [Images](images.md) for loading files and slicing sprite sheets.
189
+ [Images](images.md) covers loading files and slicing tiles; [Assets](assets.md#sprite-sheets) covers sprite sheets.
191
190
 
192
- **An image can only be drawn by the app that loaded it.** GPU textures belong to
193
- one window's OpenGL context and are not shared with another, so drawing another
194
- app's image would sample nothing and paint a plain white rectangle. Rather than
195
- let that happen quietly, it raises `ArgumentError`. In a one-window game which
196
- is nearly all of them — this never comes up.
191
+ **Only the app that loaded an image can draw it.** A GPU texture belongs to one
192
+ window's OpenGL context. Drawing another app's image would sample nothing and
193
+ paint a plain white rectangle, so the renderer raises `ArgumentError` instead. A
194
+ one-window game never meets this.
197
195
 
198
196
  ## Drawing by id
199
197
 
200
- Game logic names an asset; it does not hold one. That is not a convenience —
201
- the scene layer may hold `RGame::Util` values but no `RGame::Core` handle at
202
- all, so a Symbol or a path is the only thing a node *can* carry.
198
+ **Game logic names an asset; it does not hold one.** The scene layer may hold
199
+ `RGame::Util` values but no `RGame::Core` handle. A Symbol or a path is the only
200
+ thing a node *can* carry.
203
201
 
204
- An id is normally a **root-relative path**, resolved through the app's
205
- [asset manager](assets.md) and then remembered:
202
+ An id is normally a **root-relative path**. The renderer resolves it through the
203
+ app's [asset manager](assets.md) and remembers the result:
206
204
 
207
205
  ```ruby
208
- renderer.sprite('example 09/player.json', row, col, x, y, flip_x: false, z: 0)
206
+ renderer.sprite('hero.json', row, col, x, y, flip_x: false, z: 0)
209
207
  renderer.image('space.png', cx, cy, angle: 0, scale: 1)
210
208
  renderer.background('space.png')
211
- renderer.tilemap('map/island.tmx', layer, camera_x, camera_y, viewport_w, viewport_h)
209
+ renderer.tilemap('map/island.tmx', layer, cull_x, cull_y, cull_w, cull_h, elapsed: 0.0) # draws in world coordinates
210
+ renderer.nine_slice(:panel, x, y, width, height, z: 0, tint: nil)
212
211
  ```
213
212
 
214
- Nothing has to be set up for that: `Renderer.new(app)` takes the app's own
215
- manager, so a path just works. `Renderer.new(app, assets: other)` overrides it.
213
+ Paths need no setup. `Renderer.new(app)` uses the app's own manager.
214
+ `Renderer.new(app, assets: other)` uses a different one.
216
215
 
217
216
  ### Registering
218
217
 
219
- `register_*` pre-binds an id to an object you chose, and wins over the asset
220
- manager. It is for the two things a path cannot name: an id that is not a file,
221
- and an object the game assembled itself.
218
+ `register_*` binds an id to an object you choose, and takes priority over the
219
+ asset manager. Use it for what a path cannot name: an id that is not a file, and
220
+ an object the game built itself.
222
221
 
223
222
  ```ruby
224
223
  renderer.register_image(:space, app.assets.image('space.png'))
@@ -230,10 +229,10 @@ renderer.register_ui_atlas(atlas) # every element under its own name
230
229
  renderer.image(:space, 100, 100)
231
230
  ```
232
231
 
233
- **Nine-slices are registration-only.** Their ids name an *element of an atlas*,
234
- not a file, so there is nothing for a manager to resolve them to.
232
+ **Nine-slices must be registered.** Their ids name an *element of an atlas*, not
233
+ a file, so an asset manager has nothing to resolve.
235
234
 
236
- ### What resolution does
235
+ ### How the renderer resolves an id
237
236
 
238
237
  | Given | |
239
238
  |---|---|
@@ -243,19 +242,18 @@ not a file, so there is nothing for a manager to resolve them to.
243
242
  | A `Symbol` that is not registered | `KeyError`, naming the id and the type |
244
243
  | `nil` | `TypeError` |
245
244
 
246
- A Symbol is never offered to the asset manager, because only a String can be a
247
- path. So a typo'd Symbol says "no sheet registered for `:heor`" rather than
248
- whatever a loader makes of being handed a Symbol for a filename — and a broken
249
- *file* still raises its own `LoadError` naming it, which is a different bug
250
- wanting a different fix.
245
+ The renderer never offers a Symbol to the asset manager, because only a String
246
+ can be a path. A mistyped Symbol therefore raises "no sheet registered for
247
+ `:heor`". A broken *file* raises its own `LoadError` naming the file. The two
248
+ errors point at two different fixes.
251
249
 
252
- Resolution happens once per id and the answer is kept, so per-frame drawing
253
- neither re-resolves nor allocates a lookup key.
250
+ The renderer resolves each id once and keeps the answer. Per-frame drawing
251
+ neither resolves again nor allocates a lookup key.
254
252
 
255
253
  ## Transform blocks
256
254
 
257
- Each of these applies to everything drawn inside it, and undoes itself
258
- afterwards — including when the block raises.
255
+ Each block applies to everything drawn inside it and undoes itself afterwards,
256
+ even when the block raises.
259
257
 
260
258
  ```ruby
261
259
  renderer.translated(dx, dy) { ... }
@@ -265,7 +263,7 @@ renderer.clipped(x, y, width, height) { ... }
265
263
  renderer.layered(band) { ... } # see "Draw order" above
266
264
  ```
267
265
 
268
- They nest, and they compose in the order they are opened:
266
+ Blocks nest and compose in the order you open them:
269
267
 
270
268
  ```ruby
271
269
  renderer.translated(-camera.x, -camera.y) do # world space -> screen space
@@ -275,18 +273,24 @@ renderer.translated(-camera.x, -camera.y) do # world space -> screen space
275
273
  end
276
274
  ```
277
275
 
278
- `translated` is how a camera works, and the reason it is a *draw-time* transform
279
- rather than something baked into positions is that the same world can then be
280
- drawn twice, under two different offsets — which is what split-screen is.
276
+ **A camera is a `translated` block.** Because the offset applies at draw time,
277
+ the same world can be drawn twice under two different offsets. That is
278
+ split-screen.
279
+
280
+ `rotated(0, …)`, `translated(0, 0)` and `scaled(1)` cost nothing. They skip the
281
+ transform and run the block, so unrotated drawing pays nothing.
281
282
 
282
- `rotated(0, …)`, `translated(0, 0)` and `scaled(1)` are free: they skip the
283
- transform entirely and just run the block, so unrotated drawing pays nothing.
283
+ **Inside a scene graph you rarely open a transform block yourself.** The examples
284
+ on this page drive the renderer from an `App`, in window coordinates. For a
285
+ `Node2D`, the traversal pushes the node's transform before it calls `on_draw`. A
286
+ node therefore draws at *its own* origin, and passing its position would apply it
287
+ twice. See [Scene graph](scene_graph.md#drawing-happens-in-local-space).
284
288
 
285
289
  ### Clipping and split-screen
286
290
 
287
- A clip **narrows**. Nesting one inside another intersects them, so a child can
288
- never draw outside the region its parent allowed. Two clipped blocks are a
289
- split screen:
291
+ **A clip narrows.** A nested clip intersects with its parent, so a child never
292
+ draws outside the region its parent allowed. Two clipped blocks make a split
293
+ screen:
290
294
 
291
295
  ```ruby
292
296
  def draw
@@ -300,17 +304,17 @@ def draw
300
304
  end
301
305
  ```
302
306
 
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.
307
+ **A game does not write that.**
308
+ [`RGame::Engine::WorldView`](scene_graph.md#view-transforms-and-the-camera) does it
309
+ once per active player, with the layout's rectangles and each player's camera.
310
+ Call `clipped` directly for a region of your own, such as a minimap or a
311
+ scrolling list.
308
312
 
309
313
  ## Recordings: bake once, replay cheaply
310
314
 
311
- A tile layer is a couple of thousand quads that have not changed since the level
312
- loaded. `record` bakes a block of drawing so that replaying it costs one call
313
- per texture, however many draws went into it:
315
+ **`record` bakes a block of drawing, and a replay costs one call per texture.** A
316
+ tile layer holds a few thousand quads that stay the same once the level loads, so
317
+ it is the typical case:
314
318
 
315
319
  ```ruby
316
320
  def draw
@@ -322,58 +326,58 @@ def draw
322
326
  end
323
327
  ```
324
328
 
325
- Nothing is drawn at bake time — the block's output goes into the recording
326
- instead of into the frame. `record` must be called inside `draw` like everything
327
- else, which is why the example bakes on the first frame rather than in
328
- `initialize`.
329
+ Baking draws nothing; the block's output goes into the recording, not the frame.
330
+ `record` must run inside `draw` like every other call. The example therefore
331
+ bakes on the first frame, not in `initialize`.
329
332
 
330
333
  ```ruby
331
334
  baked.draw(x = 0, y = 0, z: 0, color: nil)
332
335
  baked.batch_count # GL calls one replay costs
333
- baked.width # the size of what was baked
336
+ baked.vertex_count # vertices baked in
337
+ baked.width # the size of what was baked, with #height
334
338
  baked.empty?
335
339
  ```
336
340
 
337
- **Positions, texture coordinates, colours and any transforms inside the block
338
- are baked in.** The transform in effect when the recording is *drawn* applies on
339
- top, so a baked layer scrolls under a camera without being rebuilt, and the same
340
- recording can be stamped in several places:
341
+ **A recording bakes in positions, texture coordinates, colours and any
342
+ transforms inside the block.** The transform in effect at replay applies on top.
343
+ A baked layer scrolls under a camera without a rebuild, and one recording can be
344
+ stamped in several places:
341
345
 
342
346
  ```ruby
343
347
  5.times { |i| @bush.draw(i * 120, 300) }
344
348
  ```
345
349
 
346
- **`color:` tints the replay** each recorded colour is multiplied by it, so a
347
- whole baked layer can be faded out at once.
350
+ **`color:` tints the replay.** The renderer multiplies each recorded colour by
351
+ it, so you can fade a whole baked layer at once.
348
352
 
349
- **Clipping cannot be baked.** Clipping happens when pixels are rasterised, so a
350
- clip rectangle captured in one place would be wrong everywhere else the
351
- recording is drawn. Pushing a clip inside a `record` block raises; clip the
352
- replay instead, which is what was meant anyway:
353
+ **A recording cannot contain a clip.** Clipping happens at rasterisation, so a
354
+ clip captured in one place would be wrong everywhere else the recording is drawn.
355
+ Pushing a clip inside a `record` block raises. Clip the replay instead:
353
356
 
354
357
  ```ruby
355
358
  @renderer.clipped(0, 0, 400, 600) { @ground.draw(-@camera.x, -@camera.y) }
356
359
  ```
357
360
 
358
- Recordings do not nest, and a block that raises leaves nothing half-recorded
359
- behind. A recording keeps the images baked into it alive, so a sprite sheet
360
- dropped after baking does not take its texture with it.
361
+ Recordings do not nest. A block that raises leaves no half-built recording
362
+ behind. A recording keeps its baked images alive, so dropping a sprite sheet
363
+ after baking does not free its texture.
361
364
 
362
365
  ## Testing what a scene draws
363
366
 
364
- The renderer is an interface, not a class your game should name. Game logic
365
- receives one and calls methods on it; a headless spec passes a recording fake
366
- instead and asserts on the calls:
367
+ **Treat the renderer as an interface, not a class your game names.** Game logic
368
+ receives a renderer and calls its methods. A headless spec passes a recording
369
+ fake instead and asserts on the calls. rgame's own suite uses `FakeRenderer`
370
+ from `spec/support/`:
367
371
 
368
372
  ```ruby
369
373
  renderer = FakeRenderer.new
370
- health_bar.draw(renderer)
374
+ health_bar.on_draw(renderer, nil)
371
375
 
372
376
  expect(renderer.calls_to(:rect).map(&:args)).to eq([[10, 10, 64, 8]])
373
377
  ```
374
378
 
375
- Recordings are faked too, and the fake keeps the two questions apart what was
376
- baked, and where it was replayed:
379
+ The fake also records recordings, and keeps two questions apart: what was baked,
380
+ and where it was replayed.
377
381
 
378
382
  ```ruby
379
383
  ground = renderer.record { ... } # => a FakeRecording
@@ -382,17 +386,13 @@ expect(ground.calls.size).to eq(tiles.size) # baked once, not per frame
382
386
  expect(ground.draws.map(&:args)).to eq([[-camera.x, -camera.y]])
383
387
  ```
384
388
 
385
- That runs with no window, no GPU and no clock. The fake and the real renderer
386
- are both checked against one shared contract (`spec/support/shared_examples/
387
- a_renderer.rb`), so the fake cannot drift into describing a renderer that does
388
- not exist — which would leave a green test suite and a game that no longer runs.
389
+ These specs run with no window, no GPU and no clock. One shared contract,
390
+ `spec/support/shared_examples/a_renderer.rb`, checks both the fake and the real
391
+ renderer. A fake that drifted from the real renderer would keep the suite green
392
+ while the game stopped drawing.
389
393
 
390
394
  ## Text
391
395
 
392
396
  `renderer.text(string, x, y)` draws a line of text, and `text_width` measures
393
- one. See [Text](text.md) for fonts, the shipped default and what it covers.
394
-
395
- ## What is not here yet
396
-
397
- Audio and drawing by asset id (`sprite(:hero, row, col, …)`) are still to come.
398
- Today an image is passed as an object rather than looked up in a registry.
397
+ one. Both take a String or an [`Engine::Text`](toolbox.md#text--the-string-a-node-draws). [Text](text.md) covers fonts, the shipped default and the characters it
398
+ covers.