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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +251 -0
- data/LICENSE +26 -0
- data/README.md +325 -0
- data/docs/api/README.md +224 -0
- data/docs/api/app.md +249 -0
- data/docs/api/assets.md +463 -0
- data/docs/api/audio.md +249 -0
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1112 -0
- data/docs/api/drawing.md +398 -0
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +219 -0
- data/docs/api/images.md +118 -0
- data/docs/api/input.md +476 -0
- data/docs/api/internals.md +289 -0
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +539 -0
- data/docs/api/signals.md +147 -0
- data/docs/api/systems.md +231 -0
- data/docs/api/text.md +133 -0
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +493 -0
- data/docs/api/ui.md +927 -0
- data/docs/api/values.md +316 -0
- data/examples/assets/README.md +322 -0
- data/examples/assets/blip.ogg +0 -0
- data/examples/assets/glyphs.json +5 -0
- data/examples/assets/glyphs.png +0 -0
- data/examples/assets/hero.json +12 -0
- data/examples/assets/hero.png +0 -0
- data/examples/assets/icons.json +13 -0
- data/examples/assets/icons.png +0 -0
- data/examples/assets/music.ogg +0 -0
- data/examples/assets/skills.json +10 -0
- data/examples/assets/skills.png +0 -0
- data/examples/assets/tileset.png +0 -0
- data/examples/assets/tileset.tsx +65 -0
- data/examples/assets/town.tmx +26 -0
- data/examples/assets/ui.json +11 -0
- data/examples/assets/ui.png +0 -0
- data/examples/collision/locales/en.yml +8 -0
- data/examples/collision/main.rb +316 -0
- data/examples/collision_tiles/locales/en.yml +9 -0
- data/examples/collision_tiles/main.rb +274 -0
- data/examples/fullscreen/locales/en.yml +10 -0
- data/examples/fullscreen/main.rb +216 -0
- data/examples/game_menu/locales/en.yml +8 -0
- data/examples/game_menu/main.rb +170 -0
- data/examples/input_glyphs/locales/en.yml +14 -0
- data/examples/input_glyphs/main.rb +213 -0
- data/examples/jump_topdown/locales/en.yml +9 -0
- data/examples/jump_topdown/main.rb +178 -0
- data/examples/localization/locales/de.yml +12 -0
- data/examples/localization/locales/en.yml +13 -0
- data/examples/localization/main.rb +158 -0
- data/examples/menu_navigation/locales/en.yml +23 -0
- data/examples/menu_navigation/main.rb +365 -0
- data/examples/music/locales/en.yml +7 -0
- data/examples/music/main.rb +134 -0
- data/examples/pathfinding/locales/en.yml +17 -0
- data/examples/pathfinding/main.rb +298 -0
- data/examples/pooling/locales/en.yml +7 -0
- data/examples/pooling/main.rb +259 -0
- data/examples/quick_wheel/locales/en.yml +16 -0
- data/examples/quick_wheel/main.rb +184 -0
- data/examples/radial_menu/locales/en.yml +16 -0
- data/examples/radial_menu/main.rb +184 -0
- data/examples/save_load/locales/en.yml +11 -0
- data/examples/save_load/main.rb +207 -0
- data/examples/save_load_ids/locales/en.yml +11 -0
- data/examples/save_load_ids/main.rb +322 -0
- data/examples/scroll_map/locales/en.yml +4 -0
- data/examples/scroll_map/main.rb +140 -0
- data/examples/signals/locales/en.yml +6 -0
- data/examples/signals/main.rb +278 -0
- data/examples/skill_bar/locales/en.yml +14 -0
- data/examples/skill_bar/main.rb +159 -0
- data/examples/sound/locales/en.yml +6 -0
- data/examples/sound/main.rb +122 -0
- data/examples/split_screen/locales/en.yml +9 -0
- data/examples/split_screen/main.rb +304 -0
- data/examples/sprite/locales/en.yml +8 -0
- data/examples/sprite/main.rb +180 -0
- data/examples/timer/locales/en.yml +12 -0
- data/examples/timer/main.rb +273 -0
- data/examples/velocity/locales/en.yml +6 -0
- data/examples/velocity/main.rb +196 -0
- data/examples/walk/locales/en.yml +4 -0
- data/examples/walk/main.rb +99 -0
- data/exe/rgame +9 -0
- data/lib/rgame/boot.rb +3 -0
- data/lib/rgame/cli/new_project.rb +139 -0
- data/lib/rgame/cli/templates/Gemfile.tt +23 -0
- data/lib/rgame/cli/templates/README.md.tt +93 -0
- data/lib/rgame/cli/templates/Rakefile.tt +9 -0
- data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
- data/lib/rgame/cli/templates/game.rb.tt +23 -0
- data/lib/rgame/cli/templates/gitignore.tt +12 -0
- data/lib/rgame/cli/templates/main.rb.tt +11 -0
- data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
- data/lib/rgame/cli/templates/rspec.tt +2 -0
- data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
- data/lib/rgame/cli/templates/ruby-version.tt +1 -0
- data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
- data/lib/rgame/cli.rb +66 -0
- data/lib/rgame/core/app.rb +46 -0
- data/lib/rgame/core/asset_manager.rb +206 -0
- data/lib/rgame/core/audio.rb +145 -0
- data/lib/rgame/core/font.rb +46 -0
- data/lib/rgame/core/gamepad.rb +55 -0
- data/lib/rgame/core/image.rb +55 -0
- data/lib/rgame/core/input.rb +71 -0
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +142 -0
- data/lib/rgame/core/recording.rb +54 -0
- data/lib/rgame/core/renderer.rb +354 -0
- data/lib/rgame/core/sprite_sheet.rb +105 -0
- data/lib/rgame/core/tile_map_renderer.rb +172 -0
- data/lib/rgame/core/ui_atlas.rb +101 -0
- data/lib/rgame/core/virtual_gamepad.rb +26 -0
- data/lib/rgame/core.rb +18 -0
- data/lib/rgame/core_ext.so +0 -0
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +50 -0
- data/lib/rgame/engine/animator.rb +44 -0
- data/lib/rgame/engine/audio_bus.rb +24 -0
- data/lib/rgame/engine/audio_director.rb +59 -0
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +78 -0
- data/lib/rgame/engine/circle_collider.rb +34 -0
- data/lib/rgame/engine/collision_box.rb +59 -0
- data/lib/rgame/engine/collision_system.rb +132 -0
- data/lib/rgame/engine/component.rb +64 -0
- data/lib/rgame/engine/components/action_trigger.rb +40 -0
- data/lib/rgame/engine/components/animated_sprite.rb +71 -0
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +45 -0
- data/lib/rgame/engine/components/character_body.rb +50 -0
- data/lib/rgame/engine/components/circle_collider.rb +80 -0
- data/lib/rgame/engine/components/collision_world.rb +231 -0
- data/lib/rgame/engine/components/despawn_offscreen.rb +42 -0
- data/lib/rgame/engine/components/feet_collider.rb +61 -0
- data/lib/rgame/engine/components/hop.rb +76 -0
- data/lib/rgame/engine/components/identity.rb +73 -0
- data/lib/rgame/engine/components/mover.rb +285 -0
- data/lib/rgame/engine/components/navigator.rb +145 -0
- data/lib/rgame/engine/components/path_follow.rb +176 -0
- data/lib/rgame/engine/components/player_controller.rb +27 -0
- data/lib/rgame/engine/components/pool.rb +53 -0
- data/lib/rgame/engine/components/screen_wrap.rb +49 -0
- data/lib/rgame/engine/components/sprite.rb +47 -0
- data/lib/rgame/engine/components/targeting.rb +52 -0
- data/lib/rgame/engine/components/thrust_controller.rb +65 -0
- data/lib/rgame/engine/components/tile_world.rb +109 -0
- data/lib/rgame/engine/components/timer.rb +75 -0
- data/lib/rgame/engine/components/velocity.rb +43 -0
- data/lib/rgame/engine/components/wander_controller.rb +64 -0
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +45 -0
- data/lib/rgame/engine/debug_overlay.rb +111 -0
- data/lib/rgame/engine/i18n/plural.rb +45 -0
- data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
- data/lib/rgame/engine/i18n/template.rb +59 -0
- data/lib/rgame/engine/i18n.rb +322 -0
- data/lib/rgame/engine/input/action_mapper.rb +100 -0
- data/lib/rgame/engine/input/actions.rb +92 -0
- data/lib/rgame/engine/input/input_map.rb +196 -0
- data/lib/rgame/engine/layout.rb +82 -0
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +555 -0
- data/lib/rgame/engine/path.rb +76 -0
- data/lib/rgame/engine/player.rb +69 -0
- data/lib/rgame/engine/player_layer.rb +70 -0
- data/lib/rgame/engine/players.rb +205 -0
- data/lib/rgame/engine/pool.rb +51 -0
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +87 -0
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/signal.rb +75 -0
- data/lib/rgame/engine/spatial_hash.rb +116 -0
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +147 -0
- data/lib/rgame/engine/tile_map_layer.rb +82 -0
- data/lib/rgame/engine/tileset.rb +99 -0
- data/lib/rgame/engine/timer.rb +51 -0
- data/lib/rgame/engine/ui/button.rb +248 -0
- data/lib/rgame/engine/ui/column.rb +20 -0
- data/lib/rgame/engine/ui/icon_button.rb +93 -0
- data/lib/rgame/engine/ui/menu.rb +290 -0
- data/lib/rgame/engine/ui/navigation.rb +57 -0
- data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
- data/lib/rgame/engine/ui/option_button.rb +163 -0
- data/lib/rgame/engine/ui/panel_button.rb +32 -0
- data/lib/rgame/engine/ui/panel_menu.rb +36 -0
- data/lib/rgame/engine/ui/pointing.rb +146 -0
- data/lib/rgame/engine/ui/radial_menu.rb +85 -0
- data/lib/rgame/engine/ui/ring.rb +55 -0
- data/lib/rgame/engine/ui/row.rb +21 -0
- data/lib/rgame/engine/ui/shape_style.rb +102 -0
- data/lib/rgame/engine/ui/stack.rb +58 -0
- data/lib/rgame/engine/ui/stepping.rb +93 -0
- data/lib/rgame/engine/ui/text_button.rb +59 -0
- data/lib/rgame/engine/view.rb +76 -0
- data/lib/rgame/engine/viewports.rb +171 -0
- data/lib/rgame/engine/world_view.rb +71 -0
- data/lib/rgame/engine.rb +86 -0
- data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
- data/lib/rgame/fonts/OFL.txt +102 -0
- data/lib/rgame/game.rb +271 -0
- data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
- data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
- data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
- data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
- data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
- data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
- data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
- data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
- data/lib/rgame/rubocop/default.yml +39 -0
- data/lib/rgame/rubocop/plugin.rb +45 -0
- data/lib/rgame/rubocop.rb +11 -0
- data/lib/rgame/util/color.rb +23 -0
- data/lib/rgame/util/controls.rb +169 -0
- data/lib/rgame/util/route_search.rb +27 -0
- data/lib/rgame/util/save_file.rb +107 -0
- data/lib/rgame/util/solid_grid.rb +37 -0
- data/lib/rgame/util/tensor.rb +3 -0
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +123 -0
- data/lib/rgame/util.rb +10 -0
- data/lib/rgame/util_ext.so +0 -0
- data/lib/rgame/version.rb +12 -0
- data/lib/rgame.rb +5 -0
- data/licenses/SDL2/LICENSE.txt +18 -0
- 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`.
|