rgame 0.2.0 → 0.3.1

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 +144 -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 +3 -5
  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 +167 -12
  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/text.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Text
2
2
 
3
+ **The renderer comes with a font, and `text` draws with it.** Most games need
4
+ nothing more:
5
+
3
6
  ```ruby
4
7
  require 'rgame'
5
8
  require 'rgame/core'
@@ -14,25 +17,29 @@ class MyGame < RGame::Core::App
14
17
  @renderer.text('Score: 1200', 10, 10)
15
18
  end
16
19
  end
17
- ```
18
20
 
19
- That is the whole of it for most cases — the renderer has a font already, and
20
- `text` uses it.
21
+ MyGame.new.run
22
+ ```
21
23
 
22
24
  ## Where text goes
23
25
 
24
- `text(string, x, y, …)` puts the **top-left corner** of the line at `(x, y)`,
25
- the same corner every other drawing method takes. Typography works from the
26
- baseline; a caller placing a label does not have to.
26
+ `text(string, x, y, …)` puts the **top-left corner** of the line at `(x, y)`.
27
+ Every other drawing method uses the same corner. Typography measures from the
28
+ baseline, but a caller placing a label does not have to.
27
29
 
28
30
  ```ruby
29
31
  renderer.text(string, x, y, z: 10, color: nil, font: nil)
30
- renderer.text_width(string, font: nil) # => Float, pixels
31
- renderer.text_height(font: nil) # => Integer, the line height
32
+ renderer.text_width(string, font: nil) # => Float pixels
33
+ renderer.text_height(font: nil) # => Integer the line height
32
34
  ```
33
35
 
34
- **A string is one line.** Newlines are not special. Two lines are two calls,
35
- stepped by `text_height`:
36
+ **`string` is a String or anything with `to_str`**, such as an
37
+ [`Engine::Text`](toolbox.md#text--the-string-a-node-draws), which a node passes
38
+ as it is. `text` and `text_width` raise `TypeError` for `nil`, a number, or a
39
+ `to_str` that returns something other than a String.
40
+
41
+ **A string is one line.** A newline has no special meaning. Draw two lines with
42
+ two calls, stepped by `text_height`:
36
43
 
37
44
  ```ruby
38
45
  lines.each_with_index do |line, i|
@@ -40,15 +47,25 @@ lines.each_with_index do |line, i|
40
47
  end
41
48
  ```
42
49
 
43
- **`text_width` and `text` agree.** They walk the same code, so a label measured
44
- and then centred lands where it was measured to:
50
+ **`text_width` and `text` agree.** They run the same code, so a label centred by
51
+ its measured width lands exactly there:
45
52
 
46
53
  ```ruby
47
54
  @renderer.text(label, (width - @renderer.text_width(label)) / 2, 20)
48
55
  ```
49
56
 
50
- Unlike the drawing methods, `text_width` and `text_height` work **outside**
51
- `draw` measuring touches no GPU, and laying out a menu happens while updating.
57
+ **`text_width` and `text_height` also work outside `draw`**, unlike the drawing
58
+ methods. Measuring touches no GPU, and a menu lays itself out while updating.
59
+
60
+ A label built from a changing value, like a score, should come from
61
+ [`RGame::Engine::Text`](toolbox.md#text--the-string-a-node-draws). It renders
62
+ the string only when a variable or the language changes:
63
+
64
+ ```ruby
65
+ @score = RGame::Engine::Text.new('hud.score', :score) # once
66
+
67
+ renderer.text(@score.with(score: @points), 10, 10) # every frame
68
+ ```
52
69
 
53
70
  ## Fonts
54
71
 
@@ -62,55 +79,55 @@ font.text_width('Hello') # => 38.7
62
79
  renderer.text('Hello', 10, 10, font: font)
63
80
  ```
64
81
 
65
- A `Font` is **one typeface at one pixel size**. Two sizes are two fonts. Like an
66
- image, it belongs to the app whose GPU context holds its glyphs, and drawing it
67
- through another app's renderer raises rather than painting blank boxes.
82
+ A `Font` is **one typeface at one pixel size**. Two sizes need two fonts. A font
83
+ belongs to the app whose GPU context holds its glyphs, like an image. Drawing it
84
+ through another app's renderer raises instead of painting blank boxes.
68
85
 
