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/assets.md CHANGED
@@ -1,11 +1,14 @@
1
- # Sheets, atlases and maps
1
+ # Assets: sheets, atlases, maps and sounds
2
2
 
3
- The classes between a file on disk and a draw call: a sprite sheet sliced into
4
- frames, a nine-slice panel stretched to any size, a UI atlas, a tile map, and
5
- the asset manager that loads and caches all of them.
3
+ **The asset manager loads and caches every file a game uses**: images, sound
4
+ samples, songs, sprite sheets, UI atlases, tile maps and plain text. This page
5
+ covers the manager and the Ruby classes it builds from those files: a sprite sheet
6
+ sliced into frames, a nine-slice panel stretched to any size, a UI atlas and a
7
+ tile map. [Images](images.md) and [Audio](audio.md) cover the C-backed `Image`,
8
+ `Sample` and `Song`.
6
9
 
7
- They are pure Ruby, but they live in `RGame::Core` because they hold images, and
8
- an image is a GPU handle. Game logic names them by id and never holds one see
10
+ **These classes are pure Ruby, but live in `RGame::Core` because they hold GPU or
11
+ audio handles.** Game logic names assets by id and never holds one; see
9
12
  [Testing what a scene draws](drawing.md#testing-what-a-scene-draws).
10
13
 
11
14
  | Page section | Class |
@@ -16,25 +19,40 @@ an image is a GPU handle. Game logic names them by id and never holds one — se
16
19
  | [UI atlases](#ui-atlases) | `RGame::Core::UiAtlas` |
17
20
  | [Tile maps](#tile-maps) | `RGame::Core::TileMapRenderer` |
18
21
 
19
- *This page grows as the rest lands.*
20
-
21
22
  ## The asset manager
22
23
 
23
- The one place file-backed assets are loaded and cached. Every game has one, and
24
- does not build it `app.assets` does, rooted at the app's `media_root:`:
24
+ **The asset manager loads and caches every file-backed asset.** Every game has
25
+ one, and none builds it. `app.assets` does, rooted at the app's `media_root:`:
25
26
 
26
27
  ```ruby
27
28
  app.assets.image('space.png') # => RGame::Core::Image
28
- app.assets.sound('example 09/boom.ogg') # => RGame::Core::Sample
29
- app.assets.song('example 09/theme.ogg') # => RGame::Core::Song
30
- app.assets.sheet('example 09/player.json') # => RGame::Core::SpriteSheet
29
+ app.assets.sound('sounds/boom.ogg') # => RGame::Core::Sample
30
+ app.assets.song('music/theme.ogg') # => RGame::Core::Song
31
+ app.assets.sheet('sheets/player.json') # => RGame::Core::SpriteSheet
31
32
  app.assets.ui_atlas('ui/ui_atlas.json') # => RGame::Core::UiAtlas
32
33
  app.assets.read('data/levels.txt') # => String
33
34
  ```
34
35
 
35
- Paths are relative to the media root; an absolute one is used as it stands. Two
36
- spellings of the same file `'a/b.png'`, `'a/./b.png'`, the absolute form
37
- are one cache entry, not three.
36
+ Paths are relative to the media root; an absolute path is used as given. Every
37
+ accessor returns the same object each time. A file requested twice is read,
38
+ decoded and uploaded once. Several spellings of one file share one cache entry:
39
+ `'a/b.png'`, `'a/./b.png'` and the absolute form.
40
+
41
+ The manager gives a game one object that knows what is loaded. Setup code builds
42
+ no paths by hand and constructs no images inline.
43
+
44
+ ### Listing what is there
45
+
46
+ ```ruby
47
+ app.assets.glob('locales/**/*.yml') # => ["locales/de.yml", "locales/en.yml"]
48
+ ```
49
+
50
+ `glob(pattern)` returns the paths under the media root that match `pattern`,
51
+ relative to the root and sorted. The order is therefore the same on every
52
+ platform, whatever order the file system lists them in. A directory that does not
53
+ exist matches nothing and returns `[]`. An absolute pattern is used as given, and
54
+ its matches come back absolute. `glob` loads nothing and caches nothing; hand
55
+ each path to an accessor to load it.
38
56
 
39
57
  ### Adding an asset type
40
58
 
@@ -43,21 +61,16 @@ app.assets.add_loader(:level) { |path| MyLevel.parse(File.read(path)) }
43
61
  app.assets.level('levels/one.json') # cached and grouped like any other
44
62
  ```
45
63
 
46
- The built-in types go through the same mechanism at construction, so an added
47
- one is not a second-class citizen. It exists because some types cannot be built
48
- from inside `RGame::Core` at all see [Tile maps](#tile-maps).
49
-
50
- Every path is **relative to the media root**, and every accessor returns the
51
- same object each time it is asked — so a file wanted twice is read, decoded and
52
- uploaded once. That is the point: loading stops being scattered across a game's
53
- setup, building paths ad hoc and constructing images inline, and becomes one
54
- object that knows what is loaded.
64
+ The built-in leaf types (`image`, `sound`, `song` and `read`) register through the
65
+ same method at construction, so an added type works exactly like them. `sheet` and
66
+ `ui_atlas` are composites built from those; see below. `add_loader` exists because `RGame::Core`
67
+ cannot build some types itself; see [Tile maps](#tile-maps).
55
68
 
56
69
  ### Groups, and what `release` frees
57
70
 
58
- Each cached asset remembers the **set of groups** that asked for it. An
59
- ungrouped load belongs to a permanent sentinel and survives every `release`; a
60
- grouped one is reference counted.
71
+ **Each cached asset remembers the set of groups that asked for it.** An
72
+ ungrouped load belongs to a permanent group and survives every `release`. A
73
+ grouped load is reference counted.
61
74
 
62
75
  ```ruby
63
76
  app.assets.image('ui/buttons.png') # ungrouped: permanent
@@ -70,49 +83,47 @@ app.assets.release(:level1) # drops lvl1/* unless another group still holds it
70
83
  app.assets.clear # drops everything, permanent included
71
84
  ```
72
85
 
73
- An asset two levels both loaded survives until **both** release it, so two
74
- scenes can share a texture without either one pulling it out from under the
75
- other. A cache *hit* under a new group is tagged with it too the alternative
76
- silently loses the second group's claim.
86
+ An asset that two levels loaded stays until **both** release it. Two scenes can
87
+ therefore share a texture safely. A cache *hit* under a new group also adds that
88
+ group. Otherwise the second group's claim would be lost without a trace.
77
89
 
78
- Releasing drops this cache's reference. When the GPU texture actually goes is
79
- the collector's business; `Image.debug_live_textures` is there if you want to
80
- watch it happen.
90
+ Releasing drops this cache's reference. The garbage collector decides when the
91
+ GPU texture goes. Watch it with `Image.debug_live_textures`.
81
92
 
82
- `release` refuses the permanent sentinel by name, because releasing it would
83
- drop every ungrouped asset the opposite of what "permanent" means. Use
84
- `clear`.
93
+ `release` refuses the permanent group. Releasing it would drop every ungrouped
94
+ asset, the opposite of "permanent". Use `clear` instead.
85
95
 
86
96
  ### Composites share their parts
87
97
 
88
- A sprite sheet is a descriptor plus an image, and **both are pulled through this
89
- same cache**. So these hand back one upload between them:
98
+ **A sprite sheet is a descriptor plus an image, and the manager loads both
99
+ through its own cache.** These two calls therefore share one upload:
90
100
 
91
101
  ```ruby
92
102
  sheet = app.assets.sheet('sheets/hero.json') # names hero.png inside
93
103
  image = app.assets.image('sheets/hero.png') # the same texture, not a second one
94
104
  ```
95
105
 
96
- The descriptor's image is resolved *next to the descriptor*, which is what lands
97
- it on the same cache key a standalone load would use. Release the sheet's group
98
- and its PNG goes with it.
106
+ The manager resolves the descriptor's image *next to the descriptor*. That gives
107
+ it the same cache key a standalone load would use. Releasing the sheet's group
108
+ releases its PNG too.
99
109
 
100
- **One known gap.** A composite tags its parts with the group that first built
101
- it. If a *second* group later asks for the same already-cached composite, only
102
- the composite's own key is re-tagged, not its parts so releasing the first
103
- group can drop a PNG the second still expects. Fine for the usual "each level
104
- owns its assets" pattern, and it would take per-part tracking to close.
110
+ **One known gap remains.** A composite tags its parts with the group that first
111
+ built it. When a *second* group requests the cached composite, only the
112
+ composite's own key gains the new tag, not its parts. Releasing the first group
113
+ can then drop a PNG the second group still expects. The usual pattern, where each
114
+ level owns its assets, is unaffected. Closing the gap would need per-part
115
+ tracking.
105
116
 
106
117
  ### Failure
107
118
 
108
- A loader's own error comes through unchanged `Image::LoadError`,
109
- `Sample::LoadError`, `Errno::ENOENT` naming the file. A load that failed
110
- leaves **nothing** behind: no cache entry and no group tag, so a retry is a
111
- clean retry rather than a half-registered asset that can never be released.
119
+ A loader's own error passes through unchanged and names the file:
120
+ `Image::LoadError`, `Sample::LoadError`, `Errno::ENOENT`. **A failed load leaves
121
+ nothing behind**: no cache entry and no group tag. A retry starts clean, with no
122
+ half-registered asset that can never be released.
112
123
 
113
124
  ### Testing without files
114
125
 
115
- Every asset type maps to a loader proc, and they are injectable:
126
+ Every asset type maps to a loader proc, and you can inject your own:
116
127
 
117
128
  ```ruby
118
129
  assets = RGame::Core::AssetManager.new(
@@ -121,18 +132,17 @@ assets = RGame::Core::AssetManager.new(
121
132
  )
122
133
  ```
123
134
 
124
- The defaults name `Image` and `Audio` only *inside* their bodies, never at load
125
- time. That is deliberate: it means the caching, path resolution and grouping
126
- which is all of the logic here can be specced with no window, no GL context
127
- and no files at all.
135
+ The default loaders name `Image` and `Audio` only *inside* their bodies, never at
136
+ load time. Specs can therefore cover all the manager's logic with no window, no GL
137
+ context and no files: caching, path resolution and grouping.
128
138
 
129
139
  ## Sprite sheets
130
140
 
131
- A sheet is one image plus a JSON descriptor, sliced into frames at load time and
132
- drawn one frame at a time.
141
+ A sheet is one image plus a JSON descriptor. It slices the image into frames at
142
+ load time and draws one frame at a time.
133
143
 
134
144
  ```ruby
135
- sheet = RGame::Core::SpriteSheet.load(app, 'media/hero.json')
145
+ sheet = app.assets.sheet('hero.json')
136
146
 
137
147
  sheet.frame_width # => 16
138
148
  sheet.grid # => [rows, columns]
@@ -159,14 +169,14 @@ sheet.draw(renderer, row, col, x, y, flip_x: false, z: 0)
159
169
  }
160
170
  ```
161
171
 
162
- `image` is resolved **next to the descriptor**, so a sheet can be moved as a
163
- pair of files without editing either. `frame_width` and `frame_height` are the
164
- only required keys; a descriptor missing one raises `ArgumentError` naming it.
172
+ The sheet resolves `image` **next to the descriptor**, so you can move both files
173
+ together without editing either. Only `frame_width` and `frame_height` are
174
+ required. A descriptor missing one raises `ArgumentError` naming the key.
165
175
 
166
176
  ### A frame can be smaller than its cell
167
177
 
168
- Cells sit on a fixed `cell_width` x `cell_height` grid. What gets *drawn* is a
169
- `frame_width` x `frame_height` rectangle offset by `origin_x` / `origin_y`
178
+ Cells sit on a fixed `cell_width` x `cell_height` grid. The sheet *draws* a
179
+ `frame_width` x `frame_height` rectangle, offset by `origin_x` / `origin_y`
170
180
  inside its cell:
171
181
 
172
182
  ```
@@ -178,58 +188,58 @@ cell (32x32) frame (16x24) at origin (8, 4)
178
188
  └──────────────┘ └────┴────┴────┘
179
189
  ```
180
190
 
181
- That is what lets a sheet whose cells are sized for the widest pose — an attack,
182
- a swing — still expose a tight, centred box for walking, so a character does not
183
- appear to change size when its animation changes. Leave the four keys out and
184
- frame == cell, which is what a simple sheet wants.
191
+ Cells can fit the widest pose, such as an attack swing, while walking frames keep
192
+ a tight, centred box. A character then keeps its apparent size when its animation
193
+ changes. Without the four keys, frame equals cell, which suits a simple sheet.
185
194
 
186
- Only whole cells count: a sheet 70 pixels wide with 16-pixel cells has four
187
- columns, and the six leftover pixels are ignored rather than becoming a narrow
188
- fifth.
195
+ Only whole cells count. A 70-pixel sheet with 16-pixel cells has four columns.
196
+ The sheet ignores the six leftover pixels instead of making a narrow fifth column.
189
197
 
190
198
  ### Facing
191
199
 
192
- `flip_x` mirrors the frame **inside the same rectangle**, so a character
193
- occupies the same pixels whichever way it faces:
200
+ **`flip_x` mirrors the frame inside the same rectangle**, so a character covers
201
+ the same pixels whichever way it faces:
194
202
 
195
203
  ```ruby
196
204
  sheet.draw(renderer, row, col, x, y, flip_x: moving_left)
197
205
  ```
198
206
 
199
- There is no width to add back — see
200
- [Mirroring](drawing.md#mirroring) for why, if you are coming from Gosu.
207
+ You add no width back; [Mirroring](drawing.md#mirroring) explains why.
201
208
 
202
- ### Animations are handed back raw
209
+ ### Animations come back raw
203
210
 
204
- `#animations` returns the descriptor's table untouched. This class knows nothing
205
- about time: which frame to show at a given moment is the scene layer's job, and
206
- it builds its own animation state from that hash. Keeping the raw form here is
207
- what lets the two sides evolve separately.
211
+ **`#animations` returns the descriptor's table untouched.** The sheet knows
212
+ nothing about time. The scene layer decides which frame to show, and builds its
213
+ own animation state from that hash. The raw form lets each side change on its
214
+ own.
208
215
 
209
- A sheet with no `animations` key gets `{}`, not `nil` a sheet of static tiles
210
- is a legitimate sheet, and a caller should not have to branch.
216
+ A sheet without an `animations` key returns `{}`, not `nil`. A sheet of static
217
+ tiles is a valid sheet, and callers should not have to branch.
211
218
 
212
219
  ### Slicing costs nothing
213
220
 
214
- Every frame is cut once, at construction, as a view onto the single upload. A
215
- sheet of two hundred frames is two hundred small objects and **one** texture, and
216
- `#draw` is an array index plus one draw call. Nothing is re-cut per frame.
221
+ The sheet cuts every frame once, at construction, as a view onto the single
222
+ upload. Two hundred frames are two hundred small objects and **one** texture.
223
+ `#draw` is an array index plus one draw call. Nothing is cut again per frame.
217
224
 
218
225
  ### Loading
219
226
 
220
227
  ```ruby
221
- RGame::Core::SpriteSheet.load(app, path) # standalone
222
- RGame::Core::SpriteSheet.new(image, atlas) # from an already-loaded image
228
+ app.assets.sheet(path) # what a game calls: cached and grouped
229
+ RGame::Core::SpriteSheet.new(image, atlas) # from an already-loaded image and parsed descriptor
230
+ RGame::Core::SpriteSheet.load(app, path) # reads both files directly, bypassing the cache
223
231
  ```
224
232
 
225
- Use `.load` for a game with a sheet or two and no asset manager. The asset
226
- manager uses the second form, with an image it has already cached, so a sheet's
227
- PNG is shared with a standalone load of the same file rather than decoded twice.
233
+ **Load sheets through `app.assets.sheet`.** Every app has an asset manager. It
234
+ builds the sheet with `.new`, from an image it pulls through its own cache, so the
235
+ sheet's PNG is shared with `app.assets.image` of the same file. `.load` reads the
236
+ descriptor and decodes the image itself, outside any cache. A second `.load` of
237
+ the same file decodes and uploads it again. `UiAtlas` has the same three forms.
228
238
 
229
239
  ## Nine-slices
230
240
 
231
- A bordered texture drawn at any size, by cutting it into nine pieces and
232
- treating each differently.
241
+ A nine-slice draws a bordered texture at any size. It cuts the texture into nine
242
+ pieces and treats each piece differently.
233
243
 
234
244
  ```ruby
235
245
  panel = RGame::Core::NineSlice.new(image, x: 0, y: 0, w: 26, h: 28,
@@ -248,37 +258,36 @@ panel.draw(renderer, x, y, width, height, z: 0, color: nil)
248
258
  └──┴────────┴──┘
249
259
  ```
250
260
 
251
- One small piece of art fills a button, a dialog or a health bar of any size,
252
- without the corners smearing.
261
+ One small piece of art fills a button, a dialog or a health bar of any size, and
262
+ the corners never smear.
253
263
 
254
- `(x, y, w, h)` is the source rectangle **inside** the image, so one sheet can
255
- hold many of them — which is what a [UI atlas](#ui-atlases) does with it.
264
+ `(x, y, w, h)` is the source rectangle **inside** the image, so one sheet can hold
265
+ many nine-slices. A [UI atlas](#ui-atlases) relies on this.
256
266
 
257
267
  ### Tiled, not stretched
258
268
 
259
- Edges and the centre **repeat**. Stretching a 7-pixel motif would blur exactly
260
- the detail the art was drawn for; repeating it keeps pixel art crisp at every
261
- widget size. Each band is clipped to itself, so the last tile in a row is
262
- cropped cleanly rather than spilling into the corner beside it and the loops
263
- always start one more tile rather than stopping short, because a gap at the seam
264
- is more visible than an overhang that gets cropped.
269
+ **Edges and the centre repeat.** Stretching a 7-pixel motif would blur the detail
270
+ the art was drawn for. Repeating it keeps pixel art crisp at every size. Each
271
+ band clips to itself, so the last tile in a row is cropped cleanly instead of
272
+ spilling into the corner. The loops always start one extra tile, because a gap at
273
+ the seam shows more than a cropped overhang.
265
274
 
266
275
  ### `border` and `scale`
267
276
 
268
- `border` is either a uniform integer or a hash:
277
+ `border` takes a uniform integer or a hash:
269
278
 
270
279
  ```ruby
271
280
  border: 7
272
281
  border: { left: 2, right: 6, top: 4, bottom: 4 }
273
282
  ```
274
283
 
275
- `scale` is an **integer pixel scale for the chrome itself**. Source art is
276
- small corners are often 7 pixels so a scale of 2 or 3 gives legible borders
277
- on a 640x480 screen with no blurring at all, because every source pixel becomes
278
- a whole square of screen pixels. It scales the pieces *and* the step between
284
+ **`scale` is an integer pixel scale for the border art itself.** Source art is
285
+ small, with corners often 7 pixels wide. A scale of 2 or 3 makes borders legible
286
+ on a 640x480 screen without blur, because each source pixel becomes a whole
287
+ square of screen pixels. `scale` multiplies both the pieces and the step between
279
288
  tiles, so the tiling stays seamless.
280
289
 
281
- ### Edge cases, and what they do
290
+ ### Edge cases
282
291
 
283
292
  | | |
284
293
  |---|---|
@@ -289,24 +298,29 @@ tiles, so the tiling stays seamless.
289
298
 
290
299
  ### What it costs
291
300
 
292
- The nine pieces are cut once at construction, as views onto the one upload, so
293
- `#draw` allocates nothing. It issues one call per tile, which is what makes
294
- `scale` worth having: a panel drawn at 3x is a ninth of the tiles of the same
295
- panel drawn at 1x.
301
+ The nine-slice cuts its pieces once at construction, as views onto the one
302
+ upload, so `#draw` allocates nothing. It issues one call per tile. That is where
303
+ `scale` pays: a panel at 3x needs a ninth of the tiles of the same panel at 1x.
304
+
305
+ Inside a scene, draw a registered nine-slice by id with
306
+ `renderer.nine_slice(id, x, y, width, height, z: 0, tint: nil)`. Its `tint:` is
307
+ `NineSlice#draw`'s `color:`.
296
308
 
297
309
  ## UI atlases
298
310
 
299
- One sheet of UI chrome, cut into named [nine-slices](#nine-slices).
311
+ A UI atlas cuts one sheet of UI art into named [nine-slices](#nine-slices) and
312
+ named images.
300
313
 
301
314
  ```ruby
302
315
  atlas = app.assets.ui_atlas('ui/ui_atlas.json')
303
316
  renderer.register_ui_atlas(atlas)
304
317
 
305
318
  renderer.nine_slice(:button_idle, x, y, width, height)
319
+ renderer.image(:home, cx, cy)
306
320
  ```
307
321
 
308
- A button has four states, a panel has one, a scrollbar has three pieces all
309
- small, and all cheaper as sub-rectangles of one texture than as a dozen files.
322
+ A button has four states, a panel one, a scrollbar three pieces. All are small,
323
+ and sub-rectangles of one texture cost less than a dozen files.
310
324
 
311
325
  ### The descriptor
312
326
 
@@ -319,94 +333,117 @@ small, and all cheaper as sub-rectangles of one texture than as a dozen files.
319
333
  "button_focus": { "x": 43, "y": 59, "w": 26, "h": 28, "border": 7 },
320
334
  "panel": { "x": 0, "y": 0, "w": 32, "h": 32, "scale": 2,
321
335
  "border": { "left": 4, "right": 4, "top": 8, "bottom": 4 } }
336
+ },
337
+ "images": {
338
+ "home": { "x": 0, "y": 96, "w": 50, "h": 50 },
339
+ "gear": { "x": 50, "y": 96, "w": 50, "h": 50 }
322
340
  }
323
341
  }
324
342
  ```
325
343
 
326
- `image` is resolved next to the descriptor. Each entry is a source rectangle
327
- plus a `border` a uniform integer or one value per side — and an optional
328
- `scale` that overrides the sheet-wide one. A sheet with no `scale` draws at 1.
344
+ The atlas resolves `image` next to the descriptor. Each `nine_slices` entry holds
345
+ a source rectangle and a `border`: a uniform integer or one value per side. An
346
+ optional `scale` overrides the sheet-wide one. A sheet without `scale` draws at 1.
347
+
348
+ Each `images` entry is a bare rectangle, cut from the sheet with
349
+ `Image#subimage`. An icon draws whole, so it needs no border. The draw call's
350
+ `scale:` sets its size, so it needs no scale either. `atlas.images` is a Hash of
351
+ name to `Image`. Either section may be missing or `null`, and one atlas may hold
352
+ both.
329
353
 
330
354
  ### Element names, not filenames
331
355
 
332
- `nine_slices` is keyed by whatever the descriptor calls each element, and those
333
- names are what a widget asks for. That is why nine-slices are the one asset the
334
- renderer resolves **by registration only** `:button_focus` is not a file and
335
- never can be. `register_ui_atlas` binds every element in one call:
356
+ **Both sections are keyed by element name, and widgets ask for those names.** So
357
+ the renderer resolves nine-slices **by registration only**: `:button_focus` is
358
+ not a file and never can be. `register_ui_atlas` binds every element of both
359
+ kinds in one call. It registers nine-slices with `register_nine_slice` and images
360
+ with `register_image`:
336
361
 
337
362
  ```ruby
338
- renderer.register_ui_atlas(atlas) # all of them
363
+ renderer.register_ui_atlas(atlas) # all of them
339
364
  renderer.register_nine_slice(:panel, atlas.nine_slices[:panel]) # or one
365
+ renderer.register_image(:home, atlas.images[:home])
340
366
  ```
341
367
 
342
368
  ### When an entry is wrong
343
369
 
344
- A descriptor holds a dozen of these, so a broken one **names itself**:
370
+ **A broken entry names itself.** A descriptor holds a dozen elements. A nine-slice
371
+ whose border does not fit, or an image rectangle past the sheet's edge, raises
372
+ with the element's name:
345
373
 
346
374
  ```
347
375
  ArgumentError: ui atlas element :button_idle: nine-slice borders (40, 40, 40, 40)
348
376
  do not fit in a 26x28 rect
349
377
  ```
350
378
 
351
- Without the element name the failure is arithmetic from inside `NineSlice`, and
352
- finding the culprit means bisecting the JSON by hand.
379
+ Without the name, the error would be bare arithmetic from inside `NineSlice`.
380
+ Finding the culprit would mean bisecting the JSON by hand.
353
381
 
354
- Parsing happens once, at load. Nothing here is touched again per frame.
382
+ The atlas parses once, at load, and touches nothing again per frame.
355
383
 
356
384
  ## Tile maps
357
385
 
358
- Draws a Tiled map: the static layers baked once, the animated tiles drawn each
359
- frame and culled to the viewport.
386
+ `TileMapRenderer` draws a Tiled map. It bakes the static layers once, and draws
387
+ animated tiles each frame, culled to a rectangle of the world.
360
388
 
361
389
  ```ruby
362
- tiles = app.assets.tilemap('map/island.tmx')
390
+ tiles = app.assets.tilemap('map/island.tmx') # => RGame::Core::TileMapRenderer
363
391
 
364
- renderer.tilemap('map/island.tmx', 0, camera_x, camera_y, view_w, view_h, elapsed: seconds)
392
+ renderer.tilemap('map/island.tmx', 0, cull_x, cull_y, cull_w, cull_h, elapsed: seconds)
365
393
  # ... the scene draws its actors here ...
366
- renderer.tilemap('map/island.tmx', 1, camera_x, camera_y, view_w, view_h, elapsed: seconds)
394
+ renderer.tilemap('map/island.tmx', 1, cull_x, cull_y, cull_w, cull_h, elapsed: seconds)
367
395
  ```
368
396
 
369
- ### One call per layer, because the actors go between them
397
+ **A game rarely makes these calls.** [`TileMapLayer`](components.md#tileworld)
398
+ mounts one node per layer and draws it.
370
399
 
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.
400
+ **Tiles draw in world coordinates.** A tile at column 3 lands at
401
+ `3 * tile_width`. The caller's transform, usually a `WorldView`'s camera, puts it
402
+ on screen. The rectangle is only a **cull rect**: the part of the world worth
403
+ drawing. A camera supplies it but does not move the result, so one map can be
404
+ drawn through several cameras in one frame.
405
+
406
+ ### One call per layer, so actors fit between layers
407
+
408
+ **Each call draws one layer, in the order the caller chooses.** A scene can put
409
+ its actors between two layers: trunks under, canopies over. Which layers those
410
+ are depends on the scene, not the map, so the call takes no `z`. In a game,
411
+ [`TileMapLayer`](components.md#tileworld) mounts a node per layer, and the scene
412
+ tree orders them.
376
413
 
377
414
  ### What it costs
378
415
 
379
- Within each layer, every tile that is **not** animated is baked into a
380
- [recording](drawing.md#recordings-bake-once-replay-cheaply) the first time that
381
- layer is drawn. Scrolling it afterwards is one call per texture, however many
382
- thousand tiles went into it. The handful that *are* animated are drawn
383
- individually, **culled to the viewport** so a map far larger than the screen
384
- costs only what is on screen.
416
+ **The renderer bakes each layer's non-animated tiles into a
417
+ [recording](drawing.md#recordings-bake-once-replay-cheaply)** the first time it
418
+ draws that layer. Scrolling the layer then costs one call per texture, however
419
+ many thousand tiles it holds. The few animated tiles draw individually, **culled
420
+ to the viewport**. A map far larger than the screen costs only what is on screen.
385
421
 
386
- Two maps sharing a tileset share one GPU upload, because the tiles come through
387
- the asset manager rather than being loaded by the map.
422
+ Two maps that share a tileset share one GPU upload, because tiles load through the
423
+ asset manager, not through the map.
388
424
 
389
- ### Animation is advanced by you
425
+ ### You advance the animation
390
426
 
391
- `elapsed` is seconds, and it is an argument rather than a clock this reads:
427
+ **`elapsed` is seconds, passed as an argument.** The renderer reads no clock:
392
428
 
393
429
  ```ruby
394
- def update(dt) = @elapsed += dt
395
- def draw(renderer)
396
- renderer.tilemap(@id, @layer, camera.x, camera.y, w, h, elapsed: @elapsed)
430
+ def on_update(dt) = @elapsed += dt
431
+
432
+ def on_draw(renderer, view)
433
+ camera = view.camera
434
+ renderer.tilemap(@id, @layer, camera.x, camera.y, view.width, view.height, elapsed: @elapsed)
397
435
  end
398
436
  ```
399
437
 
400
- Stop accumulating and the water freezes; accumulate slower and it runs slow; a
401
- spec passes `0.15` and gets the second frame. See
402
- [the frame loop](app.md#the-frame-loop) for why nothing on a draw path reads a
403
- clock.
438
+ Stop accumulating and the water freezes. Accumulate slower and it runs slow. A
439
+ spec passes `0.15` and gets the second frame. [The frame loop](app.md#the-frame-loop)
440
+ explains why nothing on a draw path reads a clock.
404
441
 
405
- ### It is wired up, not built in
442
+ ### Installed, not built in
406
443
 
407
- `RGame::Core` cannot parse a `.tmx` that is the engine layer's job, and Core
408
- is not allowed to know the engine layer exists. So the type is *installed*, by
409
- the one class that may name both:
444
+ **`RGame::Core` cannot parse a `.tmx`.** Parsing belongs to the engine layer, and
445
+ Core may not know that layer exists. `RGame::Game`, the one class that may name
446
+ both, installs the type:
410
447
 
411
448
  ```ruby
412
449
  app.assets.add_loader(:tilemap) do |path|
@@ -417,9 +454,10 @@ app.assets.add_loader(:tilemap) do |path|
417
454
  end
418
455
  ```
419
456
 
420
- Until that runs, `app.assets` has no `tilemap` accessor and a tilemap draw id
421
- raises `KeyError` which is the honest answer, rather than a half-working
422
- subsystem.
457
+ Every `RGame::Game` installs this loader when it is built. A plain
458
+ `RGame::Core::App` has none: its `app.assets` has no `tilemap` accessor, and a
459
+ tilemap draw id raises `KeyError`. A clear error beats a half-working subsystem.
423
460
 
424
- `TileMapRenderer#map` hands the parsed map back, for the scene's own collision
425
- and world-bounds queries.
461
+ `TileMapRenderer#map` returns the parsed map, for the scene's own collision and
462
+ world-bounds queries. [Tile maps](tile_maps.md) documents `TileMap` and `Tileset`,
463
+ and which Tiled features rgame reads.