rgame 0.4.0-x86_64-linux-gnu

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 (239) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +251 -0
  3. data/LICENSE +26 -0
  4. data/README.md +325 -0
  5. data/docs/api/README.md +224 -0
  6. data/docs/api/app.md +249 -0
  7. data/docs/api/assets.md +463 -0
  8. data/docs/api/audio.md +249 -0
  9. data/docs/api/cli.md +259 -0
  10. data/docs/api/components.md +1112 -0
  11. data/docs/api/drawing.md +398 -0
  12. data/docs/api/examples.md +263 -0
  13. data/docs/api/game.md +219 -0
  14. data/docs/api/images.md +118 -0
  15. data/docs/api/input.md +476 -0
  16. data/docs/api/internals.md +289 -0
  17. data/docs/api/localization.md +285 -0
  18. data/docs/api/scene_graph.md +539 -0
  19. data/docs/api/signals.md +147 -0
  20. data/docs/api/systems.md +231 -0
  21. data/docs/api/text.md +133 -0
  22. data/docs/api/tile_maps.md +237 -0
  23. data/docs/api/toolbox.md +493 -0
  24. data/docs/api/ui.md +927 -0
  25. data/docs/api/values.md +316 -0
  26. data/examples/assets/README.md +322 -0
  27. data/examples/assets/blip.ogg +0 -0
  28. data/examples/assets/glyphs.json +5 -0
  29. data/examples/assets/glyphs.png +0 -0
  30. data/examples/assets/hero.json +12 -0
  31. data/examples/assets/hero.png +0 -0
  32. data/examples/assets/icons.json +13 -0
  33. data/examples/assets/icons.png +0 -0
  34. data/examples/assets/music.ogg +0 -0
  35. data/examples/assets/skills.json +10 -0
  36. data/examples/assets/skills.png +0 -0
  37. data/examples/assets/tileset.png +0 -0
  38. data/examples/assets/tileset.tsx +65 -0
  39. data/examples/assets/town.tmx +26 -0
  40. data/examples/assets/ui.json +11 -0
  41. data/examples/assets/ui.png +0 -0
  42. data/examples/collision/locales/en.yml +8 -0
  43. data/examples/collision/main.rb +316 -0
  44. data/examples/collision_tiles/locales/en.yml +9 -0
  45. data/examples/collision_tiles/main.rb +274 -0
  46. data/examples/fullscreen/locales/en.yml +10 -0
  47. data/examples/fullscreen/main.rb +216 -0
  48. data/examples/game_menu/locales/en.yml +8 -0
  49. data/examples/game_menu/main.rb +170 -0
  50. data/examples/input_glyphs/locales/en.yml +14 -0
  51. data/examples/input_glyphs/main.rb +213 -0
  52. data/examples/jump_topdown/locales/en.yml +9 -0
  53. data/examples/jump_topdown/main.rb +178 -0
  54. data/examples/localization/locales/de.yml +12 -0
  55. data/examples/localization/locales/en.yml +13 -0
  56. data/examples/localization/main.rb +158 -0
  57. data/examples/menu_navigation/locales/en.yml +23 -0
  58. data/examples/menu_navigation/main.rb +365 -0
  59. data/examples/music/locales/en.yml +7 -0
  60. data/examples/music/main.rb +134 -0
  61. data/examples/pathfinding/locales/en.yml +17 -0
  62. data/examples/pathfinding/main.rb +298 -0
  63. data/examples/pooling/locales/en.yml +7 -0
  64. data/examples/pooling/main.rb +259 -0
  65. data/examples/quick_wheel/locales/en.yml +16 -0
  66. data/examples/quick_wheel/main.rb +184 -0
  67. data/examples/radial_menu/locales/en.yml +16 -0
  68. data/examples/radial_menu/main.rb +184 -0
  69. data/examples/save_load/locales/en.yml +11 -0
  70. data/examples/save_load/main.rb +207 -0
  71. data/examples/save_load_ids/locales/en.yml +11 -0
  72. data/examples/save_load_ids/main.rb +322 -0
  73. data/examples/scroll_map/locales/en.yml +4 -0
  74. data/examples/scroll_map/main.rb +140 -0
  75. data/examples/signals/locales/en.yml +6 -0
  76. data/examples/signals/main.rb +278 -0
  77. data/examples/skill_bar/locales/en.yml +14 -0
  78. data/examples/skill_bar/main.rb +159 -0
  79. data/examples/sound/locales/en.yml +6 -0
  80. data/examples/sound/main.rb +122 -0
  81. data/examples/split_screen/locales/en.yml +9 -0
  82. data/examples/split_screen/main.rb +304 -0
  83. data/examples/sprite/locales/en.yml +8 -0
  84. data/examples/sprite/main.rb +180 -0
  85. data/examples/timer/locales/en.yml +12 -0
  86. data/examples/timer/main.rb +273 -0
  87. data/examples/velocity/locales/en.yml +6 -0
  88. data/examples/velocity/main.rb +196 -0
  89. data/examples/walk/locales/en.yml +4 -0
  90. data/examples/walk/main.rb +99 -0
  91. data/exe/rgame +9 -0
  92. data/lib/rgame/boot.rb +3 -0
  93. data/lib/rgame/cli/new_project.rb +139 -0
  94. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  95. data/lib/rgame/cli/templates/README.md.tt +93 -0
  96. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  97. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  98. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  99. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  100. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  101. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  102. data/lib/rgame/cli/templates/rspec.tt +2 -0
  103. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  104. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  105. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  106. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  107. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  108. data/lib/rgame/cli.rb +66 -0
  109. data/lib/rgame/core/app.rb +46 -0
  110. data/lib/rgame/core/asset_manager.rb +206 -0
  111. data/lib/rgame/core/audio.rb +145 -0
  112. data/lib/rgame/core/font.rb +46 -0
  113. data/lib/rgame/core/gamepad.rb +55 -0
  114. data/lib/rgame/core/image.rb +55 -0
  115. data/lib/rgame/core/input.rb +71 -0
  116. data/lib/rgame/core/locale.rb +22 -0
  117. data/lib/rgame/core/nine_slice.rb +142 -0
  118. data/lib/rgame/core/recording.rb +54 -0
  119. data/lib/rgame/core/renderer.rb +354 -0
  120. data/lib/rgame/core/sprite_sheet.rb +105 -0
  121. data/lib/rgame/core/tile_map_renderer.rb +172 -0
  122. data/lib/rgame/core/ui_atlas.rb +101 -0
  123. data/lib/rgame/core/virtual_gamepad.rb +26 -0
  124. data/lib/rgame/core.rb +18 -0
  125. data/lib/rgame/core_ext.so +0 -0
  126. data/lib/rgame/engine/actor_blockers.rb +131 -0
  127. data/lib/rgame/engine/animation_set.rb +50 -0
  128. data/lib/rgame/engine/animator.rb +44 -0
  129. data/lib/rgame/engine/audio_bus.rb +24 -0
  130. data/lib/rgame/engine/audio_director.rb +59 -0
  131. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  132. data/lib/rgame/engine/camera.rb +78 -0
  133. data/lib/rgame/engine/circle_collider.rb +34 -0
  134. data/lib/rgame/engine/collision_box.rb +59 -0
  135. data/lib/rgame/engine/collision_system.rb +132 -0
  136. data/lib/rgame/engine/component.rb +64 -0
  137. data/lib/rgame/engine/components/action_trigger.rb +40 -0
  138. data/lib/rgame/engine/components/animated_sprite.rb +71 -0
  139. data/lib/rgame/engine/components/box_collider.rb +99 -0
  140. data/lib/rgame/engine/components/camera_follow.rb +45 -0
  141. data/lib/rgame/engine/components/character_body.rb +50 -0
  142. data/lib/rgame/engine/components/circle_collider.rb +80 -0
  143. data/lib/rgame/engine/components/collision_world.rb +231 -0
  144. data/lib/rgame/engine/components/despawn_offscreen.rb +42 -0
  145. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  146. data/lib/rgame/engine/components/hop.rb +76 -0
  147. data/lib/rgame/engine/components/identity.rb +73 -0
  148. data/lib/rgame/engine/components/mover.rb +285 -0
  149. data/lib/rgame/engine/components/navigator.rb +145 -0
  150. data/lib/rgame/engine/components/path_follow.rb +176 -0
  151. data/lib/rgame/engine/components/player_controller.rb +27 -0
  152. data/lib/rgame/engine/components/pool.rb +53 -0
  153. data/lib/rgame/engine/components/screen_wrap.rb +49 -0
  154. data/lib/rgame/engine/components/sprite.rb +47 -0
  155. data/lib/rgame/engine/components/targeting.rb +52 -0
  156. data/lib/rgame/engine/components/thrust_controller.rb +65 -0
  157. data/lib/rgame/engine/components/tile_world.rb +109 -0
  158. data/lib/rgame/engine/components/timer.rb +75 -0
  159. data/lib/rgame/engine/components/velocity.rb +43 -0
  160. data/lib/rgame/engine/components/wander_controller.rb +64 -0
  161. data/lib/rgame/engine/components/world.rb +133 -0
  162. data/lib/rgame/engine/contact_set.rb +74 -0
  163. data/lib/rgame/engine/culling.rb +45 -0
  164. data/lib/rgame/engine/debug_overlay.rb +111 -0
  165. data/lib/rgame/engine/i18n/plural.rb +45 -0
  166. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  167. data/lib/rgame/engine/i18n/template.rb +59 -0
  168. data/lib/rgame/engine/i18n.rb +322 -0
  169. data/lib/rgame/engine/input/action_mapper.rb +100 -0
  170. data/lib/rgame/engine/input/actions.rb +92 -0
  171. data/lib/rgame/engine/input/input_map.rb +196 -0
  172. data/lib/rgame/engine/layout.rb +82 -0
  173. data/lib/rgame/engine/nav_grid.rb +87 -0
  174. data/lib/rgame/engine/node2d.rb +555 -0
  175. data/lib/rgame/engine/path.rb +76 -0
  176. data/lib/rgame/engine/player.rb +69 -0
  177. data/lib/rgame/engine/player_layer.rb +70 -0
  178. data/lib/rgame/engine/players.rb +205 -0
  179. data/lib/rgame/engine/pool.rb +51 -0
  180. data/lib/rgame/engine/presentation.rb +171 -0
  181. data/lib/rgame/engine/scene/scene_stack.rb +87 -0
  182. data/lib/rgame/engine/sealed_privates.rb +54 -0
  183. data/lib/rgame/engine/signal.rb +75 -0
  184. data/lib/rgame/engine/spatial_hash.rb +116 -0
  185. data/lib/rgame/engine/text.rb +194 -0
  186. data/lib/rgame/engine/tile_blockers.rb +63 -0
  187. data/lib/rgame/engine/tile_map.rb +147 -0
  188. data/lib/rgame/engine/tile_map_layer.rb +82 -0
  189. data/lib/rgame/engine/tileset.rb +99 -0
  190. data/lib/rgame/engine/timer.rb +51 -0
  191. data/lib/rgame/engine/ui/button.rb +248 -0
  192. data/lib/rgame/engine/ui/column.rb +20 -0
  193. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  194. data/lib/rgame/engine/ui/menu.rb +290 -0
  195. data/lib/rgame/engine/ui/navigation.rb +57 -0
  196. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  197. data/lib/rgame/engine/ui/option_button.rb +163 -0
  198. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  199. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  200. data/lib/rgame/engine/ui/pointing.rb +146 -0
  201. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  202. data/lib/rgame/engine/ui/ring.rb +55 -0
  203. data/lib/rgame/engine/ui/row.rb +21 -0
  204. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  205. data/lib/rgame/engine/ui/stack.rb +58 -0
  206. data/lib/rgame/engine/ui/stepping.rb +93 -0
  207. data/lib/rgame/engine/ui/text_button.rb +59 -0
  208. data/lib/rgame/engine/view.rb +76 -0
  209. data/lib/rgame/engine/viewports.rb +171 -0
  210. data/lib/rgame/engine/world_view.rb +71 -0
  211. data/lib/rgame/engine.rb +86 -0
  212. data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
  213. data/lib/rgame/fonts/OFL.txt +102 -0
  214. data/lib/rgame/game.rb +271 -0
  215. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  216. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  217. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  218. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  219. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  220. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  221. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  222. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  223. data/lib/rgame/rubocop/default.yml +39 -0
  224. data/lib/rgame/rubocop/plugin.rb +45 -0
  225. data/lib/rgame/rubocop.rb +11 -0
  226. data/lib/rgame/util/color.rb +23 -0
  227. data/lib/rgame/util/controls.rb +169 -0
  228. data/lib/rgame/util/route_search.rb +27 -0
  229. data/lib/rgame/util/save_file.rb +107 -0
  230. data/lib/rgame/util/solid_grid.rb +37 -0
  231. data/lib/rgame/util/tensor.rb +3 -0
  232. data/lib/rgame/util/tile_sweep.rb +36 -0
  233. data/lib/rgame/util/z.rb +123 -0
  234. data/lib/rgame/util.rb +10 -0
  235. data/lib/rgame/util_ext.so +0 -0
  236. data/lib/rgame/version.rb +12 -0
  237. data/lib/rgame.rb +5 -0
  238. data/licenses/SDL2/LICENSE.txt +18 -0
  239. metadata +309 -0
