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,263 @@
1
+ # Examples
2
+
3
+ `examples/` holds one small program per concept. Each is a single `main.rb` that
4
+ runs on its own:
5
+
6
+ ```
7
+ ruby examples/walk/main.rb
8
+ ```
9
+
10
+ **An example answers "how do I do *X*".** The complete games under
11
+ `test_projects/` answer that poorly. An example shows one concept in one file,
12
+ and its header comment explains the concept at length. This page is the index;
13
+ the file is the long version.
14
+
15
+ Every example has an acceptance test as well. A scripted input track lives at
16
+ `tools/drive/examples/<name>.rb`. The harness feeds that track to the unmodified
17
+ program and reports what the game asked for:
18
+
19
+ ```
20
+ ruby tools/drive_test_project.rb examples/walk/main.rb --ticks 240
21
+ ```
22
+
23
+ The examples' assets live under `examples/assets/` and are redistributable, so
24
+ every example runs from a fresh clone. Most examples need no art at all.
25
+
26
+ ## Movement and drawing
27
+
28
+ ### walk
29
+
30
+ A player-controlled sprite: the smallest complete game. It is a plain node with
31
+ three components, and no component knows about the others.
32
+
33
+ **Uses:** `Node2D`, `Components::AnimatedSprite`, `Components::CharacterBody`,
34
+ `Components::PlayerController`, `InputMap.default`.
35
+
36
+ ### sprite
37
+
38
+ One frame drawn at a node, with no animation. Most things in a game look like
39
+ this. The draw passes no position and no angle, because the node's transform
40
+ already applies.
41
+
42
+ **Uses:** `Components::Sprite`, `Image#subimage`, `renderer.register_image`.
43
+
44
+ ### velocity
45
+
46
+ The second way a node moves. A character has an *intent*. A rock has a velocity,
47
+ and a component integrates it, spin included.
48
+
49
+ **Uses:** `Components::Velocity`, `Components::ScreenWrap`, `Components::World`,
50
+ `Components::WorldBounds`.
51
+
52
+ ## The world
53
+
54
+ ### scroll_map
55
+
56
+ A Tiled map larger than the window, scrolled by a player. No call pans the
57
+ camera. A component on a node points the camera, so scrolling is walking.
58
+
59
+ **Uses:** the `:tilemap` asset loader, `Components::TileWorld`, `TileMapLayer`,
60
+ `WorldView`, `Camera`, `Components::CameraFollow`.
61
+
62
+ ### collision
63
+
64
+ Object-to-object collision. A scene-scoped system pairs up shapes each step and
65
+ tells both sides they overlapped. It never learns what either object is.
66
+
67
+ **Uses:** `Components::CollisionWorld`, `Components::CircleCollider`,
68
+ `Components::BoxCollider`, `Components::Velocity`, `Engine::Text`.
69
+
70
+ ### collision_tiles
71
+
72
+ A character against a grid of solid tiles, sliding along a wall while walking
73
+ diagonally. East of the start, a spiky ball stops the hero and costs a life. One
74
+ feet box is thus stopped by two indexes: the map's grid and the broadphase.
75
+ `blocked_by: %i[tiles spike]` is the only place the difference shows. The life is
76
+ spent in `on_blocked`, because a blocked pair ends up touching, not overlapping.
77
+ `on_hit` never fires for it.
78
+
79
+ **Uses:** `Components::TileWorld`, `Components::CollisionWorld`,
80
+ `Components::FeetCollider`, `Components::CharacterBody` with
81
+ `blocked_by: %i[tiles spike]` and `on_blocked`, `Components::CameraFollow`,
82
+ `Engine::Text`.
83
+
84
+ ### jump_topdown
85
+
86
+ A hop in a top-down view. "Up" on screen is north, so a jump cannot move the
87
+ character. The sprite rises along `Hop`'s parabola. The feet box, the shadow and
88
+ the camera stay on the ground. A hop at the fence therefore does not clear it,
89
+ because the part that collides never leaves the ground. The game decides what a
90
+ hop may cross, through `airborne?`.
91
+
92
+ **Uses:** `Components::Hop`, `Node2D#elevation`, `Components::AnimatedSprite`,
93
+ `Components::FeetCollider`, `Components::CharacterBody`, `Components::TileWorld`,
94
+ `Components::CameraFollow`, `InputMap.default.merge`.
95
+
96
+ ### pathfinding
97
+
98
+ Pick a tile, and the hero works out how to get there. Small dots show the route
99
+ the search found, one per tile. Lines show the route the hero walks. The
100
+ navigator pulls the route tight, keeping each line straight as long as the hero's
101
+ feet box fits.
102
+
103
+ **Uses:** `Components::Navigator`, `Components::TileWorld#nav_grid`,
104
+ `Components::AnimatedSprite`, `Components::ActionTrigger`,
105
+ `Components::CameraFollow`, `Engine::Text.computed` over `I18n.t` with plurals.
106
+
107
+ ## Structure
108
+
109
+ ### signals
110
+
111
+ Declaring your own signal. A pressure plate announces that it was pressed, and
112
+ does nothing more. The door and the lamp connect to it; the plate never names
113
+ them.
114
+
115
+ **Uses:** `Signal::DSL`, `Signal.define`, `Components::ActionTrigger`, the
116
+ connect handle.
117
+
118
+ ### timer
119
+
120
+ Periodic behaviour that no input drives: a spawn cadence and a one-shot, with two
121
+ cadences on one node.
122
+
123
+ **Uses:** `Components::Timer` (repeating and `repeating: false`, and `as:`),
124
+ `Engine::Timer`.
125
+
126
+ ### pooling
127
+
128
+ Spawning many things without building any of them. The allocation count on
129
+ screen makes the case.
130
+
131
+ **Uses:** `Components::Pool`, `Engine::Pool`, `Components::DespawnOffscreen`,
132
+ `Components::Timer`, `Engine::Text`.
133
+
134
+ ## UI
135
+
136
+ ### game_menu
137
+
138
+ A menu that opens over a running world. Pausing belongs to a node, so only the
139
+ hero stops while the villagers walk on.
140
+
141
+ **Uses:** `PlayerLayer`, `UI::PanelMenu`, `UI::PanelButton`, `UI::Menu#open` /
142
+ `#close`, `Node2D#paused`, `renderer.nine_slice`.
143
+
144
+ ### menu_navigation
145
+
146
+ Several screens (title, settings, back) and settings that change something real
147
+ and survive a restart. It contrasts pushing a scene with replacing one.
148
+
149
+ **Uses:** `Scene::SceneStack`, `UI::OptionButton`, `Util::SaveFile`,
150
+ `RGame::Game`'s fullscreen, scale mode and volume.
151
+
152
+ ### radial_menu
153
+
154
+ Choosing by pointing. Eight icons sit on a wheel, focused by the direction of the
155
+ stick or the arrow keys. A released stick selects nothing, so pressing A at rest
156
+ never picks what the stick passed on its way back.
157
+
158
+ **Uses:** `UI::RadialMenu`, `UI::IconButton` on a disc `UI::ShapeStyle`,
159
+ `ui_radial_x` / `ui_radial_y`, and a UI atlas's `images` (`icons.json`).
160
+
161
+ ### quick_wheel
162
+
163
+ The same eight icons, on a wheel held open by Tab or the left shoulder button.
164
+ Releasing the button chooses. A stick released a moment before the button still
165
+ chooses; a stick at rest chooses nothing. The world drifts at a quarter speed
166
+ while the wheel is open.
167
+
168
+ **Uses:** `UI::RadialMenu` with `trigger:`, `UI::Pointing`'s grace window,
169
+ `UI::Menu#on_opened` / `#on_closed`, `InputMap.default.merge`.
170
+
171
+ ### skill_bar
172
+
173
+ A tool bar with five tools in a row. Left and right step through them and Enter
174
+ uses one. The number keys use a tool directly, without moving the focus. Holding
175
+ a number uses its tool once. Pressing a tool's number and Enter together also
176
+ uses it once.
177
+
178
+ **Uses:** `UI::Row`, `UI::Stepping` taking its axis from the layout,
179
+ `UI::Button`'s `hotkey:` and `activate_on: :press`, captioned `UI::IconButton`s
180
+ on a disc `UI::ShapeStyle`, `InputMap.default.merge`, and a UI atlas's `images`
181
+ (`skills.json`).
182
+
183
+ ## Audio
184
+
185
+ ### sound
186
+
187
+ A sound effect fired by a button, and the path it travels. A node may not name
188
+ the audio device, so it emits a fact and a director plays it.
189
+
190
+ **Uses:** `Core::Sample`, `Engine::AudioBus`, `Engine::AudioDirector`,
191
+ `Engine::Text`.
192
+
193
+ ### music
194
+
195
+ The other kind of sound: one streamed voice. You can stop it and ask whether it
196
+ plays, and starting it again does not restart it.
197
+
198
+ **Uses:** `Core::Song`, `AudioBus.play_music` / `.stop_music`,
199
+ `Engine::AudioDirector`.
200
+
201
+ ## Players and input
202
+
203
+ ### split_screen
204
+
205
+ Two players in one world. `WorldView` draws the world once per viewport, through
206
+ that viewport's camera. The world never knows how often it is drawn.
207
+
208
+ **Uses:** `Game.new(players: 2)`, `Engine::Players`, `Engine::WorldView`,
209
+ `Engine::PlayerLayer`, `Engine::Camera`, `Components::CameraFollow`,
210
+ `input_owner`.
211
+
212
+ ### input_glyphs
213
+
214
+ Prompts that match the device in the player's hands. They switch between
215
+ keyboard and controller mid-session. Nothing listens for a plugged-in pad; using
216
+ the pad takes the seat.
217
+
218
+ **Uses:** `Controls.gamepad?`, `InputMap#button_for`, `Engine::Players` with
219
+ `on_unassigned_input` defaulting to `:takeover`, `renderer.sprite`.
220
+
221
+ ## The window
222
+
223
+ ### fullscreen
224
+
225
+ Opening fullscreen, switching while the game runs, and all four scale modes with
226
+ the layout following each.
227
+
228
+ **Uses:** `RGame::Game.new(fullscreen:)`, `App#fullscreen?` / `#fullscreen=`,
229
+ `RGame::Game#scale_mode=`, the `view` a node is drawn with,
230
+ `InputMap.default.merge`.
231
+
232
+ ## Language
233
+
234
+ ### localization
235
+
236
+ The same screen in English and German. A `Text` with a `count` picks its plural
237
+ form, and one with a variable shows the current locale. German lacks one key, so
238
+ that line falls back to English. The player's language choice overrides the
239
+ operating system's and is saved.
240
+
241
+ **Uses:** `Engine::I18n`, `Engine::Text` and `Text.literal`, `UI::Menu`'s
242
+ `scope:`, `RGame::Game.new(locales:)`, `RGame::Core.preferred_locales`,
243
+ `Util::SaveFile`.
244
+
245
+ ## Persistence
246
+
247
+ ### save_load
248
+
249
+ Writing game state to disk and restoring it. The tree is not saved: a scene is a
250
+ recipe, and a save file is state. The variable holding a singular thing restores
251
+ it, and array order restores a flock.
252
+
253
+ **Uses:** `Util::SaveFile`, the `examples/walk` composition with a
254
+ `WanderController`.
255
+
256
+ ### save_load_ids
257
+
258
+ The case `save_load` leaves out: a collection whose members can be lost, and one
259
+ saved object that refers to another. A reference forces ids; a changing
260
+ collection alone does not.
261
+
262
+ **Uses:** `Components::Identity`, `Util::SaveFile`, a save of records rather
263
+ than positions.
data/docs/api/game.md CHANGED
@@ -1,10 +1,9 @@
1
1
  # `RGame::Game`
