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,14 @@
1
+ # The English table: every string this example draws.
2
+ en:
3
+ help:
4
+ seat: Press A on a controller to hand it the seat
5
+ back: Enter or Space on the keyboard takes it back
6
+ devices:
7
+ keyboard: Keyboard
8
+ controller: Controller
9
+ # What this game calls each action. The button shown beside it changes with
10
+ # the device; the word does not.
11
+ actions:
12
+ ui_confirm: Confirm
13
+ ui_cancel: Cancel
14
+ fire: Wave
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Input glyphs — the prompt matches the thing in your hand.
4
+ #
5
+ # Run it:
6
+ #
7
+ # ruby examples/input_glyphs/main.rb
8
+ #
9
+ # Arrow keys, WASD or a stick walk the hero. Press **A** on a controller and the
10
+ # panel switches to controller prompts; press **Enter** or **Space** on the
11
+ # keyboard and it switches back. It exercises:
12
+ # - Controls.gamepad? — is the device in the player's hands a controller;
13
+ # - InputMap#button_for — which of an action's ids that device can press;
14
+ # - Engine::Players with one seat — `on_unassigned_input` defaulting to
15
+ # `:takeover`, which is what keyboard-to-controller switching *is*;
16
+ # - renderer.sprite — a glyph sheet indexed by button id.
17
+ #
18
+ # ## Nothing here listens for a controller being plugged in
19
+ #
20
+ # A game with one seat treats an unassigned device that somebody uses as *that
21
+ # player* picking it up, rather than as a second player arriving. So plugging a
22
+ # pad in does nothing at all, and pressing A on it hands the seat over: the
23
+ # player's `device` changes, and every prompt below follows it on the next frame.
24
+ #
25
+ # That is the engine's `:takeover` policy, and a one-seat game gets it without
26
+ # asking. `examples/split_screen` is the other setting of the same dial.
27
+ #
28
+ # **It switches back, through `ui_confirm` and nothing else.** Once the pad has
29
+ # the seat the keyboard is unassigned, so Enter or Space hands it back — but W
30
+ # does not, and neither does any other key. One action rather than "any input" is
31
+ # deliberate: a stick resting a little off centre must never count as somebody
32
+ # picking up a controller, and the rule is the same rule in both directions. A
33
+ # game that genuinely wants any key to return to the keyboard sets
34
+ # `players.on_unassigned_input = :ignore` and calls `players.seat(device)` on its
35
+ # own terms.
36
+ #
37
+ # ## One entry lists both, and a prompt has to undo that
38
+ #
39
+ # An `InputMap` entry names the key *and* the pad button for one action:
40
+ #
41
+ # fire: { buttons: [Controls::KEY_SPACE, Controls::PAD_A] }
42
+ #
43
+ # That is right for reading input — a device only answers for its own kind of id,
44
+ # so polling needs no branch — and exactly wrong for showing it. A prompt saying
45
+ # "press Space or A" tells the player about hardware they are not holding.
46
+ #
47
+ # `InputMap#button_for(action, device)` is the question the other way round: of
48
+ # the ids bound to this action, which one can *this* device press? It compares
49
+ # `Controls.pad_button?(id)` against `Controls.gamepad?(device)` — two id spaces
50
+ # that were made disjoint for exactly this — and gives back the first that
51
+ # matches, or nil.
52
+ #
53
+ # The first match is not arbitrary: `ui_confirm` lists Return before Space, so
54
+ # the panel says Return. The order an entry is written in is the order a prompt
55
+ # prefers.
56
+ #
57
+ # ## A glyph is a button id rendered as a picture
58
+ #
59
+ # Which makes the whole of it a lookup keyed by that id:
60
+ #
61
+ # GLYPH_COLUMN = { Controls::KEY_SPACE => 0, ... }.freeze
62
+ #
63
+ # The sheet is one row of 64x64 frames, drawn with `renderer.sprite`, and the
64
+ # table says which column belongs to which id. There is no cleverness to it, and
65
+ # that is the point — the hard part was deciding *which* id to draw, and that
66
+ # happened above.
67
+ #
68
+ # The table covers the five ids this example's three actions can produce. A game
69
+ # with a rebinding screen needs one entry per id it lets a player bind, or a text
70
+ # fallback for the ones it has no art for; here a missing glyph draws nothing and
71
+ # leaves the label to speak for itself.
72
+ #
73
+ # ## What it does not solve
74
+ #
75
+ # **Axes.** Walking is bound to `move_x`/`move_y`, which are a stick and pairs of
76
+ # keys rather than buttons, so `button_for` gives nil for them and the panel does
77
+ # not try: the picture for "the left stick" or "the arrow keys" is one image
78
+ # standing for several ids, which is a different question with a different
79
+ # answer. The three prompts here are all single buttons.
80
+ #
81
+ # **Which controller.** The pad glyphs are Xbox-shaped because SDL's button names
82
+ # are (`PAD_A`, `PAD_B`), and a PlayStation pad reports the same ids under
83
+ # different labels. Shipping a second sheet and choosing between them by
84
+ # controller name is a real thing games do, and it is another table keyed by the
85
+ # same ids.
86
+
87
+ $LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
88
+ require 'rgame/game'
89
+
90
+ WIDTH = 640
91
+ HEIGHT = 480
92
+ ASSETS = File.expand_path('../assets', __dir__)
93
+ LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
94
+
95
+ SPEED = 90.0
96
+
97
+ # The glyph sheet: one row of 64x64 frames, in the order examples/assets/README.md
98
+ # lists them.
99
+ SHEET = 'glyphs.json'
100
+ GLYPH = 64
101
+
102
+ # A prompt is a picture of a button, so the table is keyed by button id. These
103
+ # five are what this example's three actions resolve to on the two kinds of
104
+ # device.
105
+ GLYPH_COLUMN = {
106
+ RGame::Util::Controls::KEY_SPACE => 0,
107
+ RGame::Util::Controls::KEY_RETURN => 1,
108
+ RGame::Util::Controls::KEY_ESCAPE => 2,
109
+ RGame::Util::Controls::PAD_A => 3,
110
+ RGame::Util::Controls::PAD_B => 4
111
+ }.freeze
112
+
113
+ # The panel: three actions, and the button each one is on for the device the
114
+ # player is holding right now.
115
+ class Prompts < RGame::Engine::Node2D
116
+ PANEL = RGame::Util::Color.new(18, 22, 30, 220)
117
+ INK = RGame::Util::Color.new(228, 232, 240)
118
+ DIM = RGame::Util::Color.new(150, 158, 172)
119
+
120
+ # The action to ask about, and what this game calls it. Both fixed: the label
121
+ # is the game's word for the action, and the button is what changes.
122
+ ROWS = { ui_confirm: RGame::Engine::Text.new('actions.ui_confirm'),
123
+ ui_cancel: RGame::Engine::Text.new('actions.ui_cancel'),
124
+ fire: RGame::Engine::Text.new('actions.fire') }.freeze
125
+
126
+ # A text chosen by state rather than built from it: one Text per state, so there
127
+ # is nothing to render again but a language switch.
128
+ DEVICE_NAME = { false => RGame::Engine::Text.new('devices.keyboard'),
129
+ true => RGame::Engine::Text.new('devices.controller') }.freeze
130
+
131
+ WIDTH_PX = 300
132
+ ROW_H = 72
133
+ PAD = 14
134
+ HEADER_H = 34
135
+
136
+ def on_add
137
+ # One seat, so the player is the primary one. Held rather than looked up per
138
+ # frame: which player this is cannot change, while the device they hold can.
139
+ @player = system(RGame::Engine::Players).primary
140
+ end
141
+
142
+ def on_draw(renderer, _view)
143
+ device = @player.device
144
+ renderer.rect(0, 0, WIDTH_PX, HEADER_H + (ROWS.size * ROW_H), color: PANEL)
145
+ renderer.text(DEVICE_NAME.fetch(RGame::Util::Controls.gamepad?(device)), PAD, PAD, color: INK)
146
+
147
+ ROWS.each_with_index do |(action, label), index|
148
+ draw_row(renderer, action, label, device, HEADER_H + (index * ROW_H))
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ # hot-path
155
+ def draw_row(renderer, action, label, device, y)
156
+ id = @player.input_map.button_for(action, device)
157
+ column = GLYPH_COLUMN[id]
158
+ renderer.sprite(SHEET, 0, column, PAD, y) unless column.nil?
159
+ renderer.text(label, PAD + GLYPH + PAD, y + ((GLYPH - 16) / 2), color: id.nil? ? DIM : INK)
160
+ end
161
+ end
162
+
163
+ # Something to walk, so that picking up a controller has a reason to happen.
164
+ # `examples/walk`'s hero, kept inside the window.
165
+ class Hero < RGame::Engine::Node2D
166
+ def initialize(**)
167
+ super
168
+ add_component(RGame::Engine::Components::AnimatedSprite.new(sheet: 'hero.json'))
169
+ add_component(RGame::Engine::Components::CharacterBody.new(speed: SPEED))
170
+ add_component(RGame::Engine::Components::PlayerController.new)
171
+ end
172
+
173
+ def on_update(_dt)
174
+ self.x = x.clamp(0, WIDTH - width)
175
+ self.y = y.clamp(0, HEIGHT - height)
176
+ end
177
+ end
178
+
179
+ class Scene < RGame::Engine::Node2D
180
+ BACKDROP = RGame::Util::Color.new(30, 36, 46)
181
+ MARGIN = 16
182
+
183
+ # The hero first, so the panel is drawn over them rather than under: a node
184
+ # draws before its later siblings, and the two do overlap once the hero is
185
+ # walked into the corner.
186
+ def initialize
187
+ super
188
+ @help_seat = RGame::Engine::Text.new('help.seat')
189
+ @help_back = RGame::Engine::Text.new('help.back')
190
+ end
191
+
192
+ def on_add
193
+ add_node(Hero.new(x: 470, y: 300))
194
+ add_node(Prompts.new(x: MARGIN, y: MARGIN))
195
+ end
196
+
197
+ def on_draw(renderer, view)
198
+ renderer.rect(0, 0, view.width, view.height, color: BACKDROP)
199
+ renderer.text(@help_seat, MARGIN, view.height - 52)
200
+ renderer.text(@help_back, MARGIN, view.height - 30)
201
+ end
202
+ end
203
+
204
+ game = RGame::Game.new(
205
+ root: Scene.new,
206
+ caption: 'Input glyphs',
207
+ width: WIDTH,
208
+ height: HEIGHT,
209
+ media_root: ASSETS,
210
+ locales: LOCALES
211
+ )
212
+
213
+ 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, Space / A to hop
5
+ box: The red box stays on the ground. So does everything it collides with
6
+ fence: "Walk south into the fence and hop: the picture clears it, the feet do not"
7
+ status:
8
+ airborne: In the air
9
+ grounded: On the ground
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Jump top-down — a hop that leaves the ground without leaving its spot on it.
4
+ #
5
+ # Run it:
6
+ #
7
+ # ruby examples/jump_topdown/main.rb
8
+ #
9
+ # Arrow keys, WASD, a d-pad or a left stick walk the hero; Space or the pad's A
10
+ # button hops. The trees and the fence are solid. It exercises:
11
+ # - Components::Hop — a parabola in time, written to the node's elevation;
12
+ # - Node2D#elevation — how far a node's picture is drawn above where it stands;
13
+ # - Components::AnimatedSprite — the picture that rises with it;
14
+ # - Components::FeetCollider and Components::CharacterBody — the shape and the
15
+ # steps that stay on the ground, from `examples/collision_tiles`;
16
+ # - Components::TileWorld and Components::CameraFollow — the map and a camera
17
+ # that does not bob;
18
+ # - InputMap.default.merge — the :jump action this game declares.
19
+ #
20
+ # ## A jump is a drawing offset
21
+ #
22
+ # In a top-down view, "up" on the screen is *north*. So a jump cannot move the
23
+ # node up the screen: that would be walking north, and the feet box would go with
24
+ # it, into whatever is north of the hero. What leaves the ground is the picture.
25
+ #
26
+ # `Hop` writes its height to `node.elevation`, and `elevation` is not part of the
27
+ # transform: `y` does not change, `world_y` does not change, the feet box does not
28
+ # move, and neither does the camera following them. `AnimatedSprite` is the one
29
+ # thing that reads it, and draws the sprite that far above the spot. Watch the red
30
+ # box during a hop — it never leaves the floor, and the shadow sits on it, getting
31
+ # smaller as the hero gets further away from it.
32
+ #
33
+ # ## So a hop does not clear a fence
34
+ #
35
+ # Walk up to the fence and hop over it. The sprite rises well above it, and the
36
+ # hero stops exactly where walking stopped them, because what collides never
37
+ # left the ground. That is correct for a hop and would be wrong for a vault, and
38
+ # the difference is a decision about the map rather than about the jump.
39
+ #
40
+ # ## What this does not solve
41
+ #
42
+ # **Crossing anything.** `hop.airborne?` is the seam for that: a game whose
43
+ # chasm tiles should be passable in the air reads it where it decides what is
44
+ # solid. `TileWorld` does not consult it, and `Hop` knows nothing about tiles.
45
+ # Which tiles a jump clears, and what happens to a hero who lands on one, are
46
+ # rules of a particular game, and a jump component that had an opinion about
47
+ # them would be wrong for the next game.
48
+ #
49
+ # **Height against the scenery.** The lifted sprite keeps the draw order it has
50
+ # on the ground, so a hop next to a tree still passes behind the canopy that
51
+ # would hide the hero walking there. A game with tall jumps would sort by the
52
+ # feet and draw the lifted part over the scenery.
53
+
54
+ $LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
55
+ require 'rgame/game'
56
+
57
+ WIDTH = 640
58
+ HEIGHT = 480
59
+ ASSETS = File.expand_path('../assets', __dir__)
60
+ LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
61
+
62
+ MAP = 'town.tmx'
63
+ SPEED = 80.0 # px/s
64
+
65
+ # A hop about as tall as the hero's head, over half a second: enough to read as a
66
+ # jump at this sprite size, short enough that it is over before the next press.
67
+ HOP_PEAK = 18.0
68
+ HOP_DURATION = 0.5
69
+
70
+ FEET_WIDTH = 12
71
+ FEET_HEIGHT = 6
72
+
73
+ CAMERA_OFFSET_X = 8
74
+ CAMERA_OFFSET_Y = 19
75
+
76
+ # North of the fence, two tiles above it: one walk down reaches it.
77
+ START_X = 384.0
78
+ START_Y = 264.0
79
+
80
+ Controls = RGame::Util::Controls
81
+
82
+ # A walker from `examples/collision_tiles` with one more component. The only
83
+ # drawing it does itself is what stays on the ground: the shadow and the feet box.
84
+ class Hero < RGame::Engine::Node2D
85
+ FEET = RGame::Util::Color.rgba(255, 110, 110, 120)
86
+ SHADOW = RGame::Util::Color.rgba(0, 0, 0, 90)
87
+
88
+ # The shadow on the ground is an ellipse this wide and tall, and shrinks to half
89
+ # its size once the hero is this far above it.
90
+ SHADOW_RADIUS_X = 7.0
91
+ SHADOW_RADIUS_Y = 3.0
92
+ SHADOW_HALF_AT = 18.0
93
+
94
+ attr_reader :hop
95
+
96
+ def initialize(camera:, **)
97
+ super(**)
98
+ add_component(RGame::Engine::Components::AnimatedSprite.new(sheet: 'hero.json'))
99
+ @collider = add_component(RGame::Engine::Components::FeetCollider.new(
100
+ width: FEET_WIDTH, height: FEET_HEIGHT
101
+ ))
102
+ add_component(RGame::Engine::Components::CharacterBody.new(speed: SPEED, blocked_by: [:tiles]))
103
+ add_component(RGame::Engine::Components::PlayerController.new)
104
+ # The one line that differs from `examples/collision_tiles`. It needs nothing
105
+ # from its siblings and none of them know it is here: it writes the node's
106
+ # elevation, and the sprite already draws lifted by whatever that is.
107
+ @hop = add_component(RGame::Engine::Components::Hop.new(peak: HOP_PEAK, duration: HOP_DURATION))
108
+ add_component(RGame::Engine::Components::CameraFollow.new(
109
+ camera: camera, offset_x: CAMERA_OFFSET_X, offset_y: CAMERA_OFFSET_Y
110
+ ))
111
+ end
112
+
113
+ # Both drawn at the feet, and neither reads the node's elevation: this is the
114
+ # part of the hero that is on the ground. The shadow is a circle squashed into an
115
+ # ellipse, so it is placed first and squashed second — squashing first would
116
+ # squash the placement too. It shrinks through the circle's radius rather than
117
+ # the squash, which keeps the ellipse's proportions. `z: -1` puts it under the
118
+ # sprite, which draws at 0.
119
+ def on_draw(renderer, _view)
120
+ box = @collider.box
121
+ shrink = 1.0 / (1.0 + (@hop.height / SHADOW_HALF_AT))
122
+ renderer.translated(box.offset_x + (box.width / 2.0), box.offset_y + (box.height / 2.0)) do
123
+ renderer.scaled(SHADOW_RADIUS_X, SHADOW_RADIUS_Y) do
124
+ renderer.circle(0, 0, shrink, color: SHADOW, z: -1)
125
+ end
126
+ end
127
+ renderer.rect(box.offset_x, box.offset_y, box.width, box.height, color: FEET)
128
+ end
129
+ end
130
+
131
+ # The scene from `examples/collision_tiles`, less the spiky ball.
132
+ class Scene < RGame::Engine::Node2D
133
+ STATE = { true => RGame::Engine::Text.new('status.airborne'),
134
+ false => RGame::Engine::Text.new('status.grounded') }.freeze
135
+
136
+ def initialize
137
+ super
138
+ @help_walk = RGame::Engine::Text.new('help.walk')
139
+ @help_box = RGame::Engine::Text.new('help.box')
140
+ @help_fence = RGame::Engine::Text.new('help.fence')
141
+ end
142
+
143
+ def on_add
144
+ map = root.context.assets.tilemap(MAP).map
145
+ players = root.system(RGame::Engine::Players)
146
+
147
+ add_component(RGame::Engine::Components::TileWorld.new(
148
+ map: map, tilemap_id: MAP, cameras: players.map(&:camera)
149
+ ))
150
+
151
+ view = add_node(RGame::Engine::WorldView.new)
152
+ actors = RGame::Engine::TileMapLayer.mount(view)
153
+ @hero = actors.add_node(Hero.new(camera: players.primary.camera, x: START_X, y: START_Y))
154
+ end
155
+
156
+ def on_draw(renderer, _view)
157
+ renderer.text(@help_walk, 12, 12)
158
+ renderer.text(@help_box, 12, 34)
159
+ renderer.text(@help_fence, 12, 56)
160
+ renderer.text(STATE[@hero.hop.airborne?], 12, 78)
161
+ end
162
+ end
163
+
164
+ game = RGame::Game.new(
165
+ root: Scene.new,
166
+ caption: 'Jump top-down',
167
+ width: WIDTH,
168
+ height: HEIGHT,
169
+ media_root: ASSETS,
170
+ locales: LOCALES,
171
+ # :jump is this game's own action. Space is also in the default map as :fire and
172
+ # :ui_confirm, which nothing in this scene reads.
173
+ input_map: RGame::Engine::InputMap.default.merge(
174
+ jump: { buttons: [Controls::KEY_SPACE, Controls::PAD_A] }
175
+ )
176
+ )
177
+
178
+ game.start
@@ -0,0 +1,12 @@
1
+ # The German table. It has no `zero:`, because German says "0 Äpfel", and it
2
+ # lacks `hud.hint` on purpose: main.rb's header says what that shows.
3
+ de:
4
+ title: Lokalisierung
5
+ hud:
6
+ apples:
7
+ one: "%{count} Apfel"
8
+ other: "%{count} Äpfel"
9
+ locale: "Aktuelles Gebietsschema: %{locale}"
10
+ language:
11
+ title: Sprache
12
+ system: Systemsprache verwenden
@@ -0,0 +1,13 @@
1
+ # The English table, and the default: a key German lacks shows this text.
2
+ en:
3
+ title: Localization
4
+ hud:
5
+ apples:
6
+ zero: No apples
7
+ one: "%{count} apple"
8
+ other: "%{count} apples"
9
+ locale: "Current locale: %{locale}"
10
+ hint: Left and right change the count
11
+ language:
12
+ title: Language
13
+ system: Use the system language
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Localization — the same screen in two languages.
4
+ #
5
+ # Run it:
6
+ #
7
+ # ruby examples/localization/main.rb
8
+ #
9
+ # Left and right change the number of apples. Up and down move through the
10
+ # language menu, and Enter picks one. Quit and run it again: the language you
11
+ # picked is still the one on screen. It exercises:
12
+ # - Engine::I18n — tables in locales/, a fallback to English, CLDR plurals;
13
+ # - Engine::Text — a key built once, with a `count` and with a variable;
14
+ # - UI::Menu's `scope:`, and `Text.literal` for text that is never translated;
15
+ # - RGame::Game's `locales:`, and the language it picks from the OS;
16
+ # - Util::SaveFile, holding the language a player picked.
17
+ #
18
+ # ## Nothing is looked up while drawing
19
+ #
20
+ # Every string on screen is an Engine::Text built in `initialize`. Reading one
21
+ # compares its variables and `I18n.generation` with what it last rendered, and
22
+ # returns the same String while nothing changed. Picking a language moves the
23
+ # generation, so every Text and every button renders again on its next read,
24
+ # and nothing here tells them to.
25
+ #
26
+ # ## The tables sit beside this file
27
+ #
28
+ # `Game.new(locales: File.join(__dir__, 'locales'))` loads `en.yml` and `de.yml`
29
+ # from here. Every example shares `examples/assets/` as its media root, so a
30
+ # table there would be loaded into every example.
31
+ #
32
+ # ## German lacks a key, on purpose
33
+ #
34
+ # `de.yml` has no `hud.hint`. In German the hint shows the English text, because
35
+ # German's fallback chain ends at the default locale, English. A player reads a
36
+ # sentence in a language the game ships rather than `hud.hint`. That is the right
37
+ # failure for a game, and the wrong one to ship on purpose: a project from
38
+ # `rgame new` has a locales spec that fails until German has the key.
39
+ #
40
+ # German also has no `zero:` for the apples. English says "No apples", German
41
+ # says "0 Äpfel": an explicit `zero:` belongs to the table that has it.
42
+ #
43
+ # ## Language names are literals
44
+ #
45
+ # "English" and "Deutsch" are `Text.literal`, not keys. A language is named in
46
+ # its own language whichever one is current, so its name must not translate.
47
+ # The third button is a key, and the menu's `scope: 'language'` makes `system`
48
+ # read `language.system`. A literal has no key, so the scope does not touch it.
49
+ #
50
+ # ## What this does not solve
51
+ #
52
+ # The engine layer cannot measure text yet, so the buttons are a fixed width,
53
+ # chosen wide enough for the longer label in either language: "Use the system
54
+ # language". A third language with longer words would need a wider slot.
55
+
56
+ $LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
57
+ require 'rgame/game'
58
+
59
+ UI = RGame::Engine::UI
60
+ I18n = RGame::Engine::I18n
61
+ Text = RGame::Engine::Text
62
+
63
+ WIDTH = 640
64
+ HEIGHT = 360
65
+ ASSETS = File.expand_path('../assets', __dir__)
66
+ LOCALES = File.expand_path('locales', __dir__)
67
+
68
+ # The language on screen, and the file that remembers a player's choice.
69
+ #
70
+ # `RGame::Game` picks a language from the OS before `start`. A choice saved
71
+ # here wins over that, and "use the system language" forgets the choice again.
72
+ class Language
73
+ def initialize(save, preferred)
74
+ @save = save
75
+ @preferred = preferred
76
+ end
77
+
78
+ # Called between `Game.new`, which loaded the tables, and `start`. `choose`
79
+ # takes the saved locale if a table covers it, and the OS's otherwise, so a
80
+ # hand-edited file holding a language this game lacks changes nothing.
81
+ def restore
82
+ saved = @save.read[:language]
83
+ I18n.locale = I18n.choose([saved, *@preferred]) if saved.is_a?(String) && !saved.empty?
84
+ end
85
+
86
+ def pick(locale)
87
+ I18n.locale = locale
88
+ @save.write(language: locale.name)
89
+ end
90
+
91
+ def follow_system
92
+ @save.delete
93
+ I18n.locale = I18n.choose(@preferred)
94
+ end
95
+ end
96
+
97
+ # The whole screen: a heading, three lines of text and the language menu.
98
+ class Screen < RGame::Engine::Node2D
99
+ MARGIN = 40
100
+ MENU_Y = 196
101
+ ITEM_WIDTH = 280
102
+ ITEM_HEIGHT = 34
103
+
104
+ def initialize(language:, **)
105
+ super(**)
106
+ @language = language
107
+ @apples_held = 3
108
+ @title = Text.new('title')
109
+ @apples = Text.new('hud.apples', :count)
110
+ @locale = Text.new('hud.locale', :locale)
111
+ @hint = Text.new('hud.hint')
112
+ @heading = Text.new('language.title')
113
+ end
114
+
115
+ def on_add
116
+ menu = add_node(UI::Menu.new(x: MARGIN, y: MENU_Y, scope: 'language',
117
+ layout: UI::Column.new(item_width: ITEM_WIDTH, item_height: ITEM_HEIGHT)))
118
+ menu.add(UI::PanelButton.new(label: Text.literal('English'))).on_activated { @language.pick(:en) }
119
+ menu.add(UI::PanelButton.new(label: Text.literal('Deutsch'))).on_activated { @language.pick(:de) }
120
+ menu.add(UI::PanelButton.new(label: 'system')).on_activated { @language.follow_system }
121
+ end
122
+
123
+ def on_control(actions)
124
+ @apples_held += 1 if actions.pressed?(:ui_right)
125
+ @apples_held -= 1 if actions.pressed?(:ui_left) && @apples_held.positive?
126
+ end
127
+
128
+ def on_draw(renderer, _view)
129
+ renderer.text(@title, MARGIN, 24)
130
+ renderer.text(@apples.with(count: @apples_held), MARGIN, 72)
131
+ renderer.text(@locale.with(locale: I18n.locale.name), MARGIN, 96)
132
+ renderer.text(@hint, MARGIN, 120)
133
+ renderer.text(@heading, MARGIN, MENU_Y - 30)
134
+ end
135
+ end
136
+
137
+ # `dir:` is normally left out and the file lands in the platform's own data
138
+ # directory. It is overridable so a driven run writes somewhere disposable.
139
+ save = RGame::Util::SaveFile.new('language.json', game: 'rgame-examples',
140
+ dir: ENV.fetch('RGAME_SAVE_DIR', nil))
141
+ language = Language.new(save, RGame::Core.preferred_locales)
142
+
143
+ game = RGame::Game.new(
144
+ root: Screen.new(language: language),
145
+ caption: 'Localization',
146
+ width: WIDTH,
147
+ height: HEIGHT,
148
+ media_root: ASSETS,
149
+ locales: LOCALES
150
+ )
151
+
152
+ language.restore
153
+
154
+ # The buttons draw nine-slices, which name elements of an atlas rather than
155
+ # files, so the atlas is registered before the first frame.
156
+ game.renderer.register_ui_atlas(game.assets.ui_atlas('ui.json'))
157
+
158
+ game.start
@@ -0,0 +1,23 @@
1
+ # The English table: every string this example draws.
2
+ en:
3
+ title:
4
+ heading: Menu navigation
5
+ help: Up and down to move, Enter to choose
6
+ play: Play
7
+ settings: Settings
8
+ quit: Quit
9
+ settings:
10
+ heading: Settings — left and right change a value
11
+ fullscreen: Fullscreen
12
+ scale: Scale mode
13
+ volume: Volume
14
+ back: Back
15
+ # Quoted: YAML reads a bare on and off as true and false.
16
+ 'on': "on"
17
+ 'off': "off"
18
+ disabled: disabled
19
+ stretch: stretch
20
+ letterbox: letterbox
21
+ integer: integer
22
+ play:
23
+ help: Enter makes a noise — Escape returns to the title