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
data/docs/api/README.md CHANGED
@@ -1,46 +1,51 @@
1
1
  # rgame API guide
2
2
 
3
- Reference documentation for using rgame from Ruby. The engine is written in C
4
- and exposed as two Ruby extensions, with the scene graph a game is actually
5
- written in sitting on top of them in pure Ruby; nothing here assumes you will
6
- read or write any C.
3
+ These pages document how to use rgame from Ruby. The engine is written in C and
4
+ ships as two Ruby extensions. The scene graph sits on top of them in pure Ruby,
5
+ and a game is written against that scene graph. Nothing here asks you to read
6
+ or write C.
7
7
 
8
8
  | Page | Covers |
9
9
  |---|---|
10
10
  | This page | Loading the library, the three namespaces, a working program, testing |
11
+ | [The `rgame` command](cli.md) | `rgame new NAME` — starting a project, and the layout it gives you |
11
12
  | [App](app.md) | `RGame::Core::App` — the window and the frame loop |
12
13
  | [Game](game.md) | `RGame::Game` — the entry point that wires both halves together |
13
14
  | [Input](input.md) | `RGame::Core::Input`, `RGame::Util::Controls`, `RGame::Core::Gamepad` |
14
15
  | [Drawing](drawing.md) | `RGame::Core::Renderer` — shapes, images, transforms, clipping, recordings |
15
- | [Images](images.md) | `RGame::Core::Image` — loading PNGs, subimages, sprite sheets |
16
+ | [Images](images.md) | `RGame::Core::Image` — loading PNGs, subimages, tiles |
16
17
  | [Text](text.md) | `RGame::Core::Font` and `Renderer#text` |
17
18
  | [Audio](audio.md) | `RGame::Core::Audio`, `Sample`, `Song` — samples and streamed music |
18
- | [Sheets, atlases and maps](assets.md) | `RGame::Core::SpriteSheet` and the rest of the asset layer |
19
- | [Values](values.md) | `RGame::Util::Color`, `RGame::Util::Tensor`, `RGame::Util::Z` |
19
+ | [Assets](assets.md) | `RGame::Core::AssetManager`, `SpriteSheet`, `NineSlice`, `UiAtlas`, `TileMapRenderer` |
20
+ | [Values](values.md) | `RGame::Util::Color`, `Tensor`, `SolidGrid`, `RouteSearch`, `TileSweep`, `Z`, `SaveFile` |
21
+ | [Examples](examples.md) | What each program under `examples/` demonstrates |
20
22
 
21
- The scene graph `RGame::Engine`, the layer a game is actually written in:
23
+ The scene graph is `RGame::Engine`, the layer a game is written in:
22
24
 
23
25
  | Page | Covers |
24
26
  |---|---|
25
27
  | [Scene graph](scene_graph.md) | `Node2D`, the tree, the lifecycle, transforms and the camera |
26
28
  | [Components](components.md) | Reusable behaviour attached to a node |
27
29
  | [Systems](systems.md) | Services a subtree shares — collision worlds, tile worlds |
28
- | [UI](ui.md) | `PlayerLayer` and `UI::Menu` — a player's own screen, navigated by focus |
30
+ | [Tile maps](tile_maps.md) | `TileMap` and `Tileset` — a Tiled map as data: loading, cells, solidity, animated tiles |
31
+ | [UI](ui.md) | `PlayerLayer` and `UI::Menu` — a player's own screen, and a list or wheel navigated by focus |
29
32
  | [Signals](signals.md) | The typed observer pattern nodes talk through |
30
- | [Toolbox](toolbox.md) | What a game author reaches for directly: pooling, timers, camera, i18n, the audio bus |
33
+ | [Toolbox](toolbox.md) | What a game author reaches for directly: the text a node draws, pooling, paths and routes, timers, the camera, collision boxes, the audio bus |
34
+ | [Localization](localization.md) | `I18n` and translation tables — where they go, plurals, the fallback chain, the player's language, missing keys in specs |
31
35
  | [Internal building blocks](internals.md) | What components are built from: collision maths, the spatial index, animation playback |