69
- The renderer builds its own font at 18px on first use. Replace it and every
70
- unqualified `text` call follows:
86
+ The renderer builds its own 18px font on first use. Replace it, and every `text`
87
+ call without a `font:` follows:
71
88
 
72
89
  ```ruby
73
90
  @renderer.font = RGame::Core::Font.new(self, 24)
74
91
  ```
75
92
 
76
- A file that cannot be read or is not a TrueType font raises
93
+ A file that is unreadable or not a TrueType font raises
77
94
  `RGame::Core::Font::LoadError`, naming the path.
78
95
 
79
96
  ### The default font, and what it covers
80
97
 
81
- The engine ships **Liberation Sans** and uses it when no path is given. There is
82
- no font-*name* lookup and no system font database a font is a file.
98
+ **The engine ships Liberation Sans and uses it when you pass no path.** It never
99
+ looks a font up by name and never asks a system font database. A font is a file.
83
100
 
84
- That is a deliberate trade. Asking the operating system for "Arial" (which is
85
- what Gosu does) means a different font on every machine, so a UI laid out on the
86
- developer's box can overflow on a player's. Shipping one means text renders
87
- identically everywhere, at the cost of ~400 KB in the gem.
101
+ This is a deliberate trade. Asking the operating system for "Arial" gets whatever
102
+ that machine keeps under the name, or a substitute. A UI laid out on the
103
+ developer's machine can then overflow on a player's. A shipped font renders
104
+ identically everywhere, and costs about 400 KB in the gem.
88
105
 
89
106
  | | |
90
107
  |---|---|
91
108
  | Covers | English, German, French, Italian, Spanish, Portuguese, Nordic, Polish — in full, including `ß`, `ẞ`, `« »`, curly quotes and `€`. Greek and Cyrillic too. |
92
109
  | Does not cover | CJK, Arabic, Hebrew, Devanagari. Pass your own font file for those; no font of this size includes them. |
93
110
 
94
- Text is UTF-8. A malformed byte draws one replacement character and the rest of
95
- the string survives a bad byte in a data file costs a visible box, not the
96
- label.
111
+ Text is UTF-8. A malformed byte draws one replacement character, and the rest of
112
+ the string still draws. A bad byte in a data file costs one visible box, not the
113
+ whole label.
97
114
 
98
115
  ## What it costs
99
116
 
100
- Glyphs are rasterised the first time they are drawn and kept in a texture atlas
101
- afterwards, so the cost is bounded by the **characters** a game uses, not by the
102
- strings it draws. A score that changes every frame is free after the first ten
103
- digits; a whole Latin character set fits on one 512×512 page, so a line of text
104
- is one draw call.
117
+ **The engine rasterises each glyph the first time it is drawn**, then keeps it in
118
+ a texture atlas. Cost therefore grows with the **characters** a game uses, not
119
+ with the strings it draws. A score that changes every frame costs no more glyph
120
+ work after the ten digits. A whole Latin character set fits on one 512×512 page,
121
+ so a line of text is one draw call.
105
122
 
106
- A font that is only measured and never drawn allocates no video memory at all.
123
+ A font that is only measured, never drawn, uses no video memory.
107
124
 
108
- Nothing needs freeing a font's atlas is released when the font is collected,
109
- in either order relative to its app. `Font.debug_live_pages` reports how many
110
- atlas pages exist and is there for tests, not for gameplay.
125
+ Nothing needs freeing. The engine releases a font's atlas when the font is
126
+ collected, whether before or after its app. `Font.debug_live_pages` returns how
127
+ many atlas pages exist. It serves tests, not gameplay.
111
128
 
112
129
  ## What is not here
113
130
 
