rgame 0.1.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 (265) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +228 -0
  3. data/README.md +142 -243
  4. data/docs/api/README.md +141 -90
  5. data/docs/api/app.md +125 -68
  6. data/docs/api/assets.md +203 -166
  7. data/docs/api/audio.md +130 -89
  8. data/docs/api/cli.md +259 -0
  9. data/docs/api/components.md +1045 -254
  10. data/docs/api/drawing.md +187 -119
  11. data/docs/api/examples.md +263 -0
  12. data/docs/api/game.md +165 -45
  13. data/docs/api/images.md +39 -39
  14. data/docs/api/input.md +344 -85
  15. data/docs/api/internals.md +241 -62
  16. data/docs/api/localization.md +285 -0
  17. data/docs/api/scene_graph.md +503 -123
  18. data/docs/api/signals.md +81 -76
  19. data/docs/api/systems.md +197 -64
  20. data/docs/api/text.md +58 -41
  21. data/docs/api/tile_maps.md +237 -0
  22. data/docs/api/toolbox.md +400 -147
  23. data/docs/api/ui.md +927 -0
  24. data/docs/api/values.md +234 -19
  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/README.md +6 -5
  92. data/ext/rgame_core/app/app.c +215 -11
  93. data/ext/rgame_core/app/locale.c +67 -0
  94. data/ext/rgame_core/app/locale.h +28 -0
  95. data/ext/rgame_core/audio/audio.c +113 -2
  96. data/ext/rgame_core/example.rb +18 -56
  97. data/ext/rgame_core/extconf.rb +16 -113
  98. data/ext/rgame_core/graphics/canvas.c +45 -4
  99. data/ext/rgame_core/graphics/canvas.h +65 -10
  100. data/ext/rgame_core/graphics/clip.c +22 -13
  101. data/ext/rgame_core/include/rgame/core.h +151 -5
  102. data/ext/rgame_core/input/gamepad.c +57 -3
  103. data/ext/rgame_core/ruby/audio_ext.c +10 -5
  104. data/ext/rgame_core/ruby/core_ext.c +46 -7
  105. data/ext/rgame_core/ruby/core_ext.h +3 -0
  106. data/ext/rgame_core/ruby/locale_ext.c +44 -0
  107. data/ext/rgame_core/ruby/recording_ext.c +1 -1
  108. data/ext/rgame_core/ruby/renderer_ext.c +42 -19
  109. data/ext/rgame_util/color_ext.c +12 -3
  110. data/ext/rgame_util/extconf.rb +2 -20
  111. data/ext/rgame_util/route_search.c +305 -0
  112. data/ext/rgame_util/route_search.h +86 -0
  113. data/ext/rgame_util/route_search_ext.c +150 -0
  114. data/ext/rgame_util/solid_grid.c +58 -0
  115. data/ext/rgame_util/solid_grid.h +49 -0
  116. data/ext/rgame_util/solid_grid_ext.c +161 -0
  117. data/ext/rgame_util/tile_sweep.c +164 -0
  118. data/ext/rgame_util/tile_sweep.h +62 -0
  119. data/ext/rgame_util/tile_sweep_ext.c +155 -0
  120. data/ext/rgame_util/util_ext.c +3 -0
  121. data/ext/rgame_util/util_ext.h +15 -0
  122. data/lib/rgame/boot.rb +0 -10
  123. data/lib/rgame/cli/new_project.rb +139 -0
  124. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  125. data/lib/rgame/cli/templates/README.md.tt +93 -0
  126. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  127. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  128. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  129. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  130. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  131. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  132. data/lib/rgame/cli/templates/rspec.tt +2 -0
  133. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  134. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  135. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  136. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  137. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  138. data/lib/rgame/cli.rb +66 -0
  139. data/lib/rgame/core/app.rb +6 -42
  140. data/lib/rgame/core/asset_manager.rb +13 -31
  141. data/lib/rgame/core/audio.rb +37 -16
  142. data/lib/rgame/core/font.rb +0 -3
  143. data/lib/rgame/core/input.rb +35 -41
  144. data/lib/rgame/core/locale.rb +22 -0
  145. data/lib/rgame/core/nine_slice.rb +0 -21
  146. data/lib/rgame/core/recording.rb +3 -1
  147. data/lib/rgame/core/renderer.rb +75 -84
  148. data/lib/rgame/core/sprite_sheet.rb +0 -3
  149. data/lib/rgame/core/tile_map_renderer.rb +77 -65
  150. data/lib/rgame/core/ui_atlas.rb +28 -13
  151. data/lib/rgame/core.rb +1 -8
  152. data/lib/rgame/engine/actor_blockers.rb +131 -0
  153. data/lib/rgame/engine/animation_set.rb +1 -0
  154. data/lib/rgame/engine/audio_director.rb +36 -6
  155. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  156. data/lib/rgame/engine/camera.rb +55 -10
  157. data/lib/rgame/engine/circle_collider.rb +4 -2
  158. data/lib/rgame/engine/collision_box.rb +26 -1
  159. data/lib/rgame/engine/collision_system.rb +110 -22
  160. data/lib/rgame/engine/component.rb +35 -1
  161. data/lib/rgame/engine/components/action_trigger.rb +0 -1
  162. data/lib/rgame/engine/components/animated_sprite.rb +31 -23
  163. data/lib/rgame/engine/components/box_collider.rb +99 -0
  164. data/lib/rgame/engine/components/camera_follow.rb +45 -0
  165. data/lib/rgame/engine/components/character_body.rb +21 -41
  166. data/lib/rgame/engine/components/circle_collider.rb +47 -11
  167. data/lib/rgame/engine/components/collision_world.rb +159 -31
  168. data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
  169. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  170. data/lib/rgame/engine/components/hop.rb +76 -0
  171. data/lib/rgame/engine/components/identity.rb +73 -0
  172. data/lib/rgame/engine/components/mover.rb +285 -0
  173. data/lib/rgame/engine/components/navigator.rb +145 -0
  174. data/lib/rgame/engine/components/path_follow.rb +123 -31
  175. data/lib/rgame/engine/components/player_controller.rb +5 -2
  176. data/lib/rgame/engine/components/pool.rb +1 -1
  177. data/lib/rgame/engine/components/screen_wrap.rb +33 -11
  178. data/lib/rgame/engine/components/sprite.rb +22 -6
  179. data/lib/rgame/engine/components/targeting.rb +9 -11
  180. data/lib/rgame/engine/components/thrust_controller.rb +1 -1
  181. data/lib/rgame/engine/components/tile_world.rb +70 -29
  182. data/lib/rgame/engine/components/timer.rb +1 -1
  183. data/lib/rgame/engine/components/velocity.rb +23 -7
  184. data/lib/rgame/engine/components/wander_controller.rb +6 -2
  185. data/lib/rgame/engine/components/world.rb +133 -0
  186. data/lib/rgame/engine/contact_set.rb +74 -0
  187. data/lib/rgame/engine/culling.rb +45 -0
  188. data/lib/rgame/engine/debug_overlay.rb +23 -18
  189. data/lib/rgame/engine/i18n/plural.rb +45 -0
  190. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  191. data/lib/rgame/engine/i18n/template.rb +59 -0
  192. data/lib/rgame/engine/i18n.rb +276 -51
  193. data/lib/rgame/engine/input/action_mapper.rb +76 -22
  194. data/lib/rgame/engine/input/actions.rb +63 -12
  195. data/lib/rgame/engine/input/input_map.rb +196 -0
  196. data/lib/rgame/engine/layout.rb +82 -0
  197. data/lib/rgame/engine/nav_grid.rb +87 -0
  198. data/lib/rgame/engine/node2d.rb +364 -80
  199. data/lib/rgame/engine/path.rb +4 -6
  200. data/lib/rgame/engine/player.rb +69 -0
  201. data/lib/rgame/engine/player_layer.rb +70 -0
  202. data/lib/rgame/engine/players.rb +205 -0
  203. data/lib/rgame/engine/presentation.rb +171 -0
  204. data/lib/rgame/engine/scene/scene_stack.rb +29 -7
  205. data/lib/rgame/engine/sealed_privates.rb +54 -0
  206. data/lib/rgame/engine/spatial_hash.rb +53 -8
  207. data/lib/rgame/engine/text.rb +194 -0
  208. data/lib/rgame/engine/tile_blockers.rb +63 -0
  209. data/lib/rgame/engine/tile_map.rb +2 -3
  210. data/lib/rgame/engine/tile_map_layer.rb +82 -0
  211. data/lib/rgame/engine/tileset.rb +2 -4
  212. data/lib/rgame/engine/timer.rb +2 -2
  213. data/lib/rgame/engine/ui/button.rb +248 -0
  214. data/lib/rgame/engine/ui/column.rb +20 -0
  215. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  216. data/lib/rgame/engine/ui/menu.rb +290 -0
  217. data/lib/rgame/engine/ui/navigation.rb +57 -0
  218. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  219. data/lib/rgame/engine/ui/option_button.rb +163 -0
  220. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  221. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  222. data/lib/rgame/engine/ui/pointing.rb +146 -0
  223. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  224. data/lib/rgame/engine/ui/ring.rb +55 -0
  225. data/lib/rgame/engine/ui/row.rb +21 -0
  226. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  227. data/lib/rgame/engine/ui/stack.rb +58 -0
  228. data/lib/rgame/engine/ui/stepping.rb +93 -0
  229. data/lib/rgame/engine/ui/text_button.rb +59 -0
  230. data/lib/rgame/engine/view.rb +76 -0
  231. data/lib/rgame/engine/viewports.rb +171 -0
  232. data/lib/rgame/engine/world_view.rb +71 -0
  233. data/lib/rgame/engine.rb +43 -25
  234. data/lib/rgame/game.rb +164 -22
  235. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  236. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  237. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  238. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  239. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  240. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  241. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  242. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  243. data/lib/rgame/rubocop/default.yml +39 -0
  244. data/lib/rgame/rubocop/plugin.rb +45 -0
  245. data/lib/rgame/rubocop.rb +11 -0
  246. data/lib/rgame/util/color.rb +20 -24
  247. data/lib/rgame/util/controls.rb +106 -44
  248. data/lib/rgame/util/route_search.rb +27 -0
  249. data/lib/rgame/util/save_file.rb +107 -0
  250. data/lib/rgame/util/solid_grid.rb +37 -0
  251. data/lib/rgame/util/tensor.rb +0 -9
  252. data/lib/rgame/util/tile_sweep.rb +36 -0
  253. data/lib/rgame/util/z.rb +123 -0
  254. data/lib/rgame/util.rb +5 -3
  255. data/lib/rgame/version.rb +1 -1
  256. data/lib/rgame.rb +0 -15
  257. metadata +176 -20
  258. data/lib/rgame/engine/actor.rb +0 -53
  259. data/lib/rgame/engine/body.rb +0 -49
  260. data/lib/rgame/engine/cached_label.rb +0 -33
  261. data/lib/rgame/engine/camera_view.rb +0 -28
  262. data/lib/rgame/engine/input/player_controller.rb +0 -14
  263. data/lib/rgame/engine/matrix.rb +0 -32
  264. data/lib/rgame/engine/resettable.rb +0 -67
  265. data/lib/rgame/engine/tile_collision.rb +0 -78
