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,463 @@
1
+ # Assets: sheets, atlases, maps and sounds
2
+
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`.
9
+
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
12
+ [Testing what a scene draws](drawing.md#testing-what-a-scene-draws).
13
+
14
+ | Page section | Class |
15
+ |---|---|
16
+ | [The asset manager](#the-asset-manager) | `RGame::Core::AssetManager` |
17
+ | [Sprite sheets](#sprite-sheets) | `RGame::Core::SpriteSheet` |
18
+ | [Nine-slices](#nine-slices) | `RGame::Core::NineSlice` |
19
+ | [UI atlases](#ui-atlases) | `RGame::Core::UiAtlas` |
20
+ | [Tile maps](#tile-maps) | `RGame::Core::TileMapRenderer` |
21
+
22
+ ## The asset manager
23
+
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:`:
26
+
27
+ ```ruby
28
+ app.assets.image('space.png') # => RGame::Core::Image
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
32
+ app.assets.ui_atlas('ui/ui_atlas.json') # => RGame::Core::UiAtlas
33
+ app.assets.read('data/levels.txt') # => String
34
+ ```
35
+
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.
56
+
57
+ ### Adding an asset type
58
+
59
+ ```ruby
60
+ app.assets.add_loader(:level) { |path| MyLevel.parse(File.read(path)) }
61
+ app.assets.level('levels/one.json') # cached and grouped like any other
62
+ ```
63
+
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).
68
+
69
+ ### Groups, and what `release` frees
70
+
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.
74
+
75
+ ```ruby
76
+ app.assets.image('ui/buttons.png') # ungrouped: permanent
77
+ app.assets.preload(:level1, image: ['lvl1/bg.png'],
78
+ sound: ['lvl1/hit.ogg'],
79
+ sheet: ['lvl1/foes.json'])
80
+ app.assets.image('shared.png', :level2) # one group, by hand
81
+
82
+ app.assets.release(:level1) # drops lvl1/* unless another group still holds it
83
+ app.assets.clear # drops everything, permanent included
84
+ ```
85
+
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.
89
+
90
+ Releasing drops this cache's reference. The garbage collector decides when the
91
+ GPU texture goes. Watch it with `Image.debug_live_textures`.
92
+
93
+ `release` refuses the permanent group. Releasing it would drop every ungrouped
94
+ asset, the opposite of "permanent". Use `clear` instead.
95
+
96
+ ### Composites share their parts
97
+
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:
100
+
101
+ ```ruby
102
+ sheet = app.assets.sheet('sheets/hero.json') # names hero.png inside
103
+ image = app.assets.image('sheets/hero.png') # the same texture, not a second one
104
+ ```
105
+
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.
109
+
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.
116
+
117
+ ### Failure
118
+
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.
123
+
124
+ ### Testing without files
125
+
126
+ Every asset type maps to a loader proc, and you can inject your own:
127
+
128
+ ```ruby
129
+ assets = RGame::Core::AssetManager.new(
130
+ root: '/media', app: nil,
131
+ loaders: { image: ->(path) { FakeImage.new(path) } }
132
+ )
133
+ ```
134
+
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.
138
+
139
+ ## Sprite sheets
140
+
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.
143
+
144
+ ```ruby
145
+ sheet = app.assets.sheet('hero.json')
146
+
147
+ sheet.frame_width # => 16
148
+ sheet.grid # => [rows, columns]
149
+ sheet.animations # => the raw table from the descriptor
150
+
151
+ sheet.draw(renderer, row, col, x, y, flip_x: false, z: 0)
152
+ ```
153
+
154
+ ### The descriptor
155
+
156
+ ```json
157
+ {
158
+ "image": "hero.png",
159
+ "frame_width": 16,
160
+ "frame_height": 24,
161
+ "cell_width": 32,
162
+ "cell_height": 32,
163
+ "origin_x": 8,
164
+ "origin_y": 4,
165
+ "animations": {
166
+ "walk_left": { "row": 1, "frames": 4, "fps": 8 },
167
+ "stand": { "row": 0, "col": 1, "frames": 1, "fps": 1 }
168
+ }
169
+ }
170
+ ```
171
+
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.
175
+
176
+ ### A frame can be smaller than its cell
177
+
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`
180
+ inside its cell:
181
+
182
+ ```
183
+ cell (32x32) frame (16x24) at origin (8, 4)
184
+ ┌──────────────┐ ┌──────────────┐
185
+ │ │ │ ┌────┐ │
186
+ │ │ │ │ │ │
187
+ │ │ │ │ │ │
188
+ └──────────────┘ └────┴────┴────┘
189
+ ```
190
+
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.
194
+
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.
197
+
198
+ ### Facing
199
+
200
+ **`flip_x` mirrors the frame inside the same rectangle**, so a character covers
201
+ the same pixels whichever way it faces:
202
+
203
+ ```ruby
204
+ sheet.draw(renderer, row, col, x, y, flip_x: moving_left)
205
+ ```
206
+
207
+ You add no width back; [Mirroring](drawing.md#mirroring) explains why.
208
+
209
+ ### Animations come back raw
210
+
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.
215
+
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.
218
+
219
+ ### Slicing costs nothing
220
+
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.
224
+
225
+ ### Loading
226
+
227
+ ```ruby
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
231
+ ```
232
+
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.
238
+
239
+ ## Nine-slices
240
+
241
+ A nine-slice draws a bordered texture at any size. It cuts the texture into nine
242
+ pieces and treats each piece differently.
243
+
244
+ ```ruby
245
+ panel = RGame::Core::NineSlice.new(image, x: 0, y: 0, w: 26, h: 28,
246
+ border: 7, scale: 3)
247
+
248
+ panel.draw(renderer, x, y, width, height, z: 0, color: nil)
249
+ ```
250
+
251
+ ```
252
+ ┌──┬────────┬──┐ corners: fixed size
253
+ │tl│ top │tr│ top / bottom: tiled across
254
+ ├──┼────────┼──┤ left / right: tiled down
255
+ │l │ centre │ r│ centre: tiled both ways
256
+ ├──┼────────┼──┤
257
+ │bl│ bottom │br│
258
+ └──┴────────┴──┘
259
+ ```
260
+
261
+ One small piece of art fills a button, a dialog or a health bar of any size, and
262
+ the corners never smear.
263
+
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.
266
+
267
+ ### Tiled, not stretched
268
+
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.
274
+
275
+ ### `border` and `scale`
276
+
277
+ `border` takes a uniform integer or a hash:
278
+
279
+ ```ruby
280
+ border: 7
281
+ border: { left: 2, right: 6, top: 4, bottom: 4 }
282
+ ```
283
+
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
288
+ tiles, so the tiling stays seamless.
289
+
290
+ ### Edge cases
291
+
292
+ | | |
293
+ |---|---|
294
+ | A rectangle smaller than its own borders | draws its corners and no bands |
295
+ | A border with no room for a centre (`left + right == w`) | fine — a bar that stretches only vertically |
296
+ | Borders wider than the source rect | `ArgumentError`, naming the borders and the rect |
297
+ | `scale` of zero or less | `ArgumentError` — the tiling loop would never advance |
298
+
299
+ ### What it costs
300
+
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:`.
308
+
309
+ ## UI atlases
310
+
311
+ A UI atlas cuts one sheet of UI art into named [nine-slices](#nine-slices) and
312
+ named images.
313
+
314
+ ```ruby
315
+ atlas = app.assets.ui_atlas('ui/ui_atlas.json')
316
+ renderer.register_ui_atlas(atlas)
317
+
318
+ renderer.nine_slice(:button_idle, x, y, width, height)
319
+ renderer.image(:home, cx, cy)
320
+ ```
321
+
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.
324
+
325
+ ### The descriptor
326
+
327
+ ```json
328
+ {
329
+ "image": "buttons.png",
330
+ "scale": 3,
331
+ "nine_slices": {
332
+ "button_idle": { "x": 11, "y": 59, "w": 26, "h": 28, "border": 7 },
333
+ "button_focus": { "x": 43, "y": 59, "w": 26, "h": 28, "border": 7 },
334
+ "panel": { "x": 0, "y": 0, "w": 32, "h": 32, "scale": 2,
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 }
340
+ }
341
+ }
342
+ ```
343
+
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.
353
+
354
+ ### Element names, not filenames
355
+
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`:
361
+
362
+ ```ruby
363
+ renderer.register_ui_atlas(atlas) # all of them
364
+ renderer.register_nine_slice(:panel, atlas.nine_slices[:panel]) # or one
365
+ renderer.register_image(:home, atlas.images[:home])
366
+ ```
367
+
368
+ ### When an entry is wrong
369
+
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:
373
+
374
+ ```
375
+ ArgumentError: ui atlas element :button_idle: nine-slice borders (40, 40, 40, 40)
376
+ do not fit in a 26x28 rect
377
+ ```
378
+
379
+ Without the name, the error would be bare arithmetic from inside `NineSlice`.
380
+ Finding the culprit would mean bisecting the JSON by hand.
381
+
382
+ The atlas parses once, at load, and touches nothing again per frame.
383
+
384
+ ## Tile maps
385
+
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.
388
+
389
+ ```ruby
390
+ tiles = app.assets.tilemap('map/island.tmx') # => RGame::Core::TileMapRenderer
391
+
392
+ renderer.tilemap('map/island.tmx', 0, cull_x, cull_y, cull_w, cull_h, elapsed: seconds)
393
+ # ... the scene draws its actors here ...
394
+ renderer.tilemap('map/island.tmx', 1, cull_x, cull_y, cull_w, cull_h, elapsed: seconds)
395
+ ```
396
+
397
+ **A game rarely makes these calls.** [`TileMapLayer`](components.md#tileworld)
398
+ mounts one node per layer and draws it.
399
+
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.
413
+
414
+ ### What it costs
415
+
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.
421
+
422
+ Two maps that share a tileset share one GPU upload, because tiles load through the
423
+ asset manager, not through the map.
424
+
425
+ ### You advance the animation
426
+
427
+ **`elapsed` is seconds, passed as an argument.** The renderer reads no clock:
428
+
429
+ ```ruby
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)
435
+ end
436
+ ```
437
+
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.
441
+
442
+ ### Installed, not built in
443
+
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:
447
+
448
+ ```ruby
449
+ app.assets.add_loader(:tilemap) do |path|
450
+ map, image_path = RGame::Engine::TileMap.load(path)
451
+ tiles = app.assets.image(image_path).tiles(map.tileset.tile_width,
452
+ map.tileset.tile_height)
453
+ RGame::Core::TileMapRenderer.new(map, tiles)
454
+ end
455
+ ```
456
+
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.
460
+
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.