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