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/input.md ADDED
@@ -0,0 +1,476 @@
1
+ # Input
2
+
3
+ Input has four pieces, in two layers:
4
+
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
+
15
+ **`RGame::Core::Gamepad`** adds a readout of the plugged-in controllers, for
16
+ menus.
17
+
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.
26
+
27
+ ```ruby
28
+ require 'rgame/game'
29
+
30
+ Controls = RGame::Util::Controls
31
+
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
40
+ ```
41
+
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
51
+
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
+ ```
58
+
59
+ An entry uses up to three kinds of source, and may combine them:
60
+
61
+ | Key | Read with | Meaning |
62
+ |---|---|---|
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 |
66
+
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.
84
+
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.
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.
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] })
164
+ ```
165
+
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.
170
+
171
+ ## `RGame::Engine::ActionMapper`
172
+
173
+ Each player has one `ActionMapper`. It polls that player's device through their
174
+ map and returns the `Actions` snapshot game logic reads.
175
+
176
+ ```ruby
177
+ mapper = RGame::Engine::ActionMapper.new(map, device: Controls.gamepad(0))
178
+ actions = mapper.poll(input)
179
+
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
184
+ ```
185
+
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.
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 |
242
+ |---|---|---|
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.
266
+
267
+ ## `RGame::Core::Input`
268
+
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:
291
+
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
+ ```
298
+
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`.
302
+
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`,
334
+ `PAD_GUIDE`, `PAD_START`, `PAD_LEFT_STICK`, `PAD_RIGHT_STICK`,
335
+ `PAD_LEFT_SHOULDER`, `PAD_RIGHT_SHOULDER`, `PAD_DPAD_UP`, `PAD_DPAD_DOWN`,
336
+ `PAD_DPAD_LEFT`, `PAD_DPAD_RIGHT`.
337
+
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.
347
+
348
+ **Devices**: `KEYBOARD`, `GAMEPAD_FIRST`, `MAX_GAMEPADS`, and
349
+ `Controls.gamepad(slot)`.
350
+
351
+ **This module holds the vocabulary only**, with no binding tables.
352
+ `RGame::Engine::InputMap` says what an id *means*, one map per player.
353
+
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.
369
+
370
+ ## `RGame::Core::Gamepad`
371
+
372
+ `Gamepad` tells a menu what is plugged in, for screens like "Player 2: connect a
373
+ controller". Button reads go through `Input`.
374
+
375
+ ```ruby
376
+ pads = RGame::Core::Gamepad.new(app)
377
+
378
+ pads.count # how many are connected
379
+ pads.max_slots # how many slots exist
380
+ pads.connected?(0) # is slot 0 filled?
381
+ pads.name(0) # => "Xbox Controller" — or nil
382
+ pads.device(0) # the id Input wants for that slot
383
+ pads.each_connected { |slot, name| ... } # lowest slot first
384
+ ```
385
+
386
+ `device(slot)` connects `Gamepad` to `Input`. A menu that finds a pad can drive
387
+ it without knowing how devices are numbered.
388
+
389
+ An out-of-range slot returns an answer instead of raising, so a UI loop needs no
390
+ bounds checks.
391
+
392
+ ### Slots are stable across a replug
393
+
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.
397
+
398
+ Two identical controllers report the same hardware id, so "the slot that
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.
402
+
403
+ ## Reacting to hot-plug
404
+
405
+ `Gamepad` answers "what is connected now". The `App` hooks report when that
406
+ changes:
407
+
408
+ ```ruby
409
+ require 'rgame/core'
410
+
411
+ class MyGame < RGame::Core::App
412
+ def initialize
413
+ super(width: 800, height: 600, caption: 'demo')
414
+ @pads = RGame::Core::Gamepad.new(self)
415
+ @input = RGame::Core::Input.new(self)
416
+ end
417
+
418
+ def gamepad_connected(slot)
419
+ puts "controller in slot #{slot}: #{@pads.name(slot)}"
420
+ end
421
+
422
+ def gamepad_disconnected(slot)
423
+ puts "controller left slot #{slot}"
424
+ end
425
+
426
+ # Read whichever device player one currently has.
427
+ def frame_begin
428
+ @device = @pads.connected?(0) ? @pads.device(0) : RGame::Util::Controls::KEYBOARD
429
+ end
430
+
431
+ def update(_dt)
432
+ @moving_left = @input.down?(RGame::Util::Controls::KEY_LEFT, device: @device)
433
+ end
434
+ end
435
+ ```
436
+
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.
439
+
440
+ ## `RGame::Core::VirtualGamepad`
441
+
442
+ **`VirtualGamepad` plugs a synthetic controller into a running `App`, for tests
443
+ that need the gamepad path with no hardware.** SDL fabricates the device inside
444
+ the process. The `App` seats it and calls `gamepad_connected` on its next frame,
445
+ as it would for a real pad, and `Input` reads its buttons and axes. It serves
446
+ tests, not gameplay; rgame's own Core suite drives its gamepad and hot-plug specs
447
+ through it.
448
+
449
+ ```ruby
450
+ pad = RGame::Core::VirtualGamepad.new # raises RuntimeError unless an App is open
451
+
452
+ pad.set_button(0, true) # button 0, which Input reads as Controls::PAD_A
453
+ pad.set_axis(0, -32_768) # axis 0 is Controls::AXIS_LEFT_X, fully left
454
+ pad.button_down?(0) # the raw button, before controller mapping
455
+ pad.game_controller? # does SDL have a mapping for it?
456
+ pad.attached? # is it still a live device?
457
+ pad.detach # the App calls gamepad_disconnected
458
+
459
+ RGame::Core::VirtualGamepad.pump # apply pad state SDL has not applied yet
460
+ RGame::Core::VirtualGamepad.sdl_error # what SDL last said
461
+ ```
462
+
463
+ **Buttons and axes take SDL's own numbers.** A button number is a `Controls` pad
464
+ id minus `Controls::BUTTON_GAMEPAD_FIRST`. An axis number is the `Controls` axis
465
+ id itself. `set_axis` takes -32768 to 32767 and raises `RangeError` outside it.
466
+
467
+ `set_button` and `set_axis` return true when SDL accepts the change. SDL may
468
+ still apply a change later: outside an `App`'s frame loop a press reads back
469
+ only after one `VirtualGamepad.pump`. A test that must see a press land checks
470
+ `button_down?` and pumps until it does.
471
+
472
+ `detach` does nothing when called a second time. A pad that is never detached
473
+ stays plugged in until the last `App` is destroyed, because collecting the
474
+ object does not unplug it. SDL shuts down with the last `App` and takes every
475
+ virtual pad with it. After that every method except `detach` raises
476
+ `RuntimeError`.