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
@@ -0,0 +1,13 @@
1
+ {
2
+ "image": "icons.png",
3
+ "images": {
4
+ "home": { "x": 0, "y": 0, "w": 50, "h": 50 },
5
+ "gear": { "x": 50, "y": 0, "w": 50, "h": 50 },
6
+ "save": { "x": 100, "y": 0, "w": 50, "h": 50 },
7
+ "star": { "x": 150, "y": 0, "w": 50, "h": 50 },
8
+ "trophy": { "x": 200, "y": 0, "w": 50, "h": 50 },
9
+ "audio_on": { "x": 250, "y": 0, "w": 50, "h": 50 },
10
+ "music_on": { "x": 300, "y": 0, "w": 50, "h": 50 },
11
+ "locked": { "x": 350, "y": 0, "w": 50, "h": 50 }
12
+ }
13
+ }
Binary file
Binary file
@@ -0,0 +1,10 @@
1
+ {
2
+ "image": "skills.png",
3
+ "images": {
4
+ "wand": { "x": 0, "y": 0, "w": 64, "h": 64 },
5
+ "wrench": { "x": 64, "y": 0, "w": 64, "h": 64 },
6
+ "torch": { "x": 128, "y": 0, "w": 64, "h": 64 },
7
+ "hammer": { "x": 192, "y": 0, "w": 64, "h": 64 },
8
+ "watering_can": { "x": 256, "y": 0, "w": 64, "h": 64 }
9
+ }
10
+ }
Binary file
Binary file
@@ -0,0 +1,65 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!-- Kenney "Tiny Town" (CC0) sliced as a Tiled tileset. See README.md.
3
+
4
+ A tile is solid to RGame::Engine::Tileset because it carries a collision
5
+ shape here — an <objectgroup> with at least one object — not because of
6
+ anything in code. The trees and fences below are the obstacles the examples
7
+ use; every other tile is walkable. -->
8
+ <tileset version="1.10" tiledversion="1.10.2" name="tiny_town" tilewidth="16" tileheight="16" tilecount="132" columns="12">
9
+ <image source="tileset.png" width="192" height="176"/>
10
+ <tile id="3">
11
+ <objectgroup draworder="index">
12
+ <object id="1" x="0" y="0" width="16" height="16"/>
13
+ </objectgroup>
14
+ </tile>
15
+ <tile id="4">
16
+ <objectgroup draworder="index">
17
+ <object id="1" x="0" y="0" width="16" height="16"/>
18
+ </objectgroup>
19
+ </tile>
20
+ <tile id="5">
21
+ <objectgroup draworder="index">
22
+ <object id="1" x="0" y="0" width="16" height="16"/>
23
+ </objectgroup>
24
+ </tile>
25
+ <tile id="15">
26
+ <objectgroup draworder="index">
27
+ <object id="1" x="0" y="0" width="16" height="16"/>
28
+ </objectgroup>
29
+ </tile>
30
+ <tile id="16">
31
+ <objectgroup draworder="index">
32
+ <object id="1" x="0" y="0" width="16" height="16"/>
33
+ </objectgroup>
34
+ </tile>
35
+ <tile id="27">
36
+ <objectgroup draworder="index">
37
+ <object id="1" x="0" y="0" width="16" height="16"/>
38
+ </objectgroup>
39
+ </tile>
40
+ <tile id="28">
41
+ <objectgroup draworder="index">
42
+ <object id="1" x="0" y="0" width="16" height="16"/>
43
+ </objectgroup>
44
+ </tile>
45
+ <tile id="44">
46
+ <objectgroup draworder="index">
47
+ <object id="1" x="0" y="0" width="16" height="16"/>
48
+ </objectgroup>
49
+ </tile>
50
+ <tile id="45">
51
+ <objectgroup draworder="index">
52
+ <object id="1" x="0" y="0" width="16" height="16"/>
53
+ </objectgroup>
54
+ </tile>
55
+ <tile id="46">
56
+ <objectgroup draworder="index">
57
+ <object id="1" x="0" y="0" width="16" height="16"/>
58
+ </objectgroup>
59
+ </tile>
60
+ <tile id="47">
61
+ <objectgroup draworder="index">
62
+ <object id="1" x="0" y="0" width="16" height="16"/>
63
+ </objectgroup>
64
+ </tile>
65
+ </tileset>
@@ -0,0 +1,26 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!-- Authored for the examples; edit it in Tiled. See README.md.
3
+
4
+ 60x40 tiles at 16px = 960x640 pixels, larger than the 640x480 window on
5
+ both axes so the camera has somewhere to scroll. Two layers: the ground is
6
+ entirely walkable, and everything solid lives in "obstacles" - a tree
7
+ border, a fence right across the middle, and scattered trees.
8
+
9
+ The fence has exactly one gap, at x=12..14, and it is deliberately far
10
+ west of both clearings. A gap sitting between them would be no obstacle at
11
+ all: the shortest route through it would cost exactly the straight-line
12
+ distance, and a pathfinder would have nothing to show. Placed out here, a
13
+ walker starting in the north clearing has to head away from the south one
14
+ to get through - a route you can watch being taken. The fence runs the full
15
+ interior width for the same reason: stopping it a tile short of the border
16
+ leaves a second gap nobody planned, and the route quietly uses that
17
+ instead. -->
18
+ <map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="60" height="40" tilewidth="16" tileheight="16" infinite="0" nextlayerid="3" nextobjectid="1">
19
+ <tileset firstgid="1" source="tileset.tsx"/>
20
+ <layer id="1" name="ground" width="60" height="40">
21
+ <data encoding="base64" compression="zlib">eJztWU1vwzAIzexzt3O7czdNyv//fU0PlqwM8/EAJ5t6eFIbJ5gHGGNclmV5Y1CIZ6vwjQaU3Jokl3qmBaXTyoxpwdmwjXn0fgGLg9E7lRkrDtmF+Z6bcx8viN57GVabcfF/2fAO4EOpr1aPWbhuuAH4TNInK283W0fzPSLXVcP8mf7N4u6R6+UbEWvZHGf5F9VTY8MCzpG1fnsdpLys3TettqTGI/hWwid18Dubj4T7hi8A34QsjrMXlhyM2jRrL7TKzOKXFXdRep/hHOOpz/v/Px3655wPPHaOsB13Nmjjo3dGfDm9VuV7lDxtLGfFVM+XyyGe+bm1O/ssM/LvX4WU91G+M/apjDkovmfIx1mIimdN7WqV9/xGqhGs4/9t/SL+1dZynE/X5XevLGIv0vbfrPpK83LjSJ1K5VXN2VPq82flVUkHT3/Sq+/IJl5bUH5ttZ3n/JtVk2TJ9fJFfWj1XWS9P6Nfx9XAkXdsfX4ZyT0inr35Sbpj4XxwxvU7sgvFw9pviV6/1rpK+h8dG0feH6F38Uj90ezo6U/u90qvniiQHqLmm6N7ICg3DaJykaYGReKaGkd7PxFcuT0jIs9KXK2cZ8VDlD57e0XJ1fJ6AGJxLzQ=</data>
22
+ </layer>
23
+ <layer id="2" name="obstacles" width="60" height="40">
24
+ <data encoding="base64" compression="zlib">eJztmU0OAiEMhdnofg7gEdT7385xQVIJP4X2wZvgl5DZOGD7WiidI4RwO8ex0XMXos278Ah1fe/KOXrfXxVXUt+abTWbWjDFT0lfja5XhDF/kb5u5a830pbV+YuanwmEvmw2SqS+KJ1lDFv2eQ9SfVt7xehe0vLlDF9/8crflm6rdY2g92e2s857f/bUERF3JX2tazHomrMr1ddiu0f9jfC9XL9WP/fGpsUmy3w9v7fmr1wjnccyb+2MtNTXs+vn1UR9n+d4KYdE+w7DeAc/fa9yX2a8/0ZG/pemztsxf5lA6RqfnvquugflyPlAo++M+ysqp7T1c0rJJ4h+FPLuyJi/SKS+I98JUnK+Y9r/U31z8R7xjAPp45m1isf3oxFW5dCs/hULyPqK0QdafUd9wta3RPWvWPmfv796e/ZoJJ49mh521Xen5wfjJRxI</data>
25
+ </layer>
26
+ </map>
@@ -0,0 +1,11 @@
1
+ {
2
+ "image": "ui.png",
3
+ "scale": 1,
4
+ "nine_slices": {
5
+ "panel": { "x": 0, "y": 0, "w": 32, "h": 32, "border": 8 },
6
+ "button_idle": { "x": 32, "y": 0, "w": 32, "h": 32, "border": 8 },
7
+ "button_focus": { "x": 64, "y": 0, "w": 32, "h": 32, "border": 8 },
8
+ "button_pressed": { "x": 96, "y": 0, "w": 32, "h": 32, "border": 8 },
9
+ "button_disabled": { "x": 128, "y": 0, "w": 32, "h": 32, "border": 8 }
10
+ }
11
+ }
Binary file
@@ -0,0 +1,8 @@
1
+ # The English table: every string this example draws.
2
+ en:
3
+ help:
4
+ walk: Arrows / WASD walk the pale circle — the other two need nobody
5
+ crate: "Enter a crate: it blinks once and counts you, you stay lit inside"
6
+ circles: Circle into circle does nothing — same layer, one line ignoring it
7
+ hud:
8
+ visits: "visits: %{count}"
@@ -0,0 +1,316 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Collision — two shapes touching.
4
+ #
5
+ # Run it:
6
+ #
7
+ # ruby examples/collision/main.rb
8
+ #
9
+ # Arrow keys or WASD walk the pale circle. The other circles need nobody. Drive
10
+ # into a crate and both of you light up; drive into another circle and neither
11
+ # of you does. It exercises:
12
+ # - Components::CollisionWorld — the scene-scoped system that pairs shapes up
13
+ # each step and tells them when they start and stop overlapping;
14
+ # - Components::CircleCollider — a round shape on a node;
15
+ # - Components::BoxCollider — the rectangular one, and the two mix freely;
16
+ # - Components::Velocity — from `examples/velocity`, so that things move into
17
+ # each other without anybody pressing a key;
18
+ # - Engine::Text — a crate's counter, a key with a variable, rendered again
19
+ # only on a contact.
20
+ #
21
+ # The pale circle stays lit for as long as it is inside a crate, and the crate
22
+ # blinks once and adds one to its counter. Two readings of the same pair of
23
+ # signals: one brackets a contact, the other marks the moment it began.
24
+ #
25
+ # ## The system is on the scene, the shapes are on the nodes
26
+ #
27
+ # `CollisionWorld` is a component mounted on the scene node, so it is born with
28
+ # the scene and torn down with it, and it rides the ordinary update traversal
29
+ # rather than needing a step of its own. The shapes are components on the things
30
+ # that have shapes.
31
+ #
32
+ # Nothing connects the two by hand. A collider registers with
33
+ # `node.system(CollisionWorld)` when its node enters the tree and unregisters
34
+ # when it leaves, and the engine fires both hooks — so a spawned or despawned
35
+ # entity cannot leak a registration, and no game code is asked to remember.
36
+ #
37
+ # That is also why every component here is added in `initialize` rather than in
38
+ # `on_add`: a node assembled outside the tree collects all of its components
39
+ # before any of them attaches, so the order they go on in never matters.
40
+ #
41
+ # ## A contact is reported to both sides, and neither is asked what it means
42
+ #
43
+ # Each step, the system buckets every collider, finds the overlapping pairs, and
44
+ # tells both colliders of each pair about the other. It stops there. It does not
45
+ # know what a crate is, and it never looks at `layer`.
46
+ #
47
+ # `layer` is an opaque tag, and the whole of the rule in this example is one
48
+ # line in `Mover`:
49
+ #
50
+ # collider.on_hit { |other| @touching += 1 unless other.layer == :mover }
51
+ #
52
+ # The two circles that drift through each other *are* reported, and that line is
53
+ # what makes it look like nothing happened. Pushing the test up into the system
54
+ # would mean the system deciding that same-layer things never interact — which is
55
+ # wrong the moment one kind of thing has to hurt another of its own kind.
56
+ #
57
+ # ## A contact is two edges, not a state
58
+ #
59
+ # `on_hit` fires on the step a pair starts overlapping. `on_separated` fires on
60
+ # the step it stops. Nothing fires on the steps in between, however long the two
61
+ # sit inside each other, and a pair is reported once however many broadphase
62
+ # cells it happens to span.
63
+ #
64
+ # That is what lets a handler do something that must happen exactly once —
65
+ # `@score += 100`, play a sound, spend a life — with nothing to remember and
66
+ # nothing to guard. `Crate` below counts arrivals in one line because of it.
67
+ #
68
+ # The two signals also come in balanced pairs, which is the other thing to see on
69
+ # screen. `Mover` keeps a count: up on `on_hit`, down on `on_separated`, lit
70
+ # while it is above zero. That count is right no matter how many crates the
71
+ # circle is inside at once, because the edges are **per pair** — a second crate
72
+ # is a second `on_hit`, not a louder version of the first.
73
+ #
74
+ # A contact also ends when the other side is destroyed or leaves the tree, and
75
+ # `on_separated` fires then too. Nothing here dies, but a shooter whose bullets
76
+ # vanish on impact depends on it: without it a ship could stay "in contact" with
77
+ # a rock that no longer exists.
78
+ #
79
+ # ## A box and a circle collide with each other
80
+ #
81
+ # `CircleCollider` and `BoxCollider` are not two separate worlds. Both answer the
82
+ # same broadphase question (the bounding box to bucket me in) and the same
83
+ # narrowphase one (`overlap?`), and the pair settles the actual test between
84
+ # themselves by handing each other their numbers. So one world holds both, and
85
+ # every circle here collides with every crate.
86
+ #
87
+ # ## An AABB does not turn with its node
88
+ #
89
+ # The drifting circles spin — the spoke is drawn so you can see it — and their
90
+ # collision shape is unaffected, because a circle is the same circle at every
91
+ # angle. The crates carry an axis-aligned box and do not spin, and that is a
92
+ # choice rather than a preference: a box that turned with its node would have to
93
+ # be recomputed into a wider box every frame, and the wider box is wrong in a
94
+ # different way. A thing that spins wants a circle.
95
+ #
96
+ # ## `cell_size` is the one number this system asks a game to pick
97
+ #
98
+ # The broadphase is a spatial hash: a lattice of square cells, each collider
99
+ # dropped into the cells its bounding box covers, and only colliders sharing a
100
+ # cell ever tested against each other. The faint grid on the backdrop is drawn at
101
+ # that size, so the number is visible rather than a constant at the top of a
102
+ # file.
103
+ #
104
+ # Pick it at about the size of the things being bucketed. Much smaller and one
105
+ # collider spans a block of cells and is tested from every one of them; much
106
+ # larger and everything lands in the same cell and the hash has quietly become
107
+ # the loop over all pairs it exists to avoid.
108
+ #
109
+ # ## What it does not solve
110
+ #
111
+ # Response. Nothing here is pushed out of anything: a contact is a notification,
112
+ # and these shapes overlap and carry on. Stopping a body at a wall is a different
113
+ # problem with different machinery, and `examples/collision_tiles` is where it
114
+ # lives.
115
+
116
+ $LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
117
+ require 'rgame/game'
118
+
119
+ WIDTH = 640
120
+ HEIGHT = 480
121
+ ASSETS = File.expand_path('../assets', __dir__)
122
+ LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
123
+
124
+ # The broadphase cell, and the grid drawn on the backdrop. Everything bucketed
125
+ # here is 32 to 96 pixels across, so the cell is one of those.
126
+ CELL_SIZE = 64
127
+
128
+ WALK_SPEED = 150.0
129
+
130
+ # How long a crate stays lit after a circle arrives. Long enough to see at a
131
+ # glance, short enough that two arrivals read as two.
132
+ FLASH_TIME = 0.3
133
+
134
+ # A round thing that lights up while it is inside a crate. Its two subclasses
135
+ # differ only in what moves them — the shape, the layer and the rule are the same
136
+ # for both.
137
+ #
138
+ # `@touching` is the balanced pair of edges, kept as a count rather than a flag
139
+ # because a circle can be inside two crates at once: the two in the middle of the
140
+ # field overlap, and driving into the pair is two `on_hit` calls before either
141
+ # `on_separated` arrives. A flag would go dark on leaving the first of them.
142
+ class Mover < RGame::Engine::Node2D
143
+ RADIUS = 16
144
+ BODY = RGame::Util::Color.new(236, 233, 220)
145
+ HIT = RGame::Util::Color.new(255, 214, 92)
146
+ SPOKE = RGame::Util::Color.new(60, 66, 82)
147
+
148
+ def initialize(**)
149
+ super(width: RADIUS * 2, height: RADIUS * 2, **)
150
+ @touching = 0
151
+ collider = add_component(RGame::Engine::Components::CircleCollider.new(radius: RADIUS, layer: :mover))
152
+ # Both edges are reported to both colliders, each handed the other, so this is
153
+ # written entirely from this node's side. The guard is the example's subject:
154
+ # same-layer pairs are reported and these lines drop them. It has to be the
155
+ # same guard on both, or the count would not come back to zero.
156
+ collider.on_hit { |other| @touching += 1 unless other.layer == :mover }
157
+ collider.on_separated { |other| @touching -= 1 unless other.layer == :mover }
158
+ end
159
+
160
+ # The centre is where the traversal has already put the renderer, and the
161
+ # circle's collision centre is the node's origin — the same point.
162
+ def on_draw(renderer, _view)
163
+ renderer.circle(0, 0, RADIUS, color: @touching.positive? ? HIT : BODY)
164
+ # Drawn at the node's own angle, which is what makes the spin visible at all:
165
+ # a spinning circle looks like a still one without a mark on it.
166
+ renderer.line(0, 0, RADIUS, 0, thickness: 3, color: SPOKE)
167
+ end
168
+ end
169
+
170
+ # Moves because it was given a velocity, and for no other reason.
171
+ class Drifter < Mover
172
+ def initialize(vx:, vy:, spin: 0.0, **)
173
+ super(**)
174
+ add_component(RGame::Engine::Components::Velocity.new(vx: vx, vy: vy, spin: spin))
175
+ add_component(RGame::Engine::Components::ScreenWrap.new(margin: RADIUS))
176
+ end
177
+ end
178
+
179
+ # The one with a hand on it. Same shape, same layer, same rule.
180
+ class Walker < Mover
181
+ def initialize(**)
182
+ super
183
+ add_component(RGame::Engine::Components::CharacterBody.new(speed: WALK_SPEED))
184
+ add_component(RGame::Engine::Components::PlayerController.new)
185
+ add_component(RGame::Engine::Components::ScreenWrap.new(margin: RADIUS))
186
+ end
187
+ end
188
+
189
+ # A rectangle that never moves and counts the circles that have arrived.
190
+ #
191
+ # Two of them overlap on purpose, in the middle of the field. That pair is in
192
+ # contact from the first step to the last, the system reports it exactly once,
193
+ # and neither counter moves — which is the same one-line rule `Mover` uses, seen
194
+ # from the other side.
195
+ #
196
+ # A visit is one circle arriving, and it is counted by adding one in the handler,
197
+ # because `on_hit` is the arrival rather than the overlap. Two circles standing
198
+ # in this crate at once is two visits: the edges are per pair, so each of them
199
+ # announced itself.
200
+ class Crate < RGame::Engine::Node2D
201
+ BODY = RGame::Util::Color.new(88, 104, 136)
202
+ HIT = RGame::Util::Color.new(150, 196, 255)
203
+ INK = RGame::Util::Color.new(180, 190, 210)
204
+
205
+ def initialize(width:, height:, **)
206
+ super
207
+ @flash = 0.0
208
+ @touches = 0
209
+ @label = RGame::Engine::Text.new('hud.visits', :count)
210
+ # The box is offset to sit around the node's origin, which is where a circle's
211
+ # centre already is — so both shapes here are drawn and collide about the
212
+ # same point. A sprite that wants a small box at its feet moves the offset
213
+ # instead; nothing says the box has to be centred.
214
+ collider = add_component(RGame::Engine::Components::BoxCollider.new(
215
+ width: width, height: height,
216
+ offset_x: -width / 2, offset_y: -height / 2, layer: :wall
217
+ ))
218
+ collider.on_hit do |other|
219
+ next if other.layer == :wall
220
+
221
+ @touches += 1
222
+ @flash = FLASH_TIME
223
+ end
224
+ end
225
+
226
+ # Only the flash, which is a cosmetic timer and the one thing here that has to
227
+ # be advanced rather than announced. The counter is not touched: it moves on the
228
+ # edge, in the handler above.
229
+ def on_update(dt) = @flash -= dt
230
+
231
+ def on_draw(renderer, _view)
232
+ renderer.rect(-width / 2, -height / 2, width, height, color: @flash.positive? ? HIT : BODY)
233
+ renderer.text(@label.with(count: @touches), -width / 2, (-height / 2) - 22, color: INK)
234
+ end
235
+ end
236
+
237
+ class Scene < RGame::Engine::Node2D
238
+ BACKDROP = RGame::Util::Color.new(26, 30, 38)
239
+ GRID = RGame::Util::Color.new(38, 44, 56)
240
+
241
+ # Centre x, centre y, width, height. The first two overlap.
242
+ CRATES = [
243
+ [300, 200, 96, 40],
244
+ [336, 244, 40, 96],
245
+ [520, 140, 72, 56],
246
+ [150, 340, 64, 64]
247
+ ].freeze
248
+
249
+ # Centre x, centre y, vx, vy, spin in degrees per second. Fixed rather than
250
+ # random, so two runs of this example can be compared without a seed.
251
+ DRIFTERS = [
252
+ [40, 200, 120.0, 0.0, 90.0],
253
+ [600, 140, -95.0, 0.0, -140.0],
254
+ [150, 104, 0.0, 105.0, 0.0]
255
+ ].freeze
256
+
257
+ # Both systems are mounted here rather than in `on_add`, so they exist before
258
+ # any child's collider attaches and goes looking for them.
259
+ def initialize
260
+ super
261
+ @help_walk = RGame::Engine::Text.new('help.walk')
262
+ @help_crate = RGame::Engine::Text.new('help.crate')
263
+ @help_circles = RGame::Engine::Text.new('help.circles')
264
+ add_component(RGame::Engine::Components::World.new(width: WIDTH, height: HEIGHT))
265
+ add_component(RGame::Engine::Components::CollisionWorld.new(cell_size: CELL_SIZE))
266
+ end
267
+
268
+ def on_add
269
+ CRATES.each { |x, y, w, h| add_node(Crate.new(x: x, y: y, width: w, height: h)) }
270
+ DRIFTERS.each do |x, y, vx, vy, spin|
271
+ add_node(Drifter.new(x: x, y: y, vx: vx, vy: vy, spin: spin))
272
+ end
273
+ add_node(Walker.new(x: 80, y: 430))
274
+ end
275
+
276
+ def on_draw(renderer, view)
277
+ renderer.rect(0, 0, view.width, view.height, color: BACKDROP)
278
+ draw_cells(renderer, view)
279
+
280
+ renderer.text(@help_walk, 12, 12)
281
+ renderer.text(@help_crate, 12, 34)
282
+ renderer.text(@help_circles, 12, 56)
283
+ end
284
+
285
+ private
286
+
287
+ # The broadphase's own lattice, so `cell_size` is something you can look at.
288
+ # A while loop rather than a Range: this runs every frame, and a fresh Range
289
+ # every frame is the allocation the hot-path cops exist to refuse.
290
+ #
291
+ # hot-path
292
+ def draw_cells(renderer, view)
293
+ x = CELL_SIZE
294
+ while x < view.width
295
+ renderer.rect(x, 0, 1, view.height, color: GRID)
296
+ x += CELL_SIZE
297
+ end
298
+
299
+ y = CELL_SIZE
300
+ while y < view.height
301
+ renderer.rect(0, y, view.width, 1, color: GRID)
302
+ y += CELL_SIZE
303
+ end
304
+ end
305
+ end
306
+
307
+ game = RGame::Game.new(
308
+ root: Scene.new,
309
+ caption: 'Collision',
310
+ width: WIDTH,
311
+ height: HEIGHT,
312
+ media_root: ASSETS,
313
+ locales: LOCALES
314
+ )
315
+
316
+ game.start
@@ -0,0 +1,9 @@
1
+ # The English table: every string this example draws.
2
+ en:
3
+ help:
4
+ walk: Arrows / WASD / gamepad to walk — trees and the fence are solid
5
+ slide: "Hold down and left against the fence: you slide to its one gap"
6
+ box: The red box is what collides. The rest of the sprite is a picture
7
+ ball: "Walk east into the spiky ball: it stops you, and it costs a life"
8
+ hud:
9
+ lives: "Lives: %{lives}"