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
data/docs/api/game.md ADDED
@@ -0,0 +1,219 @@
1
+ # `RGame::Game`
2
+
3
+ `Game` is a game's entry point, and the one class that knows both halves of the
4
+ engine.
5
+
6
+ ```ruby
7
+ require 'rgame/game'
8
+
9
+ class HelloScene < RGame::Engine::Node2D
10
+ def on_draw(renderer, _view) = renderer.text('Hello world!', 250, 200)
11
+ end
12
+
13
+ RGame::Game.new(root: HelloScene.new, caption: 'Hello').start
14
+ ```
15
+
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
+
21
+ ```ruby
22
+ RGame::Game.new(root:, width: 640, height: 480, caption: 'RGame',
23
+ media_root: 'media', input_map: nil, device: Controls::KEYBOARD,
24
+ players: 1, input: nil, fullscreen: false, scale_mode: :letterbox,
25
+ locales: 'locales')
26
+ ```
27
+
28
+ | Reader | |
29
+ |---|---|
30
+ | `root` | the node tree |
31
+ | `renderer` | what scenes draw through |
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 |
35
+ | `assets`, `audio`, `media_root`, `width`, `height`, `fps` | inherited from [App](app.md) |
36
+
37
+ A node reaches `players` and `viewports` as systems:
38
+ `node.system(RGame::Engine::Players)` and `node.system(RGame::Engine::Viewports)`.
39
+
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.
42
+
43
+ ### `scale_mode:` — what `width` and `height` mean
44
+
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
+
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.
143
+
144
+ ## Reaching the game from a node
145
+
146
+ A node anywhere in the tree reaches the asset manager through the root's
147
+ context. No constructor has to pass it along:
148
+
149
+ ```ruby
150
+ sheet = node.context.assets.sheet('player.json') # node.context is node.root.context
151
+ ```
152
+
153
+ A component reaches the same object as `context`.
154
+
155
+ ## Input
156
+
157
+ `input_map:` names a game's actions in terms of physical ids from
158
+ [`RGame::Util::Controls`](input.md):
159
+
160
+ ```ruby
161
+ require 'rgame/game'
162
+
163
+ Controls = RGame::Util::Controls
164
+
165
+ class Root < RGame::Engine::Node2D; end
166
+
167
+ RGame::Game.new(
168
+ root: Root.new,
169
+ input_map: RGame::Engine::InputMap.new(
170
+ move_x: { axis: [Controls::KEY_LEFT, Controls::KEY_RIGHT], # -1.0 .. 1.0
171
+ stick: Controls::AXIS_LEFT_X },
172
+ fire: { buttons: [Controls::KEY_SPACE, Controls::PAD_A] } # held / pressed / released
173
+ )
174
+ ).start
175
+ ```
176
+
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.
181
+
182
+ `device:` picks the device that drives player one. It defaults to the keyboard;
183
+ pass `Controls.gamepad(slot)` for a controller.
184
+
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).
190
+
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.
193
+
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.
199
+
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?`.
203
+
204
+ ## Subclassing it
205
+
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.
209
+
210
+ ```ruby
211
+ require 'rgame/game'
212
+
213
+ class MyGame < RGame::Game
214
+ def button_down(id)
215
+ super # keeps F1 and F2 working
216
+ @paused = !@paused if id == RGame::Util::Controls::KEY_SPACE
217
+ end
218
+ end
219
+ ```
@@ -0,0 +1,118 @@
1
+ # Images
2
+
3
+ `RGame::Core::Image` is a picture on the GPU. Loading one decodes a PNG and
4
+ uploads it once. Subimages, tiles and whole sprite sheets are all *views* of that
5
+ single upload.
6
+
7
+ ```ruby
8
+ require 'rgame/core'
9
+
10
+ img = RGame::Core::Image.new(app, 'hero.png')
11
+ frame = img.subimage(0, 0, 16, 16)
12
+ walk = RGame::Core::Image.load_tiles(app, 'hero.png', 16, 16)
13
+ ```
14
+
15
+ [Drawing](drawing.md) covers putting images on screen.
16
+
17
+ **A game loads images through the [asset manager](assets.md)**, with
18
+ `app.assets.image(path)`. The manager resolves the path against `media_root` and
19
+ caches the image. `Image.new` and `Image.load_tiles` bypass that cache, and resolve a
20
+ relative path against the working directory.
21
+
22
+ ## Loading
23
+
24
+ ```ruby
25
+ image = RGame::Core::Image.new(app, 'assets/hero.png')
26
+ image.width # => 64
27
+ image.height # => 32
28
+ ```
29
+
30
+ **The `app` argument is required and comes first.** A texture lives inside one
31
+ OpenGL context, so an image belongs to a window. Naming the app lets two windows
32
+ work side by side. It also lets the image keep its app alive as long as it needs.
33
+
34
+ rgame reads PNG only. A file it cannot read or decode raises
35
+ `RGame::Core::Image::LoadError`, with the path in the message:
36
+
37
+ ```ruby
38
+ begin
39
+ RGame::Core::Image.new(app, 'assets/typo.png')
40
+ rescue RGame::Core::Image::LoadError => e
41
+ warn e.message # => "could not read assets/typo.png"
42
+ end
43
+ ```
44
+
45
+ Greyscale and palette PNGs load too. The loader converts them to RGBA, so the
46
+ engine handles one pixel format.
47
+
48
+ **Images always use nearest-neighbour sampling**, with no setting to change it.
49
+ The engine draws pixel art, and pixel art should never blur when scaled up.
50
+
51
+ ## Slicing: subimages and tiles
52
+
53
+ ```ruby
54
+ sheet = RGame::Core::Image.new(app, 'tiles.png') # say 64x32
55
+
56
+ sheet.subimage(16, 0, 16, 16) # one 16x16 region
57
+ sheet.tile_count(16, 16) # => 8 (4 columns x 2 rows)
58
+ sheet.tile(16, 16, 5) # the sixth tile
59
+ sheet.tiles(16, 16) # => [Image, Image, ...] all eight
60
+ sheet.each_tile(16, 16) { |t| } # the same, without building the Array
61
+ ```
62
+
63
+ `Image.load_tiles(app, path, w, h)` combines `new` and `tiles`. It is the usual
64
+ way to open a sprite sheet:
65
+
66
+ ```ruby
67
+ frames = RGame::Core::Image.load_tiles(app, 'explosion.png', 32, 32)
68
+ ```
69
+
70
+ Three rules apply to all of these methods.
71
+
72
+ **Nothing is decoded or uploaded twice.** A hundred tiles are a hundred small Ruby
73
+ objects over one texture. Slice sheets at load time without worrying about cost.
74
+
75
+ **Tiles come back in reading order**: left to right, then top to bottom. Sprite
76
+ sheets number their frames the same way.
77
+
78
+ **A partial tile at the right or bottom edge is not a tile.** Slicing a 70-pixel
79
+ sheet into 16s yields four columns. The six leftover pixels count as padding,
80
+ because half a sprite is never wanted.
81
+
82
+ ### Coordinates are relative to what you cut from
83
+
84
+ `subimage` on a subimage composes, and cannot escape its parent:
85
+
86
+ ```ruby
87
+ row = sheet.subimage(0, 16, 64, 16) # the bottom row of the sheet
88
+ tile = row.subimage(32, 0, 16, 16) # 32 pixels into *the row*, not the sheet
89
+ ```
90
+
91
+ **Bad coordinates raise; they never return `nil`.** A rectangle that does not fit
92
+ raises `ArgumentError`. An out-of-range tile index raises `IndexError`:
93
+
94
+ ```ruby
95
+ sheet.subimage(0, 0, 999, 999) # ArgumentError: does not fit in a 64x32 image
96
+ sheet.tile(16, 16, 99) # IndexError: 8 tiles of 16x16
97
+ ```
98
+
99
+ A `nil` would travel a long way: into an asset table, and out again three scenes
100
+ later. It would finally fail as a `NoMethodError` that no longer points at the
101
+ wrong coordinates.
102
+
103
+ ## Lifetime
104
+
105
+ **You never free an image.** The engine releases the texture when the last view
106
+ of it is garbage-collected, in any order. Tiles still in use keep the upload
107
+ alive after the sheet is dropped. Dropping the window first also works.
108
+
109
+ ```ruby
110
+ sheet = RGame::Core::Image.new(app, 'tiles.png')
111
+ ground = sheet.tile(16, 16, 0)
112
+ sheet = nil # the upload stays — `ground` is still a view of it
113
+ ```
114
+
115
+ A leaked GPU texture shows no symptoms at first. Nothing slows down and nothing
116
+ looks wrong, while video memory fills over an hour of play.
117
+ `Image.debug_live_textures` returns how many uploads exist. Tests assert against
118
+ it; it is not part of the drawing API.