rgame 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +135 -1
  3. data/README.md +67 -65
  4. data/docs/api/README.md +86 -82
  5. data/docs/api/app.md +125 -68
  6. data/docs/api/assets.md +203 -165
  7. data/docs/api/audio.md +130 -89
  8. data/docs/api/cli.md +259 -0
  9. data/docs/api/components.md +1032 -265
  10. data/docs/api/drawing.md +160 -160
  11. data/docs/api/examples.md +263 -0
  12. data/docs/api/game.md +149 -50
  13. data/docs/api/images.md +39 -39
  14. data/docs/api/input.md +226 -148
  15. data/docs/api/internals.md +241 -62
  16. data/docs/api/localization.md +285 -0
  17. data/docs/api/scene_graph.md +397 -244
  18. data/docs/api/signals.md +81 -76
  19. data/docs/api/systems.md +186 -73
  20. data/docs/api/text.md +58 -41
  21. data/docs/api/tile_maps.md +237 -0
  22. data/docs/api/toolbox.md +395 -146
  23. data/docs/api/ui.md +883 -54
  24. data/docs/api/values.md +216 -33
  25. data/examples/assets/README.md +322 -0
  26. data/examples/assets/blip.ogg +0 -0
  27. data/examples/assets/glyphs.json +5 -0
  28. data/examples/assets/glyphs.png +0 -0
  29. data/examples/assets/hero.json +12 -0
  30. data/examples/assets/hero.png +0 -0
  31. data/examples/assets/icons.json +13 -0
  32. data/examples/assets/icons.png +0 -0
  33. data/examples/assets/music.ogg +0 -0
  34. data/examples/assets/skills.json +10 -0
  35. data/examples/assets/skills.png +0 -0
  36. data/examples/assets/tileset.png +0 -0
  37. data/examples/assets/tileset.tsx +65 -0
  38. data/examples/assets/town.tmx +26 -0
  39. data/examples/assets/ui.json +11 -0
  40. data/examples/assets/ui.png +0 -0
  41. data/examples/collision/locales/en.yml +8 -0
  42. data/examples/collision/main.rb +316 -0
  43. data/examples/collision_tiles/locales/en.yml +9 -0
  44. data/examples/collision_tiles/main.rb +274 -0
  45. data/examples/fullscreen/locales/en.yml +10 -0
  46. data/examples/fullscreen/main.rb +216 -0
  47. data/examples/game_menu/locales/en.yml +8 -0
  48. data/examples/game_menu/main.rb +170 -0
  49. data/examples/input_glyphs/locales/en.yml +14 -0
  50. data/examples/input_glyphs/main.rb +213 -0
  51. data/examples/jump_topdown/locales/en.yml +9 -0
  52. data/examples/jump_topdown/main.rb +178 -0
  53. data/examples/localization/locales/de.yml +12 -0
  54. data/examples/localization/locales/en.yml +13 -0
  55. data/examples/localization/main.rb +158 -0
  56. data/examples/menu_navigation/locales/en.yml +23 -0
  57. data/examples/menu_navigation/main.rb +365 -0
  58. data/examples/music/locales/en.yml +7 -0
  59. data/examples/music/main.rb +134 -0
  60. data/examples/pathfinding/locales/en.yml +17 -0
  61. data/examples/pathfinding/main.rb +298 -0
  62. data/examples/pooling/locales/en.yml +7 -0
  63. data/examples/pooling/main.rb +259 -0
  64. data/examples/quick_wheel/locales/en.yml +16 -0
  65. data/examples/quick_wheel/main.rb +184 -0
  66. data/examples/radial_menu/locales/en.yml +16 -0
  67. data/examples/radial_menu/main.rb +184 -0
  68. data/examples/save_load/locales/en.yml +11 -0
  69. data/examples/save_load/main.rb +207 -0
  70. data/examples/save_load_ids/locales/en.yml +11 -0
  71. data/examples/save_load_ids/main.rb +322 -0
  72. data/examples/scroll_map/locales/en.yml +4 -0
  73. data/examples/scroll_map/main.rb +140 -0
  74. data/examples/signals/locales/en.yml +6 -0
  75. data/examples/signals/main.rb +278 -0
  76. data/examples/skill_bar/locales/en.yml +14 -0
  77. data/examples/skill_bar/main.rb +159 -0
  78. data/examples/sound/locales/en.yml +6 -0
  79. data/examples/sound/main.rb +122 -0
  80. data/examples/split_screen/locales/en.yml +9 -0
  81. data/examples/split_screen/main.rb +304 -0
  82. data/examples/sprite/locales/en.yml +8 -0
  83. data/examples/sprite/main.rb +180 -0
  84. data/examples/timer/locales/en.yml +12 -0
  85. data/examples/timer/main.rb +273 -0
  86. data/examples/velocity/locales/en.yml +6 -0
  87. data/examples/velocity/main.rb +196 -0
  88. data/examples/walk/locales/en.yml +4 -0
  89. data/examples/walk/main.rb +99 -0
  90. data/exe/rgame +9 -0
  91. data/ext/rgame_core/app/app.c +33 -3
  92. data/ext/rgame_core/app/locale.c +67 -0
  93. data/ext/rgame_core/app/locale.h +28 -0
  94. data/ext/rgame_core/audio/audio.c +39 -2
  95. data/ext/rgame_core/example.rb +0 -49
  96. data/ext/rgame_core/extconf.rb +0 -125
  97. data/ext/rgame_core/include/rgame/core.h +38 -2
  98. data/ext/rgame_core/ruby/audio_ext.c +10 -5
  99. data/ext/rgame_core/ruby/core_ext.c +30 -7
  100. data/ext/rgame_core/ruby/core_ext.h +3 -0
  101. data/ext/rgame_core/ruby/locale_ext.c +44 -0
  102. data/ext/rgame_core/ruby/recording_ext.c +1 -1
  103. data/ext/rgame_core/ruby/renderer_ext.c +20 -20
  104. data/ext/rgame_util/extconf.rb +2 -20
  105. data/ext/rgame_util/route_search.c +305 -0
  106. data/ext/rgame_util/route_search.h +86 -0
  107. data/ext/rgame_util/route_search_ext.c +150 -0
  108. data/ext/rgame_util/solid_grid.c +58 -0
  109. data/ext/rgame_util/solid_grid.h +49 -0
  110. data/ext/rgame_util/solid_grid_ext.c +161 -0
  111. data/ext/rgame_util/tile_sweep.c +164 -0
  112. data/ext/rgame_util/tile_sweep.h +62 -0
  113. data/ext/rgame_util/tile_sweep_ext.c +155 -0
  114. data/ext/rgame_util/util_ext.c +3 -0
  115. data/ext/rgame_util/util_ext.h +15 -0
  116. data/lib/rgame/boot.rb +0 -10
  117. data/lib/rgame/cli/new_project.rb +139 -0
  118. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  119. data/lib/rgame/cli/templates/README.md.tt +93 -0
  120. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  121. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  122. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  123. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  124. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  125. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  126. data/lib/rgame/cli/templates/rspec.tt +2 -0
  127. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  128. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  129. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  130. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  131. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  132. data/lib/rgame/cli.rb +66 -0
  133. data/lib/rgame/core/app.rb +6 -44
  134. data/lib/rgame/core/asset_manager.rb +13 -31
  135. data/lib/rgame/core/audio.rb +37 -16
  136. data/lib/rgame/core/font.rb +0 -3
  137. data/lib/rgame/core/locale.rb +22 -0
  138. data/lib/rgame/core/nine_slice.rb +0 -21
  139. data/lib/rgame/core/renderer.rb +6 -63
  140. data/lib/rgame/core/sprite_sheet.rb +0 -3
  141. data/lib/rgame/core/tile_map_renderer.rb +2 -19
  142. data/lib/rgame/core/ui_atlas.rb +28 -13
  143. data/lib/rgame/core.rb +1 -8
  144. data/lib/rgame/engine/actor_blockers.rb +131 -0
  145. data/lib/rgame/engine/animation_set.rb +1 -0
  146. data/lib/rgame/engine/audio_director.rb +36 -6
  147. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  148. data/lib/rgame/engine/camera.rb +3 -3
  149. data/lib/rgame/engine/circle_collider.rb +4 -2
  150. data/lib/rgame/engine/collision_box.rb +26 -1
  151. data/lib/rgame/engine/collision_system.rb +110 -22
  152. data/lib/rgame/engine/component.rb +34 -10
  153. data/lib/rgame/engine/components/action_trigger.rb +0 -1
  154. data/lib/rgame/engine/components/animated_sprite.rb +27 -25
  155. data/lib/rgame/engine/components/box_collider.rb +99 -0
  156. data/lib/rgame/engine/components/camera_follow.rb +6 -5
  157. data/lib/rgame/engine/components/character_body.rb +20 -61
  158. data/lib/rgame/engine/components/circle_collider.rb +47 -11
  159. data/lib/rgame/engine/components/collision_world.rb +159 -31
  160. data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
  161. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  162. data/lib/rgame/engine/components/hop.rb +76 -0
  163. data/lib/rgame/engine/components/identity.rb +73 -0
  164. data/lib/rgame/engine/components/mover.rb +285 -0
  165. data/lib/rgame/engine/components/navigator.rb +145 -0
  166. data/lib/rgame/engine/components/path_follow.rb +123 -31
  167. data/lib/rgame/engine/components/player_controller.rb +5 -2
  168. data/lib/rgame/engine/components/pool.rb +1 -1
  169. data/lib/rgame/engine/components/screen_wrap.rb +33 -11
  170. data/lib/rgame/engine/components/sprite.rb +12 -6
  171. data/lib/rgame/engine/components/targeting.rb +9 -11
  172. data/lib/rgame/engine/components/thrust_controller.rb +1 -1
  173. data/lib/rgame/engine/components/tile_world.rb +41 -13
  174. data/lib/rgame/engine/components/timer.rb +1 -1
  175. data/lib/rgame/engine/components/velocity.rb +23 -7
  176. data/lib/rgame/engine/components/wander_controller.rb +6 -2
  177. data/lib/rgame/engine/components/world.rb +133 -0
  178. data/lib/rgame/engine/contact_set.rb +74 -0
  179. data/lib/rgame/engine/culling.rb +4 -6
  180. data/lib/rgame/engine/debug_overlay.rb +5 -11
  181. data/lib/rgame/engine/i18n/plural.rb +45 -0
  182. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  183. data/lib/rgame/engine/i18n/template.rb +59 -0
  184. data/lib/rgame/engine/i18n.rb +276 -51
  185. data/lib/rgame/engine/input/action_mapper.rb +0 -26
  186. data/lib/rgame/engine/input/actions.rb +2 -8
  187. data/lib/rgame/engine/input/input_map.rb +39 -21
  188. data/lib/rgame/engine/nav_grid.rb +87 -0
  189. data/lib/rgame/engine/node2d.rb +248 -133
  190. data/lib/rgame/engine/path.rb +4 -6
  191. data/lib/rgame/engine/players.rb +6 -13
  192. data/lib/rgame/engine/presentation.rb +171 -0
  193. data/lib/rgame/engine/scene/scene_stack.rb +4 -4
  194. data/lib/rgame/engine/sealed_privates.rb +54 -0
  195. data/lib/rgame/engine/spatial_hash.rb +51 -19
  196. data/lib/rgame/engine/text.rb +194 -0
  197. data/lib/rgame/engine/tile_blockers.rb +63 -0
  198. data/lib/rgame/engine/tile_map.rb +2 -3
  199. data/lib/rgame/engine/tile_map_layer.rb +0 -2
  200. data/lib/rgame/engine/tileset.rb +2 -4
  201. data/lib/rgame/engine/timer.rb +2 -2
  202. data/lib/rgame/engine/ui/button.rb +248 -0
  203. data/lib/rgame/engine/ui/column.rb +20 -0
  204. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  205. data/lib/rgame/engine/ui/menu.rb +246 -71
  206. data/lib/rgame/engine/ui/navigation.rb +57 -0
  207. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  208. data/lib/rgame/engine/ui/option_button.rb +163 -0
  209. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  210. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  211. data/lib/rgame/engine/ui/pointing.rb +146 -0
  212. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  213. data/lib/rgame/engine/ui/ring.rb +55 -0
  214. data/lib/rgame/engine/ui/row.rb +21 -0
  215. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  216. data/lib/rgame/engine/ui/stack.rb +58 -0
  217. data/lib/rgame/engine/ui/stepping.rb +93 -0
  218. data/lib/rgame/engine/ui/text_button.rb +59 -0
  219. data/lib/rgame/engine/viewports.rb +2 -5
  220. data/lib/rgame/engine/world_view.rb +5 -4
  221. data/lib/rgame/engine.rb +31 -25
  222. data/lib/rgame/game.rb +99 -27
  223. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  224. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  225. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  226. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  227. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  228. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  229. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  230. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  231. data/lib/rgame/rubocop/default.yml +39 -0
  232. data/lib/rgame/rubocop/plugin.rb +45 -0
  233. data/lib/rgame/rubocop.rb +11 -0
  234. data/lib/rgame/util/color.rb +20 -24
  235. data/lib/rgame/util/controls.rb +14 -28
  236. data/lib/rgame/util/route_search.rb +27 -0
  237. data/lib/rgame/util/save_file.rb +107 -0
  238. data/lib/rgame/util/solid_grid.rb +37 -0
  239. data/lib/rgame/util/tensor.rb +0 -9
  240. data/lib/rgame/util/tile_sweep.rb +36 -0
  241. data/lib/rgame/util/z.rb +4 -14
  242. data/lib/rgame/util.rb +4 -3
  243. data/lib/rgame/version.rb +1 -1
  244. data/lib/rgame.rb +0 -15
  245. metadata +152 -11
  246. data/lib/rgame/engine/actor.rb +0 -53
  247. data/lib/rgame/engine/body.rb +0 -49
  248. data/lib/rgame/engine/cached_label.rb +0 -33
  249. data/lib/rgame/engine/input/player_controller.rb +0 -14
  250. data/lib/rgame/engine/matrix.rb +0 -32
  251. data/lib/rgame/engine/resettable.rb +0 -67
  252. data/lib/rgame/engine/tile_collision.rb +0 -78
  253. data/lib/rgame/engine/ui/menu_item.rb +0 -84