32
36
 
33
- **The engine is a work in progress.** A window opens, the loop runs, input
34
- works, shapes, images and text can be drawn, sound plays, and a scene graph with
35
- split-screen players runs on top of it — the games under `examples/` are built
36
- on exactly what is documented here. What is missing is a UI *toolkit*: [UI](ui.md)
37
- gives each player a region of the screen, focus and activation, and stops there —
38
- no layout, no scrolling lists, no text entry. Pages here describe what exists
39
- today and grow as more lands.
37
+ **The engine is a work in progress.** It opens a window, runs the loop, reads
38
+ input, draws shapes, images and text, and plays sound. A scene graph with
39
+ split-screen players runs on top. The games under `test_projects/` use exactly
40
+ what these pages document. The missing piece is a UI *toolkit*. [UI](ui.md)
41
+ gives each player a region of the screen, menus with focus and activation, and a
42
+ column, row or ring of equal-sized buttons. It has no general layout, no scrolling
43
+ lists and no text entry. These pages describe
44
+ what exists and grow with the engine.
40
45
 
41
46
  ## Loading it
42
47
 
43
- Three requires, each a strict superset of the last:
48
+ rgame has three entry points. Each one loads everything the one before it does:
44
49
 
45
50
  ```ruby
46
51
  require 'rgame' # RGame::Util + RGame::Engine — no graphics libraries at all
@@ -48,23 +53,22 @@ require 'rgame/core' # adds the window, the GPU and the sound device (SDL2 + Op
48
53
  require 'rgame/game' # all of it, wired together — what a game writes
49
54
  ```
50
55
 
51
- A game wants the last one. `RGame::Game` is the entry point; see
56
+ A game requires the last one. Its entry point is `RGame::Game`; see
52
57
  [Game](game.md).
53
58
 
54
- The first is **everything that runs without a window**: the value types and the
55
- whole scene graph, in a process with no SDL and no OpenGL loaded. That is what
56
- lets game logic and its specs run with no display present, and it is asserted
57
- rather than assumed — `spec/rgame/no_graphics_spec.rb` reads the process's own
58
- memory map.
59
+ `require 'rgame'` loads **everything that runs without a window**: the value
60
+ types and the whole scene graph. The process loads no SDL and no OpenGL, so game
61
+ logic and its specs run with no display. `spec/rgame/no_graphics_spec.rb` checks
62
+ this by reading the process's own memory map.
59
63
 
60
- Nothing is forced through those files: `rgame/util`, `rgame/engine` and
61
- `rgame/core` are separately requirable, which is how the Core spec suite loads
62
- exactly one layer.
64
+ You can also require `rgame/util`, `rgame/engine` and `rgame/core` on their own.
65
+ The Core spec suite does this to load exactly one layer.
63
66
 
64
- For convenience, `rgame/core` also defines `RGame::Util::Controls` (the input
65
- id vocabulary), because the input classes need it.
67
+ `rgame/core` also loads `RGame::Util::Controls`, the input id vocabulary,
68
+ because the input classes need it.
66
69
 
67
- Both extensions must be compiled before they can be required:
70
+ `gem install rgame` compiles both extensions. In a checkout of the repository,
71
+ compile them before you require anything:
68
72
 