data/docs/api/images.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Images
2
2
 
3
3
  `RGame::Core::Image` is a picture on the GPU. Loading one decodes a PNG and
4
- uploads it; everything after that — subimages, tiles, whole sprite sheets is a
5
- *view* of that single upload.
4
+ uploads it once. Subimages, tiles and whole sprite sheets are all *views* of that
5
+ single upload.
6
6
 
7
7
  ```ruby
8
8
  require 'rgame/core'
@@ -12,8 +12,12 @@ frame = img.subimage(0, 0, 16, 16)
12
12
  walk = RGame::Core::Image.load_tiles(app, 'hero.png', 16, 16)
13
13
  ```
14
14
 
15
- There is nothing to draw them with yet — the renderer is the next piece of the
16
- engine to land. What works today is loading, slicing and measuring.
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.
17
21
 
18
22
  ## Loading
19
23
 
@@ -23,13 +27,12 @@ image.width # => 64
23
27
  image.height # => 32
24
28
  ```
25
29
 
26
- The `app` argument is required and comes first. A texture lives inside one
27
- OpenGL context, so an image genuinely is an image *of* a window rather than a
28
- free-floating object and saying so is what makes two windows work, and what
29
- lets the image keep its app alive for as long as it needs it.
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.
30
33
 
31
- PNG is the only format. A file that cannot be read or decoded raises
32
- `RGame::Core::Image::LoadError` with the path in the message:
34
+ rgame reads PNG only. A file it cannot read or decode raises
35
+ `RGame::Core::Image::LoadError`, with the path in the message:
33
36
 
34
37
  ```ruby
