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/audio.md
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
# Audio
|
|
2
2
|
|
|
3
|
+
rgame plays sound through three classes. `Audio` is the sound device. A `Sample`
|
|
4
|
+
is a short sound that can play over itself. A `Song` is a long sound streamed
|
|
5
|
+
from disk.
|
|
6
|
+
|
|
7
|
+
**A scene in an `RGame::Game` never touches these classes.** It emits on
|
|
8
|
+
[`AudioBus`](toolbox.md#audiobus--decoupled-audio-facts), for example
|
|
9
|
+
`RGame::Engine::AudioBus.play_sound('hit.ogg')`, and the game's director plays it.
|
|
10
|
+
The example below drives the device directly from a plain `App`:
|
|
11
|
+
|
|
3
12
|
```ruby
|
|
4
13
|
require 'rgame/core'
|
|
5
14
|
|
|
6
15
|
class MyGame < RGame::Core::App
|
|
7
16
|
def initialize
|
|
8
17
|
super(width: 800, height: 600, caption: 'demo')
|
|
9
|
-
@
|
|
10
|
-
@
|
|
11
|
-
@music = @audio.song('assets/theme.ogg')
|
|
18
|
+
@hit = audio.sample('assets/hit.ogg')
|
|
19
|
+
@music = audio.song('assets/theme.ogg')
|
|
12
20
|
@music.play(looping: true)
|
|
13
21
|
end
|
|
14
22
|
|
|
@@ -16,35 +24,40 @@ class MyGame < RGame::Core::App
|
|
|
16
24
|
@hit.play if id == RGame::Util::Controls::KEY_SPACE
|
|
17
25
|
end
|
|
18
26
|
end
|
|
19
|
-
```
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
MyGame.new.run
|
|
29
|
+
```
|
|
23
30
|
|
|
24
|
-
Ogg Vorbis and WAV
|
|
25
|
-
here](#what-is-not-here).
|
|
31
|
+
rgame decodes Ogg Vorbis and WAV, and nothing else. See
|
|
32
|
+
[What is not here](#what-is-not-here).
|
|
26
33
|
|
|
27
34
|
## The device
|
|
28
35
|
|
|
29
36
|
```ruby
|
|
30
|
-
audio =
|
|
31
|
-
audio.backend
|
|
32
|
-
audio.volume
|
|
37
|
+
audio = app.audio # the one a game uses
|
|
38
|
+
audio.backend # => "PulseAudio"
|
|
39
|
+
audio.volume # => 1.0
|
|
33
40
|
audio.volume = 0.8
|
|
34
41
|
```
|
|
35
42
|
|
|
36
|
-
**
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
**Use `App#audio`.** An app opens the device on first use and hands it the app's
|
|
44
|
+
asset manager. That manager lets `play_sound('hurt.ogg')` name a file.
|
|
45
|
+
`RGame::Core::Audio.new` builds a standalone device for a tool or a spec. It has
|
|
46
|
+
no manager, so it plays only objects it receives or has registered.
|
|
47
|
+
|
|
48
|
+
**The device is not tied to a window.** Unlike an `Image`, a sound belongs to no
|
|
49
|
+
GL context. It survives a window resize or rebuild, and `Audio.new` takes no app.
|
|
50
|
+
An app holds one device only because a program wants exactly one, like its asset
|
|
51
|
+
manager.
|
|
39
52
|
|
|
40
|
-
**A machine
|
|
41
|
-
null backend and plays
|
|
42
|
-
`"Null"`.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
`#backend`; nothing else changes.
|
|
53
|
+
**A machine without sound hardware still gets a working device.** It opens a
|
|
54
|
+
null backend and plays silence instead of raising, and `#backend` returns
|
|
55
|
+
`"Null"`. A game should run on a CI runner, in a container, or on a laptop with
|
|
56
|
+
its sound card off. Crashing at startup over sound would be the worse failure.
|
|
57
|
+
Ask `#backend` if your game needs to know; nothing else changes.
|
|
46
58
|
|
|
47
|
-
`#volume` is the master volume
|
|
59
|
+
`#volume` is the master volume. The device multiplies it into everything it
|
|
60
|
+
plays.
|
|
48
61
|
|
|
49
62
|
## Samples
|
|
50
63
|
|
|
@@ -55,21 +68,20 @@ hit.play
|
|
|
55
68
|
hit.play # a second voice, over the first
|
|
56
69
|
```
|
|
57
70
|
|
|
58
|
-
A sample
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
A sample decodes once, into memory, and plays as often as you like. **Playing a
|
|
72
|
+
sample that is already sounding layers another voice over it.** It does not
|
|
73
|
+
restart. A fast run of footsteps therefore sounds like footsteps, not one
|
|
74
|
+
stuttering step.
|
|
62
75
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
A sample is fire-and-forget. A single play has no handle and cannot be stopped.
|
|
77
|
+
Volume belongs to the sample and reaches every voice it has out, including those
|
|
78
|
+
already sounding.
|
|
66
79
|
|
|
67
|
-
|
|
68
|
-
memory
|
|
69
|
-
track belongs in a `Song`.
|
|
80
|
+
**Use samples for short sounds only.** A sample holds its whole decoded length in
|
|
81
|
+
memory, roughly 10 MB per minute of CD-quality stereo. Put music in a `Song`.
|
|
70
82
|
|
|
71
|
-
Two samples loaded from the **same path** share one decoded copy
|
|
72
|
-
|
|
83
|
+
Two samples loaded from the **same path** share one decoded copy. Loading a file
|
|
84
|
+
a second time costs nothing.
|
|
73
85
|
|
|
74
86
|
## Songs
|
|
75
87
|
|
|
@@ -81,46 +93,71 @@ music.volume = 0.6
|
|
|
81
93
|
music.stop
|
|
82
94
|
```
|
|
83
95
|
|
|
84
|
-
A song
|
|
85
|
-
|
|
96
|
+
A song streams from its file as it plays. A three-minute track costs a buffer,
|
|
97
|
+
not forty megabytes.
|
|
86
98
|
|
|
87
|
-
**A song is one voice.** Playing
|
|
88
|
-
the beginning
|
|
89
|
-
`play`
|
|
99
|
+
**A song is one voice.** Playing a song that is already playing restarts it from
|
|
100
|
+
the beginning, and so does playing it after `stop`. Songs cannot pause: `stop`
|
|
101
|
+
then `play` starts from the top.
|
|
90
102
|
|
|
91
103
|
`#playing?` and `#looping?` report what the song was last told to do.
|
|
92
|
-
`#looping?` is
|
|
104
|
+
`#looping?` is a flag, not a count.
|
|
93
105
|
|
|
94
|
-
**
|
|
95
|
-
|
|
96
|
-
one before starting the new one.
|
|
106
|
+
**The engine allows several songs at once**; a crossfade needs two. A game that
|
|
107
|
+
wants one song at a time stops the old one before starting the next.
|
|
97
108
|
|
|
98
109
|
## Playing by id
|
|
99
110
|
|
|
100
|
-
|
|
101
|
-
because a scene may not hold a `Sample`.
|
|
111
|
+
**Gameplay names a sound; it does not hold one.** Drawing follows the same rule,
|
|
112
|
+
because a scene may not hold a `Sample`. `examples/sound` and `examples/music`
|
|
113
|
+
show both kinds. The first fires and layers a sample from a button. The second
|
|
114
|
+
loops, stops and restarts a song.
|
|
102
115
|
|
|
103
116
|
```ruby
|
|
104
|
-
audio.
|
|
105
|
-
audio.
|
|
106
|
-
|
|
107
|
-
audio.play_sound(:hit)
|
|
108
|
-
audio.play_music(:theme) # loops
|
|
117
|
+
audio.play_sound('hurt.ogg')
|
|
118
|
+
audio.play_music('theme.ogg') # loops
|
|
109
119
|
audio.stop_music
|
|
110
120
|
```
|
|
111
121
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
**Ids come in two kinds, the same two the renderer uses.** A **String is a
|
|
123
|
+
root-relative path**. The device resolves it through the asset manager on first
|
|
124
|
+
use and remembers the result. A **Symbol is a name the game chose**, and only
|
|
125
|
+
registration binds one:
|
|
115
126
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
```ruby
|
|
128
|
+
audio.register_sound(:hit, app.assets.sound('hurt.ogg'))
|
|
129
|
+
audio.register_music(:theme, app.assets.song('theme.ogg'))
|
|
130
|
+
audio.play_sound(:hit)
|
|
131
|
+
audio.play_music(:theme)
|
|
132
|
+
```
|
|
119
133
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
is
|
|
134
|
+
Registration also *overrides* a path. A game uses that to bind a sound it built
|
|
135
|
+
instead of loaded. An id that is neither registered nor resolvable raises
|
|
136
|
+
`KeyError`. `nil` raises `TypeError`, because an asset that resolved to nothing
|
|
137
|
+
is a different bug from a mistyped name.
|
|
138
|
+
|
|
139
|
+
**The device caches every resolution, and must.** `play_music` asks the song
|
|
140
|
+
whether it is already playing. Resolving one path to two `Song` objects would
|
|
141
|
+
defeat that check and restart the track on every request.
|
|
142
|
+
|
|
143
|
+
**A game wires none of this.** When `RGame::Game` starts, it subscribes an
|
|
144
|
+
[`AudioDirector`](toolbox.md) to the global `AudioBus`. When the loop ends, it
|
|
145
|
+
unsubscribes it. A scene that emits on the bus is heard with no setup. The engine
|
|
146
|
+
owns both steps because each failure is invisible. Without a director, the tree
|
|
147
|
+
runs and the events fire, but nothing plays. A director left on the bus keeps
|
|
148
|
+
the device, the asset manager and the whole `App` alive for the life of the
|
|
149
|
+
process. A single game never notices, but a process running two games does.
|
|
150
|
+
|
|
151
|
+
**`play_music` is idempotent.** Asking for the track already playing does
|
|
152
|
+
nothing. A scene that repeats the request each time it is entered never restarts
|
|
153
|
+
the music mid-loop.
|
|
154
|
+
|
|
155
|
+
**`stop_music` stops the song `play_music` most recently started**, not whatever
|
|
156
|
+
is sounding. The engine keeps no process-wide "current song", because one song at
|
|
157
|
+
a time is a game's policy. **`play_music` with a different track does not stop the
|
|
158
|
+
previous one**, so both play. To switch tracks, call `stop_music` first. You stop
|
|
159
|
+
a `Song` you started by hand yourself. `stop_music` with nothing playing does
|
|
160
|
+
nothing.
|
|
124
161
|
|
|
125
162
|
## Loading and failure
|
|
126
163
|
|
|
@@ -132,13 +169,18 @@ RGame::Core::Sample.new(audio, path) # the same thing
|
|
|
132
169
|
RGame::Core::Song.new(audio, path)
|
|
133
170
|
```
|
|
134
171
|
|
|
135
|
-
|
|
136
|
-
depend, and a stand-in device can offer
|
|
137
|
-
[Testing](#testing)).
|
|
172
|
+
**Prefer `audio.sample` and `audio.song`.** They read in the direction the
|
|
173
|
+
objects depend, and a stand-in device can offer them, while `Sample.new` cannot
|
|
174
|
+
be faked (see [Testing](#testing)).
|
|
175
|
+
|
|
176
|
+
`audio.sample`, `audio.song` and the two constructors resolve a relative path
|
|
177
|
+
against the working directory, and cache nothing. `app.assets.sound(path)` and
|
|
178
|
+
`app.assets.song(path)` resolve against `media_root` and cache, and a String id
|
|
179
|
+
passed to `play_sound` or `play_music` goes through them.
|
|
138
180
|
|
|
139
|
-
A file
|
|
140
|
-
|
|
141
|
-
|
|
181
|
+
A file the engine cannot read or decode raises `RGame::Core::Sample::LoadError`
|
|
182
|
+
or `RGame::Core::Song::LoadError`, naming the file. Both inherit from
|
|
183
|
+
`StandardError`, so an ordinary `rescue` catches them:
|
|
142
184
|
|
|
143
185
|
```ruby
|
|
144
186
|
@music = begin
|
|
@@ -149,12 +191,12 @@ rescue RGame::Core::Song::LoadError => e
|
|
|
149
191
|
end
|
|
150
192
|
```
|
|
151
193
|
|
|
152
|
-
The
|
|
153
|
-
`.ogg
|
|
194
|
+
The engine checks the **content, not the extension**. It refuses a text file
|
|
195
|
+
named `.ogg`.
|
|
154
196
|
|
|
155
197
|
## Volume
|
|
156
198
|
|
|
157
|
-
Every volume
|
|
199
|
+
Every volume behaves the same way, whether the device's, a sample's or a song's:
|
|
158
200
|
|
|
159
201
|
| Value | Effect |
|
|
160
202
|
|---|---|
|
|
@@ -163,33 +205,32 @@ Every volume — the device's, a sample's, a song's — behaves the same way:
|
|
|
163
205
|
| above `1.0` | amplified; clipping is yours to avoid |
|
|
164
206
|
| below `0.0` | clamped to `0.0` |
|
|
165
207
|
|
|
166
|
-
|
|
167
|
-
easing curve undershoots
|
|
168
|
-
negative volume would
|
|
208
|
+
**A negative volume clamps to silence instead of raising.** A fader driven by a
|
|
209
|
+
slider or an easing curve undershoots all the time, and silence is the useful
|
|
210
|
+
answer. A true negative volume would invert the phase, which sounds *louder*.
|
|
169
211
|
|
|
170
|
-
|
|
212
|
+
The mixer stores volumes as 32-bit floats, so `0.8` reads back as
|
|
171
213
|
`0.800000011920929`. Compare with a tolerance, not with `==`.
|
|
172
214
|
|
|
173
215
|
## What it costs
|
|
174
216
|
|
|
175
|
-
Nothing needs freeing
|
|
176
|
-
collected
|
|
177
|
-
|
|
178
|
-
in either order.
|
|
217
|
+
**Nothing needs freeing.** A sample or song releases its memory when it is
|
|
218
|
+
collected. Each keeps its device alive while it exists. Dropping your reference
|
|
219
|
+
to the `Audio` while a sound remains is safe, in either order.
|
|
179
220
|
|
|
180
|
-
`RGame::Core::Audio.debug_live_sounds`
|
|
181
|
-
It
|
|
221
|
+
`RGame::Core::Audio.debug_live_sounds` returns how many samples and songs exist.
|
|
222
|
+
It serves tests, not gameplay.
|
|
182
223
|
|
|
183
224
|
## Testing
|
|
184
225
|
|
|
185
|
-
Audio follows the
|
|
186
|
-
|
|
187
|
-
|
|
226
|
+
**Audio follows the drawing pattern.** The engine layer receives a device and
|
|
227
|
+
calls it by method name, never by class. A headless spec can substitute a device
|
|
228
|
+
that makes no sound and records every call.
|
|
188
229
|
|
|
189
|
-
`spec/support/fake_audio.rb`
|
|
190
|
-
`spec/support/shared_examples/an_audio_server.rb`
|
|
191
|
-
the real device
|
|
192
|
-
|
|
230
|
+
rgame's own suite uses `spec/support/fake_audio.rb` as that stand-in.
|
|
231
|
+
`spec/support/shared_examples/an_audio_server.rb` defines the contract, and both
|
|
232
|
+
the fake and the real device run against it. A fake that drifted from the device
|
|
233
|
+
would keep `rake spec` green while the game played nothing.
|
|
193
234
|
|
|
194
235
|
```ruby
|
|
195
236
|
audio = FakeAudio.new
|
|
@@ -199,10 +240,10 @@ expect(audio.played?('hit.ogg')).to be(true)
|
|
|
199
240
|
expect(audio.calls.map(&:name)).to eq(%i[sample sample_play])
|
|
200
241
|
```
|
|
201
242
|
|
|
202
|
-
|
|
243
|
+
That spec loads no file, opens no device and needs no sound card.
|
|
203
244
|
|
|
204
245
|
## What is not here
|
|
205
246
|
|
|
206
|
-
MP3
|
|
207
|
-
audio, effects and filters, fades,
|
|
208
|
-
|
|
247
|
+
rgame audio has no MP3 or FLAC; it decodes Vorbis and WAV only, to keep the gem
|
|
248
|
+
small. It also lacks positional and 3D audio, effects and filters, fades,
|
|
249
|
+
pausing, seeking, per-play handles, playback position and recording.
|
data/docs/api/cli.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# The `rgame` command
|
|
2
|
+
|
|
3
|
+
Installing the gem puts one command on your PATH.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
gem install rgame
|
|
7
|
+
rgame new tictactoe
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
| Command | Does |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `rgame new NAME` | Creates the directory `NAME` and writes a runnable project into it |
|
|
13
|
+
| `rgame version` | Prints the installed engine version |
|
|
14
|
+
| `rgame help` | Prints usage |
|
|
15
|
+
|
|
16
|
+
`rgame new` accepts a name made of letters, digits, underscores and dashes,
|
|
17
|
+
starting with a letter, and refuses anything else. It also refuses a
|
|
18
|
+
path that exists and is not a directory, and a directory that holds anything. It
|
|
19
|
+
writes into an existing *empty* directory.
|
|
20
|
+
|
|
21
|
+
## What `rgame new tictactoe` writes
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
tictactoe/
|
|
25
|
+
├── Gemfile rgame, plus rspec and rubocop for development
|
|
26
|
+
├── Rakefile rake spec, rake rubocop, rake
|
|
27
|
+
├── README.md
|
|
28
|
+
├── .ruby-version the Ruby that ran `rgame new`
|
|
29
|
+
├── .gitignore .rspec .rubocop.yml
|
|
30
|
+
├── main.rb boots the game and nothing else
|
|
31
|
+
├── game.rb class TictactoeGame < RGame::Game
|
|
32
|
+
├── assets/ the game's media_root
|
|
33
|
+
│ └── locales/
|
|
34
|
+
│ └── en.yml the English translation table
|
|
35
|
+
├── nodes/
|
|
36
|
+
│ └── root.rb class Root < RGame::Engine::Node2D
|
|
37
|
+
└── spec/
|
|
38
|
+
├── spec_helper.rb
|
|
39
|
+
├── locales_spec.rb
|
|
40
|
+
└── nodes/
|
|
41
|
+
└── root_spec.rb
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
cd tictactoe
|
|
48
|
+
bundle install
|
|
49
|
+
bundle exec rspec # passes
|
|
50
|
+
bundle exec rubocop # green
|
|
51
|
+
ruby main.rb # a window saying "Hello from tictactoe!"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The generator builds the class name from the project name. It splits on
|
|
55
|
+
underscores and dashes and capitalises each part. `tic_tac_toe` and
|
|
56
|
+
`tic-tac-toe` both give `TicTacToeGame`.
|
|
57
|
+
|
|
58
|
+
The project records two versions, both taken from the running generator rather
|
|
59
|
+
than from a template. The `Gemfile` pins the engine loosely
|
|
60
|
+
(`gem 'rgame', '~> 0.3'` from rgame 0.3.0). `.ruby-version` records the exact Ruby that ran
|
|
61
|
+
`rgame new`, the one interpreter the project is known to work on. The Gemfile
|
|
62
|
+
reads that file instead of repeating the number:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
ruby file: '.ruby-version'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Version managers and Bundler both read `.ruby-version`, so the two cannot drift
|
|
69
|
+
apart.
|
|
70
|
+
|
|
71
|
+
**Bundler treats that line as an exact requirement.** On any other Ruby,
|
|
72
|
+
`bundle install` refuses to run. `4.0` does not match `4.0.5`; it matches only
|
|
73
|
+
`4.0`. To accept a range, state it in the `Gemfile`. `.ruby-version` must stay a
|
|
74
|
+
plain version number, because version managers read it:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
ruby '~> 4.0' # instead of `ruby file: '.ruby-version'`
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Why the layout looks like this
|
|
81
|
+
|
|
82
|
+
**The generated tree follows the engine's own layering.** The layout makes the
|
|
83
|
+
right split the easy one in a new project. Three files carry it.
|
|
84
|
+
|
|
85
|
+
**`game.rb` is the only file that requires `rgame/game`,** so it is the only one
|
|
86
|
+
that loads SDL and OpenGL. It is the project's counterpart of
|
|
87
|
+
[`RGame::Game`](game.md): the one class allowed to know both halves of the
|
|
88
|
+
engine.
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
require 'rgame/game'
|
|
92
|
+
require_relative 'nodes/root'
|
|
93
|
+
|
|
94
|
+
class TictactoeGame < RGame::Game
|
|
95
|
+
WIDTH = 640
|
|
96
|
+
HEIGHT = 480
|
|
97
|
+
|
|
98
|
+
def initialize(**)
|
|
99
|
+
super(root: Root.new,
|
|
100
|
+
caption: 'Tictactoe',
|
|
101
|
+
width: WIDTH,
|
|
102
|
+
height: HEIGHT,
|
|
103
|
+
media_root: File.join(__dir__, 'assets'),
|
|
104
|
+
**)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The bare `**` forwards every keyword to `RGame::Game`, so all its options still
|
|
110
|
+
work. Pass `players: 2` for split-screen. Pass `input:` to drive the game from a
|
|
111
|
+
scripted input backend with no hardware attached.
|
|
112
|
+
|
|
113
|
+
**`nodes/` requires `rgame`,** the graphics-free half. That loads `RGame::Util`
|
|
114
|
+
and `RGame::Engine`, and no graphics library. A node receives a renderer at draw
|
|
115
|
+
time and calls its methods by name. It never stores the renderer and never
|
|
116
|
+
learns its class.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
require 'rgame'
|
|
120
|
+
|
|
121
|
+
class Root < RGame::Engine::Node2D
|
|
122
|
+
def initialize
|
|
123
|
+
super
|
|
124
|
+
@greeting = RGame::Engine::Text.new('root.greeting')
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def on_draw(renderer, _view)
|
|
128
|
+
renderer.text(@greeting, 20, 20)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Override `on_control(actions)`, `on_update(dt)` and `on_draw(renderer, view)`,
|
|
134
|
+
not `control`, `update` or `draw`. The engine does its bookkeeping in the outer
|
|
135
|
+
methods and calls these hooks, so there is no `super` to forget. See
|
|
136
|
+
[Scene graph](scene_graph.md).
|
|
137
|
+
|
|
138
|
+
**`spec/spec_helper.rb` also requires `rgame`,** and `.rspec` loads it before
|
|
139
|
+
every spec. It requires every file under `nodes/` too. The generated suite
|
|
140
|
+
therefore runs headless. It has no window, no GPU and no clock, and `RGame::Core` is undefined.
|
|
141
|
+
A spec that names Core fails loudly instead of opening a window.
|
|
142
|
+
|
|
143
|
+
For the same reason, the generated spec uses a plain spy, not a verified double.
|
|
144
|
+
The renderer it replaces lives on the far side of a line the suite does not
|
|
145
|
+
cross. The generated `.rubocop.yml` turns `RSpec/VerifiedDoubles` off and writes
|
|
146
|
+
down that reason:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
RSpec.describe Root do
|
|
150
|
+
describe '#on_draw' do
|
|
151
|
+
it 'draws its greeting from the English table' do
|
|
152
|
+
renderer = spy('renderer')
|
|
153
|
+
|
|
154
|
+
described_class.new.on_draw(renderer, nil)
|
|
155
|
+
|
|
156
|
+
expect(renderer).to have_received(:text).with('Hello from tictactoe!', 20, 20)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The node passes its `Text` to `text`, so the spy records the `Text`. It still
|
|
163
|
+
matches the String, because a `Text` is `==` to the String it reads. See
|
|
164
|
+
[`Text`](toolbox.md#text--the-string-a-node-draws).
|
|
165
|
+
|
|
166
|
+
Put new game logic under `nodes/`, and the whole simulation stays testable in
|
|
167
|
+
milliseconds with no display, however large the game grows. Logic in `game.rb`
|
|
168
|
+
loses that.
|
|
169
|
+
|
|
170
|
+
## Text comes from a translation table
|
|
171
|
+
|
|
172
|
+
**The generated root node draws a key, not a String.** `assets/locales/en.yml`
|
|
173
|
+
holds the text, in Rails' format:
|
|
174
|
+
|
|
175
|
+
```yaml
|
|
176
|
+
en:
|
|
177
|
+
root:
|
|
178
|
+
greeting: "Hello from tictactoe!"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`RGame::Game` loads every `.yml` under `assets/locales/` and picks the player's
|
|
182
|
+
language from their operating system. See
|
|
183
|
+
[Game](game.md#translations-and-the-players-language). The node builds an
|
|
184
|
+
[`Engine::Text`](toolbox.md#text--the-string-a-node-draws) once and draws it
|
|
185
|
+
every frame. To add a language, add a file such as `de.yml` with the same keys.
|
|
186
|
+
[Localization](localization.md) covers the format, plurals and the fallback.
|
|
187
|
+
|
|
188
|
+
**The generated spec helper loads the same tables before every example.** It
|
|
189
|
+
reads the files once, then calls `I18n.reset`, loads each table and sets
|
|
190
|
+
`I18n.missing = :raise`. Every example therefore starts in the default locale
|
|
191
|
+
with the game's own tables, whatever the example before it loaded. A spec that
|
|
192
|
+
draws a key no table has fails with `I18n::MissingKey`.
|
|
193
|
+
|
|
194
|
+
The spec helper names `assets/locales` itself, because `spec/` cannot load
|
|
195
|
+
`game.rb`. Move the directory in one place and every spec that draws a key
|
|
196
|
+
fails. Reloading costs about 8 µs per key per example.
|
|
197
|
+
|
|
198
|
+
**`spec/locales_spec.rb` fails while a language lacks a key.** It checks that
|
|
199
|
+
the default locale has a table, and that `I18n.missing_keys` is empty for every
|
|
200
|
+
loaded locale. Its failure names each locale and its missing keys, such as
|
|
201
|
+
`{de: ["root.greeting"]}`. In the game itself, a missing key falls back to the
|
|
202
|
+
default locale's text, and a key no table has shows as itself.
|
|
203
|
+
|
|
204
|
+
## The generated RuboCop configuration
|
|
205
|
+
|
|
206
|
+
The generator loads `rubocop-performance`, `rubocop-rspec` and rgame's own cops,
|
|
207
|
+
relaxes the `Metrics/*` cops for a game's long `update` and `draw` methods, and
|
|
208
|
+
allows short coordinate names. `RSpec/SpecFilePathFormat` skips
|
|
209
|
+
`spec/locales_spec.rb`, which describes `I18n` but checks the tables rather than a
|
|
210
|
+
source file.
|
|
211
|
+
|
|
212
|
+
**The gem ships its cops as a RuboCop plugin.** The generated `.rubocop.yml`
|
|
213
|
+
loads it by path and class:
|
|
214
|
+
|
|
215
|
+
```yaml
|
|
216
|
+
plugins:
|
|
217
|
+
- rgame/rubocop:
|
|
218
|
+
plugin_class_name: RuboCop::Game::Plugin
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The plain form, `- rgame`, would make RuboCop require the whole engine just to
|
|
222
|
+
lint. An existing project gets the cops by adding those three lines.
|
|
223
|
+
|
|
224
|
+
| Cop | Refuses | Where |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| `Game/NoInterpolationInHotPath` | string interpolation in a per-frame method | everywhere but `spec/` |
|
|
227
|
+
| `Game/NoNeedlessAllocation` | a throwaway Array or Range literal on a per-frame path | everywhere but `spec/` |
|
|
228
|
+
| `Game/DrawInLocalSpace` | a node's draw method reading its own `x`, `y` or `world_x` | everywhere |
|
|
229
|
+
| `Game/NoLiteralText` | a String literal passed to `text` or `text_width` | everywhere |
|
|
230
|
+
| `Game/NoCoreInEngineLayer` | naming `RGame::Core`, or requiring `rgame/core` or `rgame/game` | `nodes/` and `spec/` |
|
|
231
|
+
|
|
232
|
+
A per-frame method is `update`, `control`, `draw`, `on_update`, `on_control` or
|
|
233
|
+
`on_draw`, or any method with a `# hot-path` comment on the line above its
|
|
234
|
+
`def`. For a label that changes, the answer to the first cop is an
|
|
235
|
+
[`Engine::Text`](toolbox.md#text--the-string-a-node-draws).
|
|
236
|
+
|
|
237
|
+
`Game/NoCoreInEngineLayer` guards the headless line the layout above draws. A
|
|
238
|
+
spec that names `RGame::Core` already fails when it runs, but the cop also
|
|
239
|
+
catches a branch no spec reaches. A bare `Core` counts only inside
|
|
240
|
+
`module RGame`, so a game's own `Core` module passes.
|
|
241
|
+
|
|
242
|
+
`Game/NoEngineInCoreLayer` ships too, switched off. It guards the engine's own
|
|
243
|
+
repository, and a game has no layer for it to guard.
|
|
244
|
+
|
|
245
|
+
## Adding to the generator
|
|
246
|
+
|
|
247
|
+
`rgame new` derives its file list from `lib/rgame/cli/templates/`. A new file in
|
|
248
|
+
a generated project needs a new template and nothing else; there is no manifest.
|
|
249
|
+
The generator writes only files, so it creates a directory only by writing a
|
|
250
|
+
template into it.
|
|
251
|
+
Templates are ERB and may call `app_name`, `game_class`, `caption`,
|
|
252
|
+
`ruby_version` and `rgame_requirement`.
|
|
253
|
+
|
|
254
|
+
**No template may have a name starting with a dot.** The gemspec packages
|
|
255
|
+
`lib/**/*` with `Dir.glob`, which skips dotfiles. A template called `.gitignore`
|
|
256
|
+
would work in a checkout but be missing from the installed gem. Dotfile
|
|
257
|
+
templates therefore use a plain name (`gitignore.tt`).
|
|
258
|
+
`RGame::CLI::NewProject::DOTFILES` renames them on the way out.
|
|
259
|
+
`spec/packaging_spec.rb` fails if a dotfile template appears.
|