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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +135 -1
- data/README.md +67 -65
- data/docs/api/README.md +86 -82
- data/docs/api/app.md +125 -68
- data/docs/api/assets.md +203 -165
- data/docs/api/audio.md +130 -89
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1032 -265
- data/docs/api/drawing.md +160 -160
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +149 -50
- data/docs/api/images.md +39 -39
- data/docs/api/input.md +226 -148
- data/docs/api/internals.md +241 -62
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +397 -244
- data/docs/api/signals.md +81 -76
- data/docs/api/systems.md +186 -73
- data/docs/api/text.md +58 -41
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +395 -146
- data/docs/api/ui.md +883 -54
- data/docs/api/values.md +216 -33
- 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/ext/rgame_core/app/app.c +33 -3
- data/ext/rgame_core/app/locale.c +67 -0
- data/ext/rgame_core/app/locale.h +28 -0
- data/ext/rgame_core/audio/audio.c +39 -2
- data/ext/rgame_core/example.rb +0 -49
- data/ext/rgame_core/extconf.rb +0 -125
- data/ext/rgame_core/include/rgame/core.h +38 -2
- data/ext/rgame_core/ruby/audio_ext.c +10 -5
- data/ext/rgame_core/ruby/core_ext.c +30 -7
- data/ext/rgame_core/ruby/core_ext.h +3 -0
- data/ext/rgame_core/ruby/locale_ext.c +44 -0
- data/ext/rgame_core/ruby/recording_ext.c +1 -1
- data/ext/rgame_core/ruby/renderer_ext.c +20 -20
- data/ext/rgame_util/extconf.rb +2 -20
- data/ext/rgame_util/route_search.c +305 -0
- data/ext/rgame_util/route_search.h +86 -0
- data/ext/rgame_util/route_search_ext.c +150 -0
- data/ext/rgame_util/solid_grid.c +58 -0
- data/ext/rgame_util/solid_grid.h +49 -0
- data/ext/rgame_util/solid_grid_ext.c +161 -0
- data/ext/rgame_util/tile_sweep.c +164 -0
- data/ext/rgame_util/tile_sweep.h +62 -0
- data/ext/rgame_util/tile_sweep_ext.c +155 -0
- data/ext/rgame_util/util_ext.c +3 -0
- data/ext/rgame_util/util_ext.h +15 -0
- data/lib/rgame/boot.rb +0 -10
- 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 +6 -44
- data/lib/rgame/core/asset_manager.rb +13 -31
- data/lib/rgame/core/audio.rb +37 -16
- data/lib/rgame/core/font.rb +0 -3
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +0 -21
- data/lib/rgame/core/renderer.rb +6 -63
- data/lib/rgame/core/sprite_sheet.rb +0 -3
- data/lib/rgame/core/tile_map_renderer.rb +2 -19
- data/lib/rgame/core/ui_atlas.rb +28 -13
- data/lib/rgame/core.rb +1 -8
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +1 -0
- data/lib/rgame/engine/audio_director.rb +36 -6
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +3 -3
- data/lib/rgame/engine/circle_collider.rb +4 -2
- data/lib/rgame/engine/collision_box.rb +26 -1
- data/lib/rgame/engine/collision_system.rb +110 -22
- data/lib/rgame/engine/component.rb +34 -10
- data/lib/rgame/engine/components/action_trigger.rb +0 -1
- data/lib/rgame/engine/components/animated_sprite.rb +27 -25
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +6 -5
- data/lib/rgame/engine/components/character_body.rb +20 -61
- data/lib/rgame/engine/components/circle_collider.rb +47 -11
- data/lib/rgame/engine/components/collision_world.rb +159 -31
- data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
- 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 +123 -31
- data/lib/rgame/engine/components/player_controller.rb +5 -2
- data/lib/rgame/engine/components/pool.rb +1 -1
- data/lib/rgame/engine/components/screen_wrap.rb +33 -11
- data/lib/rgame/engine/components/sprite.rb +12 -6
- data/lib/rgame/engine/components/targeting.rb +9 -11
- data/lib/rgame/engine/components/thrust_controller.rb +1 -1
- data/lib/rgame/engine/components/tile_world.rb +41 -13
- data/lib/rgame/engine/components/timer.rb +1 -1
- data/lib/rgame/engine/components/velocity.rb +23 -7
- data/lib/rgame/engine/components/wander_controller.rb +6 -2
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +4 -6
- data/lib/rgame/engine/debug_overlay.rb +5 -11
- 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 +276 -51
- data/lib/rgame/engine/input/action_mapper.rb +0 -26
- data/lib/rgame/engine/input/actions.rb +2 -8
- data/lib/rgame/engine/input/input_map.rb +39 -21
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +248 -133
- data/lib/rgame/engine/path.rb +4 -6
- data/lib/rgame/engine/players.rb +6 -13
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +4 -4
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/spatial_hash.rb +51 -19
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +2 -3
- data/lib/rgame/engine/tile_map_layer.rb +0 -2
- data/lib/rgame/engine/tileset.rb +2 -4
- data/lib/rgame/engine/timer.rb +2 -2
- 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 +246 -71
- 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/viewports.rb +2 -5
- data/lib/rgame/engine/world_view.rb +5 -4
- data/lib/rgame/engine.rb +31 -25
- data/lib/rgame/game.rb +99 -27
- 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 +20 -24
- data/lib/rgame/util/controls.rb +14 -28
- 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 +0 -9
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +4 -14
- data/lib/rgame/util.rb +4 -3
- data/lib/rgame/version.rb +1 -1
- data/lib/rgame.rb +0 -15
- metadata +152 -11
- data/lib/rgame/engine/actor.rb +0 -53
- data/lib/rgame/engine/body.rb +0 -49
- data/lib/rgame/engine/cached_label.rb +0 -33
- data/lib/rgame/engine/input/player_controller.rb +0 -14
- data/lib/rgame/engine/matrix.rb +0 -32
- data/lib/rgame/engine/resettable.rb +0 -67
- data/lib/rgame/engine/tile_collision.rb +0 -78
- 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
|
-
|
|
3
|
+
Input has four pieces, in two layers:
|
|
4
4
|
|
|
5
|
-
- **`RGame::Util::Controls`**
|
|
6
|
-
arrow key", "the A button"
|
|
7
|
-
without
|
|
8
|
-
- **`RGame::Core::Input`**
|
|
9
|
-
device
|
|
10
|
-
- **`RGame::Engine::InputMap`**
|
|
11
|
-
|
|
12
|
-
- **`RGame::Engine::ActionMapper`**
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
15
|
+
**`RGame::Core::Gamepad`** adds a readout of the plugged-in controllers, for
|
|
16
|
+
menus.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
rgame has no mouse support, by design.
|
|
19
19
|
|
|
20
20
|
## Which layer do I want?
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
`actions.held?(:fire)`, and
|
|
24
|
-
`RGame::Core::Input
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
only
|
|
68
|
-
|
|
69
|
-
and each player's device
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
|
86
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
|
99
|
-
the arrows
|
|
100
|
-
`input_map
|
|
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
|
|
103
|
-
|
|
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
|
|
110
|
-
no source, an empty button list, an axis that is not a pair.
|
|
111
|
-
the
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
135
|
-
|
|
136
|
-
leaving the dead zone ramps from zero
|
|
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
|
|
139
|
-
|
|
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
|
|
144
|
-
`RGame::Engine::Player` owns a device, an `InputMap`, a camera and a UI root
|
|
145
|
-
the action *names*
|
|
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:`
|
|
152
|
-
can play
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
`ui_confirm` rebinds "press to
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
188
|
-
lockout
|
|
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
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
pumps events.
|
|
207
|
-
|
|
208
|
-
directly would make a held key
|
|
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
|
-
|
|
217
|
-
|
|
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
|
|
226
|
-
|
|
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
|
|
236
|
-
keyboard key
|
|
237
|
-
|
|
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
|
-
|
|
242
|
-
`require 'rgame/core'
|
|
243
|
-
|
|
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
|
|
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
|
|
260
|
-
|
|
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
|
-
**
|
|
264
|
-
(Windows on a PC, Command on a Mac), the print-screen cluster, and
|
|
265
|
-
whose position depends on the layout. Adding
|
|
266
|
-
`ext/rgame_core/include/rgame/core.h`, a `_Static_assert` against
|
|
267
|
-
scancode, and a constant here
|
|
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
|
|
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
|
-
|
|
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
|
|
279
|
-
`AXIS_TRIGGER_LEFT`, `AXIS_TRIGGER_RIGHT`. Sticks read −1.0 to 1.0 with **y
|
|
280
|
-
positive downwards
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
348
|
+
**Devices**: `KEYBOARD`, `GAMEPAD_FIRST`, `MAX_GAMEPADS`, and
|
|
285
349
|
`Controls.gamepad(slot)`.
|
|
286
350
|
|
|
287
|
-
This module
|
|
288
|
-
|
|
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,
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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"
|
|
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)`
|
|
311
|
-
|
|
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
|
-
|
|
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
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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.
|