69
73
  ```
70
74
  make ext # builds both, copies them into lib/rgame/
@@ -72,45 +76,45 @@ make ext # builds both, copies them into lib/rgame/
72
76
 
73
77
  ## The three namespaces
74
78
 
75
- Everything lives under `RGame`, split three ways two of them by what they
76
- depend on, the third by what it is *for*:
79
+ Everything lives under `RGame`. What a class depends on decides between `Util`
80
+ and `Core`. `Engine` holds what a game is written in.
77
81
 
78
82
  | | `RGame::Util` | `RGame::Core` | `RGame::Engine` |
79
83
  |---|---|---|---|
80
84
  | Contains | shareable *values* — no window, no GPU, nothing to release | things owning a window, GPU or OS handle | game concepts: the scene graph a game is written in |
81
- | Today | `Color`, `Tensor`, `Controls`, `Z` | `App`, `Input`, `Gamepad`, `Image`, `Renderer`, `Recording`, `Font`, `Audio`, `SpriteSheet`, `AssetManager` | `Node2D`, components, systems, signals, `TileMap`, `Player`, `InputMap`, `UI::Menu` |
85
+ | Classes | `Color`, `Tensor`, `Controls`, `Z`, `SolidGrid`, `RouteSearch`, `TileSweep`, `SaveFile` | `App`, `Input`, `Gamepad`, `Image`, `Renderer`, `Recording`, `Font`, `Audio`, `SpriteSheet`, `NineSlice`, `UiAtlas`, `TileMapRenderer`, `AssetManager` | `Node2D`, components, systems, signals, `TileMap`, `Player`, `InputMap`, `UI::Menu` |
82
86
  | Loading it costs | nothing | SDL2 + OpenGL in your process | nothing |
83
87
 
84
- The rule for splitting the bottom two: **a value goes in `Util`; only a
85
- handle-owner goes in `Core`.** A colour is a value. A window is not.
88
+ **A value goes in `Util`; only a handle-owner goes in `Core`.** A colour is a
89
+ value. A window is not.
86
90
 
87
- `RGame::Engine` sits above both, and its rule is what makes the split worth
88
- having:
91
+ `RGame::Engine` sits above both and follows three rules:
89
92
 
90
- - it may hold `Util` values freely as attributes a `Color`, a `Tensor`;
91
- - it may **not name `Core` at all** no require, no constant, no attribute;
92
- - it reaches `Core` only through objects handed to it. A node's `on_draw`
93
- receives a renderer and calls methods on it by name, never storing it and
94
- never asking what class it is.
93
+ - It may hold `Util` values as attributes, such as a `Color` or a `Tensor`.
94
+ - It may **not name `Core` at all**: no require, no constant, no attribute.
95
+ - It reaches `Core` only through objects it receives. A node's `on_draw`
96
+ receives a renderer and calls its methods by name. The node never stores the
97
+ renderer and never checks its class.
95
98
 
96
- This is not tidiness. It is what keeps a whole game its rules, its scenes, its
97
- collisions runnable and testable with no window, which the testing section
98
- below relies on. Two RuboCop cops enforce it in both directions, so a stray
99
- reference is a failing lint rather than a discovery made later.
99
+ These rules keep a whole game runnable and testable with no window: its rules,
100
+ its scenes, its collisions. The testing section below relies on that. RuboCop
101
+ cops enforce the rules in both directions inside rgame's own repository, and a
102
+ project from `rgame new` runs the first of them over its `nodes/` and `spec/`.
103
+ See [the generated RuboCop configuration](cli.md#the-generated-rubocop-configuration).
100
104
 
101
- `RGame::Game` is the single exception, and the only class directly under
102
- `RGame`: introducing the two halves to each other is exactly what it is for, and
103
- confining that to one file is what keeps the rule checkable everywhere else.
105
+ `RGame::Game` is the one exception: the only class that names both `Engine` and
106
+ `Core`. It exists to connect the two halves. Keeping that in one file lets the
107
+ rule hold everywhere else.
104
108
 
105
109
  ## A complete program
106
110
 
107
- A game is a tree of nodes plus `RGame::Game` to run it.
111
+ A game is a tree of nodes, run by `RGame::Game`.
108
112
 
109
113
  ```ruby
110
114
  require 'rgame/game'
111
115
 
112
116
  # One game object: a square the player walks around. Pure Engine — it names no
113
- # graphics class, so it runs just as happily in a spec with no window.
117
+ # graphics class, so it runs unchanged in a spec with no window.
114
118
  class Hero < RGame::Engine::Node2D
115
119
  SPEED = 200.0
116
120
 
@@ -135,10 +139,11 @@ class Hero < RGame::Engine::Node2D
135
139
  end
136
140
 
137
141
  # The renderer is handed in and never stored; `view` is the viewport being
