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/signals.md
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
# Signals
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
`RGame::Engine::Signal` is pure Ruby — no graphics — and lives in
|
|
3
|
+
**A signal is the engine's typed observer.** It holds a list of listener blocks
|
|
4
|
+
and `emit`s to them. Decoupled parts of the engine talk through signals. A
|
|
5
|
+
`UI::Button` reports that it was activated. A collider reports a hit. Gameplay
|
|
6
|
+
asks the audio layer to play a sound. The emitter never knows who listens, or
|
|
7
|
+
whether anyone does.
|
|
8
|
+
|
|
9
|
+
No central dispatcher exists, and no string or symbol event types need matching.
|
|
10
|
+
A signal *is* the channel. The attribute that exposes it names it, and its arity
|
|
11
|
+
is fixed when you define it.
|
|
12
|
+
|
|
13
|
+
`RGame::Engine::Signal` is pure Ruby, with no graphics, in
|
|
15
14
|
`lib/rgame/engine/signal.rb`.
|
|
16
15
|
|
|
17
16
|
## The Signal class: `Signal.define`
|
|
@@ -19,28 +18,29 @@ defined.
|
|
|
19
18
|
`Signal.define(*fields)` builds a signal **class**. Each instance is one channel:
|
|
20
19
|
|
|
21
20
|
```ruby
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
require 'rgame'
|
|
22
|
+
|
|
23
|
+
ClickSignal = RGame::Engine::Signal.define # carries no payload
|
|
24
|
+
ChangeSignal = RGame::Engine::Signal.define(:index, :value) # carries two values
|
|
24
25
|
|
|
25
26
|
sig = ChangeSignal.new
|
|
26
27
|
handle = sig.connect { |index, value| puts "#{index} -> #{value}" }
|
|
27
|
-
sig.emit(index: 2, value: :hard)
|
|
28
|
-
sig.disconnect(handle)
|
|
28
|
+
sig.emit(index: 2, value: :hard) # prints "2 -> hard"
|
|
29
|
+
sig.disconnect(handle) # stops that listener
|
|
29
30
|
```
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
A signal has three instance methods:
|
|
32
33
|
|
|
33
|
-
- **`connect(&block)`**
|
|
34
|
-
fire in the order they connected.
|
|
35
|
-
- **`emit(...)`**
|
|
36
|
-
- **`disconnect(handle)`**
|
|
34
|
+
- **`connect(&block)`** registers a listener and returns it as the *handle*.
|
|
35
|
+
Listeners fire in the order they connected.
|
|
36
|
+
- **`emit(...)`** notifies every listener.
|
|
37
|
+
- **`disconnect(handle)`** removes the listener that `connect` returned.
|
|
37
38
|
|
|
38
39
|
### Keyword in, positional out
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
signature
|
|
42
|
-
missing field raises at the call site
|
|
43
|
-
**positionally**:
|
|
41
|
+
**`emit` takes keywords; listeners receive positional values.** The field names
|
|
42
|
+
give `emit` a self-documenting signature. You call `emit(index:, value:)`, not
|
|
43
|
+
`emit(2, :hard)`, so a wrong or missing field raises at the call site:
|
|
44
44
|
|
|
45
45
|
```ruby
|
|
46
46
|
ChangeSignal = Signal.define(:index, :value)
|
|
@@ -48,29 +48,29 @@ sig.connect { |index, value| ... } # positional params
|
|
|
48
48
|
sig.emit(index: 2, value: :hard) # keyword args -> it.call(2, :hard)
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
`it.call(x, y)`.
|
|
51
|
+
Ruby blocks bind positional parameters cleanly but handle keywords awkwardly. The
|
|
52
|
+
generated `emit` therefore translates `emit(x:, y:)` into `it.call(x, y)`.
|
|
54
53
|
|
|
55
|
-
A
|
|
54
|
+
A signal with a single field takes its argument positionally, without a keyword:
|
|
56
55
|
|
|
57
56
|
```ruby
|
|
58
57
|
PlaySound = Signal.define(:id)
|
|
59
|
-
sig
|
|
58
|
+
sig = PlaySound.new
|
|
59
|
+
sig.connect { puts "play #{it}" }
|
|
60
60
|
sig.emit(:boom)
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
### No per-emit allocation
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
into an array or hash.
|
|
67
|
-
`*splat`
|
|
68
|
-
|
|
65
|
+
**`emit` allocates nothing.** It passes its arguments straight to each listener,
|
|
66
|
+
never collecting them into an array or hash. The explicit fields make this
|
|
67
|
+
possible; a `*splat` signature would allocate. Some signals fire every frame, and
|
|
68
|
+
the engine never allocates on the hot path.
|
|
69
69
|
|
|
70
70
|
## The DSL: declaring a signal slot
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
public method to subscribe, and a way to emit:
|
|
72
|
+
Wiring a signal onto a class by hand repeats itself. The class needs an ivar for
|
|
73
|
+
the instance, a public method to subscribe, and a way to emit:
|
|
74
74
|
|
|
75
75
|
```ruby
|
|
76
76
|
# Without the DSL:
|
|
@@ -80,63 +80,68 @@ def on_clicked(&block) = @on_clicked.connect(&block)
|
|
|
80
80
|
def activate = @on_clicked.emit
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
`RGame::Engine::Signal::DSL`
|
|
84
|
-
slots with `signal`:
|
|
83
|
+
`RGame::Engine::Signal::DSL` reduces that to one declaration. `extend` it, then
|
|
84
|
+
declare slots with `signal`:
|
|
85
85
|
|
|
86
86
|
```ruby
|
|
87
|
-
|
|
87
|
+
require 'rgame'
|
|
88
|
+
|
|
89
|
+
class Lever < RGame::Engine::Node2D
|
|
88
90
|
extend RGame::Engine::Signal::DSL
|
|
89
91
|
|
|
90
|
-
signal :
|
|
91
|
-
|
|
92
|
+
signal :on_pulled # a no-arg signal
|
|
93
|
+
signal :on_changed, RGame::Engine::Signal.define(:index, :value) # a typed one
|
|
92
94
|
|
|
93
|
-
def
|
|
95
|
+
def pull = on_pulled_signal.emit
|
|
94
96
|
end
|
|
97
|
+
|
|
98
|
+
lever = Lever.new
|
|
99
|
+
lever.on_pulled { puts 'pulled' }
|
|
100
|
+
lever.pull
|
|
95
101
|
```
|
|
96
102
|
|
|
97
|
-
`signal :
|
|
103
|
+
`signal :on_pulled` generates two methods:
|
|
98
104
|
|
|
99
|
-
- **`
|
|
100
|
-
|
|
101
|
-
- **`
|
|
102
|
-
|
|
105
|
+
- **`on_pulled(&block)`** is *public*. It subscribes a listener and returns the
|
|
106
|
+
handle. Observers call it: `lever.on_pulled { ... }`.
|
|
107
|
+
- **`on_pulled_signal`** is *private*. It returns the `Signal` instance, built on
|
|
108
|
+
first use. The class emits through it: `on_pulled_signal.emit`.
|
|
103
109
|
|
|
104
|
-
The
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
The reader builds the signal on first use, so the host wires **nothing** in
|
|
111
|
+
`initialize`. Pass a signal class as the second argument for a typed slot. Omit
|
|
112
|
+
it for a signal without a payload.
|
|
107
113
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
frame, hand-write it against a direct ivar instead.
|
|
114
|
+
**The DSL costs one extra method call per emit.** Emitting goes through the
|
|
115
|
+
private reader instead of a bare ivar. `emit` itself stays an ordinary `def`. UI
|
|
116
|
+
and per-frame signals never notice. For a signal emitted thousands of times per
|
|
117
|
+
frame, write it by hand against an ivar.
|
|
113
118
|
|
|
114
119
|
## Two shapes of signal
|
|
115
120
|
|
|
116
|
-
**Per-instance signals
|
|
117
|
-
|
|
118
|
-
`on_changed`. The `signal` macro
|
|
119
|
-
|
|
121
|
+
**Per-instance signals use the DSL.** Each object owns its channels. UI works this
|
|
122
|
+
way: every `UI::Button` has its own `on_activated`, and every `UI::OptionButton`
|
|
123
|
+
its own `on_changed`. The `signal` macro stores the instance in an ivar, which
|
|
124
|
+
suits exactly this case.
|
|
120
125
|
|
|
121
|
-
**A shared
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
**A shared hub holds signals at module level.** Use one when a single channel
|
|
127
|
+
serves the whole game. `RGame::Engine::AudioBus` holds its signals at module scope
|
|
128
|
+
and exposes them through readers. Gameplay anywhere calls
|
|
129
|
+
`RGame::Engine::AudioBus.play_sound(:boom)`, and the `AudioDirector` connects
|
|
130
|
+
once to `AudioBus.on_play_sound`. The DSL does not apply, because there is no
|
|
131
|
+
instance; the hub writes its readers by hand.
|
|
127
132
|
|
|
128
133
|
## When to reach for a signal
|
|
129
134
|
|
|
130
135
|
Follow the engine's communication rules (see [Scene graph](scene_graph.md)):
|
|
131
136
|
|
|
132
|
-
- **Parent → child:** call methods directly.
|
|
133
|
-
|
|
134
|
-
- **Child → parent, or sibling → sibling:** the child *exposes* a signal and the
|
|
135
|
-
parent
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
- **
|
|
139
|
-
a module-level hub like `AudioBus`.
|
|
140
|
-
|
|
141
|
-
Keep the emitter ignorant of its listeners
|
|
142
|
-
|
|
137
|
+
- **Parent → child:** call methods directly. The parent holds the reference, so
|
|
138
|
+
it needs no signal.
|
|
139
|
+
- **Child → parent, or sibling → sibling:** the child *exposes* a signal, and the
|
|
140
|
+
parent or an observer the parent arranges subscribes. A `UI::Button` exposes
|
|
141
|
+
`on_activated`, and the scene that adds it to a menu connects to it. Edges stay
|
|
142
|
+
direct, node to node.
|
|
143
|
+
- **Concerns that cut across the game** and have no natural owner, such as
|
|
144
|
+
audio: use a module-level hub like `AudioBus`.
|
|
145
|
+
|
|
146
|
+
Keep the emitter ignorant of its listeners. A signal with no listeners emits to
|
|
147
|
+
nobody, without error.
|
data/docs/api/systems.md
CHANGED
|
@@ -1,45 +1,34 @@
|
|
|
1
1
|
# Systems & shared resources
|
|
2
2
|
|
|
3
|
-
Some things a node needs
|
|
4
|
-
shared collision world.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Scope is a property of the **owner** you attach a system to, not of the system
|
|
21
|
-
itself — the same insight behind Unreal's `UGameInstanceSubsystem` (whole session)
|
|
22
|
-
vs `UWorldSubsystem` (one level), and Godot's autoload singletons vs per-scene
|
|
23
|
-
nodes.
|
|
24
|
-
|
|
25
|
-
- **Global scope → the root node.** `root` is set once and never changes, reachable
|
|
26
|
-
from every node. Program-lifetime systems (e.g. an audio bus, i18n) are components
|
|
27
|
-
on the root: `node.root.get_component(AudioBus)`.
|
|
28
|
-
- **Scene scope → the scene node** (what `SceneStack` pushes). Scene-lifetime systems
|
|
29
|
-
(the collision world, the tilemap/world-bounds holder) are components on *that*
|
|
30
|
-
node — born when the scene is pushed, gone when it's popped:
|
|
31
|
-
`node.scene.get_component(CollisionWorld)`.
|
|
3
|
+
Some things a node needs do not live on the node: a tile map, the world bounds, a
|
|
4
|
+
shared collision world. **Shared resources are systems. A system lives on an
|
|
5
|
+
anchor node, and other nodes reach it by walking the tree**, not through
|
|
6
|
+
constructor arguments. A system is an ordinary `RGame::Engine::Component` on a
|
|
7
|
+
boundary node, found with the same `get_component` every node has.
|
|
8
|
+
|
|
9
|
+
## Two scopes, two anchor nodes
|
|
10
|
+
|
|
11
|
+
**Scope belongs to the node you attach a system to**, not to the system.
|
|
12
|
+
|
|
13
|
+
- **Global scope → the root node.** Every node can reach the root, and it never
|
|
14
|
+
changes. Systems that live as long as the program are components on the root,
|
|
15
|
+
such as `Players` and `Viewports`: `node.root.get_component(RGame::Engine::Players)`.
|
|
16
|
+
- **Scene scope → the scene node**, the node `SceneStack` pushes. Systems that
|
|
17
|
+
live as long as a scene are components on *that* node, such as the collision
|
|
18
|
+
world or the tile world. They appear when the scene is pushed and go when it is
|
|
19
|
+
popped: `node.scene.get_component(CollisionWorld)`.
|
|
32
20
|
|
|
33
21
|
## The anchors
|
|
34
22
|
|
|
35
|
-
Both anchors are
|
|
36
|
-
back-link
|
|
37
|
-
children would cache the wrong root
|
|
23
|
+
**Both anchors are methods that walk the parent chain**, not cached fields. A
|
|
24
|
+
back-link cached at add time would go stale for a node built before it is
|
|
25
|
+
mounted: its children would cache the wrong root. Resolving on every access
|
|
26
|
+
cannot go stale.
|
|
38
27
|
|
|
39
|
-
- `root`
|
|
40
|
-
- `scene`
|
|
41
|
-
the pushed scene with `scene.scene = scene
|
|
42
|
-
any scene, `scene` is `nil`.
|
|
28
|
+
- `root` is `@parent ? @parent.root : self`. The top-most node is its own root.
|
|
29
|
+
- `scene` is the nearest ancestor marked as a scene boundary. `SceneStack#push`
|
|
30
|
+
marks the pushed scene with `scene.scene = scene`, and descendants resolve up to
|
|
31
|
+
it. Outside any scene, `scene` is `nil`.
|
|
43
32
|
|
|
44
33
|
### Looking a system up
|
|
45
34
|
|
|
@@ -47,72 +36,196 @@ children would cache the wrong root); resolving on access can't.
|
|
|
47
36
|
node.system(CollisionWorld)
|
|
48
37
|
```
|
|
49
38
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
39
|
+
**`Node2D#system(klass)` checks the scene first, then the root.** A scene can
|
|
40
|
+
therefore override a global default, and nodes outside any scene still find
|
|
41
|
+
globals. To mean one scope specifically, use its anchor:
|
|
42
|
+
`node.root.get_component` or `node.scene.get_component`.
|
|
54
43
|
|
|
55
|
-
|
|
44
|
+
### Ask for a contract, not a class
|
|
56
45
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
no anchors). See [Lifecycle](scene_graph.md#lifecycle-constructing-vs-entering-the-tree).
|
|
46
|
+
**The lookup matches by ancestry**, so `klass` can be a module the system
|
|
47
|
+
includes instead of its own class. One question can then have several answers.
|
|
60
48
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
49
|
+
"How big is the world" shows this. A flat game mounts
|
|
50
|
+
[`Components::World`](components.md#world). A tile game mounts
|
|
51
|
+
[`Components::TileWorld`](components.md#tileworld), which derives the same two
|
|
52
|
+
numbers from its map. Both include `Components::WorldBounds`, so a component
|
|
53
|
+
that needs bounds asks for the *contract*:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
def on_attach
|
|
57
|
+
world = node.system(RGame::Engine::Components::WorldBounds)
|
|
58
|
+
@width = world.world_width
|
|
59
|
+
@height = world.world_height
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`ScreenWrap` and `DespawnOffscreen` work this way. They run unchanged in either
|
|
64
|
+
kind of scene and never learn which one they are in. Naming the contract keeps
|
|
65
|
+
the two implementations from drifting apart.
|
|
66
|
+
|
|
67
|
+
## Registering with a system: use the lifecycle, not `initialize`
|
|
68
|
+
|
|
69
|
+
**A system and its clients connect in the tree-lifecycle hooks**, never in
|
|
70
|
+
`initialize`. They can only connect once everything is in the live tree, and a
|
|
71
|
+
node under construction has no anchors. See
|
|
72
|
+
[Lifecycle](scene_graph.md#lifecycle-constructing-vs-entering-the-tree).
|
|
73
|
+
|
|
74
|
+
The entered-tree cascade fixes an order that makes this safe. The scene's own
|
|
75
|
+
components run `on_attach` first, so a `CollisionWorld` on the scene node exists.
|
|
76
|
+
Then the scene's `on_add` runs, then its children enter. By the time a child
|
|
77
|
+
collider attaches, the scene-scoped system it looks up is already there.
|
|
65
78
|
|
|
66
79
|
```ruby
|
|
67
80
|
# CircleCollider (engine/components/circle_collider.rb) registers itself when it
|
|
68
81
|
# enters the tree and releases the registration when it leaves — the engine fires
|
|
69
82
|
# both hooks, so a spawned/despawned entity can't leak a registration.
|
|
70
83
|
class CircleCollider < RGame::Engine::Component
|
|
71
|
-
def on_attach = node.system(CollisionWorld)
|
|
84
|
+
def on_attach = node.system(CollisionWorld)&.register(self)
|
|
72
85
|
def on_detach = node.system(CollisionWorld)&.unregister(self)
|
|
73
86
|
end
|
|
74
87
|
```
|
|
75
88
|
|
|
76
|
-
|
|
89
|
+
**A collider tolerates a missing world; most clients should not.** Both hooks
|
|
90
|
+
above use `&.`. A collider in a scene without a `CollisionWorld` is a shape that
|
|
91
|
+
reports nothing. A tile-only game wants exactly that: its character carries a
|
|
92
|
+
feet box to be *stopped* by (see [`Mover`](components.md#mover)), and there are no
|
|
93
|
+
pairs to find. The cost is that an `on_hit` handler in such a scene never fires,
|
|
94
|
+
and nothing reports it. Weigh that deliberately. A client that is useless without
|
|
95
|
+
its system raises instead, as a mover's `blocked_by:` does.
|
|
96
|
+
|
|
97
|
+
## The two systems `Game` mounts
|
|
77
98
|
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
**`RGame::Game` puts two systems on the root before the tree goes live.** Any node
|
|
100
|
+
can reach them without the game wiring anything:
|
|
80
101
|
|
|
81
102
|
| | |
|
|
82
103
|
|---|---|
|
|
83
104
|
| `node.system(RGame::Engine::Players)` | who is playing — devices, bindings, cameras, and who a newly used controller belongs to |
|
|
84
105
|
| `node.system(RGame::Engine::Viewports)` | how the screen is divided — one `View` per active player, and collapsing the split |
|
|
85
106
|
|
|
86
|
-
They are ordinary root-scoped systems, mounted the
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
are systems rather than something `Game` hands down.
|
|
107
|
+
They are ordinary root-scoped systems, mounted the way a game mounts its own. A
|
|
108
|
+
scene that needs a camera to follow asks `Players` (`players.primary.camera`). A
|
|
109
|
+
cutscene that collapses the split asks `Viewports` (`viewports.solo!(camera)`).
|
|
110
|
+
Both work from anywhere in the tree, with nothing passed in. That reach is why
|
|
111
|
+
they are systems and not objects `Game` hands down.
|
|
92
112
|
|
|
93
113
|
See [Input](input.md#players-seats-and-joining) and
|
|
94
114
|
[Scene graph](scene_graph.md#viewports-and-views).
|
|
95
115
|
|
|
116
|
+
## Collision: two indexes, one resolver
|
|
117
|
+
|
|
118
|
+
Collision is the largest structure built from systems. Three pages cover its
|
|
119
|
+
parts: the [components](components.md#boxcollider) a node carries, the
|
|
120
|
+
[systems](#systems-that-index-their-clients-the-tag-registry-pattern) a scene
|
|
121
|
+
mounts, and the
|
|
122
|
+
[building blocks](internals.md#collisionsystem--move-an-actor-against-its-blockers)
|
|
123
|
+
underneath. This section shows how they fit together.
|
|
124
|
+
|
|
125
|
+
**A node has exactly one collision shape, and exactly one component owns it.** The
|
|
126
|
+
[collider](components.md#boxcollider) *is* the shape. A
|
|
127
|
+
[mover](components.md#mover) that wants to be stopped reads its sibling's box
|
|
128
|
+
instead of building a second one. The rectangle that stops a step is the
|
|
129
|
+
rectangle that reports a contact, so retuning one retunes both.
|
|
130
|
+
|
|
131
|
+
**Collision uses two indexes, on purpose.** A tile map is already an index: the
|
|
132
|
+
wall a step would hit follows from arithmetic on the step. A `TileWorld` divides
|
|
133
|
+
by the tile size and asks the grid. Actors have no such structure, so a
|
|
134
|
+
`CollisionWorld` buckets them into a
|
|
135
|
+
[`SpatialHash`](internals.md#spatialhash--uniform-grid-broadphase) each step.
|
|
136
|
+
Baking tile shapes into the broadphase would rebuild an index the grid already
|
|
137
|
+
is. It would also cost time every frame on a map of tens of thousands of tiles.
|
|
138
|
+
|
|
139
|
+
**The unification happens one level up.** A **blocker source** answers one
|
|
140
|
+
question over plain numbers: where does this box land when it moves `dx`? Three
|
|
141
|
+
sources exist:
|
|
142
|
+
|
|
143
|
+
- `TileBlockers`, over the grid;
|
|
144
|
+
- `ActorBlockers`, over the broadphase;
|
|
145
|
+
- `BoundsBlockers`, over the world's edges.
|
|
146
|
+
|
|
147
|
+
A blocked mover builds a
|
|
148
|
+
[`CollisionSystem`](internals.md#collisionsystem--move-an-actor-against-its-blockers)
|
|
149
|
+
at attach, from the sources its `blocked_by:` names. That system asks each source
|
|
150
|
+
and takes the most restrictive answer on each axis. The axis-separated order that
|
|
151
|
+
produces wall-sliding therefore exists **once**. An actor slides off a villager
|
|
152
|
+
exactly as it slides off a fence.
|
|
153
|
+
|
|
154
|
+
A source may also answer `travel?`: can this box move along a segment without
|
|
155
|
+
being stopped? `TileBlockers` does. A [`Navigator`](components.md#navigator) uses
|
|
156
|
+
it to check a route against the same resolver that will stop its walk.
|
|
157
|
+
|
|
158
|
+
| | Mounted on the scene | Owned by the node |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| Tiles | [`TileWorld`](components.md#tileworld), which hands out one shared `TileBlockers` | — |
|
|
161
|
+
| Actors | [`CollisionWorld`](components.md#collisionworld), the broadphase | an `ActorBlockers` per mover, holding its own collider and layer list |
|
|
162
|
+
| The world's edge | any [`WorldBounds`](components.md#world) | a `BoundsBlockers` |
|
|
163
|
+
| The step | — | one `CollisionSystem`, built at attach from the names above |
|
|
164
|
+
|
|
165
|
+
Neither system needs the other, and most scenes mount one. `examples/scroll_map`
|
|
166
|
+
has a map and no broadphase. `examples/collision` has a broadphase and no map.
|
|
167
|
+
`examples/collision_tiles` mounts both, and the difference shows only in the names
|
|
168
|
+
listed in `blocked_by`.
|
|
169
|
+
|
|
170
|
+
"What stops a mover" and "what a collider touches" remain two questions, and a
|
|
171
|
+
game sees the difference. See
|
|
172
|
+
[Blocking and overlapping](#blocking-and-overlapping-are-two-reports-and-a-pair-gets-one-of-them)
|
|
173
|
+
below, after the broadphase.
|
|
174
|
+
|
|
96
175
|
## Systems that index their clients (the tag-registry pattern)
|
|
97
176
|
|
|
98
|
-
A many-to-many system
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
(engine/components/collision_world.rb) holds a `SpatialHash
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
data-locality; it
|
|
177
|
+
**A many-to-many system keeps its own index of registered clients**, so it checks
|
|
178
|
+
only nearby candidates instead of walking the tree for every pair. Broadphase
|
|
179
|
+
collision works this way, on the scene node. `CollisionWorld`
|
|
180
|
+
(engine/components/collision_world.rb) holds a `SpatialHash`: a spatial index of
|
|
181
|
+
registered colliders, rebuilt each `update`. The index ignores shape, so
|
|
182
|
+
`CircleCollider` and `BoxCollider` share one and collide with each other. The
|
|
183
|
+
index holds node references only. It carries no component data and gives no
|
|
184
|
+
data-locality benefit; it is a lightweight registry.
|
|
106
185
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
186
|
+
**`CollisionWorld` ignores layers.** It reports every overlapping pair to both
|
|
187
|
+
colliders. The owning node decides what a contact *means* by reading the other
|
|
188
|
+
collider's `layer` tag:
|
|
110
189
|
|
|
111
190
|
```ruby
|
|
112
191
|
collider.on_hit { |other| queue_free if other.layer == :bullet } # in a Rock node
|
|
113
192
|
```
|
|
114
193
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
194
|
+
**A contact arrives as two edges, not as a state.** `on_hit` fires on the step a
|
|
195
|
+
pair starts overlapping. `on_separated` fires on the step it stops. Each fires
|
|
196
|
+
once per pair, with nothing in between. A handler can therefore count, play a
|
|
197
|
+
sound or spend a life without guarding itself. That is why the system keeps an
|
|
198
|
+
`Engine::ContactSet` per collider, instead of forwarding each step's broadphase
|
|
199
|
+
results.
|
|
200
|
+
|
|
201
|
+
`CollisionWorld` is a normal component on the scene node. It runs in the
|
|
202
|
+
`update` traversal, where its broadphase runs, and goes away with the scene.
|
|
203
|
+
`examples/collision` shows the whole loop: circles and crates registering,
|
|
204
|
+
overlapping and separating through this system.
|
|
205
|
+
|
|
206
|
+
### Blocking and overlapping are two reports, and a pair gets one of them
|
|
207
|
+
|
|
208
|
+
**The same broadphase answers a second question.** A
|
|
209
|
+
[mover](components.md#mover) that names a collider layer in `blocked_by:` is
|
|
210
|
+
*stopped* by every box with that layer. It stops flush against the box's edge, as
|
|
211
|
+
against a solid tile. One `BoxCollider` can thus be a wall to one actor and a
|
|
212
|
+
trigger for another. Who declared the layer decides which; the collider itself
|
|
213
|
+
says nothing.
|
|
214
|
+
|
|
215
|
+
**The two reports never both fire for one pair.** Blocking leaves the boxes
|
|
216
|
+
exactly touching. `CollisionBox.overlap?` uses the half-open span `[x, x + w)` on
|
|
217
|
+
purpose. On a grid, pieces on neighbouring squares border each other constantly,
|
|
218
|
+
and an inclusive test would report each as a contact. Measured on two 12×6 boxes:
|
|
219
|
+
touching exactly, `on_hit` does not fire; overlapping by half a pixel, it does.
|
|
220
|
+
That convention makes the two reports mutually exclusive by construction.
|
|
221
|
+
|
|
222
|
+
A blocked pair therefore reports **no** contact. A mover that must both stop and
|
|
223
|
+
react listens to `on_blocked`, not `on_hit`; see [`Mover`](components.md#mover).
|
|
224
|
+
The rule of thumb:
|
|
225
|
+
|
|
226
|
+
| The question | The report | Where it lives |
|
|
227
|
+
|---|---|---|
|
|
228
|
+
| what may I not pass through | `on_blocked` / `on_unblocked` | the mover that was stopped |
|
|
229
|
+
| what am I touching | `on_hit` / `on_separated` | both colliders of the pair |
|
|
230
|
+
|
|
231
|
+
`examples/collision_tiles` shows the first, and `examples/collision` the second.
|