35
38
  begin
@@ -39,12 +42,11 @@ rescue RGame::Core::Image::LoadError => e
39
42
  end
40
43
  ```
41
44
 
42
- Greyscale and palette PNGs load fine; they are converted to RGBA on the way in,
43
- so there is only ever one pixel format in play.
45
+ Greyscale and palette PNGs load too. The loader converts them to RGBA, so the
46
+ engine handles one pixel format.
44
47
 
45
- **Images are always sampled nearest-neighbour.** There is no setting for it.
46
- The engine exists to draw pixel art, and blurring it on scale-up is never the
47
- intent.
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.
48
50
 
49
51
  ## Slicing: subimages and tiles
50
52
 
@@ -58,25 +60,24 @@ sheet.tiles(16, 16) # => [Image, Image, ...] all eight
58
60
  sheet.each_tile(16, 16) { |t| } # the same, without building the Array
59
61
  ```
60
62
 
61
- `Image.load_tiles(app, path, w, h)` is `new` plus `tiles` in one step, and is
62
- the usual way to open a sprite sheet:
63
+ `Image.load_tiles(app, path, w, h)` combines `new` and `tiles`. It is the usual
64
+ way to open a sprite sheet:
63
65
 
64
66
  ```ruby
65
67
  frames = RGame::Core::Image.load_tiles(app, 'explosion.png', 32, 32)
66
68
  ```
67
69
 
68
- Three things are worth knowing about all of these:
70
+ Three rules apply to all of these methods.
69
71
 
70
- **Nothing is decoded or uploaded twice.** A hundred tiles are a hundred small
71
- Ruby objects over one texture. Slicing a sheet is cheap enough to do at load
72
- time without thinking about it.
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.
73
74
 
74
- **Tiles come back in reading order** left to right, then top to bottom — which
75
- is how sprite-sheet frames are numbered everywhere else.
75
+ **Tiles come back in reading order**: left to right, then top to bottom. Sprite
76
+ sheets number their frames the same way.
76
77
 
77
- **A partial tile at the right or bottom edge is not a tile.** A 70-pixel-wide
78
- sheet sliced into 16s yields four columns and leaves six pixels of padding
79
- alone, because half a sprite is never what was meant.
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.
80
81
 
81
82
  ### Coordinates are relative to what you cut from
82
83
 
@@ -87,24 +88,23 @@ row = sheet.subimage(0, 16, 64, 16) # the bottom row of the sheet
87
88
  tile = row.subimage(32, 0, 16, 16) # 32 pixels into *the row*, not the sheet
88
89
  ```
89
90
 
90
- A rectangle that does not fit raises `ArgumentError`, and an out-of-range tile
91
- index raises `IndexError`, rather than either returning `nil`:
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`:
92
93
 
93
94
  ```ruby
94
95
  sheet.subimage(0, 0, 999, 999) # ArgumentError: does not fit in a 64x32 image
95
96
  sheet.tile(16, 16, 99) # IndexError: 8 tiles of 16x16
96
97
  ```
97
98
 
98
- A `nil` here would travel a long way into an asset table, out of it three
99
- scenes later before failing as a `NoMethodError` with nothing left pointing at
100
- the coordinates that were wrong.
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.
101
102
 
102
103
  ## Lifetime
103
104
 
104
- You never free an image. The texture is released when the last view of it is
105
- garbage-collected, and the order does not matter: dropping the sheet while its
106
- tiles are still in use keeps the upload alive, and dropping the window first is
107
- also fine.
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
108
 
109
109
  ```ruby
110
110
  sheet = RGame::Core::Image.new(app, 'tiles.png')
@@ -112,7 +112,7 @@ ground = sheet.tile(16, 16, 0)
112
112
  sheet = nil # the upload stays — `ground` is still a view of it
113
113
  ```
114
114
 