138
- # drawn into, which most nodes ignore. Draw from the resolved absolute
139
- # position, not from `x`/`y`. See docs/api/drawing.md.
142
+ # drawn into, which most nodes ignore. Draw in the node's own space: the
143
+ # traversal has already put the renderer on this node, so (0, 0) is here.
144
+ # See docs/api/scene_graph.md, "Drawing happens in local space".
140
145
  def on_draw(renderer, _view)
141
- renderer.rect(abs_x, abs_y, width, height)
146
+ renderer.rect(0, 0, width, height)
142
147
  end
143
148
  end
144
149
 
@@ -153,27 +158,26 @@ end
153
158
  RGame::Game.new(root: Scene.new, width: 800, height: 600, caption: 'My Game').start
154
159
  ```
155
160
 
156
- You subclass `Node2D` and override the hooks you care about — `on_control`,
157
- `on_update`, `on_draw`, and the lifecycle hooks around them. Everything you do
158
- not override is an inherited no-op, and the phase methods that do the
159
- bookkeeping (resolving the transform, driving components, descending into
160
- children) are not the ones you override — so there is no `super` to remember and
161
- no way to break the tree by forgetting one. See [Scene graph](scene_graph.md)
162
- for the full list, and [Game](game.md) for what `Game` assembles around it: the
161
+ Subclass `Node2D` and override the hooks you need: `on_control`, `on_update`,
162
+ `on_draw`, and the lifecycle hooks around them. A hook you do not override does
163
+ nothing. Separate phase methods do the bookkeeping: they push the node's
164
+ transform, drive components and descend into children. You never override
165
+ those, so there is no `super` to forget. [Scene graph](scene_graph.md) lists
166
+ every hook. [Game](game.md) describes what `Game` builds around the tree: the
163
167
  window, the renderer, the asset manager, the sound device, the input mapper and
164
168
  the players.
165
169
 
166
- Pass no `input_map:` and you get the default one used above: eight-way `move_x`
167
- / `move_y` on the arrows, WASD, the d-pad or the left stick, plus `fire`. See
168
- [Input](input.md).
170
+ Without an `input_map:`, `Game` uses the default map shown above. It binds
171
+ eight-way `move_x` / `move_y` to the arrows, WASD, the d-pad and the left stick,
172
+ and adds `fire`. See [Input](input.md).
169
173
 
170
174
  ## Testing a game built on this
171
175
 
172
- The namespace split exists so that game logic can be tested without opening a
173
- window. `require 'rgame'` gives you `Util` and the whole scene graph with no SDL
174
- and no OpenGL in the process, and the nodes from the program above run there
175
- unchanged — drive their phases directly and a simulated hour takes milliseconds:
176
+ `require 'rgame'` loads `Util` and the whole scene graph with no SDL and no
177
+ OpenGL. The nodes from the program above run there unchanged. A spec drives
178
+ their phases directly, so a simulated hour takes milliseconds:
176
179
 
180
+ <!-- doc-example: skip — an RSpec file for the Hero above, run by rspec -->
177
181
  ```ruby
178
182
  require 'rgame'
179
183
 
@@ -195,20 +199,20 @@ RSpec.describe Hero do
195
199
  end