2
2
 
3
- The entry point of a game, and the one class that knows both halves of the
3
+ `Game` is a game's entry point, and the one class that knows both halves of the
4
4
  engine.
5
5
 
6
6
  ```ruby
7
- $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
8
7
  require 'rgame/game'
9
8
 
10
9
  class HelloScene < RGame::Engine::Node2D
@@ -14,13 +13,16 @@ end
14
13
  RGame::Game.new(root: HelloScene.new, caption: 'Hello').start
15
14
  ```
16
15
 
17
- A complete game is a root node plus that. `Game` assembles what a running game
18
- needs around it the window and its loop, the renderer, the asset manager, the
19
- sound device, the input mapper, the debug overlay and drives the root node.
16
+ A root node plus those lines make a complete game. `Game` builds what a running
17
+ game needs and drives the root node. It builds the window and its loop, the
18
+ renderer, the asset manager, the sound device, the input mapper and the debug
19
+ overlay.
20
20
 
21
21
  ```ruby
22
22
  RGame::Game.new(root:, width: 640, height: 480, caption: 'RGame',
23
- media_root: 'media', input_map: nil, device: Controls::KEYBOARD)
23
+ media_root: 'media', input_map: nil, device: Controls::KEYBOARD,
24
+ players: 1, input: nil, fullscreen: false, scale_mode: :letterbox,
25
+ locales: 'locales')
24
26
  ```