115
- That is worth stating because a leaked GPU texture is invisible while it
116
- happens: nothing is slower, nothing looks wrong, and video memory fills up over
117
- an hour of play. `Image.debug_live_textures` reports how many uploads exist, and
118
- is there for tests to assert against; it is not part of the drawing API.
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.
data/docs/api/input.md CHANGED
@@ -1,119 +1,376 @@
1
1
  # Input
2
2
 
3
- Three pieces work together:
3
+ Input has four pieces, in two layers:
4
4
 
5
- - **`RGame::Util::Controls`** the vocabulary: which number means "the left
6
- arrow key", "the A button", "player 2's controller". Plain values, usable
7
- without loading any graphics library.
8
- - **`RGame::Core::Input`** translates your game's *actions* (`:fire`,
9
- `:confirm`) into those ids and asks the app whether they are held.
10
- - **`RGame::Core::Gamepad`** which controllers are plugged in, for menus.
5
+ - **`RGame::Util::Controls`** is the vocabulary. It says which number means "the
6
+ left arrow key", "the A button" or "player 2's controller". These are plain
7
+ values, usable without any graphics library.
8
+ - **`RGame::Core::Input`** is the raw query: is *this id* active on *this
9
+ device*?
10
+ - **`RGame::Engine::InputMap`** says what those ids *mean*. Each player has one
11
+ table that maps the game's actions onto physical ids.
12
+ - **`RGame::Engine::ActionMapper`** polls one player's device through their map
13
+ once per tick. It produces an `Actions` snapshot.
11
14
 
12
- There is no mouse support, by design.
15
+ **`RGame::Core::Gamepad`** adds a readout of the plugged-in controllers, for
16
+ menus.
13
17
 
14
- ## `RGame::Core::Input`
18
+ rgame has no mouse support, by design.
19
+
20
+ ## Which layer do I want?
21
+
22
+ **Use the engine layer.** A game declares its actions, reads
23
+ `actions.held?(:fire)`, and names a scancode only inside its input map. The
24
+ mapper polls `RGame::Core::Input`. Call `Input` directly only when you write
25
+ against `RGame::Core` alone, with no scene graph.
15
26
 
16
27
  ```ruby
17
- input = RGame::Core::Input.new(app)
28
+ require 'rgame/game'
29
+
30
+ Controls = RGame::Util::Controls
18
31
 
19
- input.down?(:fire) # keyboard the single-player default
20
- input.down?(:fire, device: pad) # a specific controller
21
- input.axis(:move_x, device: pad) # => Float
32
+ class MyRoot < RGame::Engine::Node2D; end
33
+
34
+ RGame::Game.new(
35
+ root: MyRoot.new,
36
+ input_map: RGame::Engine::InputMap.new(
37
+ fire: { buttons: [Controls::KEY_SPACE, Controls::PAD_A] }
38
+ )
39
+ ).start
22
40
  ```
23
41
 
24
- `down?` and `axis` read a snapshot the engine takes **once per frame**, when it
25
- pumps events. That is what makes them safe to call from `update`: a frame can
26
- run several simulation ticks, and every tick sees the same answer. Reading
27
- hardware directly would make a held key behave differently depending on how
28
- slow the previous frame was.
42
+ ## `RGame::Engine::InputMap`
43
+
44
+ An `InputMap` holds one entry per action and names physical ids directly. **A
45
+ rebinding screen edits this one table.**
46
+
47
+ ```ruby
48
+ require 'rgame'
49
+
50
+ Controls = RGame::Util::Controls
29
51
 
30
- ### Actions, not keys
52
+ map = RGame::Engine::InputMap.new(
53
+ turn: { axis: [Controls::KEY_LEFT, Controls::KEY_RIGHT], stick: Controls::AXIS_LEFT_X },
54
+ thrust: { axis: [Controls::KEY_DOWN, Controls::KEY_UP], stick: Controls::AXIS_TRIGGER_RIGHT },
55
+ fire: { buttons: [Controls::KEY_SPACE, Controls::PAD_A] }
56
+ )
57
+ ```
31
58
 
32
- You ask for `:fire`, not for the space bar. The mapping lives in a binding
33
- table, so the same game code works for a keyboard player and a controller
34
- player:
59
+ An entry uses up to three kinds of source, and may combine them:
35
60
 
36
- | Action | Keyboard | Gamepad |
61
+ | Key | Read with | Meaning |
37
62
  |---|---|---|
38
- | `:left` `:right` `:up` `:down` | arrow keys | dpad |
39
- | `:confirm` | Return | A |
40
- | `:fire` | Space | A |
63
+ | `buttons:` | `held?` / `pressed?` / `released?` | down if **any** listed id is down |
64
+ | `axis:` | `axis` | `[negative_id, positive_id]`, or a list of such pairs — a digital axis from buttons |
65
+ | `stick:` | `axis` | an analog axis id, for a real stick or a trigger |
41
66
 
42
- Asking for an action nothing is bound to raises `KeyError`.
67
+ `map[action]` returns an entry as an `InputMap::Binding`: a frozen Struct with
68
+ `buttons`, `pairs` and `stick`. `pairs` is always a list of pairs, even when the
69
+ entry gave one, and a source the entry does not use is `nil`.
43
70
 
44
- ### Devices
71
+ A list of pairs binds several controls to one axis. The default `move_x` uses
72
+ this for the arrows, WASD and the d-pad:
73
+
74
+ ```ruby
75
+ move_x: { axis: [[Controls::KEY_LEFT, Controls::KEY_RIGHT],
76
+ [Controls::PAD_DPAD_LEFT, Controls::PAD_DPAD_RIGHT]],
77
+ stick: Controls::AXIS_LEFT_X }
78
+ ```
79
+
80
+ When an action binds several axis sources, **the largest deflection wins**. No
81
+ per-device branching is needed. A keyboard reads `0.0` for every stick, and a
82
+ gamepad reads `false` for every key. The source for the other device contributes
83
+ nothing.
45
84
 
