rgame 0.4.0-x86_64-linux-gnu

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 (239) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +251 -0
  3. data/LICENSE +26 -0
  4. data/README.md +325 -0
  5. data/docs/api/README.md +224 -0
  6. data/docs/api/app.md +249 -0
  7. data/docs/api/assets.md +463 -0
  8. data/docs/api/audio.md +249 -0
  9. data/docs/api/cli.md +259 -0
  10. data/docs/api/components.md +1112 -0
  11. data/docs/api/drawing.md +398 -0
  12. data/docs/api/examples.md +263 -0
  13. data/docs/api/game.md +219 -0
  14. data/docs/api/images.md +118 -0
  15. data/docs/api/input.md +476 -0
  16. data/docs/api/internals.md +289 -0
  17. data/docs/api/localization.md +285 -0
  18. data/docs/api/scene_graph.md +539 -0
  19. data/docs/api/signals.md +147 -0
  20. data/docs/api/systems.md +231 -0
  21. data/docs/api/text.md +133 -0
  22. data/docs/api/tile_maps.md +237 -0
  23. data/docs/api/toolbox.md +493 -0
  24. data/docs/api/ui.md +927 -0
  25. data/docs/api/values.md +316 -0
  26. data/examples/assets/README.md +322 -0
  27. data/examples/assets/blip.ogg +0 -0
  28. data/examples/assets/glyphs.json +5 -0
  29. data/examples/assets/glyphs.png +0 -0
  30. data/examples/assets/hero.json +12 -0
  31. data/examples/assets/hero.png +0 -0
  32. data/examples/assets/icons.json +13 -0
  33. data/examples/assets/icons.png +0 -0
  34. data/examples/assets/music.ogg +0 -0
  35. data/examples/assets/skills.json +10 -0
  36. data/examples/assets/skills.png +0 -0
  37. data/examples/assets/tileset.png +0 -0
  38. data/examples/assets/tileset.tsx +65 -0
  39. data/examples/assets/town.tmx +26 -0
  40. data/examples/assets/ui.json +11 -0
  41. data/examples/assets/ui.png +0 -0
  42. data/examples/collision/locales/en.yml +8 -0
  43. data/examples/collision/main.rb +316 -0
  44. data/examples/collision_tiles/locales/en.yml +9 -0
  45. data/examples/collision_tiles/main.rb +274 -0
  46. data/examples/fullscreen/locales/en.yml +10 -0
  47. data/examples/fullscreen/main.rb +216 -0
  48. data/examples/game_menu/locales/en.yml +8 -0
  49. data/examples/game_menu/main.rb +170 -0
  50. data/examples/input_glyphs/locales/en.yml +14 -0
  51. data/examples/input_glyphs/main.rb +213 -0
  52. data/examples/jump_topdown/locales/en.yml +9 -0
  53. data/examples/jump_topdown/main.rb +178 -0
  54. data/examples/localization/locales/de.yml +12 -0
  55. data/examples/localization/locales/en.yml +13 -0
  56. data/examples/localization/main.rb +158 -0
  57. data/examples/menu_navigation/locales/en.yml +23 -0
  58. data/examples/menu_navigation/main.rb +365 -0
  59. data/examples/music/locales/en.yml +7 -0
  60. data/examples/music/main.rb +134 -0
  61. data/examples/pathfinding/locales/en.yml +17 -0
  62. data/examples/pathfinding/main.rb +298 -0
  63. data/examples/pooling/locales/en.yml +7 -0
  64. data/examples/pooling/main.rb +259 -0
  65. data/examples/quick_wheel/locales/en.yml +16 -0
  66. data/examples/quick_wheel/main.rb +184 -0
  67. data/examples/radial_menu/locales/en.yml +16 -0
  68. data/examples/radial_menu/main.rb +184 -0
  69. data/examples/save_load/locales/en.yml +11 -0
  70. data/examples/save_load/main.rb +207 -0
  71. data/examples/save_load_ids/locales/en.yml +11 -0
  72. data/examples/save_load_ids/main.rb +322 -0
  73. data/examples/scroll_map/locales/en.yml +4 -0
  74. data/examples/scroll_map/main.rb +140 -0
  75. data/examples/signals/locales/en.yml +6 -0
  76. data/examples/signals/main.rb +278 -0
  77. data/examples/skill_bar/locales/en.yml +14 -0
  78. data/examples/skill_bar/main.rb +159 -0
  79. data/examples/sound/locales/en.yml +6 -0
  80. data/examples/sound/main.rb +122 -0
  81. data/examples/split_screen/locales/en.yml +9 -0
  82. data/examples/split_screen/main.rb +304 -0
  83. data/examples/sprite/locales/en.yml +8 -0
  84. data/examples/sprite/main.rb +180 -0
  85. data/examples/timer/locales/en.yml +12 -0
  86. data/examples/timer/main.rb +273 -0
  87. data/examples/velocity/locales/en.yml +6 -0
  88. data/examples/velocity/main.rb +196 -0
  89. data/examples/walk/locales/en.yml +4 -0
  90. data/examples/walk/main.rb +99 -0
  91. data/exe/rgame +9 -0
  92. data/lib/rgame/boot.rb +3 -0
  93. data/lib/rgame/cli/new_project.rb +139 -0
  94. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  95. data/lib/rgame/cli/templates/README.md.tt +93 -0
  96. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  97. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  98. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  99. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  100. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  101. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  102. data/lib/rgame/cli/templates/rspec.tt +2 -0
  103. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  104. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  105. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  106. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  107. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  108. data/lib/rgame/cli.rb +66 -0
  109. data/lib/rgame/core/app.rb +46 -0
  110. data/lib/rgame/core/asset_manager.rb +206 -0
  111. data/lib/rgame/core/audio.rb +145 -0
  112. data/lib/rgame/core/font.rb +46 -0
  113. data/lib/rgame/core/gamepad.rb +55 -0
  114. data/lib/rgame/core/image.rb +55 -0
  115. data/lib/rgame/core/input.rb +71 -0
  116. data/lib/rgame/core/locale.rb +22 -0
  117. data/lib/rgame/core/nine_slice.rb +142 -0
  118. data/lib/rgame/core/recording.rb +54 -0
  119. data/lib/rgame/core/renderer.rb +354 -0
  120. data/lib/rgame/core/sprite_sheet.rb +105 -0
  121. data/lib/rgame/core/tile_map_renderer.rb +172 -0
  122. data/lib/rgame/core/ui_atlas.rb +101 -0
  123. data/lib/rgame/core/virtual_gamepad.rb +26 -0
  124. data/lib/rgame/core.rb +18 -0
  125. data/lib/rgame/core_ext.so +0 -0
  126. data/lib/rgame/engine/actor_blockers.rb +131 -0
  127. data/lib/rgame/engine/animation_set.rb +50 -0
  128. data/lib/rgame/engine/animator.rb +44 -0
  129. data/lib/rgame/engine/audio_bus.rb +24 -0
  130. data/lib/rgame/engine/audio_director.rb +59 -0
  131. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  132. data/lib/rgame/engine/camera.rb +78 -0
  133. data/lib/rgame/engine/circle_collider.rb +34 -0
  134. data/lib/rgame/engine/collision_box.rb +59 -0
  135. data/lib/rgame/engine/collision_system.rb +132 -0
  136. data/lib/rgame/engine/component.rb +64 -0
  137. data/lib/rgame/engine/components/action_trigger.rb +40 -0
  138. data/lib/rgame/engine/components/animated_sprite.rb +71 -0
  139. data/lib/rgame/engine/components/box_collider.rb +99 -0
  140. data/lib/rgame/engine/components/camera_follow.rb +45 -0
  141. data/lib/rgame/engine/components/character_body.rb +50 -0
  142. data/lib/rgame/engine/components/circle_collider.rb +80 -0
  143. data/lib/rgame/engine/components/collision_world.rb +231 -0
  144. data/lib/rgame/engine/components/despawn_offscreen.rb +42 -0
  145. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  146. data/lib/rgame/engine/components/hop.rb +76 -0
  147. data/lib/rgame/engine/components/identity.rb +73 -0
  148. data/lib/rgame/engine/components/mover.rb +285 -0
  149. data/lib/rgame/engine/components/navigator.rb +145 -0
  150. data/lib/rgame/engine/components/path_follow.rb +176 -0
  151. data/lib/rgame/engine/components/player_controller.rb +27 -0
  152. data/lib/rgame/engine/components/pool.rb +53 -0
  153. data/lib/rgame/engine/components/screen_wrap.rb +49 -0
  154. data/lib/rgame/engine/components/sprite.rb +47 -0
  155. data/lib/rgame/engine/components/targeting.rb +52 -0
  156. data/lib/rgame/engine/components/thrust_controller.rb +65 -0
  157. data/lib/rgame/engine/components/tile_world.rb +109 -0
  158. data/lib/rgame/engine/components/timer.rb +75 -0
  159. data/lib/rgame/engine/components/velocity.rb +43 -0
  160. data/lib/rgame/engine/components/wander_controller.rb +64 -0
  161. data/lib/rgame/engine/components/world.rb +133 -0
  162. data/lib/rgame/engine/contact_set.rb +74 -0
  163. data/lib/rgame/engine/culling.rb +45 -0
  164. data/lib/rgame/engine/debug_overlay.rb +111 -0
  165. data/lib/rgame/engine/i18n/plural.rb +45 -0
  166. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  167. data/lib/rgame/engine/i18n/template.rb +59 -0
  168. data/lib/rgame/engine/i18n.rb +322 -0
  169. data/lib/rgame/engine/input/action_mapper.rb +100 -0
  170. data/lib/rgame/engine/input/actions.rb +92 -0
  171. data/lib/rgame/engine/input/input_map.rb +196 -0
  172. data/lib/rgame/engine/layout.rb +82 -0
  173. data/lib/rgame/engine/nav_grid.rb +87 -0
  174. data/lib/rgame/engine/node2d.rb +555 -0
  175. data/lib/rgame/engine/path.rb +76 -0
  176. data/lib/rgame/engine/player.rb +69 -0
  177. data/lib/rgame/engine/player_layer.rb +70 -0
  178. data/lib/rgame/engine/players.rb +205 -0
  179. data/lib/rgame/engine/pool.rb +51 -0
  180. data/lib/rgame/engine/presentation.rb +171 -0
  181. data/lib/rgame/engine/scene/scene_stack.rb +87 -0
  182. data/lib/rgame/engine/sealed_privates.rb +54 -0
  183. data/lib/rgame/engine/signal.rb +75 -0
  184. data/lib/rgame/engine/spatial_hash.rb +116 -0
  185. data/lib/rgame/engine/text.rb +194 -0
  186. data/lib/rgame/engine/tile_blockers.rb +63 -0
  187. data/lib/rgame/engine/tile_map.rb +147 -0
  188. data/lib/rgame/engine/tile_map_layer.rb +82 -0
  189. data/lib/rgame/engine/tileset.rb +99 -0
  190. data/lib/rgame/engine/timer.rb +51 -0
  191. data/lib/rgame/engine/ui/button.rb +248 -0
  192. data/lib/rgame/engine/ui/column.rb +20 -0
  193. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  194. data/lib/rgame/engine/ui/menu.rb +290 -0
  195. data/lib/rgame/engine/ui/navigation.rb +57 -0
  196. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  197. data/lib/rgame/engine/ui/option_button.rb +163 -0
  198. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  199. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  200. data/lib/rgame/engine/ui/pointing.rb +146 -0
  201. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  202. data/lib/rgame/engine/ui/ring.rb +55 -0
  203. data/lib/rgame/engine/ui/row.rb +21 -0
  204. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  205. data/lib/rgame/engine/ui/stack.rb +58 -0
  206. data/lib/rgame/engine/ui/stepping.rb +93 -0
  207. data/lib/rgame/engine/ui/text_button.rb +59 -0
  208. data/lib/rgame/engine/view.rb +76 -0
  209. data/lib/rgame/engine/viewports.rb +171 -0
  210. data/lib/rgame/engine/world_view.rb +71 -0
  211. data/lib/rgame/engine.rb +86 -0
  212. data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
  213. data/lib/rgame/fonts/OFL.txt +102 -0
  214. data/lib/rgame/game.rb +271 -0
  215. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  216. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  217. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  218. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  219. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  220. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  221. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  222. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  223. data/lib/rgame/rubocop/default.yml +39 -0
  224. data/lib/rgame/rubocop/plugin.rb +45 -0
  225. data/lib/rgame/rubocop.rb +11 -0
  226. data/lib/rgame/util/color.rb +23 -0
  227. data/lib/rgame/util/controls.rb +169 -0
  228. data/lib/rgame/util/route_search.rb +27 -0
  229. data/lib/rgame/util/save_file.rb +107 -0
  230. data/lib/rgame/util/solid_grid.rb +37 -0
  231. data/lib/rgame/util/tensor.rb +3 -0
  232. data/lib/rgame/util/tile_sweep.rb +36 -0
  233. data/lib/rgame/util/z.rb +123 -0
  234. data/lib/rgame/util.rb +10 -0
  235. data/lib/rgame/util_ext.so +0 -0
  236. data/lib/rgame/version.rb +12 -0
  237. data/lib/rgame.rb +5 -0
  238. data/licenses/SDL2/LICENSE.txt +18 -0
  239. metadata +309 -0