196
200
  ```
197
201
 
198
- Two things make that work:
202
+ Two properties make this work:
199
203
 
200
- - **`update` takes `dt` as an argument rather than reading a clock**, so a test
201
- passes whatever timestep it likes. That means tests are not dependent on real time and can simulate game behavior based on time in miliseconds.
202
- - **A node never holds a renderer.** `on_draw` is given one, so drawing can be
203
- checked by passing a recording double and asserting on what the node asked
204
- for see the renderer contract in `spec/support/shared_examples/`. All tests can be run completely headless.
204
+ - **`update` takes `dt` as an argument and reads no clock.** A test passes any
205
+ timestep it likes, so it can simulate minutes of play in milliseconds.
206
+ - **A node never holds a renderer.** `on_draw` receives one. A spec passes a
207
+ recording double and asserts on what the node asked it to draw. rgame's own
208
+ suite checks its fake renderer against the real one with a shared contract in
209
+ `spec/support/shared_examples/`. The whole suite runs headless.
205
210
 
206
- `hero.x` is asserted rather than `abs_x` because this node has no parent here.
207
- Absolute position accumulates from the parent, and a node with no parent resolves
208
- to the origin. Put it under a root and `abs_x` is what the rest of the engine
209
- reads. See [Scene graph](scene_graph.md#absolute-position).
211
+ The spec asserts `hero.x`, not `world_x`, because this hero has no parent. The
212
+ world transform accumulates from the parent, and a node without one resolves to
213
+ the origin. Under a root, game logic reads `world_x`. Drawing reads neither; see
214
+ [Scene graph](scene_graph.md#drawing-happens-in-local-space).
210
215
 
211
- Keep the parts of your game that decide *what happens* in `RGame::Engine` the
212
- layer cannot name `RGame::Core`, so it cannot accidentally acquire a dependency
213
- on a window. Then the only code that needs one is the thin layer that puts
214
- pixels on screen.
216
+ Keep the code that decides *what happens* in `RGame::Engine`. That layer cannot
217
+ name `RGame::Core`, so it cannot come to depend on a window. Only the thin layer
218
+ that puts pixels on screen then needs one.
data/docs/api/app.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # `RGame::Core::App`
2
2
 
3
- The window and the frame loop. Subclass it, override the hooks you need, call
4
- `run`.
3
+ `App` owns the window and runs the frame loop. Subclass it, override the hooks
4
+ you need, and call `run`.
5
5
 
6
6
  ```ruby
7
7
  require 'rgame/core'
@@ -13,44 +13,44 @@ end
13
13
  MyGame.new.run # returns when the loop stops
14
14
  ```
15
15
 
16
- `App.new` takes keyword arguments only. `width:`, `height:` and `caption:` are
17
- required; `media_root:` is optional and defaults to `'media'`. Creating one
18
- opens a real window immediately.
16
+ `App.new` takes keyword arguments only. It requires `width:`, `height:` and
17
+ `caption:`. `media_root:` is optional and defaults to `'media'`. The constructor
18
+ opens a real window at once.
19
19
 
20
20
  ## What the app owns
21
21
 
22
- Two things a game needs exactly one of, built on first use:
22
+ The app builds two objects on first use. A game needs exactly one of each:
23
23
 
24
24
  ```ruby
25
+ require 'rgame/core'
26
+
25
27
  class MyGame < RGame::Core::App
26
- def initialize = super(width: 640, height: 480, caption: 'demo', media_root: MEDIA)
28
+ def initialize = super(width: 640, height: 480, caption: 'demo', media_root: 'assets')
27
29
  end
28
30
 
29
- app.assets # => RGame::Core::AssetManager, rooted at media_root
30
- app.audio # => RGame::Core::Audio, the sound device
31
+ app = MyGame.new
32
+ app.assets # => RGame::Core::AssetManager rooted at media_root
33
+ app.audio # => RGame::Core::Audio — the sound device
31
34
  ```
32
35
 
33
36
  **A game never constructs either of them.** An image belongs to one OpenGL
34
- context and has to be told which, so something must hold the app and since the
35
- asset manager is the only thing in the engine that loads from a path, that
36
- something is the app itself, once, rather than a parameter threaded through
37
- every class that ends up owning an image.
37
+ context, so whatever loads it must know the app. The app builds its asset manager
38
+ once, and scenes load through it by path. No class has to pass the app along to
39
+ reach an image.
38
40
 
39
- Both are lazy, and that matters in each case. An app that draws only shapes
40
- builds no asset manager; an app that never plays anything never opens a sound
41
- device and asking for a sound is the first thing that needs one, so that is
42
- also the right moment to open it.
41
+ Both objects are built on first use. `RGame::Core::Renderer.new(app)` asks for the
42
+ asset manager, so any app that draws has one. An app that never plays a sound
43
+ never opens a sound device; the first sound request opens it.
43
44
 
44
- `media_root` is read-only and set at construction. There is deliberately no
45
- writer: changing it after an asset had loaded would leave one cache keyed
46
- against two roots.
45
+ `media_root` is read-only and fixed at construction. It has no writer: changing
46
+ the root after a load would leave one cache keyed against two roots.
47
47
 
48
- See [Sheets, atlases and maps](assets.md) for what the asset manager does.
48
+ [Assets](assets.md) describes the asset manager.
49
49
 
50
50
  ## The frame loop
51
51
 
52
- `run` drives the loop until something stops it, calling back into your object.
53
- One rendered frame looks like this:
52
+ `run` drives the loop until something stops it, and calls back into your object.
53
+ One rendered frame runs these steps:
54
54
 
55
55
  ```