25
27
 
26
28
  | Reader | |
@@ -28,46 +30,140 @@ RGame::Game.new(root:, width: 640, height: 480, caption: 'RGame',
28
30
  | `root` | the node tree |
29
31
  | `renderer` | what scenes draw through |
30
32
  | `players` | who is playing: their devices, bindings and cameras |
33
+ | `viewports` | how the screen is divided between players |
34
+ | `scale_mode`, `scale_mode=` | how the logical size maps onto the window; switchable while the game runs |
31
35
  | `assets`, `audio`, `media_root`, `width`, `height`, `fps` | inherited from [App](app.md) |
32
36
 
33
- `start` brings the tree live it hands the game to the root as its `context`,
34
- calls `enter_tree`, and runs the loop until the window closes. `F1` toggles the
35
- debug overlay and `F2` quits.
37
+ A node reaches `players` and `viewports` as systems:
38
+ `node.system(RGame::Engine::Players)` and `node.system(RGame::Engine::Viewports)`.
36
39
 
37
- **Both development keys are function keys, and `Esc` is deliberately left
38
- alone.** Escape is the button a player expects to back out of a menu, so it
39
- belongs to the game rather than to the engine's debug shortcuts — binding it
40
- here would take it away from every game built on this one.
40
+ `input:` replaces the input backend. A test harness passes a scripted backend
41
+ here to drive a game without hardware. A game passes nothing.
41
42
 
42
- ## Why this class exists at all
43
+ ### `scale_mode:` what `width` and `height` mean
43
44
 
44
- `RGame::Engine` holds game concepts and may not name
45
- `RGame::Core`; `RGame::Core` owns windows, textures and sound devices and may
46
- not know Engine exists. Two RuboCop cops enforce that. Something still has to
47
- introduce them, and **this is that something** keeping the introduction in one
48
- file is what makes the rule checkable everywhere else.
45
+ **`width` and `height` are the logical size**: the resolution the game is
46
+ designed in. `Game` maps the whole frame onto the window, whatever its size. The
47
+ view a node draws into keeps the logical size. A layout written against fixed
48
+ numbers therefore works at any window size, fullscreen included.
49
49
 
50
- The tile map is the clearest case: parsing a `.tmx` is Engine's job, drawing one
51
- is Core's, and neither may call the other. So `Game` installs the loader that
52
- joins them, and `app.assets.tilemap('map/island.tmx')` works from then on.
50
+ | | |
51
+ |---|---|
52
+ | `:letterbox` | The default. Largest uniform scale that fits, centred, bars on two sides. |
53
+ | `:integer` | The same, rounded down to a whole number. |
54
+ | `:stretch` | Fill the window, distorting if the aspect ratios differ. |
55
+ | `:disabled` | No scaling at all: `width` and `height` are the window, and the view is too. |
56
+
57
+ **`:disabled` turns scaling off and changes what the numbers mean.** The view
58
+ becomes the window, so a bigger window hands every `draw` a bigger view. A layout
59
+ written against `view.width` grows into the space. A layout written against fixed
60
+ numbers stays in the top-left corner. Choose `:disabled` for a program that uses
61
+ whatever space it gets: a tool, an editor, a program that is all HUD. Do not
62
+ choose it for a game with a designed play area. It is the only mode that pushes
63
+ no clip, translate or scale.
64
+
65
+ ```ruby
66
+ require 'rgame/game'
67
+
68
+ class Root < RGame::Engine::Node2D; end
69
+
70
+ RGame::Game.new(root: Root.new, width: 320, height: 180, scale_mode: :integer).start
71
+ ```
72
+
73
+ **Choose `:integer` for pixel art.** A whole-number factor draws every source
74
+ pixel as the same square on screen. A fractional factor gives some pixels two
75
+ screen pixels and some three, and the unevenness crawls whenever anything moves.
76
+ The mode can waste a lot of screen. A 640x480 design gets only 1x on a 1600x900
77
+ window, because 2x needs 960 rows. `RGame::Engine::Presentation` holds the
78
+ arithmetic and documents the measurements. It is pure and has its own specs.
79
+
80
+ A resize refits the mode and changes nothing else. Viewports, split-screen rects
81
+ and camera clamps already use logical units.
82
+
83
+ `examples/fullscreen` runs in every mode, chosen by an environment variable.
84
+
85
+ `fullscreen:` opens the window fullscreen, so the game shows no windowed frame
86
+ at startup. `width` and `height` then give the size the window returns to, if
87
+ the game offers a way back. See [Fullscreen](app.md#fullscreen) and
88
+ `examples/fullscreen`.
89
+
90
+ `start` brings the tree live. It hands the game to the root as its `context`,
91
+ mounts `Players` and `Viewports` on the root, and subscribes an `AudioDirector` to
92
+ the [`AudioBus`](toolbox.md#audiobus--decoupled-audio-facts). It then calls
93
+ `enter_tree` and runs the loop until the window closes. When the loop ends, it
94
+ unsubscribes the director. `F1` toggles the debug overlay and `F2` quits.
95
+
96
+ The debug overlay is an `RGame::Engine::DebugOverlay`. It shows frames per
97
+ second, the total objects allocated, and the objects allocated since its last
98
+ frame, in the bottom-right corner. `F1` calls its `toggle`.
99
+
100
+ Each tick, `Game` polls input, runs `control` and `update` on the tree, and sweeps
101
+ freed nodes. It redraws only when a tick ran or the debug overlay is visible.
102
+
103
+ **Both development keys are function keys, and `Esc` stays free.** Players expect
104
+ Escape to back out of a menu, so it belongs to the game. A debug shortcut on it
105
+ would take it away from every game built on `Game`.
106
+
107
+ ## Why this class exists
108
+
109
+ `RGame::Engine` holds game concepts and may not name `RGame::Core`.
110
+ `RGame::Core` owns windows, textures and sound devices, and may not know Engine
111
+ exists. **`Game` connects the two.** Keeping that connection in one file lets the
112
+ rule hold everywhere else. Inside rgame's own repository, two RuboCop cops enforce
113
+ it.
114
+
115
+ The tile map shows why. Engine parses a `.tmx`; Core draws it; neither may call
116
+ the other. `Game` installs the loader that joins them, so
117
+ `app.assets.tilemap('map/island.tmx')` works.
118
+
119
+ ## Translations and the player's language
120
+
121
+ **`Game.new` loads every translation table and picks the player's language**, so
122
+ a game writes no i18n setup. It lists every `.yml` under `locales:` with
123
+ `AssetManager#glob`, sorted by path, and loads each through the asset manager's
124
+ `:locale` loader into [`RGame::Engine::I18n`](localization.md).
125
+ Two files that define one locale merge in that order, so a key the later file
126
+ sets wins. It then sets `I18n.locale` to
127
+ `I18n.choose(RGame::Core.preferred_locales)`: the first locale the OS prefers
128
+ that a table covers, unshortened, or the default.
129
+
130
+ `locales:` is relative to `media_root` unless it is absolute. A directory that
131
+ does not exist loads nothing, and every key then shows as itself.
132
+
133
+ ```ruby
134
+ game = MyGame.new(root: Root.new, media_root: 'media') # loads media/locales/**/*.yml
135
+ RGame::Engine::I18n.locale = saved_language if saved_language
136
+ game.start
137
+ ```
138
+
139
+ A language the player chose and the game saved belongs between `new` and
140
+ `start`, as above: `new` has already chosen from the OS by then. Loading the same
141
+ file again through `assets.locale(path)` returns the cached result and parses
142
+ nothing.
53
143
 
54
144
  ## Reaching the game from a node
55
145
 
56
- A node deep in the tree gets at the asset manager through the root's context,
57
- so nothing has to be threaded through constructors:
146
+ A node anywhere in the tree reaches the asset manager through the root's
147
+ context. No constructor has to pass it along:
58
148
 
59
149
  ```ruby
60
- sheet = node.root.context.assets.sheet('player.json')
150
+ sheet = node.context.assets.sheet('player.json') # node.context is node.root.context
61
151
  ```
62
152
 
153
+ A component reaches the same object as `context`.
154
+
63
155
  ## Input
64
156
 
65
- `input_map:` names the actions a game has, in terms of physical ids from
157
+ `input_map:` names a game's actions in terms of physical ids from
66
158
  [`RGame::Util::Controls`](input.md):
67
159
 
68
160
  ```ruby
161
+ require 'rgame/game'
162
+
69
163
  Controls = RGame::Util::Controls
70
164
 
165
+ class Root < RGame::Engine::Node2D; end
166
+
71
167
  RGame::Game.new(
72
168
  root: Root.new,
73
169
  input_map: RGame::Engine::InputMap.new(
@@ -75,42 +171,45 @@ RGame::Game.new(
75
171
  stick: Controls::AXIS_LEFT_X },
76
172
  fire: { buttons: [Controls::KEY_SPACE, Controls::PAD_A] } # held / pressed / released
77
173
  )
78
- )
174
+ ).start
79
175
  ```
80
176
 
81
- Pass nothing and you get [`InputMap.default`](input.md): eight-way `move_x` /
82
- `move_y` on the arrows or the left stick, plus `fire`. Either way the map is
83
- merged over the universal UI set, so `ui_confirm` and `ui_cancel` work without
84
- being declared.
177
+ Without it, `Game` uses [`InputMap.default`](input.md). The default binds
178
+ eight-way `move_x` / `move_y` to the arrows, WASD, the d-pad and the left stick,
179
+ and adds `fire`. Every map merges over the universal UI set, so `ui_confirm` and
180
+ `ui_cancel` work without a declaration.
85
181
 
86
- `device:` picks what drives player one the keyboard by default, or
87
- `Controls.gamepad(slot)` for a controller.
182
+ `device:` picks the device that drives player one. It defaults to the keyboard;
183
+ pass `Controls.gamepad(slot)` for a controller.
88
184
 
89
- `players:` is how many seats the game has (default 1). Extra seats start empty
90
- and fill when somebody picks up a controller and presses confirm; an empty seat
91
- draws no viewport, so a two-seat game played by one person is an ordinary
92
- full-screen game. See [Players, seats and joining](input.md#players-seats-and-joining).
185
+ `players:` sets how many seats the game has (default 1). Extra seats start
186
+ empty. A seat fills when someone picks up a controller and presses confirm. An
187
+ empty seat draws no viewport, so one person playing a two-seat game sees an
188
+ ordinary full-screen game. See
189
+ [Players, seats and joining](input.md#players-seats-and-joining).
93
190
 
94
- A scene reads the resulting snapshot in `on_control(actions)` `actions.axis(:move_x)`,
95
- `actions.pressed?(:fire)` and never sees a key.
191
+ A scene reads the resulting snapshot in `on_control(actions)`, with calls like
192
+ `actions.axis(:move_x)` and `actions.pressed?(:fire)`. It never sees a key.
96
193
 
97
- **Input is polled once per simulation tick**, not once per rendered frame. That
98
- matters for edge queries: `pressed?` means "held now, not held at the previous
99
- poll", so whatever polls decides what a press *is*. A loop that renders faster
100
- than it simulates would otherwise consume the press between two ticks, and
101
- menus would stop responding on fast machines only.
194
+ **`Game` polls input once per simulation tick**, not once per rendered frame.
195
+ Edge queries depend on this. `pressed?` means "held now, not held at the previous
196
+ poll", so the poll rate decides what a press *is*. Polling per frame would let a
197
+ fast-rendering loop consume a press between two ticks. Menus would then ignore
198
+ input, but only on fast machines.
102
199
 
103
- Polling per tick costs nothing and loses nothing, because the C layer snapshots
104
- the keyboard once per frame: several ticks inside one frame read identical
105
- state, and the edge lands on the first of them. One press, one `pressed?`.
200
+ Polling per tick costs nothing and loses nothing. The C layer snapshots the
201
+ keyboard once per frame, so every tick inside one frame reads identical state.
202
+ The edge lands on the first tick. One press gives one `pressed?`.
106
203
 
107
204
  ## Subclassing it
108
205
 
109
- `Game` is an [`App`](app.md), so anything an App can override it can too. The
110
- loop, the fixed timestep and the catch-up cap are the engine's; a subclass adds
111
- behaviour around the tree rather than replacing the shell.
206
+ `Game` is an [`App`](app.md), so a subclass can override any `App` hook. The
207
+ engine owns the loop, the fixed timestep and the catch-up cap. A subclass adds
208
+ behaviour around the tree; it does not replace the shell.
112
209
 
113
210
  ```ruby
211
+ require 'rgame/game'
212
+
114
213
  class MyGame < RGame::Game
115
214
  def button_down(id)
116
215
  super # keeps F1 and F2 working