@@ -0,0 +1,224 @@
1
+ # rgame API guide
2
+
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
+
8
+ | Page | Covers |
9
+ |---|---|
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 |
12
+ | [App](app.md) | `RGame::Core::App` — the window and the frame loop |
13
+ | [Game](game.md) | `RGame::Game` — the entry point that wires both halves together |
14
+ | [Input](input.md) | `RGame::Core::Input`, `RGame::Util::Controls`, `RGame::Core::Gamepad`, `RGame::Core::VirtualGamepad` |
15
+ | [Drawing](drawing.md) | `RGame::Core::Renderer` — shapes, images, transforms, clipping, recordings |
16
+ | [Images](images.md) | `RGame::Core::Image` — loading PNGs, subimages, tiles |
17
+ | [Text](text.md) | `RGame::Core::Font` and `Renderer#text` |
18
+ | [Audio](audio.md) | `RGame::Core::Audio`, `Sample`, `Song` — samples and streamed music |
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 |
22
+
23
+ The scene graph is `RGame::Engine`, the layer a game is written in:
24
+
25
+ | Page | Covers |
26
+ |---|---|
27
+ | [Scene graph](scene_graph.md) | `Node2D`, the tree, the lifecycle, transforms and the camera |
28
+ | [Components](components.md) | Reusable behaviour attached to a node |
29
+ | [Systems](systems.md) | Services a subtree shares — collision worlds, tile worlds |
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 |
32
+ | [Signals](signals.md) | The typed observer pattern nodes talk through |
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 |
35
+ | [Internal building blocks](internals.md) | What components are built from: collision maths, the spatial index, animation playback |
36
+
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.
45
+
46
+ ## Loading it
47
+
48
+ rgame has three entry points. Each one loads everything the one before it does:
49
+
50
+ ```ruby
51
+ require 'rgame' # RGame::Util + RGame::Engine — no graphics libraries at all
52
+ require 'rgame/core' # adds the window, the GPU and the sound device (SDL2 + OpenGL)
53
+ require 'rgame/game' # all of it, wired together — what a game writes
54
+ ```
55
+
56
+ A game requires the last one. Its entry point is `RGame::Game`; see
57
+ [Game](game.md).
58
+
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.
63
+
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.
66
+
67
+ `rgame/core` also loads `RGame::Util::Controls`, the input id vocabulary,
68
+ because the input classes need it.
69
+
70
+ **Your install either compiled both extensions or arrived with them built.** On
71
+ Apple Silicon macOS, x86-64 Linux and 64-bit Windows, `gem install rgame` fetches
72
+ a gem that already holds `core_ext` and `util_ext`, with SDL2 inside `core_ext`,
73
+ and compiles nothing. Every other machine gets the gem that ships the C and
74
+ builds both on install, against a system SDL2. Either way the two land in the
75
+ same place and the three requires above behave the same.
76
+
77
+ In a checkout of the repository, compile them before you require anything:
78
+
79
+ ```
80
+ make ext # builds both, copies them into lib/rgame/
81
+ ```
82
+
83
+ ## The three namespaces
84
+
85
+ Everything lives under `RGame`. What a class depends on decides between `Util`
86
+ and `Core`. `Engine` holds what a game is written in.
87
+
88
+ | | `RGame::Util` | `RGame::Core` | `RGame::Engine` |
89
+ |---|---|---|---|
90
+ | 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 |
91
+ | Classes | `Color`, `Tensor`, `Controls`, `Z`, `SolidGrid`, `RouteSearch`, `TileSweep`, `SaveFile` | `App`, `Input`, `Gamepad`, `VirtualGamepad`, `Image`, `Renderer`, `Recording`, `Font`, `Audio`, `SpriteSheet`, `NineSlice`, `UiAtlas`, `TileMapRenderer`, `AssetManager` | `Node2D`, components, systems, signals, `TileMap`, `Player`, `InputMap`, `UI::Menu` |
92
+ | Loading it costs | nothing | SDL2 + OpenGL in your process | nothing |
93
+
94
+ **A value goes in `Util`; only a handle-owner goes in `Core`.** A colour is a
95
+ value. A window is not.
96
+
97
+ `RGame::Engine` sits above both and follows three rules:
98
+
99
+ - It may hold `Util` values as attributes, such as a `Color` or a `Tensor`.
100
+ - It may **not name `Core` at all**: no require, no constant, no attribute.
101
+ - It reaches `Core` only through objects it receives. A node's `on_draw`
102
+ receives a renderer and calls its methods by name. The node never stores the
103
+ renderer and never checks its class.
104
+
105
+ These rules keep a whole game runnable and testable with no window: its rules,
106
+ its scenes, its collisions. The testing section below relies on that. RuboCop
107
+ cops enforce the rules in both directions inside rgame's own repository, and a
108
+ project from `rgame new` runs the first of them over its `nodes/` and `spec/`.
109
+ See [the generated RuboCop configuration](cli.md#the-generated-rubocop-configuration).
110
+
111
+ `RGame::Game` is the one exception: the only class that names both `Engine` and
112
+ `Core`. It exists to connect the two halves. Keeping that in one file lets the
113
+ rule hold everywhere else.
114
+
115
+ ## A complete program
116
+
117
+ A game is a tree of nodes, run by `RGame::Game`.
118
+
119
+ ```ruby
120
+ require 'rgame/game'
121
+
122
+ # One game object: a square the player walks around. Pure Engine — it names no
123
+ # graphics class, so it runs unchanged in a spec with no window.
124
+ class Hero < RGame::Engine::Node2D
125
+ SPEED = 200.0
126
+
127
+ def initialize
128
+ super(x: 400, y: 300, width: 16, height: 16)
129
+ @vx = 0.0
130
+ @vy = 0.0
131
+ end
132
+
133
+ # Intent, read once per simulation tick. Never a key: `move_x` is whatever
134
+ # this player's input map binds it to — arrows, WASD or a stick.
135
+ def on_control(actions)
136
+ @vx = actions.axis(:move_x) * SPEED
137
+ @vy = actions.axis(:move_y) * SPEED
138
+ end
139
+
140
+ # `dt` is always the same fixed step, never wall-clock frame time, so
141
+ # movement is deterministic. `x`/`y` are relative to the parent.
142
+ def on_update(dt)
143
+ self.x += @vx * dt
144
+ self.y += @vy * dt
145
+ end
146
+
147
+ # The renderer is handed in and never stored; `view` is the viewport being
148
+ # drawn into, which most nodes ignore. Draw in the node's own space: the
149
+ # traversal has already put the renderer on this node, so (0, 0) is here.
150
+ # See docs/api/scene_graph.md, "Drawing happens in local space".
151
+ def on_draw(renderer, _view)
152
+ renderer.rect(0, 0, width, height)
153
+ end
154
+ end
155
+
156
+ # The root of the tree. Children are added in `on_add`, once the node is in a
157
+ # tree and can reach the game around it.
158
+ class Scene < RGame::Engine::Node2D
159
+ def on_add
160
+ add_node(Hero.new)
161
+ end
162
+ end
163
+
164
+ RGame::Game.new(root: Scene.new, width: 800, height: 600, caption: 'My Game').start
165
+ ```
166
+
167
+ Subclass `Node2D` and override the hooks you need: `on_control`, `on_update`,
168
+ `on_draw`, and the lifecycle hooks around them. A hook you do not override does
169
+ nothing. Separate phase methods do the bookkeeping: they push the node's
170
+ transform, drive components and descend into children. You never override
171
+ those, so there is no `super` to forget. [Scene graph](scene_graph.md) lists
172
+ every hook. [Game](game.md) describes what `Game` builds around the tree: the
173
+ window, the renderer, the asset manager, the sound device, the input mapper and
174
+ the players.
175
+
176
+ Without an `input_map:`, `Game` uses the default map shown above. It binds
177
+ eight-way `move_x` / `move_y` to the arrows, WASD, the d-pad and the left stick,
178
+ and adds `fire`. See [Input](input.md).
179
+
180
+ ## Testing a game built on this
181
+
182
+ `require 'rgame'` loads `Util` and the whole scene graph with no SDL and no
183
+ OpenGL. The nodes from the program above run there unchanged. A spec drives
184
+ their phases directly, so a simulated hour takes milliseconds:
185
+
186
+ <!-- doc-example: skip — an RSpec file for the Hero above, run by rspec -->
187
+ ```ruby
188
+ require 'rgame'
189
+
190
+ RSpec.describe Hero do
191
+ it 'walks right at 200 units a second' do
192
+ hero = Hero.new
193
+ # The same snapshot object the input mapper hands a node at runtime, built
194
+ # by hand with the stick pushed fully right.
195
+ actions = RGame::Engine::Actions.new(axes: { move_x: 1.0, move_y: 0.0 })
196
+
197
+ # One simulated second, sixty ticks, no window and no clock.
198
+ 60.times do
199
+ hero.control(actions)
200
+ hero.update(1.0 / 60.0)
201
+ end
202
+
203
+ expect(hero.x).to be_within(0.01).of(600.0)
204
+ end
205
+ end
206
+ ```
207
+
208
+ Two properties make this work:
209
+
210
+ - **`update` takes `dt` as an argument and reads no clock.** A test passes any
211
+ timestep it likes, so it can simulate minutes of play in milliseconds.
212
+ - **A node never holds a renderer.** `on_draw` receives one. A spec passes a
213
+ recording double and asserts on what the node asked it to draw. rgame's own
214
+ suite checks its fake renderer against the real one with a shared contract in
215
+ `spec/support/shared_examples/`. The whole suite runs headless.
216
+
217
+ The spec asserts `hero.x`, not `world_x`, because this hero has no parent. The
218
+ world transform accumulates from the parent, and a node without one resolves to
219
+ the origin. Under a root, game logic reads `world_x`. Drawing reads neither; see
220
+ [Scene graph](scene_graph.md#drawing-happens-in-local-space).
221
+
222
+ Keep the code that decides *what happens* in `RGame::Engine`. That layer cannot
223
+ name `RGame::Core`, so it cannot come to depend on a window. Only the thin layer
224
+ that puts pixels on screen then needs one.
data/docs/api/app.md ADDED
@@ -0,0 +1,249 @@
1
+ # `RGame::Core::App`
2
+
3
+ `App` owns the window and runs the frame loop. Subclass it, override the hooks
4
+ you need, and call `run`.
5
+
6
+ ```ruby
7
+ require 'rgame/core'
8
+
9
+ class MyGame < RGame::Core::App
10
+ def initialize = super(width: 800, height: 600, caption: 'My Game')
11
+ end
12
+
13
+ MyGame.new.run # returns when the loop stops
14
+ ```
15
+
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
+
20
+ ## What the app owns
21
+
22
+ The app builds two objects on first use. A game needs exactly one of each:
23
+
24
+ ```ruby
25
+ require 'rgame/core'
26
+
27
+ class MyGame < RGame::Core::App
28
+ def initialize = super(width: 640, height: 480, caption: 'demo', media_root: 'assets')
29
+ end
30
+
31
+ app = MyGame.new
32
+ app.assets # => RGame::Core::AssetManager — rooted at media_root
33
+ app.audio # => RGame::Core::Audio — the sound device
34
+ ```
35
+
36
+ **A game never constructs either of them.** An image belongs to one OpenGL
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.
40
+
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.
44
+
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
+
48
+ [Assets](assets.md) describes the asset manager.
49
+
50
+ ## The frame loop
51
+
52
+ `run` drives the loop until something stops it, and calls back into your object.
53
+ One rendered frame runs these steps:
54
+
55
+ ```
56
+ poll input and window events → button_down / button_up / resize
57
+ gamepad_connected / gamepad_disconnected
58
+ frame_begin → once, before any ticks
59
+ update(dt) → zero or more times (see below)
60
+ needs_redraw? → once; false skips the draw
61
+ draw → once, unless skipped
62
+ frame_end → once, after draw, unless skipped
63
+ ```
64
+
65
+ ### `update(dt)` runs a *fixed* number of times, not once per frame
66
+
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.
72
+
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.
76
+
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.
81
+
82
+ ### `needs_redraw?`
83
+
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`.
86
+
87
+ ```ruby
88
+ def update(_dt)
89
+ @dirty = true if something_moved
90
+ end
91
+
92
+ def needs_redraw? = @dirty
93
+
94
+ def draw
95
+ # ...
96
+ @dirty = false
97
+ end
98
+ ```
99
+
100
+ `update` runs only when a step happened. Setting `@dirty = true` there is usually
101
+ the whole rule.
102
+
103
+ ## Hooks you can override
104
+
105
+ Every hook inherits a default that does nothing. Override only what you use.
106
+
107
+ | Hook | When |
108
+ |---|---|
109
+ | `frame_begin` | Once per frame, before that frame's ticks. Sample input here. |
110
+ | `update(dt)` | One fixed simulation tick. |
111
+ | `needs_redraw?` | Before drawing; `false` skips `draw`. Default `true`. |
112
+ | `draw` | Render one frame. |
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. |
115
+ | `button_up(id)` | A key was released. |
116
+ | `resize(width, height)` | The window changed size. |
117
+ | `gamepad_connected(slot)` | A controller arrived in a player slot. |
118
+ | `gamepad_disconnected(slot)` | A controller left a slot. |
119
+
120
+ `id` is a value from [`RGame::Util::Controls`](input.md), such as
121
+ `Controls::KEY_ESCAPE`.
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
+
126
+ ### There is no built-in quit key
127
+
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
+
131
+ ```ruby
132
+ def button_down(id)
133
+ close if id == RGame::Util::Controls::KEY_ESCAPE
134
+ end
135
+ ```
136
+
137
+ ## Window methods
138
+
139
+ | Method | |
140
+ |---|---|
141
+ | `run` | Runs the loop until it stops. Returns `self`. |
142
+ | `close` | Asks the loop to stop. Safe to call from inside any hook. |
143
+ | `width`, `height` | Current window size. |
144
+ | `caption`, `caption=` | The window title. |
145
+ | `fullscreen?`, `fullscreen=` | Whether the window covers the screen. |
146
+ | `ticks_ms` | Monotonic milliseconds since startup. For measuring frames, not for drawing. |
147
+ | `fps` | Most recent frames-per-second reading, updated about once a second. |
148
+
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.
188
+
189
+ ## Raw input queries
190
+
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:
195
+
196
+ | Method | |
197
+ |---|---|
198
+ | `input_down?(device, button_id)` | Is that button held on that device? |
199
+ | `input_axis(device, axis_id)` | Analog axis value; sticks −1.0…1.0, triggers 0.0…1.0. |
200
+ | `gamepad_present?(slot)` | Is a controller plugged into that player slot? |
201
+ | `gamepad_name(slot)` | Its human-readable name, or `nil`. |
202
+ | `gamepad_count` | How many controllers are connected. |
203
+
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.
227
+
228
+ ## When a hook raises
229
+
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:
232
+
233
+ ```ruby
234
+ begin
235
+ MyGame.new.run
236
+ rescue MyGameError => e
237
+ # the loop has already stopped by the time this runs
238
+ end
239
+ ```
240
+
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.
244
+
245
+ ## Several windows in one process
246
+
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.