data/docs/api/input.md CHANGED
@@ -1,44 +1,52 @@
1
1
  # Input
2
2
 
3
- Four pieces, in two layers:
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`** the raw query: is *this id* active on *this
9
- device*.
10
- - **`RGame::Engine::InputMap`** what those ids *mean*: one table per player,
11
- mapping a game's actions onto physical ids.
12
- - **`RGame::Engine::ActionMapper`** polls one player's device through their
13
- map once per tick and produces an `Actions` snapshot.
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.
14
14
 
15
- Plus **`RGame::Core::Gamepad`**, a readout of which controllers are plugged in,
16
- for menus.
15
+ **`RGame::Core::Gamepad`** adds a readout of the plugged-in controllers, for
16
+ menus.
17
17
 
18
- There is no mouse support, by design.
18
+ rgame has no mouse support, by design.
19
19
 
20
20
  ## Which layer do I want?
21
21
 
22
- Almost always the engine layer. A game declares its actions, reads
23
- `actions.held?(:fire)`, and never names a scancode outside its input map.
24
- `RGame::Core::Input` is what the mapper polls; you reach for it directly only
25
- when writing against `RGame::Core` alone, with no scene graph.
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.
26
26
 
27
27
  ```ruby
28
+ require 'rgame/game'
29
+
30
+ Controls = RGame::Util::Controls
31
+
32
+ class MyRoot < RGame::Engine::Node2D; end
33
+
28
34
  RGame::Game.new(
29
35
  root: MyRoot.new,
30
36
  input_map: RGame::Engine::InputMap.new(
31
37
  fire: { buttons: [Controls::KEY_SPACE, Controls::PAD_A] }
32
38
  )
33
- )
39
+ ).start
34
40
  ```
35
41
 
36
42
  ## `RGame::Engine::InputMap`
37
43
 
38
- One entry per action, naming physical ids directly. **This is the single table a
39
- rebinding screen edits.**
44
+ An `InputMap` holds one entry per action and names physical ids directly. **A
45
+ rebinding screen edits this one table.**
40
46
 
41
47
  ```ruby
