rgame 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +135 -1
  3. data/README.md +67 -65
  4. data/docs/api/README.md +86 -82
  5. data/docs/api/app.md +125 -68
  6. data/docs/api/assets.md +203 -165
  7. data/docs/api/audio.md +130 -89
  8. data/docs/api/cli.md +259 -0
  9. data/docs/api/components.md +1032 -265
  10. data/docs/api/drawing.md +160 -160
  11. data/docs/api/examples.md +263 -0
  12. data/docs/api/game.md +149 -50
  13. data/docs/api/images.md +39 -39
  14. data/docs/api/input.md +226 -148
  15. data/docs/api/internals.md +241 -62
  16. data/docs/api/localization.md +285 -0
  17. data/docs/api/scene_graph.md +397 -244
  18. data/docs/api/signals.md +81 -76
  19. data/docs/api/systems.md +186 -73
  20. data/docs/api/text.md +58 -41
  21. data/docs/api/tile_maps.md +237 -0
  22. data/docs/api/toolbox.md +395 -146
  23. data/docs/api/ui.md +883 -54
  24. data/docs/api/values.md +216 -33
  25. data/examples/assets/README.md +322 -0
  26. data/examples/assets/blip.ogg +0 -0
  27. data/examples/assets/glyphs.json +5 -0
  28. data/examples/assets/glyphs.png +0 -0
  29. data/examples/assets/hero.json +12 -0
  30. data/examples/assets/hero.png +0 -0
  31. data/examples/assets/icons.json +13 -0
  32. data/examples/assets/icons.png +0 -0
  33. data/examples/assets/music.ogg +0 -0
  34. data/examples/assets/skills.json +10 -0
  35. data/examples/assets/skills.png +0 -0
  36. data/examples/assets/tileset.png +0 -0
  37. data/examples/assets/tileset.tsx +65 -0
  38. data/examples/assets/town.tmx +26 -0
  39. data/examples/assets/ui.json +11 -0
  40. data/examples/assets/ui.png +0 -0
  41. data/examples/collision/locales/en.yml +8 -0
  42. data/examples/collision/main.rb +316 -0
  43. data/examples/collision_tiles/locales/en.yml +9 -0
  44. data/examples/collision_tiles/main.rb +274 -0
  45. data/examples/fullscreen/locales/en.yml +10 -0
  46. data/examples/fullscreen/main.rb +216 -0
  47. data/examples/game_menu/locales/en.yml +8 -0
  48. data/examples/game_menu/main.rb +170 -0
  49. data/examples/input_glyphs/locales/en.yml +14 -0
  50. data/examples/input_glyphs/main.rb +213 -0
  51. data/examples/jump_topdown/locales/en.yml +9 -0
  52. data/examples/jump_topdown/main.rb +178 -0
  53. data/examples/localization/locales/de.yml +12 -0
  54. data/examples/localization/locales/en.yml +13 -0
  55. data/examples/localization/main.rb +158 -0
  56. data/examples/menu_navigation/locales/en.yml +23 -0
  57. data/examples/menu_navigation/main.rb +365 -0
  58. data/examples/music/locales/en.yml +7 -0
  59. data/examples/music/main.rb +134 -0
  60. data/examples/pathfinding/locales/en.yml +17 -0
  61. data/examples/pathfinding/main.rb +298 -0
  62. data/examples/pooling/locales/en.yml +7 -0
  63. data/examples/pooling/main.rb +259 -0
  64. data/examples/quick_wheel/locales/en.yml +16 -0
  65. data/examples/quick_wheel/main.rb +184 -0
  66. data/examples/radial_menu/locales/en.yml +16 -0
  67. data/examples/radial_menu/main.rb +184 -0
  68. data/examples/save_load/locales/en.yml +11 -0
  69. data/examples/save_load/main.rb +207 -0
  70. data/examples/save_load_ids/locales/en.yml +11 -0
  71. data/examples/save_load_ids/main.rb +322 -0
  72. data/examples/scroll_map/locales/en.yml +4 -0
  73. data/examples/scroll_map/main.rb +140 -0
  74. data/examples/signals/locales/en.yml +6 -0
  75. data/examples/signals/main.rb +278 -0
  76. data/examples/skill_bar/locales/en.yml +14 -0
  77. data/examples/skill_bar/main.rb +159 -0
  78. data/examples/sound/locales/en.yml +6 -0
  79. data/examples/sound/main.rb +122 -0
  80. data/examples/split_screen/locales/en.yml +9 -0
  81. data/examples/split_screen/main.rb +304 -0
  82. data/examples/sprite/locales/en.yml +8 -0
  83. data/examples/sprite/main.rb +180 -0
  84. data/examples/timer/locales/en.yml +12 -0
  85. data/examples/timer/main.rb +273 -0
  86. data/examples/velocity/locales/en.yml +6 -0
  87. data/examples/velocity/main.rb +196 -0
  88. data/examples/walk/locales/en.yml +4 -0
  89. data/examples/walk/main.rb +99 -0
  90. data/exe/rgame +9 -0
  91. data/ext/rgame_core/app/app.c +33 -3
  92. data/ext/rgame_core/app/locale.c +67 -0
  93. data/ext/rgame_core/app/locale.h +28 -0
  94. data/ext/rgame_core/audio/audio.c +39 -2
  95. data/ext/rgame_core/example.rb +0 -49
  96. data/ext/rgame_core/extconf.rb +0 -125
  97. data/ext/rgame_core/include/rgame/core.h +38 -2
  98. data/ext/rgame_core/ruby/audio_ext.c +10 -5
  99. data/ext/rgame_core/ruby/core_ext.c +30 -7
  100. data/ext/rgame_core/ruby/core_ext.h +3 -0
  101. data/ext/rgame_core/ruby/locale_ext.c +44 -0
  102. data/ext/rgame_core/ruby/recording_ext.c +1 -1
  103. data/ext/rgame_core/ruby/renderer_ext.c +20 -20
  104. data/ext/rgame_util/extconf.rb +2 -20
  105. data/ext/rgame_util/route_search.c +305 -0
  106. data/ext/rgame_util/route_search.h +86 -0
  107. data/ext/rgame_util/route_search_ext.c +150 -0
  108. data/ext/rgame_util/solid_grid.c +58 -0
  109. data/ext/rgame_util/solid_grid.h +49 -0
  110. data/ext/rgame_util/solid_grid_ext.c +161 -0
  111. data/ext/rgame_util/tile_sweep.c +164 -0
  112. data/ext/rgame_util/tile_sweep.h +62 -0
  113. data/ext/rgame_util/tile_sweep_ext.c +155 -0
  114. data/ext/rgame_util/util_ext.c +3 -0
  115. data/ext/rgame_util/util_ext.h +15 -0
  116. data/lib/rgame/boot.rb +0 -10
  117. data/lib/rgame/cli/new_project.rb +139 -0
  118. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  119. data/lib/rgame/cli/templates/README.md.tt +93 -0
  120. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  121. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  122. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  123. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  124. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  125. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  126. data/lib/rgame/cli/templates/rspec.tt +2 -0
  127. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  128. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  129. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  130. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  131. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  132. data/lib/rgame/cli.rb +66 -0
  133. data/lib/rgame/core/app.rb +6 -44
  134. data/lib/rgame/core/asset_manager.rb +13 -31
  135. data/lib/rgame/core/audio.rb +37 -16
  136. data/lib/rgame/core/font.rb +0 -3
  137. data/lib/rgame/core/locale.rb +22 -0
  138. data/lib/rgame/core/nine_slice.rb +0 -21
  139. data/lib/rgame/core/renderer.rb +6 -63
  140. data/lib/rgame/core/sprite_sheet.rb +0 -3
  141. data/lib/rgame/core/tile_map_renderer.rb +2 -19
  142. data/lib/rgame/core/ui_atlas.rb +28 -13
  143. data/lib/rgame/core.rb +1 -8
  144. data/lib/rgame/engine/actor_blockers.rb +131 -0
  145. data/lib/rgame/engine/animation_set.rb +1 -0
  146. data/lib/rgame/engine/audio_director.rb +36 -6
  147. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  148. data/lib/rgame/engine/camera.rb +3 -3
  149. data/lib/rgame/engine/circle_collider.rb +4 -2
  150. data/lib/rgame/engine/collision_box.rb +26 -1
  151. data/lib/rgame/engine/collision_system.rb +110 -22
  152. data/lib/rgame/engine/component.rb +34 -10
  153. data/lib/rgame/engine/components/action_trigger.rb +0 -1
  154. data/lib/rgame/engine/components/animated_sprite.rb +27 -25
  155. data/lib/rgame/engine/components/box_collider.rb +99 -0
  156. data/lib/rgame/engine/components/camera_follow.rb +6 -5
  157. data/lib/rgame/engine/components/character_body.rb +20 -61
  158. data/lib/rgame/engine/components/circle_collider.rb +47 -11
  159. data/lib/rgame/engine/components/collision_world.rb +159 -31
  160. data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
  161. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  162. data/lib/rgame/engine/components/hop.rb +76 -0
  163. data/lib/rgame/engine/components/identity.rb +73 -0
  164. data/lib/rgame/engine/components/mover.rb +285 -0
  165. data/lib/rgame/engine/components/navigator.rb +145 -0
  166. data/lib/rgame/engine/components/path_follow.rb +123 -31
  167. data/lib/rgame/engine/components/player_controller.rb +5 -2
  168. data/lib/rgame/engine/components/pool.rb +1 -1
  169. data/lib/rgame/engine/components/screen_wrap.rb +33 -11
  170. data/lib/rgame/engine/components/sprite.rb +12 -6
  171. data/lib/rgame/engine/components/targeting.rb +9 -11
  172. data/lib/rgame/engine/components/thrust_controller.rb +1 -1
  173. data/lib/rgame/engine/components/tile_world.rb +41 -13
  174. data/lib/rgame/engine/components/timer.rb +1 -1
  175. data/lib/rgame/engine/components/velocity.rb +23 -7
  176. data/lib/rgame/engine/components/wander_controller.rb +6 -2
  177. data/lib/rgame/engine/components/world.rb +133 -0
  178. data/lib/rgame/engine/contact_set.rb +74 -0
  179. data/lib/rgame/engine/culling.rb +4 -6
  180. data/lib/rgame/engine/debug_overlay.rb +5 -11
  181. data/lib/rgame/engine/i18n/plural.rb +45 -0
  182. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  183. data/lib/rgame/engine/i18n/template.rb +59 -0
  184. data/lib/rgame/engine/i18n.rb +276 -51
  185. data/lib/rgame/engine/input/action_mapper.rb +0 -26
  186. data/lib/rgame/engine/input/actions.rb +2 -8
  187. data/lib/rgame/engine/input/input_map.rb +39 -21
  188. data/lib/rgame/engine/nav_grid.rb +87 -0
  189. data/lib/rgame/engine/node2d.rb +248 -133
  190. data/lib/rgame/engine/path.rb +4 -6
  191. data/lib/rgame/engine/players.rb +6 -13
  192. data/lib/rgame/engine/presentation.rb +171 -0
  193. data/lib/rgame/engine/scene/scene_stack.rb +4 -4
  194. data/lib/rgame/engine/sealed_privates.rb +54 -0
  195. data/lib/rgame/engine/spatial_hash.rb +51 -19
  196. data/lib/rgame/engine/text.rb +194 -0
  197. data/lib/rgame/engine/tile_blockers.rb +63 -0
  198. data/lib/rgame/engine/tile_map.rb +2 -3
  199. data/lib/rgame/engine/tile_map_layer.rb +0 -2
  200. data/lib/rgame/engine/tileset.rb +2 -4
  201. data/lib/rgame/engine/timer.rb +2 -2
  202. data/lib/rgame/engine/ui/button.rb +248 -0
  203. data/lib/rgame/engine/ui/column.rb +20 -0
  204. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  205. data/lib/rgame/engine/ui/menu.rb +246 -71
  206. data/lib/rgame/engine/ui/navigation.rb +57 -0
  207. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  208. data/lib/rgame/engine/ui/option_button.rb +163 -0
  209. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  210. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  211. data/lib/rgame/engine/ui/pointing.rb +146 -0
  212. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  213. data/lib/rgame/engine/ui/ring.rb +55 -0
  214. data/lib/rgame/engine/ui/row.rb +21 -0
  215. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  216. data/lib/rgame/engine/ui/stack.rb +58 -0
  217. data/lib/rgame/engine/ui/stepping.rb +93 -0
  218. data/lib/rgame/engine/ui/text_button.rb +59 -0
  219. data/lib/rgame/engine/viewports.rb +2 -5
  220. data/lib/rgame/engine/world_view.rb +5 -4
  221. data/lib/rgame/engine.rb +31 -25
  222. data/lib/rgame/game.rb +99 -27
  223. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  224. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  225. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  226. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  227. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  228. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  229. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  230. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  231. data/lib/rgame/rubocop/default.yml +39 -0
  232. data/lib/rgame/rubocop/plugin.rb +45 -0
  233. data/lib/rgame/rubocop.rb +11 -0
  234. data/lib/rgame/util/color.rb +20 -24
  235. data/lib/rgame/util/controls.rb +14 -28
  236. data/lib/rgame/util/route_search.rb +27 -0
  237. data/lib/rgame/util/save_file.rb +107 -0
  238. data/lib/rgame/util/solid_grid.rb +37 -0
  239. data/lib/rgame/util/tensor.rb +0 -9
  240. data/lib/rgame/util/tile_sweep.rb +36 -0
  241. data/lib/rgame/util/z.rb +4 -14
  242. data/lib/rgame/util.rb +4 -3
  243. data/lib/rgame/version.rb +1 -1
  244. data/lib/rgame.rb +0 -15
  245. metadata +152 -11
  246. data/lib/rgame/engine/actor.rb +0 -53
  247. data/lib/rgame/engine/body.rb +0 -49
  248. data/lib/rgame/engine/cached_label.rb +0 -33
  249. data/lib/rgame/engine/input/player_controller.rb +0 -14
  250. data/lib/rgame/engine/matrix.rb +0 -32
  251. data/lib/rgame/engine/resettable.rb +0 -67
  252. data/lib/rgame/engine/tile_collision.rb +0 -78
  253. data/lib/rgame/engine/ui/menu_item.rb +0 -84
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Quick wheel — a radial menu held open by a button, and chosen by letting go.
4
+ #
5
+ # Run it:
6
+ #
7
+ # ruby examples/quick_wheel/main.rb
8
+ #
9
+ # Hold Tab (or the left shoulder button), point with the arrow keys (or the left
10
+ # stick), and let go of Tab to choose. Let go with nothing pointed at to choose
11
+ # nothing. It exercises:
12
+ # - UI::RadialMenu with `trigger:` — the wheel is closed until the trigger goes
13
+ # down and chooses what is focused when it comes up;
14
+ # - UI::Pointing's grace window — a stick that springs back a moment before
15
+ # the trigger still chooses what it pointed at;
16
+ # - UI::Menu#on_opened / #on_closed — what the game does while the wheel is
17
+ # open, here slowing the world to a quarter of its speed;
18
+ # - InputMap.default.merge — declaring the trigger on Tab and the left shoulder.
19
+ #
20
+ # ## Letting go is the choice
21
+ #
22
+ # There is no confirm button here, and pressing one does nothing: the player's
23
+ # thumb is on the stick and a finger is on the shoulder, and releasing the
24
+ # shoulder is the only thing left to do. `examples/radial_menu` is the other
25
+ # style of the same wheel — always open, point, then press A — and is the same
26
+ # classes without `trigger:`.
27
+ #
28
+ # ## The stick springs back first
29
+ #
30
+ # Letting go of a stick and a shoulder button at once is not at once: the stick
31
+ # is back in the middle a frame or two before the button comes up. With nothing
32
+ # to account for that, the release would find the stick at rest and choose
33
+ # nothing almost every time. So focus survives the dead zone for
34
+ # `Pointing::GRACE`, 0.15 s, which a wheel with a trigger gets without asking.
35
+ # On a keyboard the same thing happens between letting go of an arrow key and
36
+ # letting go of Tab. Hold the arrow at rest for longer than that, and the
37
+ # release chooses nothing — which is how a player changes their mind.
38
+ #
39
+ # ## The world is the game's business
40
+ #
41
+ # The wheel says when it opens and closes. Whether the world pauses, slows or
42
+ # carries on is a decision about the game, not about menus, so it is made here:
43
+ # the dots drift at a quarter speed while the wheel is open.
44
+ #
45
+ # ## What this example does not solve
46
+ #
47
+ # Captions on the wheel, and a wheel centred on the view — see
48
+ # `examples/radial_menu`. A stick that overshoots the middle as it springs back
49
+ # can briefly point at the opposite icon, which the grace window does not
50
+ # cover.
51
+
52
+ $LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
53
+ require 'rgame/game'
54
+
55
+ WIDTH = 640
56
+ HEIGHT = 480
57
+ ASSETS = File.expand_path('../assets', __dir__)
58
+ LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
59
+ Controls = RGame::Util::Controls
60
+
61
+ # The part of the scene the wheel slows down. A time scale applied to `dt` on
62
+ # the way in is the whole of slow motion, because time only enters through
63
+ # `update`.
64
+ class World < RGame::Engine::Node2D
65
+ SLOWED = 0.25
66
+
67
+ attr_accessor :time_scale
68
+
69
+ def initialize(**)
70
+ super
71
+ @time_scale = 1.0
72
+ end
73
+
74
+ def update(dt) = super(dt * @time_scale)
75
+ end
76
+
77
+ # A square that drifts and bounces off the window's edges.
78
+ class Dot < RGame::Engine::Node2D
79
+ SIZE = 12
80
+ COLOR = RGame::Util::Color.new(120, 180, 150)
81
+
82
+ def initialize(speed_x:, speed_y:, **)
83
+ super(**)
84
+ @speed_x = speed_x
85
+ @speed_y = speed_y
86
+ end
87
+
88
+ def on_update(dt)
89
+ self.x += @speed_x * dt
90
+ self.y += @speed_y * dt
91
+ @speed_x = -@speed_x unless x.between?(0, WIDTH - SIZE)
92
+ @speed_y = -@speed_y unless y.between?(0, HEIGHT - SIZE)
93
+ end
94
+
95
+ def on_draw(renderer, _view) = renderer.rect(0, 0, SIZE, SIZE, color: COLOR)
96
+ end
97
+
98
+ # The wheel, held open by `:quick_menu`, and what it last chose.
99
+ class QuickWheel < RGame::Engine::Node2D
100
+ UI = RGame::Engine::UI
101
+
102
+ RADIUS = 150
103
+ SLOT = 64
104
+
105
+ # Clockwise from the top. Each image names an entry of icons.json's `images`.
106
+ ICONS = [
107
+ %i[home home],
108
+ %i[settings gear],
109
+ %i[save save],
110
+ %i[favourite star],
111
+ %i[trophies trophy],
112
+ %i[sound audio_on],
113
+ %i[music music_on],
114
+ %i[locked locked]
115
+ ].freeze
116
+
117
+ DISC = UI::ShapeStyle.new(shape: :disc)
118
+
119
+ attr_reader :chosen
120
+
121
+ def initialize(world:, **)
122
+ super(**)
123
+ @chosen = nil
124
+ @menu = add_node(UI::RadialMenu.new(radius: RADIUS, button_width: SLOT, trigger: :quick_menu))
125
+ ICONS.each { |key, image| add_icon(key, image) }
126
+ @menu.on_opened { world.time_scale = World::SLOWED }
127
+ @menu.on_closed { world.time_scale = 1.0 }
128
+ end
129
+
130
+ private
131
+
132
+ def add_icon(key, image)
133
+ button = @menu.add(UI::IconButton.new(image: image, style: DISC, enabled: image != :locked))
134
+ button.on_activated { @chosen = key }
135
+ end
136
+ end
137
+
138
+ # The captions. Added last, so its final line is the last text of every frame —
139
+ # which is what the drive script reads.
140
+ class Caption < RGame::Engine::Node2D
141
+ NAMES = QuickWheel::ICONS.to_h { |key, _| [key, RGame::Engine::Text.new(key, scope: 'items')] }.freeze
142
+
143
+ def initialize(wheel:, **)
144
+ super(**)
145
+ @wheel = wheel
146
+ @help = RGame::Engine::Text.new('help.wheel')
147
+ @nothing = RGame::Engine::Text.new('status.nothing')
148
+ @chosen = RGame::Engine::Text.new('status.chosen', :item)
149
+ end
150
+
151
+ def on_draw(renderer, _view)
152
+ renderer.text(@help, 12, 12)
153
+ chosen = @wheel.chosen
154
+ renderer.text(chosen ? @chosen.with(item: NAMES.fetch(chosen).to_s) : @nothing, 12, HEIGHT - 30)
155
+ end
156
+ end
157
+
158
+ class Scene < RGame::Engine::Node2D
159
+ DOTS = [[60, 80, 90, 40], [400, 120, -70, 60], [200, 360, 50, -80], [520, 300, -100, -30]].freeze
160
+
161
+ def on_add
162
+ world = add_node(World.new)
163
+ DOTS.each { |x, y, speed_x, speed_y| world.add_node(Dot.new(x: x, y: y, speed_x: speed_x, speed_y: speed_y)) }
164
+ wheel = add_node(QuickWheel.new(world: world, x: WIDTH / 2, y: (HEIGHT / 2) + 6))
165
+ add_node(Caption.new(wheel: wheel))
166
+ end
167
+ end
168
+
169
+ game = RGame::Game.new(
170
+ root: Scene.new,
171
+ caption: 'Quick wheel',
172
+ width: WIDTH,
173
+ height: HEIGHT,
174
+ media_root: ASSETS,
175
+ locales: LOCALES,
176
+ input_map: RGame::Engine::InputMap.default.merge(
177
+ quick_menu: { buttons: [Controls::KEY_TAB, Controls::PAD_LEFT_SHOULDER] }
178
+ )
179
+ )
180
+
181
+ # The icons are registered by name, once: the atlas cuts each from the strip.
182
+ game.renderer.register_ui_atlas(game.assets.ui_atlas('icons.json'))
183
+
184
+ game.start
@@ -0,0 +1,16 @@
1
+ # The English table: every string this example draws.
2
+ en:
3
+ help:
4
+ point: Point the stick (or arrow keys) at an icon, then press A (or Enter)
5
+ status:
6
+ nothing: "Chosen: nothing yet"
7
+ chosen: "Chosen: %{item}"
8
+ items:
9
+ home: Home
10
+ settings: Settings
11
+ save: Save
12
+ favourite: Favourite
13
+ trophies: Trophies
14
+ sound: Sound
15
+ music: Music
16
+ locked: Locked
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Radial menu — a quick menu of icons, chosen by pointing rather than by stepping
4
+ # through a list.
5
+ #
6
+ # Run it:
7
+ #
8
+ # ruby examples/radial_menu/main.rb
9
+ #
10
+ # Point the left stick at an icon and press A. On a keyboard, hold the arrow
11
+ # keys — two at once for a diagonal — and press Enter or Space. The chosen icon
12
+ # appears, doubled, in the middle of the wheel. It exercises:
13
+ # - UI::RadialMenu — a UI::Menu that builds its own UI::Ring and
14
+ # UI::Pointing, and draws the backdrop, the dead zone and the pointer;
15
+ # - UI::IconButton — each entry, a picture tinted by its state;
16
+ # - UI::ShapeStyle — the disc behind every icon, outlined while focused;
17
+ # - `ui_radial_x` / `ui_radial_y` — the two axes Pointing reads, from the
18
+ # universal set every InputMap carries, so nothing here declares an action;
19
+ # - a UI atlas's `images` — the eight icons, cut from one strip and registered
20
+ # by name in one call.
21
+ #
22
+ # ## The direction is the selection
23
+ #
24
+ # A list menu moves focus *relative* to where it already is: down means "the
25
+ # next one". A stick is bad at that and good at pointing, so here nothing is
26
+ # stepped through at all. Whichever button lies closest to the direction the
27
+ # stick points in is focused, which on a ring cuts the circle into one sector per
28
+ # button, centred on it. Eight buttons is the number that makes a keyboard a
29
+ # first-class input too: the arrow keys, alone and in pairs, produce exactly
30
+ # eight directions, one per icon.
31
+ #
32
+ # That is the only thing that makes this a wheel rather than a list. The menu,
33
+ # its buttons and what confirming does are the same classes `examples/game_menu`
34
+ # uses; a RadialMenu is a UI::Menu handed a UI::Ring where that one has a
35
+ # UI::Column, and UI::Pointing where that one keeps the default UI::Stepping.
36
+ #
37
+ # `examples/quick_wheel` is the other common style of the same wheel: closed until
38
+ # a button is held, and chosen by letting that button go.
39
+ #
40
+ # ## Letting go selects nothing
41
+ #
42
+ # The lighter disc around the middle is the **dead zone**, drawn to scale: the
43
+ # pointer's tip inside it means the stick is not deflected far enough to mean
44
+ # anything, and no button is focused. That is what makes the obvious mistake
45
+ # impossible — a player who lets the stick spring back and then presses A has
46
+ # not asked for whatever the stick passed over on its way home, and gets nothing.
47
+ #
48
+ # It is a length on the combined vector, and much bigger than the per-axis dead
49
+ # zone every action already has. That one only stops a worn stick drifting; it
50
+ # would let a stick barely off centre choose.
51
+ #
52
+ # **Locked** is disabled, and pointing at it focuses nothing — the same rule a
53
+ # list follows by skipping it.
54
+ #
55
+ # ## White art, coloured by a tint
56
+ #
57
+ # The icons are white, and IconButton draws each one multiplied by a colour for
58
+ # its state — grey at rest, white while focused, dim while disabled — so one
59
+ # picture serves all four looks. Dark art would take none of them. While pressed
60
+ # the disc is gold, and the disc's ShapeStyle says the icon goes dark on it: what
61
+ # reads on a fill is the style's to decide, since it chose the fill.
62
+ #
63
+ # The icons are 50 pixels in 64-pixel slots and the chosen one is drawn at scale
64
+ # 2: images sample nearest-neighbour, so only a whole-number scale stays sharp.
65
+ #
66
+ # ## What this example does not solve
67
+ #
68
+ # Captions. The icons carry no text, so the wheel says what was chosen only in
69
+ # the line along the bottom. The wheel is also at a fixed position rather than
70
+ # centred on the view, because its buttons are placed when they are added and the
71
+ # view's size only exists at draw time — the same limit `examples/game_menu`
72
+ # names.
73
+
74
+ $LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
75
+ require 'rgame/game'
76
+
77
+ WIDTH = 640
78
+ HEIGHT = 480
79
+ ASSETS = File.expand_path('../assets', __dir__)
80
+ LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
81
+
82
+ # The wheel and what it chooses. The wheel itself — backdrop, dead zone, pointer
83
+ # and buttons — is a UI::RadialMenu; the chosen icon is a node added after it,
84
+ # because between nodes the tree decides what lands on top, and anything this
85
+ # node drew itself would sit under the menu's backdrop.
86
+ class QuickMenu < RGame::Engine::Node2D
87
+ UI = RGame::Engine::UI
88
+
89
+ RADIUS = 150
90
+ SLOT = 64
91
+
92
+ # Clockwise from the top. Each image names an entry of icons.json's `images`.
93
+ ICONS = [
94
+ %i[home home],
95
+ %i[settings gear],
96
+ %i[save save],
97
+ %i[favourite star],
98
+ %i[trophies trophy],
99
+ %i[sound audio_on],
100
+ %i[music music_on],
101
+ %i[locked locked]
102
+ ].freeze
103
+
104
+ DISC = UI::ShapeStyle.new(shape: :disc)
105
+
106
+ attr_reader :chosen, :chosen_image
107
+
108
+ def initialize(**)
109
+ super
110
+ @chosen = nil
111
+ @chosen_image = nil
112
+ @menu = add_node(UI::RadialMenu.new(radius: RADIUS, button_width: SLOT))
113
+ ICONS.each { |key, image| add_icon(key, image) }
114
+ add_node(ChosenIcon.new(menu: self))
115
+ end
116
+
117
+ private
118
+
119
+ def add_icon(key, image)
120
+ button = @menu.add(UI::IconButton.new(image: image, style: DISC, enabled: image != :locked))
121
+ button.on_activated do
122
+ @chosen = key
123
+ @chosen_image = image
124
+ end
125
+ end
126
+ end
127
+
128
+ # The chosen icon, doubled, in the middle of the wheel.
129
+ class ChosenIcon < RGame::Engine::Node2D
130
+ SCALE = 2
131
+
132
+ def initialize(menu:, **)
133
+ super(**)
134
+ @menu = menu
135
+ end
136
+
137
+ def on_draw(renderer, _view)
138
+ image = @menu.chosen_image
139
+ renderer.image(image, 0, 0, scale: SCALE) if image
140
+ end
141
+ end
142
+
143
+ # The captions. Added after the wheel, so it draws last and its final line is
144
+ # the last text of every frame — which is what the drive script reads.
145
+ class Caption < RGame::Engine::Node2D
146
+ NAMES = QuickMenu::ICONS.to_h { |key, _| [key, RGame::Engine::Text.new(key, scope: 'items')] }.freeze
147
+
148
+ def initialize(menu:, **)
149
+ super(**)
150
+ @menu = menu
151
+ @help = RGame::Engine::Text.new('help.point')
152
+ @nothing = RGame::Engine::Text.new('status.nothing')
153
+ @chosen = RGame::Engine::Text.new('status.chosen', :item)
154
+ end
155
+
156
+ def on_draw(renderer, _view)
157
+ renderer.text(@help, 12, 12)
158
+ chosen = @menu.chosen
159
+ renderer.text(chosen ? @chosen.with(item: NAMES.fetch(chosen).to_s) : @nothing, 12, HEIGHT - 30)
160
+ end
161
+ end
162
+
163
+ class Scene < RGame::Engine::Node2D
164
+ def on_add
165
+ menu = add_node(QuickMenu.new(x: WIDTH / 2, y: (HEIGHT / 2) + 6))
166
+ add_node(Caption.new(menu: menu))
167
+ end
168
+ end
169
+
170
+ game = RGame::Game.new(
171
+ root: Scene.new,
172
+ caption: 'Radial menu',
173
+ width: WIDTH,
174
+ height: HEIGHT,
175
+ media_root: ASSETS,
176
+ locales: LOCALES
177
+ )
178
+
179
+ # An image id that is a Symbol is a name rather than a path, so the icons are
180
+ # registered once, by hand: the atlas cuts each from the strip and registers it
181
+ # under its name.
182
+ game.renderer.register_ui_atlas(game.assets.ui_atlas('icons.json'))
183
+
184
+ game.start
@@ -0,0 +1,11 @@
1
+ # The English table: every string this example draws.
2
+ en:
3
+ help:
4
+ walk: Arrows walk the dog
5
+ keys: F5 saves F9 loads Delete discards the save
6
+ status:
7
+ saved: saved — quit and run again, or press F9
8
+ loaded: loaded
9
+ deleted: save deleted
10
+ fresh: no save yet — walk the dog, then press F5
11
+ restored: loaded the save from last time
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Save and load — writing game state to disk, and putting it back.
4
+ #
5
+ # Run it:
6
+ #
7
+ # ruby examples/save_load/main.rb
8
+ #
9
+ # Arrow keys walk the dog. **F5** saves, **F9** loads, **Delete** throws the save
10
+ # away. Quit and run it again: the dog and the sheep are where you left them,
11
+ # because a save found at startup is loaded before the first frame. It
12
+ # exercises:
13
+ # - Util::SaveFile — one JSON file, written atomically, read forgivingly;
14
+ # - the same Node2D + CharacterBody + controller composition as
15
+ # `examples/walk`, with the flock on a WanderController instead;
16
+ # - `RGAME_SEED`, so a run without a save is reproducible.
17
+ #
18
+ # ## The tree is not saved, and nothing here tries
19
+ #
20
+ # This is the shape every comparable engine settles on: **a scene is a recipe
21
+ # and a save file is state.** The scene builds itself the same way every time,
22
+ # and the save supplies the handful of facts that differ between runs. Godot's
23
+ # own guidance is this, and it is why an rgame node has no name, no path and no
24
+ # id — nothing needs one.
25
+ #
26
+ # Two kinds of thing are being restored here, and they need different answers.
27
+ #
28
+ # **The dog is singular, so a variable is its identity.** `@dog` is set where the
29
+ # dog is built, and `load_state` writes straight to it. There is no lookup, no
30
+ # name, nothing to match up — the code that made the dog is the code that puts
31
+ # it back, and it never lost track of which one it was.
32
+ #
33
+ # **The sheep are interchangeable, so their order is their identity.** The save
34
+ # holds an array of positions; loading walks the flock and the array together.
35
+ # If sheep three and seven swapped places nothing observable would change, which
36
+ # is exactly what "interchangeable" means, and it is the property that makes an
37
+ # array enough.
38
+ #
39
+ # ## What would break this, and what to do about it
40
+ #
41
+ # The flock is a fixed size and no sheep can die. That is what lets an array
42
+ # index stand in for identity — and it is the assumption to check before copying
43
+ # this shape. The moment sheep can be killed, and the survivors must keep their
44
+ # own hunger and name across a save, the array stops meaning anything and each
45
+ # sheep needs an id of its own.
46
+ #
47
+ # `Components::Identity` is that id, and `examples/save_load_ids` is where it is
48
+ # shown: sheep that can be lost, and a dog chasing one particular sheep — a
49
+ # reference between two saved things, which is what genuinely forces a name.
50
+ # Unreal's save libraries bolt a GUID onto every actor for the same reason, and
51
+ # Unity's community does the same.
52
+ #
53
+ # ## Position is not the whole of where something is
54
+ #
55
+ # The sheep resume standing still, because a WanderController's timer is not
56
+ # saved. That is a deliberate simplification and worth naming, because the real
57
+ # version of this bug is not funny: a game that saves a falling player's position
58
+ # and not their velocity restores them hanging in the air.
59
+
60
+ $LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
61
+ require 'rgame/game'
62
+
63
+ Controls = RGame::Util::Controls
64
+
65
+ WIDTH = 640
66
+ HEIGHT = 480
67
+ ASSETS = File.expand_path('../assets', __dir__)
68
+ LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
69
+
70
+ DOG_SPEED = 150.0
71
+ SHEEP_SPEED = 40.0
72
+ SHEEP_COUNT = 8
73
+
74
+ # Seeded, so a fresh run with no save always lays the flock out the same way and
75
+ # two driven runs can be compared.
76
+ DEFAULT_SEED = 0x5EED
77
+
78
+ class Pasture < RGame::Engine::Node2D
79
+ GRASS = RGame::Util::Color.new(96, 140, 84)
80
+ DOG = RGame::Util::Color.new(70, 50, 40)
81
+ SHEEP = RGame::Util::Color.new(240, 240, 235)
82
+ DOG_R = 11
83
+ SHEEP_R = 9
84
+
85
+ STATUS = { saved: RGame::Engine::Text.new('status.saved'),
86
+ loaded: RGame::Engine::Text.new('status.loaded'),
87
+ deleted: RGame::Engine::Text.new('status.deleted'),
88
+ fresh: RGame::Engine::Text.new('status.fresh'),
89
+ restored: RGame::Engine::Text.new('status.restored') }.freeze
90
+
91
+ def initialize(save:, **)
92
+ super(**)
93
+ @save = save
94
+ @help = RGame::Engine::Text.new('help.walk')
95
+ @keys = RGame::Engine::Text.new('help.keys')
96
+ @rng = Random.new(ENV.fetch('RGAME_SEED', DEFAULT_SEED).to_i)
97
+ @flock = []
98
+ @status = :fresh
99
+ end
100
+
101
+ def on_add
102
+ # Built first, always, and identically. The save does not decide what exists
103
+ # — only where it is.
104
+ @dog = add_node(build_walker(RGame::Engine::Components::PlayerController.new,
105
+ DOG_SPEED, WIDTH / 2, HEIGHT / 2))
106
+ SHEEP_COUNT.times do
107
+ @flock << add_node(build_walker(
108
+ RGame::Engine::Components::WanderController.new(rng: @rng),
109
+ SHEEP_SPEED, @rng.rand(WIDTH - 40) + 20, @rng.rand(HEIGHT - 80) + 60
110
+ ))
111
+ end
112
+
113
+ return unless @save.exist?
114
+
115
+ load_state
116
+ @status = :restored
117
+ end
118
+
119
+ def on_control(actions)
120
+ save_state if actions.pressed?(:save)
121
+ load_state if actions.pressed?(:load)
122
+ drop_save if actions.pressed?(:drop)
123
+ end
124
+
125
+ def on_draw(renderer, view)
126
+ renderer.rect(0, 0, view.width, view.height, color: GRASS)
127
+ @flock.each { |sheep| renderer.circle(sheep.x, sheep.y, SHEEP_R, color: SHEEP) }
128
+ renderer.circle(@dog.x, @dog.y, DOG_R, color: DOG)
129
+
130
+ renderer.text(@help, 12, 12)
131
+ renderer.text(@keys, 12, 34)
132
+ renderer.text(STATUS.fetch(@status), 12, 56)
133
+ end
134
+
135
+ private
136
+
137
+ # The whole save: two facts, written as plain numbers.
138
+ #
139
+ # Reading `sheep.x` from outside the node is fine here and is not the thing
140
+ # Game/DrawInLocalSpace forbids — that rule is about a node reading its *own*
141
+ # position while drawing, when its transform has already been applied.
142
+ def save_state
143
+ @save.write(dog: [@dog.x, @dog.y], sheep: @flock.map { |sheep| [sheep.x, sheep.y] })
144
+ @status = :saved
145
+ end
146
+
147
+ def load_state
148
+ state = @save.read
149
+ place(@dog, state[:dog])
150
+ # `zip` stops at the shorter of the two, so a save written when the flock
151
+ # was a different size restores what it can instead of raising. That is the
152
+ # cheap half of the versioning problem; the expensive half is a save whose
153
+ # *shape* changed, which needs a version number in the file.
154
+ @flock.zip(state.fetch(:sheep, [])) { |sheep, position| place(sheep, position) }
155
+ @status = :loaded
156
+ end
157
+
158
+ def drop_save
159
+ @save.delete
160
+ @status = :deleted
161
+ end
162
+
163
+ def place(node, position)
164
+ return if position.nil?
165
+
166
+ node.x, node.y = position
167
+ end
168
+
169
+ def build_walker(controller, speed, x, y)
170
+ node = RGame::Engine::Node2D.new(x: x, y: y)
171
+ node.add_component(RGame::Engine::Components::CharacterBody.new(speed: speed))
172
+ node.add_component(controller)
173
+ node
174
+ end
175
+ end
176
+
177
+ # `dir:` is normally left out, and the file lands in the platform's own data
178
+ # directory — ~/.local/share, Application Support or %APPDATA%. It is overridable
179
+ # here so a driven run can write somewhere disposable instead of into the home
180
+ # directory of whoever happens to be running it.
181
+ save = RGame::Util::SaveFile.new('pasture.json', game: 'rgame-examples',
182
+ dir: ENV.fetch('RGAME_SAVE_DIR', nil))
183
+
184
+ game = RGame::Game.new(
185
+ root: Pasture.new(save: save),
186
+ caption: 'Save and load',
187
+ width: WIDTH,
188
+ height: HEIGHT,
189
+ media_root: ASSETS,
190
+ locales: LOCALES,
191
+ # F5 and F9 rather than S and L, and the reason is worth knowing: **a key
192
+ # already in the default map keeps doing its default job too.** `move_y` is
193
+ # bound to W and S, so an action added on S saves *and* walks the dog
194
+ # downwards — both fire, because two actions may read one key. Nothing is
195
+ # broken and nothing warns; the game just moves when you asked it to save.
196
+ #
197
+ # F5 and F9 are free, and are what a player already expects quicksave and
198
+ # quickload to be. F1 and F2 are not free: RGame::Game keeps those for the
199
+ # debug overlay and quit.
200
+ input_map: RGame::Engine::InputMap.default.merge(
201
+ save: { buttons: [Controls::KEY_F5] },
202
+ load: { buttons: [Controls::KEY_F9] },
203
+ drop: { buttons: [Controls::KEY_DELETE] }
204
+ )
205
+ )
206
+
207
+ game.start
@@ -0,0 +1,11 @@
1
+ # The English table: every string this example draws.
2
+ en:
3
+ help:
4
+ keys: Arrows walk Tab targets Space shears N adds a sheep
5
+ save: F5 saves F9 loads Delete discards the save
6
+ status:
7
+ fresh: no save yet — shear a few, add one, then press F5
8
+ restored: loaded the save from last time
9
+ saved: saved — quit, run again, then press N for a fresh id
10
+ loaded: loaded
11
+ deleted: save deleted