56
56
  poll input and window events → button_down / button_up / resize
@@ -59,33 +59,30 @@ One rendered frame looks like this:
59
59
  update(dt) → zero or more times (see below)
60
60
  needs_redraw? → once; false skips the draw
61
61
  draw → once, unless skipped
62
+ frame_end → once, after draw, unless skipped
62
63
  ```
63
64
 
64
65
  ### `update(dt)` runs a *fixed* number of times, not once per frame
65
66
 
66
- This is the most important thing to understand about the loop.
67
+ **The simulation advances in fixed steps.** Each frame adds the real elapsed
68
+ time to an accumulator. The frame then runs every whole step that has come due.
69
+ That can be **zero** steps, when the machine renders faster than the simulation
70
+ needs. It can be **several**, when a slow frame forces the simulation to catch
71
+ up. The loop caps catch-up, so a slow frame slows time down instead of spiralling.
67
72
 
68
- The simulation advances in fixed steps. Real elapsed time accumulates, and each
69
- frame runs however many whole steps have come due which may be **zero**
70
- (the machine is rendering faster than the simulation needs) or **several** (a
71
- frame took a long time and the simulation is catching up). Catch-up is capped,
72
- so a very slow frame makes time slow down rather than spiral.
73
+ `dt` is always the same fixed step: **1/60 second**. It is never wall-clock
74
+ frame time. A fixed step makes movement reproducible. It also lets a test call
75
+ `update` directly and simulate any amount of time.
73
76
 
74
- `dt` is always the same fixed step, currently **1/60 second**. It is never
75
- wall-clock frame time. That is deliberate: it makes movement reproducible, and
76
- it is why a test can drive `update` directly and simulate any amount of time.
77
-
78
- The practical consequence: **do not sample input inside `update`.** A key held
79
- for one frame would be read once or five times depending on how slow the last
80
- frame was. Sample it in `frame_begin` instead, or rely on `Input`, which reads
81
- a snapshot taken once per frame and therefore answers identically for every
82
- tick of that frame.
77
+ **Do not sample input inside `update`.** A frame may run one tick or five, so a
78
+ key held for one frame would be read a varying number of times. Sample input in
79
+ `frame_begin`, or use `Input`. `Input` reads a snapshot taken once per frame, so
80
+ it answers the same for every tick of that frame.
83
81
 
84
82
  ### `needs_redraw?`
85
83
 
86
- Return `false` and the draw is skipped for that frame; the simulation still
87
- advances. Useful when nothing has changed and drawing is expensive. The default
88
- is `true`.
84
+ Return `false` to skip the draw for that frame. The simulation still advances.
85
+ Use it when nothing changed and drawing costs a lot. The default is `true`.
89
86
 
90
87
  ```ruby
91
88
  def update(_dt)
@@ -100,12 +97,12 @@ def draw
100
97
  end