114
- Markup (`<b>`, colour tags), bold and italic variants, multi-line layout, word
115
- wrapping, text input, and right-to-left or complex shaping. A string is one line
116
- of left-to-right glyphs.
131
+ rgame text has no markup (`<b>`, colour tags), no bold or italic variants, no
132
+ multi-line layout and no word wrapping. It has no text input, no right-to-left
133
+ text and no complex shaping. A string is one line of left-to-right glyphs.
@@ -0,0 +1,237 @@
1
+ # Tile maps
2
+
3
+ **`RGame::Engine::TileMap` and `RGame::Engine::Tileset` hold a map made in
4
+ [Tiled](https://www.mapeditor.org/) as plain data.** `TileMap` holds the grid: its
5
+ size, its layers and the tile id in every cell. `Tileset` holds what each tile id
6
+ means: which tiles are solid and which are animated. Neither class loads an image
7
+ or names a renderer, so both run headless and in specs.
8
+
9
+ A game rarely touches either class directly. The pieces that use them are:
10
+
11
+ | | Uses the map to |
12
+ |---|---|
13
+ | [`TileWorld`](components.md#tileworld) | answer solidity and world-size questions for actors |
14
+ | [`TileMapLayer`](components.md#tileworld) | draw one layer per node |
15
+ | [`TileMapRenderer`](assets.md#tile-maps) | bake and draw the tiles |
16
+ | `RGame::Game`'s `:tilemap` asset loader | load a `.tmx` and pair it with its tileset image |
17
+
18
+ Read on when a scene queries the map itself, or when you author maps for rgame.
19
+
20
+ ## Loading a map
21
+
22
+ ```ruby
23
+ require 'rgame'
24
+
25
+ map, image_path = RGame::Engine::TileMap.load('examples/assets/town.tmx')
26
+
27
+ image_path # => "examples/assets/tileset.png"
28
+ map.width # => 60 — in tiles
29
+ map.pixel_width # => 960
30
+ map.tileset # => RGame::Engine::Tileset
31
+ ```
32
+
33
+ **`TileMap.load(tmx_path)` returns two values: the map and its tileset image
34
+ path.** It reads the `.tmx`, follows it to the `.tsx` it names, parses both, and
35
+ attaches the tileset to the map. It does not load the image. An image is a GPU
36
+ handle, and the engine layer may not hold one. `RGame::Game`'s asset loader
37
+ passes the path to the asset manager instead:
38
+
39
+ ```ruby
40
+ app.assets.add_loader(:tilemap) do |path|
41
+ map, image_path = RGame::Engine::TileMap.load(path)
42
+ tiles = app.assets.image(image_path).tiles(map.tileset.tile_width,
43
+ map.tileset.tile_height)
44
+ RGame::Core::TileMapRenderer.new(map, tiles)
45
+ end
46
+ ```
47
+
48
+ In a game, reach the parsed map through that loader:
49
+ `app.assets.tilemap('map/island.tmx').map`.
50
+
51
+ **Every path resolves relative to the file that names it.** The `.tsx` resolves
52
+ next to the `.tmx`, and the image next to the `.tsx`. Tiled writes paths this
53
+ way, so you can move a map and its tileset together.
54
+
55
+ `TileMap.parse(tmx_string)` parses a `.tmx` from a String and touches no files. It
56
+ leaves `tileset` as `nil`. Attach one with `map.tileset = Tileset.parse(...)`
57
+ before asking about solidity.
58
+
59
+ ### What rgame reads from Tiled
60
+
61
+ rgame supports a subset of the `.tmx` format. A map outside it fails to parse or
62
+ parses wrongly, so author within these limits:
63
+
64
+ | | Supported |
65
+ |---|---|
66
+ | Orientation | orthogonal |
67
+ | Tilesets | **one**, in an external `.tsx` file; an embedded tileset has no `source` and `load` raises |
68
+ | Layer data | base64 with zlib compression (Tiled's "Base64 (zlib compressed)"); other encodings raise |
69
+ | Layers | tile layers only; object and image layers are ignored |
70
+ | Flipped or rotated tiles | the flip flags are stripped, so the tile draws unflipped |
71
+ | Map size | fixed; infinite maps are not read |
72
+
73
+ ## `RGame::Engine::TileMap`
74
+
75
+ ### Geometry
76
+
77
+ | Reader | |
78
+ |---|---|
79
+ | `width`, `height` | the map's size in tiles |
80
+ | `tile_width`, `tile_height` | one tile's size in pixels |
81
+ | `pixel_width`, `pixel_height` | the map's size in pixels |
82
+ | `layer_count` | how many tile layers the map has |
83
+ | `tileset`, `tileset=` | the attached `Tileset`, or `nil` |
84
+ | `tileset_source`, `firstgid` | the `.tsx` path the map names, and the tileset's first gid |
85
+
86
+ ### Reading cells
87
+
88
+ ```ruby
89
+ map.gid(0, 12, 7) # the gid in layer 0 at column 12, row 7
90
+ map.in_bounds?(60, 0) # => false — columns run 0..59
91
+ map.gid(0, -1, 0) # => 0 — outside the map, every layer is empty
92
+ ```
93
+
94
+ **A gid is Tiled's global tile id.** `0` means an empty cell. Any other gid names a
95
+ tile in the tileset; `Tileset#local_id(gid)` turns it into the tile's index in the
96
+ tileset image. Layer `0` is the bottom layer, as Tiled lists them.
97
+
98
+ `gid` answers `0` for any cell outside the map, so a caller never checks bounds
99
+ first. The map stores its gids in one
100
+ [`Util::Tensor`](values.md#rgameutiltensor), indexed column, row, layer.
101
+
102
+ ### Solidity
103
+
104
+ ```ruby
105
+ map.solid_tile?(12, 7) # any layer solid at that cell?
106
+ map.solid_at?(200.5, 116.0) # the same, from a world position in pixels
107
+ ```
108
+
109
+ **A cell is solid when any layer holds a solid tile there.** Collision considers
110
+ every layer, whatever it draws like. `solid_at?(world_x, world_y)` divides by the
111
+ tile size and asks `solid_tile?` for that cell. **Outside the map is not solid.**
112
+ Keep actors inside with `blocked_by: [:bounds]` on their mover.
113
+
114
+ Both methods need an attached tileset, which `load` provides.
115
+
116
+ Actors do not call these per step. [`TileWorld`](components.md#tileworld) reads
117
+ `solid_tile?` once per cell into a
118
+ [`Util::SolidGrid`](values.md#rgameutilsolidgrid), and collision and pathfinding
119
+ read that grid.
120
+
121
+ ### Layers drawn above the actors
122
+
123
+ ```ruby
124
+ map.above_layer?(2) # => true when Tiled marks layer 2 `above`
125
+ ```
126
+
127
+ **Mark a layer `above` in Tiled** to draw it over the actors, for tree canopies
128
+ or roofs. Add a custom **bool** property named `above` to the layer and tick it. A
129
+ layer without the property draws below.
130
+
131
+ The flag affects drawing order only. `TileWorld#first_above_layer` returns the
132
+ first flagged layer, and
133
+ [`TileMapLayer.mount`](components.md#tileworld) leaves the actors' gap below it.
134
+ A map with no flagged layer puts the actors over everything.
135
+
136
+ ### Building a map by hand
137
+
138
+ ```ruby
139
+ require 'rgame'
140
+
141
+ map = RGame::Engine::TileMap.new(
142
+ width: 2, height: 1, tile_width: 16, tile_height: 16,
143
+ tileset_source: 'tiles.tsx', firstgid: 1,
144
+ layers: [[1, 2]], # one layer, gids row by row
145
+ above: [false]
146
+ )
147
+ map.tileset = RGame::Engine::Tileset.new(
148
+ firstgid: 1, columns: 2, tile_width: 16, tile_height: 16,
149
+ image_source: 'tiles.png', animations: {}, solid_ids: Set[1]
150
+ )
151
+
152
+ map.solid_at?(20.0, 3.0) # => true — gid 2 is local tile 1, which is solid
153
+ map.solid_at?(3.0, 3.0) # => false
154
+ ```
155
+
156
+ **Specs build small maps this way**, with no files. `layers:` is an Array of
157
+ layers. Each layer is a flat Array of `width * height` gids, row by row. `above:`
158
+ is optional and defaults to no layer above.
159
+
160
+ ## `RGame::Engine::Tileset`
161
+
162
+ **A `Tileset` says what each tile in a sheet is.** It knows the sheet's geometry,
163
+ which tiles are solid, and which are animated.
164
+
165
+ | Reader | |
166
+ |---|---|
167
+ | `firstgid` | the gid of the tileset's first tile in the map |
168
+ | `columns` | how many tiles one row of the sheet holds |
169
+ | `tile_width`, `tile_height` | one tile's size in pixels |
170
+ | `image_source` | the sheet image, as the `.tsx` names it |
171
+ | `animations` | `{ local_id => [Frame, ...] }` for every animated tile |
172
+ | `animated_ids` | the local ids that have an animation |
173
+ | `solid_ids`, `solid_ids=` | a Set of the local ids that are solid |
174
+
175
+ `Tileset.parse(tsx_string, firstgid:)` parses a `.tsx` from a String. The
176
+ `firstgid` comes from the map, because Tiled stores it in the `.tmx`, not in the
177
+ `.tsx`.
178
+
179
+ ### Local ids
180
+
181
+ ```ruby
182
+ tileset.local_id(37) # => 36 when firstgid is 1
183
+ ```
184
+
185
+ **A local id is a tile's index in the sheet**, counted from 0, left to right and
186
+ top to bottom. It equals `gid - firstgid`. `Image#tiles` slices a sheet in the same
187
+ order, so a local id indexes that Array directly.
188
+
189
+ ### Solid tiles
190
+
191
+ ```ruby
192
+ tileset.solid?(gid) # takes a gid, not a local id; 0 is never solid
193
+ tileset.solid_ids << 12 # make local tile 12 solid
194
+ ```
195
+
196
+ **A tile is solid when it has a collision shape in Tiled.** Open the tileset in
197
+ Tiled's collision editor and draw any shape on the tile. `parse` marks every tile
198
+ whose `<objectgroup>` holds at least one object. The shape itself is ignored:
199
+ rgame treats a solid tile as a solid square. The map carries its collision, so
200
+ changing which tiles block needs no code.
201
+
202
+ `solid_ids` is writable, for a game that adds or removes solid tiles in code. Change
203
+ it before a `TileWorld` first asks, because the world copies solidity into its own
204
+ grid once.
205
+
206
+ ### Animated tiles
207
+
208
+ ```ruby
209
+ tileset.animations[37] # => [#<struct Frame tile_id=37, duration=250>, ...]
210
+ tileset.frame_local_id(37, 0) # => 37
211
+ tileset.frame_local_id(37, 250) # => 46
212
+ ```
213
+
214
+ **Animate a tile in Tiled's tile animation editor.** `parse` reads each frame as a
215
+ `Tileset::Frame` with a `tile_id` (a local id) and a `duration` in milliseconds.
216
+
217
+ `frame_local_id(local, ms)` returns the local id to draw for tile `local` after
218
+ `ms` milliseconds. The animation loops. A tile without an animation returns
219
+ itself. The time is an argument, not a clock read, so pausing is "stop
220
+ accumulating".
221
+
222
+ **Tiled speaks milliseconds; the engine speaks seconds.** `TileWorld` accumulates
223
+ `elapsed` in seconds, and `TileMapRenderer` converts it once per draw, as
224
+ `(elapsed * 1000).to_i`. Convert the same way when you call `frame_local_id`
225
+ yourself.
226
+
227
+ ## Testing against a map
228
+
229
+ `TileMapRenderer` draws any object that answers the tile map contract, and never
230
+ names `TileMap`. rgame's own suite states that contract in
231
+ `spec/support/shared_examples/a_tile_map.rb`. It checks both `TileMap` and the
232
+ spec stand-in `StubTileMap` against it. The contract covers `layer_count`,
233
+ `width`, `height`, `tile_width`, `tile_height`, `gid`, and a `tileset` answering
234
+ `local_id`, `animations` and `frame_local_id`.
235
+
236
+ A spec that needs a map but no files builds one [by hand](#building-a-map-by-hand),
237
+ or parses a `.tmx` string with `TileMap.parse`.