46
- Device 0 is the keyboard, and it is the default — so single-player code never
47
- mentions devices at all. Controllers follow, one per player slot:
85
+ ### One table serves every device
86
+
87
+ An entry can list a key and a pad button together, because **a device answers
88
+ only for its own kind of input**. A gamepad asked about a keyboard scancode
89
+ answers `false`; it never passes on the keyboard's state. So `fire` can be
90
+ "Space or A", and each player's device uses the half that applies to it.
91
+
92
+ ### Prompts need the device's half
93
+
94
+ Reading an action needs no branch, but **showing** one does. A prompt saying
95
+ "press Space or A" tells players about hardware they are not holding.
48
96
 
49
97
  ```ruby
50
- Controls = RGame::Util::Controls
98
+ map.button_for(:fire, Controls::KEYBOARD) # => KEY_SPACE
99
+ map.button_for(:fire, Controls.gamepad(0)) # => PAD_A
100
+ ```
51
101
 
52
- Controls::KEYBOARD # => 0
53
- Controls.gamepad(0) # the first controller
54
- Controls.gamepad(1) # the second
55
- Controls::MAX_GAMEPADS # how many slots exist
102
+ `button_for(action, device)` returns the first id bound to `action` that
103
+ `device` can press. It compares `Controls.pad_button?(id)` against
104
+ `Controls.gamepad?(device)`; the two id spaces never overlap. The first match
105
+ wins, so an entry's order is a prompt's preference. `ui_confirm` lists Return
106
+ before Space, so its prompt says Return.
107
+
108
+ `button_for` returns `nil` in three cases:
109
+
110
+ - nobody bound the action;
111
+ - the action has no buttons. A stick or digital axis is not a button, and needs
112
+ a different picture;
113
+ - the entry has nothing for that kind of device.
114
+
115
+ It allocates nothing, so a HUD may call it every frame instead of caching a
116
+ string.
117
+
118
+ `examples/input_glyphs` shows the whole idea. It draws three prompts from a glyph
119
+ sheet keyed by button id. A seat moves between the keyboard and a controller
120
+ while you watch.
121
+
122
+ ### A stick's sign is the device's
123
+
124
+ `AXIS_LEFT_Y` is positive **downwards**, like screen coordinates. An action that
125
+ wants the opposite, such as "thrust" or "climb", negates at the call site or
126
+ binds a trigger. The map stays declarative, with no inversion flag for every
127
+ reader to check.
128
+
129
+ ### The universal UI set
130
+
131
+ **Every map merges over a universal UI set**, so these actions exist whether a
132
+ game declares them or not:
133
+
134
+ - `ui_up`, `ui_down`, `ui_left`, `ui_right`, `ui_confirm`, `ui_cancel` are
135
+ buttons.
136
+ - `ui_radial_x` and `ui_radial_y` are axes on the left stick, the arrow keys and
137
+ the d-pad. A menu built with [`Pointing`](ui.md#pointing) reads them.
138
+
139
+ Keyboard navigation and menus need these actions for **every** player. The `ui_`
140
+ prefix leaves `:up` free for the game. To change a binding, declare it:
141
+
142
+ ```ruby
143
+ RGame::Engine::InputMap.new(ui_confirm: { buttons: [Controls::PAD_X] })
144
+ ```
145
+
146
+ By default the radial axes share the left stick with `move_x` and `move_y`. They
147
+ remain separate actions. A game that walks on the left stick can move its wheel
148
+ to the right stick without touching movement.
149
+
150
+ `ui_cancel` is Escape. That is why `RGame::Game` quits on `F2`: players expect
151
+ Escape to back out of a menu.
152
+
153
+ ### Defaults and rebinding
154
+
155
+ `InputMap.default` is the UI set plus eight-way movement and `fire`. `move_x` and
156
+ `move_y` sit on the arrows, WASD, the d-pad and the left stick. A game that wants
157
+ exactly this passes no `input_map:`.
158
+
159
+ `#merge` returns a copy with some actions replaced. A config screen uses it to
160
+ rebind one action without restating the rest:
161
+
162
+ ```ruby
163
+ map = RGame::Engine::InputMap.default.merge(fire: { buttons: [Controls::KEY_RETURN] })
56
164
  ```
57
165
 
58
- A device only answers for its own kind of input. Asking a gamepad about a
59
- keyboard key is `false`, never the keyboard's answer otherwise player two's
60
- pad would echo player one. The keyboard has no axes, so `axis` on it is `0.0`.
166
+ **A malformed entry raises at construction.** That covers an unknown source key,
167
+ an entry with no source, an empty button list, and an axis that is not a pair.
168
+ Otherwise the action would read as "never pressed" for the rest of the program.
169
+ Someone would discover it as a frame where nothing moves.
61
170
 
62
- ### Rebinding
171
+ ## `RGame::Engine::ActionMapper`
63
172
 
64
- A binding table is just a Hash of action to id, and the ids are ordinary values
65
- from `Util`, so a game can build its own and hand it over:
173
+ Each player has one `ActionMapper`. It polls that player's device through their
174
+ map and returns the `Actions` snapshot game logic reads.
66
175
 
67
176
  ```ruby
68
- controls = RGame::Util::Controls
69
- bindings = controls::DEFAULT_KEYBOARD.merge(fire: controls::KEY_RETURN)
177
+ mapper = RGame::Engine::ActionMapper.new(map, device: Controls.gamepad(0))
178
+ actions = mapper.poll(input)
70
179
 
71
- input = RGame::Core::Input.new(app, bindings: bindings)
180
+ actions.held?(:fire) # is it down now
181
+ actions.pressed?(:fire) # did it go down this tick
182
+ actions.released?(:fire) # did it come up this tick
183
+ actions.axis(:turn) # -1.0..1.0
72
184
  ```
73
185
 
74
- `Input.new` accepts three optional tables:
186
+ **Asking about an undeclared action raises `KeyError`**, naming the action and
187
+ listing the declared ones. A mistyped name fails on the first tick instead of
188
+ reading as "never pressed".
75
189
 
76
- | Keyword | Default | Used for |
190
+ **The device lets two players share one map.** Every query carries the device,
191
+ so two mappers over the *same* map read two different controllers. Each mapper
192
+ keeps its own previous-tick state, so their edge queries stay independent.
193
+ Reassign `mapper.device` to follow a hot-plug.
194
+
195
+ `dead_zone:` (default `0.15`) ignores a resting stick, which reports small
196
+ non-zero values. It **rescales** the range instead of cutting it off, so a stick
197
+ leaving the dead zone ramps up from zero.
198
+
199
+ `RGame::Game` builds the mappers and polls them once per tick. A game normally
200
+ sees only the `Actions` passed to `control`.
201
+
202
+ ## Players, seats and joining
203
+
204
+ `RGame::Engine::Players` is a root-scoped system that knows who is playing. Each
205
+ `RGame::Engine::Player` owns a device, an `InputMap`, a camera and a UI root.
206
+ Players share the game's action *names* but not the buttons behind them.
207
+
208
+ ```ruby
209
+ RGame::Game.new(root: MyRoot.new, players: 2)
210
+ ```
211
+
212
+ `players:` sets how many **seats** the game has, which is the most people who
213
+ can play. Player 0 starts on `Game`'s `device:`, the keyboard by default; the
214
+ other seats start empty. An empty
215
+ seat draws no viewport. A two-seat game with one player looks like an ordinary
216
+ full-screen game.
217
+
218
+ `player.active?` is `false` while that seat is empty. `players.each_active` yields
219
+ only the seated players, and `players.active_count` counts them.
220
+
221
+ ### A device is seated when someone uses it
222
+
223
+ **Plugging a controller in seats nobody.** A plug says something about hardware.
224
+ Seating a player creates a camera, a viewport and a screen split. That needs a
225
+ statement of intent: a **`ui_confirm` press** on the device.
226
+
227
+ Joining waits for one action, not for any input, so a stick resting off centre
228
+ never seats a player. It reacts to the press edge, not to a held button, so one
229
+ press does one thing. `Players` reads the press through the map of the player who
230
+ would receive the device. Rebinding `ui_confirm` therefore rebinds "press to
231
+ join".
232
+
233
+ ```ruby
234
+ players = node.system(RGame::Engine::Players)
235
+
236
+ players.on_unassigned_input = :join # :join | :takeover | :ignore
237
+ players.accepting_joins = false # temporarily refuse
238
+ players.on_joined { |player| spawn(player) }
239
+ ```
240
+
241
+ | Policy | A press on a device nobody holds | Default when |
77
242
  |---|---|---|
78
- | `bindings:` | `Controls::DEFAULT_KEYBOARD` | keyboard buttons |
79
- | `pad_bindings:` | `Controls::DEFAULT_PAD` | controller buttons |
80
- | `axis_bindings:` | `Controls::DEFAULT_AXES` | analog axes |
243
+ | `:join` | fills the next free seat | there is more than one seat |
244
+ | `:takeover` | becomes the **primary** player's device | there is one seat |
245
+ | `:ignore` | nothing; the game calls `players.seat(device)` itself | |
246
+
247
+ **`:takeover` serves single-player games.** A solo player who picks up a
248
+ controller is not a second person arriving. Their keyboard becomes unassigned,
249
+ and a `ui_confirm` press on it switches back. The last device used wins, in both
250
+ directions. Only `ui_confirm` switches; W does nothing. To switch on any key, set
251
+ `:ignore` and assign `players.primary.device` yourself. If the controller is
252
+ unplugged, the player falls back to the keyboard, so the game keeps responding.
253
+
254
+ **Under `:join` and `:ignore`, unplugging a controller empties its seat.** The
255
+ player's device becomes `nil`, so they draw no viewport until a device is seated
256
+ again. `players.seat(device)` fills the first empty seat and returns that player,
257
+ or `nil` when every seat is taken or joins are refused.
258
+
259
+ `accepting_joins = false` refuses both joins and takeovers. Use it during a
260
+ cutscene or a mid-round lockout.
261
+
262
+ `on_joined` fires with the player who received the device. A scene uses it to
263
+ spawn that player's character without polling. `examples/split_screen` shows the
264
+ whole flow in one file. The game opens full-screen for one player and splits
265
+ when a controller presses A.
81
266
 
82
- The defaults are frozen, so `merge` a copy rather than mutating them.
267
+ ## `RGame::Core::Input`
83
268
 
84
- ## `RGame::Util::Controls`
269
+ `Input` answers the raw query and nothing more.
270
+
271
+ ```ruby
272
+ input = RGame::Core::Input.new(app)
273
+
274
+ input.down?(Controls::KEY_SPACE) # keyboard
275
+ input.down?(Controls::PAD_A, device: Controls.gamepad(0)) # player 1's pad
276
+ input.axis(Controls::AXIS_LEFT_X, device: Controls.gamepad(0))
277
+ ```
278
+
279
+ **`down?` and `axis` read a snapshot the engine takes once per frame**, when it
280
+ pumps events. So they are safe to call from `update`. A frame can run several
281
+ simulation ticks, and every tick sees the same answer. Reading the hardware
282
+ directly would make a held key depend on how slow the previous frame was.
283
+
284
+ Ids are numbers that cross into C, so anything else raises `TypeError`. `Input`
285
+ applies no dead zone; it returns the hardware's answer.
286
+
287
+ ### Devices
288
+
289
+ **Device 0 is the keyboard, and the default**, so single-player code never names
290
+ a device. Controllers follow, one per player slot:
85
291
 
86
- The id vocabulary. Available from `require 'rgame'` **and** from
87
- `require 'rgame/core'`, because these are plain integers with nothing behind
88
- them a game's configuration screen can name a key without pulling in a window.
292
+ ```ruby
293
+ Controls::KEYBOARD # => 0
294
+ Controls.gamepad(0) # the first controller
295
+ Controls.gamepad(1) # the second
296
+ Controls::MAX_GAMEPADS # how many slots exist
297
+ ```
89
298
 
90
- **Keys** `KEY_LEFT`, `KEY_RIGHT`, `KEY_UP`, `KEY_DOWN`, `KEY_RETURN`,
91
- `KEY_SPACE`, `KEY_ESCAPE`, `KEY_F1`.
299
+ A device answers only for its own kind of input. A gamepad asked about a
300
+ keyboard key answers `false`. Otherwise player two's pad would echo player one's
301
+ keys. The keyboard has no axes, so `axis` on it returns `0.0`.
92
302
 
93
- **Gamepad buttons** — `PAD_A`, `PAD_B`, `PAD_X`, `PAD_Y`, `PAD_BACK`,
303
+ ## `RGame::Util::Controls`
304
+
305
+ `Controls` is the id vocabulary. Both `require 'rgame'` **and**
306
+ `require 'rgame/core'` load it. The ids are plain integers, so a configuration
307
+ screen can name a key without opening a window.
308
+
309
+ **Keys**: the 81 keys a Western keyboard reliably has.
310
+
311
+ | | |
312
+ |---|---|
313
+ | Letters | `KEY_A` … `KEY_Z` |
314
+ | Digits | `KEY_1` … `KEY_9`, `KEY_0` |
315
+ | Whitespace and editing | `KEY_RETURN`, `KEY_ESCAPE`, `KEY_BACKSPACE`, `KEY_TAB`, `KEY_SPACE` |
316
+ | Punctuation | `KEY_MINUS`, `KEY_EQUALS`, `KEY_LEFTBRACKET`, `KEY_RIGHTBRACKET`, `KEY_BACKSLASH`, `KEY_SEMICOLON`, `KEY_APOSTROPHE`, `KEY_GRAVE`, `KEY_COMMA`, `KEY_PERIOD`, `KEY_SLASH` |
317
+ | Function row | `KEY_CAPSLOCK`, `KEY_F1` … `KEY_F12` |
318
+ | Navigation | `KEY_INSERT`, `KEY_HOME`, `KEY_PAGEUP`, `KEY_DELETE`, `KEY_END`, `KEY_PAGEDOWN` |
319
+ | Arrows | `KEY_LEFT`, `KEY_RIGHT`, `KEY_UP`, `KEY_DOWN` |
320
+ | Modifiers | `KEY_LCTRL`, `KEY_LSHIFT`, `KEY_LALT`, `KEY_RCTRL`, `KEY_RSHIFT`, `KEY_RALT` |
321
+
322
+ **A scancode is a position, not a letter.** `KEY_A` is the key marked A on a
323
+ QWERTY board and Q on AZERTY. That suits `WASD` movement. A rebinding screen has
324
+ to explain it to players. The engine only compares numbers.
325
+
326
+ **Some keys are left out on purpose**: the numpad (most laptops lack one), the
327
+ GUI key (Windows on a PC, Command on a Mac), the print-screen cluster, and any key
328
+ whose position depends on the layout. Adding a key takes three edits: a
329
+ `#define` in `ext/rgame_core/include/rgame/core.h`, a `_Static_assert` against
330
+ the SDL scancode, and a constant here. `spec/rgame/util/controls_spec.rb` checks
331
+ that all three agree.
332
+
333
+ **Gamepad buttons**: `PAD_A`, `PAD_B`, `PAD_X`, `PAD_Y`, `PAD_BACK`,
94
334
  `PAD_GUIDE`, `PAD_START`, `PAD_LEFT_STICK`, `PAD_RIGHT_STICK`,
95
335
  `PAD_LEFT_SHOULDER`, `PAD_RIGHT_SHOULDER`, `PAD_DPAD_UP`, `PAD_DPAD_DOWN`,
96
336
  `PAD_DPAD_LEFT`, `PAD_DPAD_RIGHT`.
97
337
 
98
- **Axes** `AXIS_LEFT_X`, `AXIS_LEFT_Y`, `AXIS_RIGHT_X`, `AXIS_RIGHT_Y`,
99
- `AXIS_TRIGGER_LEFT`, `AXIS_TRIGGER_RIGHT`. Sticks read −1.0 to 1.0 with **y
100
- positive downwards**; triggers read 0.0 to 1.0. No dead zone is applied — where
101
- to put one is a game decision, and a resting stick genuinely does report small
102
- non-zero values.
338
+ Some buttons exist only on some hardware. They read as never pressed on a pad
339
+ without them: `PAD_MISC1` (share/capture/microphone), `PAD_PADDLE1`
340
+ `PAD_PADDLE4` (Xbox Elite), `PAD_TOUCHPAD` (PS4/PS5).
341
+
342
+ **Axes**: `AXIS_LEFT_X`, `AXIS_LEFT_Y`, `AXIS_RIGHT_X`, `AXIS_RIGHT_Y`,
343
+ `AXIS_TRIGGER_LEFT`, `AXIS_TRIGGER_RIGHT`. Sticks read −1.0 to 1.0, with **y
344
+ positive downwards**. Triggers read 0.0 to 1.0. `Controls` applies no dead zone.
345
+ A resting stick reports small non-zero values, and the game decides where to cut
346
+ them off.
103
347
 
104
- **Devices** `KEYBOARD`, `GAMEPAD_FIRST`, `MAX_GAMEPADS`, and
348
+ **Devices**: `KEYBOARD`, `GAMEPAD_FIRST`, `MAX_GAMEPADS`, and
105
349
  `Controls.gamepad(slot)`.
106
350
 
107
- **Default tables** `DEFAULT_KEYBOARD`, `DEFAULT_PAD`, `DEFAULT_AXES`.
351
+ **This module holds the vocabulary only**, with no binding tables.
352
+ `RGame::Engine::InputMap` says what an id *means*, one map per player.
108
353
 
109
- Buttons and keys share one numbering, partitioned into ranges, so a single
110
- "is it held" query serves every device. You never need the numbers themselves —
111
- use the constants.
354
+ Buttons and keys share one numbering, split into ranges. One "is it held" query
355
+ therefore serves every device. Use the constants; you never need the numbers.
356
+
357
+ A prompt has to know **which side of the split** an id is on, so `Controls` names
358
+ the check:
359
+
360
+ ```ruby
361
+ Controls.gamepad?(device) # a controller slot, or the keyboard?
362
+ Controls.pad_button?(id) # a pad button, or a key?
363
+ ```
364
+
365
+ `BUTTON_GAMEPAD_FIRST` marks the boundary. It is the C engine's own
366
+ `RGAME_BUTTON_GAMEPAD_FIRST`, checked against the header like every other id.
367
+ [`InputMap#button_for`](#prompts-need-the-devices-half) is built from these two
368
+ checks.
112
369
 
113
370
  ## `RGame::Core::Gamepad`
114
371
 
115
- A readout for menus "Player 2: connect a controller". Reading a *button* goes
116
- through `Input`; this answers what is plugged in.
372
+ `Gamepad` tells a menu what is plugged in, for screens like "Player 2: connect a
373
+ controller". Button reads go through `Input`.
117
374
 
118
375
  ```ruby
119
376
  pads = RGame::Core::Gamepad.new(app)
@@ -121,34 +378,36 @@ pads = RGame::Core::Gamepad.new(app)
121
378
  pads.count # how many are connected
122
379
  pads.max_slots # how many slots exist
123
380
  pads.connected?(0) # is slot 0 filled?
124
- pads.name(0) # => "Xbox Controller", or nil
381
+ pads.name(0) # => "Xbox Controller" or nil
125
382
  pads.device(0) # the id Input wants for that slot
126
383
  pads.each_connected { |slot, name| ... } # lowest slot first
127
384
  ```
128
385
 
129
- `device(slot)` is the bridge to `Input`: a menu that has just found a pad can
130
- drive it without knowing how devices are numbered.
386
+ `device(slot)` connects `Gamepad` to `Input`. A menu that finds a pad can drive
387
+ it without knowing how devices are numbered.
131
388
 
132
- Out-of-range slots answer rather than raising, so a UI loop needs no bounds
133
- checks.
389
+ An out-of-range slot returns an answer instead of raising, so a UI loop needs no
390
+ bounds checks.
134
391
 
135
392
  ### Slots are stable across a replug
136
393
 
137
- A controller that falls out and comes back returns to the **same** slot, so
138
- player 2 stays player 2. The engine remembers which device last occupied each
139
- slot; a genuinely new controller takes the lowest free one.
394
+ **A controller that drops out and returns gets the same slot back**, so player 2
395
+ stays player 2. The engine remembers which device last used each slot. A new
396
+ controller takes the lowest free slot.
140
397
 
141
398
  Two identical controllers report the same hardware id, so "the slot that
142
- remembers this controller" is ambiguous for them. The rule resolves it the way
143
- a player expects: two matching pads take slots 0 and 1, and whichever is
144
- unplugged gets its own slot back when it returns.
399
+ remembers this controller" is ambiguous. The engine resolves it the way players
400
+ expect. Two matching pads take slots 0 and 1. Whichever is unplugged gets its own
401
+ slot back when it returns.
145
402
 
146
403
  ## Reacting to hot-plug
147
404
 
148
- Polling with `Gamepad` answers "what is connected now". The `App` hooks tell you
149
- when that changes:
405
+ `Gamepad` answers "what is connected now". The `App` hooks report when that
406
+ changes:
150
407
 
151
408
  ```ruby
409
+ require 'rgame/core'
410
+
152
411
  class MyGame < RGame::Core::App
153
412
  def initialize
154
413
  super(width: 800, height: 600, caption: 'demo')
@@ -170,10 +429,10 @@ class MyGame < RGame::Core::App
170
429
  end
171
430
 
172
431
  def update(_dt)
173
- @moving_left = @input.down?(:left, device: @device)
432
+ @moving_left = @input.down?(RGame::Util::Controls::KEY_LEFT, device: @device)
174
433
  end
175
434
  end
176
435
  ```
177
436
 
178
- A controller unplugged mid-press has its buttons and axes cleared, so a button
179
- held at that moment does not stay stuck down.
437
+ When a controller is unplugged mid-press, the engine clears its buttons and axes.
438
+ A button held at that moment does not stay stuck down.