101
98
  ```
102
99
 
103
- Because `update` running at all means a step happened, `@dirty = true` inside
104
- `update` is usually the whole rule you need.
100
+ `update` runs only when a step happened. Setting `@dirty = true` there is usually
101
+ the whole rule.
105
102
 
106
103
  ## Hooks you can override
107
104
 
108
- Every one has an inherited no-op default, so override only what you use.
105
+ Every hook inherits a default that does nothing. Override only what you use.
109
106
 
110
107
  | Hook | When |
111
108
  |---|---|
@@ -113,20 +110,23 @@ Every one has an inherited no-op default, so override only what you use.
113
110
  | `update(dt)` | One fixed simulation tick. |
114
111
  | `needs_redraw?` | Before drawing; `false` skips `draw`. Default `true`. |
115
112
  | `draw` | Render one frame. |
116
- | `button_down(id)` | A key was pressed. Auto-repeats are filtered, so a held key fires once. |
113
+ | `frame_end` | After `draw` reaches the GPU, before the buffer swap. Not called when the draw was skipped. |
114
+ | `button_down(id)` | A key was pressed. The loop filters auto-repeats, so a held key fires once. |
117
115
  | `button_up(id)` | A key was released. |
118
116
  | `resize(width, height)` | The window changed size. |
119
117
  | `gamepad_connected(slot)` | A controller arrived in a player slot. |
120
118
  | `gamepad_disconnected(slot)` | A controller left a slot. |
121
119
 
122
- `id` is a value from [`RGame::Util::Controls`](input.md) for example
120
+ `id` is a value from [`RGame::Util::Controls`](input.md), such as
123
121
  `Controls::KEY_ESCAPE`.
124
122
 
123
+ `frame_end` is the one point where a test can read back the frame it drew.
124
+ Game code rarely needs it.
125
+
125
126
  ### There is no built-in quit key
126
127
 
127
- Closing the window stops the loop, because that really is the platform's
128
- decision. Quitting on Escape is *your* decision, so the engine does not make it
129
- for you:
128
+ Closing the window stops the loop, because the platform decides that. Quitting
129
+ on Escape is *your* decision, so the engine leaves it to you:
130
130
 
131
131
  ```ruby
132
132
  def button_down(id)
@@ -142,17 +142,56 @@ end
142
142
  | `close` | Asks the loop to stop. Safe to call from inside any hook. |
143
143
  | `width`, `height` | Current window size. |
144
144
  | `caption`, `caption=` | The window title. |
145
- | `ticks_ms` | Monotonic milliseconds since startup. For animation phase. |
145
+ | `fullscreen?`, `fullscreen=` | Whether the window covers the screen. |
146
+ | `ticks_ms` | Monotonic milliseconds since startup. For measuring frames, not for drawing. |
146
147
  | `fps` | Most recent frames-per-second reading, updated about once a second. |
147
148
 