@@ -0,0 +1,398 @@
1
+ # Drawing
2
+
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:
7
+
8
+ ```ruby
9
+ require 'rgame'
10
+ require 'rgame/core'
11
+
12
+ class MyGame < RGame::Core::App
13
+ Color = RGame::Util::Color
14
+
15
+ def initialize
16
+ super(width: 800, height: 600, caption: 'demo')
17
+ @renderer = RGame::Core::Renderer.new(self)
18
+ @hero = RGame::Core::Image.new(self, 'hero.png')
19
+ end
20
+
21
+ def draw
22
+ @renderer.rect(40, 40, 160, 100, color: Color.new(224, 64, 64))
23
+ @renderer.circle(620, 110, 70, color: Color.new(64, 96, 224))
24
+ @renderer.image(@hero, 400, 300, angle: 45)
25
+ end
26
+ end
27
+
28
+ MyGame.new.run
29
+ ```
30
+
31
+ Know two rules before anything else.
32
+
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.
37
+
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.
41
+
42
+ ## Coordinates, colours and z
43
+
44
+ | | |
45
+ |---|---|
46
+ | Origin | Top-left. x grows right, y grows **down**. |
47
+ | Angles | Degrees. A **positive angle turns clockwise** on screen. |
48
+ | `z:` | Where this call sits among **this node's own** drawing. −512…511. |
49
+ | `color:` | `nil` (white), `[r, g, b]`, `[r, g, b, a]`, or a `RGame::Util::Color`. |
50
+
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.
54
+
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.
59
+
60
+ ## Draw order
61
+
62
+ **The renderer orders a frame in three steps, coarsest first.** A drawing call
63
+ passes a number only for the last step.
64
+
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.
72
+
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".
76
+
77
+ ### Opening a layer by hand
78
+
79
+ Code that draws outside the scene tree opens its own layer. The debug overlay, a
80
+ spec and a script all do:
81
+
82
+ ```ruby
83
+ renderer.layered(:hud) do
84
+ renderer.nine_slice(:panel, x, y, w, h)
85
+ renderer.text(score, x + 8, y + 6, z: 1) # above this layer's own panel
86
+ end
87
+ ```
88
+
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.
94
+
95
+ `renderer.layer` returns the base in effect.
96
+
97
+ ### Why bands exist
98
+
99
+ A frame holds three kinds of content:
100
+
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
105
+ whole window.
106
+
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.
110
+
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
+
117
+ ### Colours and allocation
118
+
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
+
123
+ ```ruby
124
+ RED = RGame::Util::Color.new(224, 64, 64) # once
125
+
126
+ def draw
127
+ @renderer.rect(10, 10, 50, 50, color: RED) # every frame, allocation-free
128
+ end
129
+ ```
130
+
131
+ ## Shapes
132
+
133
+ ```ruby
134
+ renderer.rect(x, y, width, height, z: 50, color: nil)
135
+ renderer.quad(x1, y1, x2, y2, x3, y3, x4, y4, z: 50, color: nil)
136
+ renderer.triangle(x1, y1, x2, y2, x3, y3, z: 50, color: nil)
137
+ renderer.line(x1, y1, x2, y2, thickness: 1.0, z: 50, color: nil)
138
+ renderer.circle(cx, cy, radius, z: 50, color: nil, segments: 64)
139
+ renderer.debug_box(x, y, width, height, z: 50)
140
+ ```
141
+
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.
144
+
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.
147
+
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.
150
+
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.
153
+
154
+ ## Images
155
+
156
+ ```ruby
157
+ renderer.image(image, cx, cy, angle: 0, scale: 1, z: 0, color: nil)
158
+ renderer.image_at(image, x, y, scale_x: 1, scale_y: 1, z: 0, color: nil)
159
+ renderer.background(image, x = 0, y = 0, z: 0, color: nil)
160
+ ```
161
+
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
+
170
+ ### Mirroring
171
+
172
+ **A negative scale on `image_at` mirrors the image inside the same rectangle.**
173
+ It does not move the image:
174
+
175
+ ```ruby
176
+ renderer.image_at(frame, x, y, scale_x: facing_left ? -1 : 1)
177
+ ```
178
+
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.
183
+
184
+ A scale of `0` draws nothing.
185
+
186
+ `color:` tints the image by multiplying its pixels. White leaves the image
187
+ unchanged, and a colour with alpha fades it.
188
+
189
+ [Images](images.md) covers loading files and slicing tiles; [Assets](assets.md#sprite-sheets) covers sprite sheets.
190
+
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.
195
+
196
+ ## Drawing by id
197
+
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.
201
+
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:
204
+
205
+ ```ruby
206
+ renderer.sprite('hero.json', row, col, x, y, flip_x: false, z: 0)
207
+ renderer.image('space.png', cx, cy, angle: 0, scale: 1)
208
+ renderer.background('space.png')
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)
211
+ ```
212
+
213
+ Paths need no setup. `Renderer.new(app)` uses the app's own manager.
214
+ `Renderer.new(app, assets: other)` uses a different one.
215
+
216
+ ### Registering
217
+
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.
221
+
222
+ ```ruby
223
+ renderer.register_image(:space, app.assets.image('space.png'))
224
+ renderer.register_sheet(:hero, app.assets.sheet('hero.json'))
225
+ renderer.register_tilemap(:level1, app.assets.tilemap('island.tmx'))
226
+ renderer.register_nine_slice(:panel, atlas.nine_slices[:panel])
227
+ renderer.register_ui_atlas(atlas) # every element under its own name
228
+
229
+ renderer.image(:space, 100, 100)
230
+ ```
231
+
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.
234
+
235
+ ### How the renderer resolves an id
236
+
237
+ | Given | |
238
+ |---|---|
239
+ | An `Image` | drawn directly — `#image`, `#image_at` and `#background` all take one |
240
+ | A registered id | the registered object |
241
+ | A `String` | resolved through the asset manager, then remembered |
242
+ | A `Symbol` that is not registered | `KeyError`, naming the id and the type |
243
+ | `nil` | `TypeError` |
244
+
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.
249
+
250
+ The renderer resolves each id once and keeps the answer. Per-frame drawing
251
+ neither resolves again nor allocates a lookup key.
252
+
253
+ ## Transform blocks
254
+
255
+ Each block applies to everything drawn inside it and undoes itself afterwards,
256
+ even when the block raises.
257
+
258
+ ```ruby
259
+ renderer.translated(dx, dy) { ... }
260
+ renderer.rotated(angle, pivot_x, pivot_y) { ... }
261
+ renderer.scaled(sx, sy = sx) { ... }
262
+ renderer.clipped(x, y, width, height) { ... }
263
+ renderer.layered(band) { ... } # see "Draw order" above
264
+ ```
265
+
266
+ Blocks nest and compose in the order you open them:
267
+
268
+ ```ruby
269
+ renderer.translated(-camera.x, -camera.y) do # world space -> screen space
270
+ renderer.rotated(ship.angle, ship.x, ship.y) do
271
+ renderer.image(hull, ship.x, ship.y)
272
+ end
273
+ end
274
+ ```
275
+
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.
282
+
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).
288
+
289
+ ### Clipping and split-screen
290
+
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:
294
+
295
+ ```ruby
296
+ def draw
297
+ @renderer.clipped(0, 0, 400, 600) do
298
+ @renderer.translated(-@player_one.x, -@player_one.y) { draw_world }
299
+ end
300
+
301
+ @renderer.clipped(400, 0, 400, 600) do
302
+ @renderer.translated(400 - @player_two.x, -@player_two.y) { draw_world }
303
+ end
304
+ end
305
+ ```
306
+
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.
312
+
313
+ ## Recordings: bake once, replay cheaply
314
+
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:
318
+
319
+ ```ruby
320
+ def draw
321
+ @ground ||= @renderer.record do
322
+ @tiles.each { |tile| @renderer.image(tile.image, tile.x, tile.y) }
323
+ end
324
+
325
+ @ground.draw(-@camera.x, -@camera.y)
326
+ end
327
+ ```
328
+
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`.
332
+
333
+ ```ruby
334
+ baked.draw(x = 0, y = 0, z: 0, color: nil)
335
+ baked.batch_count # GL calls one replay costs
336
+ baked.vertex_count # vertices baked in
337
+ baked.width # the size of what was baked, with #height
338
+ baked.empty?
339
+ ```
340
+
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:
345
+
346
+ ```ruby
347
+ 5.times { |i| @bush.draw(i * 120, 300) }
348
+ ```
349
+
350
+ **`color:` tints the replay.** The renderer multiplies each recorded colour by
351
+ it, so you can fade a whole baked layer at once.
352
+
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:
356
+
357
+ ```ruby
358
+ @renderer.clipped(0, 0, 400, 600) { @ground.draw(-@camera.x, -@camera.y) }
359
+ ```
360
+
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.
364
+
365
+ ## Testing what a scene draws
366
+
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/`:
371
+
372
+ ```ruby
373
+ renderer = FakeRenderer.new
374
+ health_bar.on_draw(renderer, nil)
375
+
376
+ expect(renderer.calls_to(:rect).map(&:args)).to eq([[10, 10, 64, 8]])
377
+ ```
378
+
379
+ The fake also records recordings, and keeps two questions apart: what was baked,
380
+ and where it was replayed.
381
+
382
+ ```ruby
383
+ ground = renderer.record { ... } # => a FakeRecording
384
+
385
+ expect(ground.calls.size).to eq(tiles.size) # baked once, not per frame
386
+ expect(ground.draws.map(&:args)).to eq([[-camera.x, -camera.y]])
387
+ ```
388
+
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.
393
+
394
+ ## Text
395
+
396
+ `renderer.text(string, x, y)` draws a line of text, and `text_width` measures
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.
@@ -0,0 +1,263 @@
1
+ # Examples
2
+
3
+ `examples/` holds one small program per concept. Each is a single `main.rb` that
4
+ runs on its own:
5
+
6
+ ```
7
+ ruby examples/walk/main.rb
8
+ ```
9
+
10
+ **An example answers "how do I do *X*".** The complete games under
11
+ `test_projects/` answer that poorly. An example shows one concept in one file,
12
+ and its header comment explains the concept at length. This page is the index;
13
+ the file is the long version.
14
+
15
+ Every example has an acceptance test as well. A scripted input track lives at
16
+ `tools/drive/examples/<name>.rb`. The harness feeds that track to the unmodified
17
+ program and reports what the game asked for:
18
+
19
+ ```
20
+ ruby tools/drive_test_project.rb examples/walk/main.rb --ticks 240
21
+ ```
22
+
23
+ The examples' assets live under `examples/assets/` and are redistributable, so
24
+ every example runs from a fresh clone. Most examples need no art at all.
25
+
26
+ ## Movement and drawing
27
+
28
+ ### walk
29
+
30
+ A player-controlled sprite: the smallest complete game. It is a plain node with
31
+ three components, and no component knows about the others.
32
+
33
+ **Uses:** `Node2D`, `Components::AnimatedSprite`, `Components::CharacterBody`,
34
+ `Components::PlayerController`, `InputMap.default`.
35
+
36
+ ### sprite
37
+
38
+ One frame drawn at a node, with no animation. Most things in a game look like
39
+ this. The draw passes no position and no angle, because the node's transform
40
+ already applies.
41
+
42
+ **Uses:** `Components::Sprite`, `Image#subimage`, `renderer.register_image`.
43
+
44
+ ### velocity
45
+
46
+ The second way a node moves. A character has an *intent*. A rock has a velocity,
47
+ and a component integrates it, spin included.
48
+
49
+ **Uses:** `Components::Velocity`, `Components::ScreenWrap`, `Components::World`,
50
+ `Components::WorldBounds`.
51
+
52
+ ## The world
53
+
54
+ ### scroll_map
55
+
56
+ A Tiled map larger than the window, scrolled by a player. No call pans the
57
+ camera. A component on a node points the camera, so scrolling is walking.
58
+
59
+ **Uses:** the `:tilemap` asset loader, `Components::TileWorld`, `TileMapLayer`,
60
+ `WorldView`, `Camera`, `Components::CameraFollow`.
61
+
62
+ ### collision
63
+
64
+ Object-to-object collision. A scene-scoped system pairs up shapes each step and
65
+ tells both sides they overlapped. It never learns what either object is.
66
+
67
+ **Uses:** `Components::CollisionWorld`, `Components::CircleCollider`,
68
+ `Components::BoxCollider`, `Components::Velocity`, `Engine::Text`.
69
+
70
+ ### collision_tiles
71
+
72
+ A character against a grid of solid tiles, sliding along a wall while walking
73
+ diagonally. East of the start, a spiky ball stops the hero and costs a life. One
74
+ feet box is thus stopped by two indexes: the map's grid and the broadphase.
75
+ `blocked_by: %i[tiles spike]` is the only place the difference shows. The life is
76
+ spent in `on_blocked`, because a blocked pair ends up touching, not overlapping.
77
+ `on_hit` never fires for it.
78
+
79
+ **Uses:** `Components::TileWorld`, `Components::CollisionWorld`,
80
+ `Components::FeetCollider`, `Components::CharacterBody` with
81
+ `blocked_by: %i[tiles spike]` and `on_blocked`, `Components::CameraFollow`,
82
+ `Engine::Text`.
83
+
84
+ ### jump_topdown
85
+
86
+ A hop in a top-down view. "Up" on screen is north, so a jump cannot move the
87
+ character. The sprite rises along `Hop`'s parabola. The feet box, the shadow and
88
+ the camera stay on the ground. A hop at the fence therefore does not clear it,
89
+ because the part that collides never leaves the ground. The game decides what a
90
+ hop may cross, through `airborne?`.
91
+
92
+ **Uses:** `Components::Hop`, `Node2D#elevation`, `Components::AnimatedSprite`,
93
+ `Components::FeetCollider`, `Components::CharacterBody`, `Components::TileWorld`,
94
+ `Components::CameraFollow`, `InputMap.default.merge`.
95
+
96
+ ### pathfinding
97
+
98
+ Pick a tile, and the hero works out how to get there. Small dots show the route
99
+ the search found, one per tile. Lines show the route the hero walks. The
100
+ navigator pulls the route tight, keeping each line straight as long as the hero's
101
+ feet box fits.
102
+
103
+ **Uses:** `Components::Navigator`, `Components::TileWorld#nav_grid`,
104
+ `Components::AnimatedSprite`, `Components::ActionTrigger`,
105
+ `Components::CameraFollow`, `Engine::Text.computed` over `I18n.t` with plurals.
106
+
107
+ ## Structure
108
+
109
+ ### signals
110
+
111
+ Declaring your own signal. A pressure plate announces that it was pressed, and
112
+ does nothing more. The door and the lamp connect to it; the plate never names
113
+ them.
114
+
115
+ **Uses:** `Signal::DSL`, `Signal.define`, `Components::ActionTrigger`, the
116
+ connect handle.
117
+
118
+ ### timer
119
+
120
+ Periodic behaviour that no input drives: a spawn cadence and a one-shot, with two
121
+ cadences on one node.
122
+
123
+ **Uses:** `Components::Timer` (repeating and `repeating: false`, and `as:`),
124
+ `Engine::Timer`.
125
+
126
+ ### pooling
127
+
128
+ Spawning many things without building any of them. The allocation count on
129
+ screen makes the case.
130
+
131
+ **Uses:** `Components::Pool`, `Engine::Pool`, `Components::DespawnOffscreen`,
132
+ `Components::Timer`, `Engine::Text`.
133
+
134
+ ## UI
135
+
136
+ ### game_menu
137
+
138
+ A menu that opens over a running world. Pausing belongs to a node, so only the
139
+ hero stops while the villagers walk on.
140
+
141
+ **Uses:** `PlayerLayer`, `UI::PanelMenu`, `UI::PanelButton`, `UI::Menu#open` /
142
+ `#close`, `Node2D#paused`, `renderer.nine_slice`.
143
+
144
+ ### menu_navigation
145
+
146
+ Several screens (title, settings, back) and settings that change something real
147
+ and survive a restart. It contrasts pushing a scene with replacing one.
148
+
149
+ **Uses:** `Scene::SceneStack`, `UI::OptionButton`, `Util::SaveFile`,
150
+ `RGame::Game`'s fullscreen, scale mode and volume.
151
+
152
+ ### radial_menu
153
+
154
+ Choosing by pointing. Eight icons sit on a wheel, focused by the direction of the
155
+ stick or the arrow keys. A released stick selects nothing, so pressing A at rest
156
+ never picks what the stick passed on its way back.
157
+
158
+ **Uses:** `UI::RadialMenu`, `UI::IconButton` on a disc `UI::ShapeStyle`,
159
+ `ui_radial_x` / `ui_radial_y`, and a UI atlas's `images` (`icons.json`).
160
+
161
+ ### quick_wheel
162
+
163
+ The same eight icons, on a wheel held open by Tab or the left shoulder button.
164
+ Releasing the button chooses. A stick released a moment before the button still
165
+ chooses; a stick at rest chooses nothing. The world drifts at a quarter speed
166
+ while the wheel is open.
167
+
168
+ **Uses:** `UI::RadialMenu` with `trigger:`, `UI::Pointing`'s grace window,
169
+ `UI::Menu#on_opened` / `#on_closed`, `InputMap.default.merge`.
170
+
171
+ ### skill_bar
172
+
173
+ A tool bar with five tools in a row. Left and right step through them and Enter
174
+ uses one. The number keys use a tool directly, without moving the focus. Holding
175
+ a number uses its tool once. Pressing a tool's number and Enter together also
176
+ uses it once.
177
+
178
+ **Uses:** `UI::Row`, `UI::Stepping` taking its axis from the layout,
179
+ `UI::Button`'s `hotkey:` and `activate_on: :press`, captioned `UI::IconButton`s
180
+ on a disc `UI::ShapeStyle`, `InputMap.default.merge`, and a UI atlas's `images`
181
+ (`skills.json`).
182
+
183
+ ## Audio
184
+
185
+ ### sound
186
+
187
+ A sound effect fired by a button, and the path it travels. A node may not name
188
+ the audio device, so it emits a fact and a director plays it.
189
+
190
+ **Uses:** `Core::Sample`, `Engine::AudioBus`, `Engine::AudioDirector`,
191
+ `Engine::Text`.
192
+
193
+ ### music
194
+
195
+ The other kind of sound: one streamed voice. You can stop it and ask whether it
196
+ plays, and starting it again does not restart it.
197
+
198
+ **Uses:** `Core::Song`, `AudioBus.play_music` / `.stop_music`,
199
+ `Engine::AudioDirector`.
200
+
201
+ ## Players and input
202
+
203
+ ### split_screen
204
+
205
+ Two players in one world. `WorldView` draws the world once per viewport, through
206
+ that viewport's camera. The world never knows how often it is drawn.
207
+
208
+ **Uses:** `Game.new(players: 2)`, `Engine::Players`, `Engine::WorldView`,
209
+ `Engine::PlayerLayer`, `Engine::Camera`, `Components::CameraFollow`,
210
+ `input_owner`.
211
+
212
+ ### input_glyphs
213
+
214
+ Prompts that match the device in the player's hands. They switch between
215
+ keyboard and controller mid-session. Nothing listens for a plugged-in pad; using
216
+ the pad takes the seat.
217
+
218
+ **Uses:** `Controls.gamepad?`, `InputMap#button_for`, `Engine::Players` with
219
+ `on_unassigned_input` defaulting to `:takeover`, `renderer.sprite`.
220
+
221
+ ## The window
222
+
223
+ ### fullscreen
224
+
225
+ Opening fullscreen, switching while the game runs, and all four scale modes with
226
+ the layout following each.
227
+
228
+ **Uses:** `RGame::Game.new(fullscreen:)`, `App#fullscreen?` / `#fullscreen=`,
229
+ `RGame::Game#scale_mode=`, the `view` a node is drawn with,
230
+ `InputMap.default.merge`.
231
+
232
+ ## Language
233
+
234
+ ### localization
235
+
236
+ The same screen in English and German. A `Text` with a `count` picks its plural
237
+ form, and one with a variable shows the current locale. German lacks one key, so
238
+ that line falls back to English. The player's language choice overrides the
239
+ operating system's and is saved.
240
+
241
+ **Uses:** `Engine::I18n`, `Engine::Text` and `Text.literal`, `UI::Menu`'s
242
+ `scope:`, `RGame::Game.new(locales:)`, `RGame::Core.preferred_locales`,
243
+ `Util::SaveFile`.
244
+
245
+ ## Persistence
246
+
247
+ ### save_load
248
+
249
+ Writing game state to disk and restoring it. The tree is not saved: a scene is a
250
+ recipe, and a save file is state. The variable holding a singular thing restores
251
+ it, and array order restores a flock.
252
+
253
+ **Uses:** `Util::SaveFile`, the `examples/walk` composition with a
254
+ `WanderController`.
255
+
256
+ ### save_load_ids
257
+
258
+ The case `save_load` leaves out: a collection whose members can be lost, and one
259
+ saved object that refers to another. A reference forces ids; a changing
260
+ collection alone does not.
261
+
262
+ **Uses:** `Components::Identity`, `Util::SaveFile`, a save of records rather
263
+ than positions.