48
+ require 'rgame'
49
+
42
50
  Controls = RGame::Util::Controls
43
51
 
44
52
  map = RGame::Engine::InputMap.new(
@@ -48,73 +56,122 @@ map = RGame::Engine::InputMap.new(
48
56
  )
49
57
  ```
50
58
 
51
- Three kinds of source, and one action may combine them:
59
+ An entry uses up to three kinds of source, and may combine them:
52
60
 
53
61
  | Key | Read with | Meaning |
54
62
  |---|---|---|
55
63
  | `buttons:` | `held?` / `pressed?` / `released?` | down if **any** listed id is down |
56
- | `axis:` | `axis` | `[negative_id, positive_id]` — a digital axis from two buttons |
64
+ | `axis:` | `axis` | `[negative_id, positive_id]`, or a list of such pairs — a digital axis from buttons |
57
65
  | `stick:` | `axis` | an analog axis id, for a real stick or a trigger |
58
66
 
59
- When an action binds both `axis:` and `stick:`, **the larger deflection wins**.
60
- That needs no per-device branching: a keyboard reads `0.0` for every axis and a
61
- stick reads `false` for every key, so whichever device a player is on, the other
62
- source contributes nothing.
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`.
70
+
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.
63
84
 
64
85
  ### One table serves every device
65
86
 
66
- Listing a key and a pad button in the same entry is safe, because **a device
67
- only answers for its own kind of input** asking a gamepad about a keyboard
68
- scancode is `false`, never the keyboard's answer. So `fire` can be "Space or A",
69
- and each player's device picks out the half that applies to it.
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.
96
+
97
+ ```ruby
98
+ map.button_for(:fire, Controls::KEYBOARD) # => KEY_SPACE
99
+ map.button_for(:fire, Controls.gamepad(0)) # => PAD_A
100
+ ```
101
+
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.
70
121
 
71
122
  ### A stick's sign is the device's
72
123
 
73
124
  `AXIS_LEFT_Y` is positive **downwards**, like screen coordinates. An action that
74
- wants the opposite ("thrust", "climb") negates at the call site or binds a
75
- trigger instead the map stays declarative rather than growing an inversion
76
- flag every reader would have to check for.
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.
77
128
 
78
129
  ### The universal UI set
79
130
 
80
- Every map is merged over a universal set, so these exist whether or not a game
81
- declares them:
131
+ **Every map merges over a universal UI set**, so these actions exist whether a
132
+ game declares them or not:
82
133
 
83
- `ui_up`, `ui_down`, `ui_left`, `ui_right`, `ui_confirm`, `ui_cancel`
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.
84
138
 
85
- Keyboard navigation and menus rely on them being there for **every** player. They
86
- are prefixed so a game is free to use `:up` for something of its own, and a game
87
- that wants different bindings just declares one:
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:
88
141
 
89
142
  ```ruby
90
- InputMap.new(ui_confirm: { buttons: [Controls::PAD_X] })
143
+ RGame::Engine::InputMap.new(ui_confirm: { buttons: [Controls::PAD_X] })
91
144
  ```
92
145
 
93
- `ui_cancel` is Escape which is why `RGame::Game`'s quit key is `F2`. The button
94
- a player expects to back out of a menu belongs to the menu.
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.
95
152
 
96
153
  ### Defaults and rebinding
97
154
 
98
- `InputMap.default` is the UI set plus eight-way movement (`move_x`, `move_y`) on
99
- the arrows or the left stick, and `fire`. A game wanting exactly that passes no
100
- `input_map:` at all.
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:`.
101
158
 
102
- `#merge` returns a copy with some actions replaced, which is how a config screen
103
- rebinds one without restating the rest:
159
+ `#merge` returns a copy with some actions replaced. A config screen uses it to
160
+ rebind one action without restating the rest:
104
161
 
105
162
  ```ruby
106
163
  map = RGame::Engine::InputMap.default.merge(fire: { buttons: [Controls::KEY_RETURN] })
107
164
  ```
108
165
 
109
- A malformed entry raises at construction an unknown source key, an entry with
110
- no source, an empty button list, an axis that is not a pair. That is deliberate:
111
- the alternative is an action that reads as "never pressed" for the rest of the
112
- program, discovered as a frame nobody can move in.
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.
113
170
 
114
171
  ## `RGame::Engine::ActionMapper`
115
172
 
116
- One per player. It polls that player's device through their map and returns the
117
- `Actions` snapshot game logic reads.
173
+ Each player has one `ActionMapper`. It polls that player's device through their
174
+ map and returns the `Actions` snapshot game logic reads.
118
175
 
119
176
  ```ruby
120
177
  mapper = RGame::Engine::ActionMapper.new(map, device: Controls.gamepad(0))
@@ -126,43 +183,52 @@ actions.released?(:fire) # did it come up this tick
126
183
  actions.axis(:turn) # -1.0..1.0
127
184
  ```
128
185
 
129
- **The device is what makes two players work.** Every query carries it, so two
130
- mappers over the *same* map read two different controllers, and each keeps its
131
- own previous-frame state so their edge queries are independent. Reassign
132
- `mapper.device` to follow a hot-plug.
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".
189
+
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.
133
194
 
134
- `dead_zone:` (default `0.15`) ignores a resting stick, which genuinely reports
135
- small non-zero values. It **rescales** rather than merely cutting off, so a stick
136
- leaving the dead zone ramps from zero instead of jumping to `0.15`.
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.
137
198
 
138
- `RGame::Game` builds one of these for you and polls it once per tick; a game
139
- normally sees only the `Actions` handed to `control`.
199
+ `RGame::Game` builds the mappers and polls them once per tick. A game normally
200
+ sees only the `Actions` passed to `control`.
140
201
 
141
202
  ## Players, seats and joining
142
203
 
143
- `RGame::Engine::Players` is a root-scoped system holding who is playing. Each
144
- `RGame::Engine::Player` owns a device, an `InputMap`, a camera and a UI root
145
- the action *names* are the game's and shared, the buttons behind them are not.
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.
146
207
 
147
208
  ```ruby
148
209
  RGame::Game.new(root: MyRoot.new, players: 2)
149
210
  ```
150
211
 
151
- `players:` is how many **seats** the game has, and therefore the most people who
152
- can play it. Player 0 starts on the keyboard; the rest start empty. An empty
153
- seat draws no viewport, so a two-seat game with nobody in the second one is an
154
- ordinary full-screen single-player game.
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.
155
220
 
156
221
  ### A device is seated when someone uses it
157
222
 
158
- Not when it is plugged in. Plugging a controller in says something about
159
- hardware; seating a player creates a camera, a viewport and a screen split, and
160
- that follows a statement of intent a **`ui_confirm` press** on the device.
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.
161
226
 
162
- One action rather than "any input", because a stick resting slightly off centre
163
- must never seat a player. An edge rather than held, so one press does one thing.
164
- It is read through the map of whoever would receive the device, so rebinding
165
- `ui_confirm` rebinds "press to join" with it.
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".
166
232
 
167
233
  ```ruby
168
234
  players = node.system(RGame::Engine::Players)
@@ -178,21 +244,29 @@ players.on_joined { |player| spawn(player) }
178
244
  | `:takeover` | becomes the **primary** player's device | there is one seat |
179
245
  | `:ignore` | nothing; the game calls `players.seat(device)` itself | — |
180
246
 
181
- `:takeover` is single-player's answer: one person already playing who picks up a
247
+ **`:takeover` serves single-player games.** A solo player who picks up a
182
248
  controller is not a second person arriving. Their keyboard becomes unassigned,
183
- so using it again takes them back last device used wins. And if their
184
- controller is unplugged they fall back to the keyboard rather than the game
185
- going dead in their hands.
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.
186
258
 
187
- `accepting_joins = false` refuses both, which is what a cutscene or a mid-round
188
- lockout wants.
259
+ `accepting_joins = false` refuses both joins and takeovers. Use it during a
260
+ cutscene or a mid-round lockout.
189
261
 
190
- `on_joined` fires with the player who got the device, which is how a scene
191
- spawns their character without polling for one. See `examples/15_tiled_world`.
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.
192
266
 
193
267
  ## `RGame::Core::Input`
194
268
 
195
- The raw query, and deliberately nothing more.
269
+ `Input` answers the raw query and nothing more.
196
270
 
197
271
  ```ruby
198
272
  input = RGame::Core::Input.new(app)
@@ -202,28 +276,18 @@ input.down?(Controls::PAD_A, device: Controls.gamepad(0)) # player 1's pad
202
276
  input.axis(Controls::AXIS_LEFT_X, device: Controls.gamepad(0))
203
277
  ```
204
278
 
205
- `down?` and `axis` read a snapshot the engine takes **once per frame**, when it
206
- pumps events. That is what makes them safe to call from `update`: a frame can run
207
- several simulation ticks, and every tick sees the same answer. Reading hardware
208
- directly would make a held key behave differently depending on how slow the
209
- previous frame was.
210
-
211
- Ids are numbers, and they cross into C, so passing anything else raises
212
- `TypeError`. No dead zone is applied here — this is the hardware's answer.
213
-
214
- ### It used to hold the binding tables
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.
215
283
 
216
- It took `down?(:fire)` and resolved `:fire` through one of three tables passed to
217
- its constructor. Those tables are gone. Binding moved up to `InputMap` for two
218
- reasons: a rebinding screen has to be able to edit the table, and the engine
219
- layer may not name `RGame::Core` at all; and with a player per device, the table
220
- is a per-player value rather than a property of the one object that talks to the
221
- hardware.
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.
222
286
 
223
287
  ### Devices
224
288
 
225
- Device 0 is the keyboard, and it is the default so single-player code never
226
- mentions devices at all. Controllers follow, one per player slot:
289
+ **Device 0 is the keyboard, and the default**, so single-player code never names
290
+ a device. Controllers follow, one per player slot:
227
291
 
228
292
  ```ruby
229
293
  Controls::KEYBOARD # => 0
@@ -232,17 +296,17 @@ Controls.gamepad(1) # the second
232
296
  Controls::MAX_GAMEPADS # how many slots exist
233
297
  ```
234
298
 
235
- A device only answers for its own kind of input. Asking a gamepad about a
236
- keyboard key is `false`, never the keyboard's answer otherwise player two's pad
237
- would echo player one. The keyboard has no axes, so `axis` on it is `0.0`.
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`.
238
302
 
239
303
  ## `RGame::Util::Controls`
240
304
 
241
- The id vocabulary. Available from `require 'rgame'` **and** from
242
- `require 'rgame/core'`, because these are plain integers with nothing behind
243
- them — a game's configuration screen can name a key without pulling in a window.
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.
244
308
 
245
- **Keys** what a Western keyboard can be relied on to have, 81 of them:
309
+ **Keys**: the 81 keys a Western keyboard reliably has.
246
310
 
247
311
  | | |
248
312
  |---|---|
@@ -256,45 +320,57 @@ them — a game's configuration screen can name a key without pulling in a windo
256
320
  | Modifiers | `KEY_LCTRL`, `KEY_LSHIFT`, `KEY_LALT`, `KEY_RCTRL`, `KEY_RSHIFT`, `KEY_RALT` |
257
321
 
258
322
  **A scancode is a position, not a letter.** `KEY_A` is the key marked A on a
259
- QWERTY board and Q on AZERTY which is what you want for `WASD` movement, and
260
- what a rebinding screen has to explain to the player. The engine only ever
261
- compares numbers.
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.
262
325
 
263
- **Deliberately absent**: the numpad (most laptops have none), the GUI key
264
- (Windows on a PC, Command on a Mac), the print-screen cluster, and anything
265
- whose position depends on the layout. Adding one is a `#define` in
266
- `ext/rgame_core/include/rgame/core.h`, a `_Static_assert` against the SDL
267
- scancode, and a constant here — and the spec below checks all three agree.
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.
268
332
 
269
- **Gamepad buttons** `PAD_A`, `PAD_B`, `PAD_X`, `PAD_Y`, `PAD_BACK`,
333
+ **Gamepad buttons**: `PAD_A`, `PAD_B`, `PAD_X`, `PAD_Y`, `PAD_BACK`,
270
334
  `PAD_GUIDE`, `PAD_START`, `PAD_LEFT_STICK`, `PAD_RIGHT_STICK`,
271
335
  `PAD_LEFT_SHOULDER`, `PAD_RIGHT_SHOULDER`, `PAD_DPAD_UP`, `PAD_DPAD_DOWN`,
272
336
  `PAD_DPAD_LEFT`, `PAD_DPAD_RIGHT`.
273
337
 
274
- Plus the ones only some hardware has, which read as never pressed on a pad
338
+ Some buttons exist only on some hardware. They read as never pressed on a pad
275
339
  without them: `PAD_MISC1` (share/capture/microphone), `PAD_PADDLE1` …
276
340
  `PAD_PADDLE4` (Xbox Elite), `PAD_TOUCHPAD` (PS4/PS5).
277
341
 
278
- **Axes** `AXIS_LEFT_X`, `AXIS_LEFT_Y`, `AXIS_RIGHT_X`, `AXIS_RIGHT_Y`,
279
- `AXIS_TRIGGER_LEFT`, `AXIS_TRIGGER_RIGHT`. Sticks read −1.0 to 1.0 with **y
280
- positive downwards**; triggers read 0.0 to 1.0. No dead zone is applied — where
281
- to put one is a game decision, and a resting stick genuinely does report small
282
- non-zero values.
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.
283
347
 
284
- **Devices** `KEYBOARD`, `GAMEPAD_FIRST`, `MAX_GAMEPADS`, and
348
+ **Devices**: `KEYBOARD`, `GAMEPAD_FIRST`, `MAX_GAMEPADS`, and
285
349
  `Controls.gamepad(slot)`.
286
350
 
287
- This module is the **vocabulary only**. It carries no binding tables — what an id
288
- *means* is `RGame::Engine::InputMap`, one per player.
351
+ **This module holds the vocabulary only**, with no binding tables.
352
+ `RGame::Engine::InputMap` says what an id *means*, one map per player.
289
353
 
290
- Buttons and keys share one numbering, partitioned into ranges, so a single
291
- "is it held" query serves every device. You never need the numbers themselves —
292
- 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.
293
369
 
294
370
  ## `RGame::Core::Gamepad`
295
371
 
296
- A readout for menus "Player 2: connect a controller". Reading a *button* goes
297
- 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`.
298
374
 
299
375
  ```ruby
300
376
  pads = RGame::Core::Gamepad.new(app)
@@ -302,34 +378,36 @@ pads = RGame::Core::Gamepad.new(app)
302
378
  pads.count # how many are connected
303
379
  pads.max_slots # how many slots exist
304
380
  pads.connected?(0) # is slot 0 filled?
305
- pads.name(0) # => "Xbox Controller", or nil
381
+ pads.name(0) # => "Xbox Controller" or nil
306
382
  pads.device(0) # the id Input wants for that slot
307
383
  pads.each_connected { |slot, name| ... } # lowest slot first
308
384
  ```
309
385
 
310
- `device(slot)` is the bridge to `Input`: a menu that has just found a pad can
311
- 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.
312
388
 
313
- Out-of-range slots answer rather than raising, so a UI loop needs no bounds
314
- checks.
389
+ An out-of-range slot returns an answer instead of raising, so a UI loop needs no
390
+ bounds checks.
315
391
 
316
392
  ### Slots are stable across a replug
317
393
 
318
- A controller that falls out and comes back returns to the **same** slot, so
319
- player 2 stays player 2. The engine remembers which device last occupied each
320
- 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.
321
397
 
322
398
  Two identical controllers report the same hardware id, so "the slot that
323
- remembers this controller" is ambiguous for them. The rule resolves it the way
324
- a player expects: two matching pads take slots 0 and 1, and whichever is
325
- 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.
326
402
 
327
403
  ## Reacting to hot-plug
328
404
 
329
- Polling with `Gamepad` answers "what is connected now". The `App` hooks tell you
330
- when that changes:
405
+ `Gamepad` answers "what is connected now". The `App` hooks report when that
406
+ changes:
331
407
 
332
408
  ```ruby
409
+ require 'rgame/core'
410
+
333
411
  class MyGame < RGame::Core::App
334
412
  def initialize
335
413
  super(width: 800, height: 600, caption: 'demo')
@@ -356,5 +434,5 @@ class MyGame < RGame::Core::App
356
434
  end
357
435
  ```
358
436
 
359
- A controller unplugged mid-press has its buttons and axes cleared, so a button
360
- 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.