148
- `close` takes effect promptly the loop checks between steps, so it will not
149
- start further work in the current frame.
149
+ `close` takes effect promptly. The loop checks between steps and starts no more
150
+ work in the current frame.
151
+
152
+ `ticks_ms` is the raw clock. A draw never reads it: animation accumulates its own
153
+ time in `update`.
154
+
155
+ ### Fullscreen
156
+
157
+ ```ruby
158
+ require 'rgame/core'
159
+
160
+ app = RGame::Core::App.new(width: 640, height: 480, caption: 'demo', fullscreen: true) # opens fullscreen
161
+ app.fullscreen = !app.fullscreen? # switches either way
162
+ ```
163
+
164
+ **To open fullscreen, pass `fullscreen: true` to the constructor.** Setting it
165
+ after the window is up also works, but shows one windowed frame first. Players
166
+ read that flash as a broken startup. A game whose settings say fullscreen passes
167
+ the setting to `new`.
168
+
169
+ `width` and `height` still matter when the window opens fullscreen. The window
170
+ takes that size when it leaves fullscreen.
171
+
172
+ rgame uses **desktop** fullscreen: the window covers the screen at the screen's
173
+ own resolution. The display never changes mode. The switch is instant, needs no
174
+ mode list, and leaves other windows alone. The game gets a bigger view, not a
175
+ different one.
176
+
177
+ **Switching resizes the window**, so the loop calls
178
+ [`resize`](#hooks-you-can-override) with the new size. A user dragging a window
179
+ edge triggers the same call. Everything that lays out against the window learns
180
+ of the change through that one path. So a scene reads the `view` it is drawn
181
+ with, not the width it passed to `new`.
182
+
183
+ A layout written against fixed numbers will not follow a bigger view.
184
+ [`scale_mode:`](game.md#scale_mode--what-width-and-height-mean) on `RGame::Game`
185
+ offers the alternative: keep a logical size and scale it onto the window.
186
+
187
+ `examples/fullscreen` shows both ways to open, the switch, and every scale mode.
150
188
 
151
189
  ## Raw input queries
152
190
 
153
- `App` exposes the input snapshot directly. Most code should use
154
- [`RGame::Core::Input`](input.md), which takes symbolic action names instead of
155
- numeric ids, but these are the primitives underneath:
191
+ `App` exposes the input snapshot directly. Code written against Core alone
192
+ usually calls [`RGame::Core::Input`](input.md) instead, whose `down?` and `axis`
193
+ default the device to the keyboard. A game on `RGame::Game` reads actions and
194
+ never calls either. These queries are the primitives underneath:
156
195
 
157
196
  | Method | |
158
197
  |---|---|
@@ -162,15 +201,34 @@ numeric ids, but these are the primitives underneath:
162
201
  | `gamepad_name(slot)` | Its human-readable name, or `nil`. |
163
202
  | `gamepad_count` | How many controllers are connected. |
164
203
 
165
- Note the query is `gamepad_present?`, not `gamepad_connected?` the latter
166
- name belongs to the hot-plug *hook* above, and two methods differing only by a
167
- `?` would be a trap.
204
+ The query is named `gamepad_present?`, not `gamepad_connected?`. The hot-plug
205
+ *hook* above owns that name, and two methods that differ only by a `?` invite
206
+ mistakes.
207
+
208
+ ## The player's languages
209
+
210
+ ```ruby
211
+ require 'rgame/core'
212
+
213
+ RGame::Core.preferred_locales # => ["de-AT", "en"] — most preferred first
214
+ ```
215
+
216
+ `RGame::Core.preferred_locales` returns the locales the operating system says the
217
+ user prefers, most preferred first. It is a module function: it needs no app and
218
+ no window. Each entry is a language, followed by a hyphen and a country when the
219
+ OS names one. The list is `[]` when the OS names none, as on Linux under
220
+ `LANG=C`. It never holds `nil` or an empty String.
221
+
222
+ It returns what SDL reports, unfiltered. On Linux SDL reads `LANG`, then
223
+ `LANGUAGE`, and ignores `LC_ALL`, so a locale can appear twice. macOS and Windows
224
+ ask the OS. Picking the language a game shows is
225
+ [`I18n.choose`](localization.md#locales-and-the-fallback-chain), and `RGame::Game`
226
+ makes that choice at startup.
168
227
 
169
228
  ## When a hook raises
170
229
 
171
- An exception thrown from any hook comes back out of `run` with its class,
172
- message and backtrace intact. The loop shuts down cleanly first, so the window
173
- is not left stranded:
230
+ `run` re-raises any exception from a hook, with its class, message and backtrace
231
+ intact. The loop shuts down cleanly first and closes the window:
174
232
 
175
233
  ```ruby
176
234
  begin
@@ -180,13 +238,12 @@ rescue MyGameError => e
180
238
  end
181
239
  ```
182
240
 
183
- A **non-local exit** `throw`, `break` or `return` crossing out of a hook —
184
- cannot be carried across the loop the same way, and is reported as a
185
- `RuntimeError` telling you to use `close` instead. Use `close` to stop the
186
- loop; it is the only supported way out other than closing the window.
241
+ A **non-local exit** cannot cross the loop that way. That covers `throw`,
242
+ `break` or `return` leaving a hook. `run` reports it as a `RuntimeError` that
243
+ tells you to use `close`. Only `close` and closing the window stop the loop.
187
244
 
188
245
  ## Several windows in one process
189
246
 
190
- Creating more than one `App` works, and they may overlap in lifetime; the
191
- engine keeps SDL alive until the last one is gone. This mostly matters for test
192
- suites, which create and discard a window per example.
247
+ A process can create more than one `App`, and their lifetimes may overlap. The
248
+ engine keeps SDL alive until the last one is gone. Test suites rely on this: they
249
+ create and discard a window per example.