rgame 0.2.0 → 0.3.1
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 +144 -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 +3 -5
- 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 +167 -12
- 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/toolbox.md
CHANGED
|
@@ -1,61 +1,170 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Toolbox
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
This page covers engine classes **a game author uses directly** that belong to no
|
|
4
|
+
other chapter: pooling, the text a node draws, audio facts, the camera, collision boxes.
|
|
5
|
+
All are pure Ruby, so they stay testable headless. One section is a recipe, not a
|
|
6
|
+
class: [making a character that collides](#making-a-character-that-collides). No
|
|
7
|
+
single class answers that question.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
hand
|
|
10
|
-
|
|
9
|
+
[Internal building blocks](internals.md) covers the low-level classes *behind*
|
|
10
|
+
components, which you rarely construct by hand: collision maths, the spatial
|
|
11
|
+
index, animation playback.
|
|
11
12
|
|
|
12
|
-
##
|
|
13
|
+
## Grids
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
**The engine has no grid class of its own.** A fixed-size grid is a
|
|
16
|
+
[`RGame::Util::Tensor`](values.md#rgameutiltensor): three-dimensional, backed by
|
|
17
|
+
one flat C array. A flat 2-D grid is a `Tensor` with a depth of 1. `TileMap`
|
|
18
|
+
stacks its tile layers in one `Tensor(width, height, layer_count)`. It shows the
|
|
19
|
+
rule that the engine layer may hold `RGame::Util` values: a grid is a value, so
|
|
20
|
+
the engine owns one outright.
|
|
21
|
+
|
|
22
|
+
[`NavGrid`](#navgrid--routes-over-a-tile-grid) searches over a grid's solidity; it
|
|
23
|
+
does not store a grid. The solidity lives in a
|
|
24
|
+
[`Util::SolidGrid`](values.md#rgameutilsolidgrid), one byte per cell in C. The C
|
|
25
|
+
search must read it, and a `Tensor`'s cells are Ruby objects.
|
|
26
|
+
|
|
27
|
+
## `Text` — the string a node draws
|
|
28
|
+
|
|
29
|
+
**`RGame::Engine::Text` (`rgame/engine/text`) holds a translation key and keeps
|
|
30
|
+
the rendered String until an input changes.** The inputs are its variables and
|
|
31
|
+
`I18n.generation`. Build a `Text` once, off the per-frame path, and read it in
|
|
32
|
+
`on_draw`. A read with unchanged inputs returns the same frozen String and
|
|
33
|
+
allocates nothing. [Localization](localization.md) covers the tables it reads,
|
|
34
|
+
plurals, and the player's language.
|
|
18
35
|
|
|
19
36
|
```ruby
|
|
20
|
-
|
|
21
|
-
grid[col, row] = gid
|
|
22
|
-
grid[col, row] # row-major: index = y * width + x
|
|
23
|
-
```
|
|
37
|
+
require 'rgame'
|
|
24
38
|
|
|
25
|
-
|
|
39
|
+
i18n = RGame::Engine::I18n
|
|
40
|
+
i18n.load_hash(en: { hud: { score: 'Score: %{score}', title: 'Apples' } },
|
|
41
|
+
de: { hud: { score: 'Punkte: %{score}', title: 'Äpfel' } })
|
|
26
42
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
`Tensor(width, height, layer_count)`, and that is the worked example of the rule
|
|
30
|
-
that the engine layer may hold `RGame::Util` values: a grid is a value, so the
|
|
31
|
-
layer above owns one outright rather than being handed it.
|
|
43
|
+
score = RGame::Engine::Text.new('hud.score', :score) # built once
|
|
44
|
+
title = RGame::Engine::Text.new('title', scope: 'hud')
|
|
32
45
|
|
|
33
|
-
|
|
46
|
+
score.with(score: 7) # => "Score: 7"
|
|
47
|
+
score.with(score: 7).equal?(score.with(score: 7)) # => true — nothing changed, nothing rendered
|
|
48
|
+
title.to_s # => "Apples"
|
|
49
|
+
i18n.locale = :de
|
|
50
|
+
score.with(score: 7) # => "Punkte: 7" — the switch re-renders
|
|
51
|
+
title.to_s # => "Äpfel"
|
|
52
|
+
```
|
|
34
53
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
allocating) a `String` every frame. Construct it — and its format block — outside the per-frame
|
|
38
|
-
path (e.g. in `on_add`), then read it by value in `on_draw`:
|
|
54
|
+
In a node, the draw reads the `Text` with the current value, or passes a `Text`
|
|
55
|
+
with no names as it is:
|
|
39
56
|
|
|
40
57
|
```ruby
|
|
41
|
-
@score_label = RGame::Engine::CachedLabel.new { |score| "Score: #{score}" } # built once
|
|
42
|
-
|
|
43
58
|
def on_draw(renderer, _view)
|
|
44
|
-
renderer.text(@
|
|
59
|
+
renderer.text(@score.with(score: @points), 12, 10)
|
|
60
|
+
renderer.text(@title, 12, 30)
|
|
45
61
|
end
|
|
46
62
|
```
|
|
47
63
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
**A `Text` goes where a String goes.** It answers `to_str` with the same String
|
|
65
|
+
as `to_s`, so `Renderer#text` and `Renderer#text_width` take one directly. A
|
|
66
|
+
`Text` is also `==` to a String that reads the same, from either side. A spec's
|
|
67
|
+
spy that recorded a `Text` therefore matches `with('Apples', 12, 30)`. Two `Text`s
|
|
68
|
+
compare by identity, and `hash` stays the object's own, so a `Text` is not a
|
|
69
|
+
String-keyed Hash key.
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
require 'rgame'
|
|
73
|
+
|
|
74
|
+
RGame::Engine::I18n.load_hash(en: { hud: { title: 'Apples' } })
|
|
75
|
+
title = RGame::Engine::Text.new('hud.title')
|
|
76
|
+
|
|
77
|
+
title == 'Apples' # => true
|
|
78
|
+
'Apples' == title # => true
|
|
79
|
+
title.to_str.equal?(title.to_s) # => true — the same String, no copy
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Variables are keywords
|
|
83
|
+
|
|
84
|
+
`Text.new(key, *names, scope: nil)` gives the `Text` a `with` whose keywords are
|
|
85
|
+
`names`. A missing or unknown keyword raises Ruby's own `ArgumentError`, on the
|
|
86
|
+
first frame that reads it. Every `Text` with the same names shares one generated
|
|
87
|
+
`with`, whatever order the names came in. `names` returns them sorted.
|
|
88
|
+
|
|
89
|
+
A `Text` with no names is read with `to_s`, and `with` with no keywords returns
|
|
90
|
+
the same. On a `Text` that has names, `to_s` reads it with the values its last
|
|
91
|
+
`with` was given, and renders them again after a locale switch. So one node can
|
|
92
|
+
set the values in `update`, and whatever draws the `Text` passes it on without
|
|
93
|
+
knowing them: a [button's label](ui.md#labels-are-translation-keys) works this
|
|
94
|
+
way. Before the first `with`, `to_s` and `to_str` raise `ArgumentError`, naming
|
|
95
|
+
the keywords `with` needs, and a renderer handed the `Text` lets that error
|
|
96
|
+
through.
|
|
97
|
+
|
|
98
|
+
The values belong to the `Text`, not to whoever set them. Two nodes sharing one
|
|
99
|
+
`Text` with names show whatever the last `with` gave, so give each node its own. A name must be usable as a Ruby local variable:
|
|
100
|
+
`Text.new('x', :Name)` and `Text.new('x', :end)` raise `ArgumentError`.
|
|
101
|
+
|
|
102
|
+
A key whose translation is a plural needs `:count` among the names, and picks its
|
|
103
|
+
form by `count` as [`I18n.t` does](localization.md#plurals).
|
|
104
|
+
|
|
105
|
+
### Scope
|
|
106
|
+
|
|
107
|
+
`scope: 'hud'` with key `'title'` resolves `'hud.title'`. `key` returns the key
|
|
108
|
+
without its scope. `scope=` changes the scope, and the next read resolves again.
|
|
109
|
+
A [`UI::Menu`'s `scope:`](ui.md#labels-are-translation-keys) sets the scope of
|
|
110
|
+
the labels its buttons build from keys.
|
|
111
|
+
|
|
112
|
+
### When it renders again
|
|
113
|
+
|
|
114
|
+
A read renders again when any keyword differs from the last read, by `==`, or
|
|
115
|
+
when `I18n.generation` has moved. The generation moves on every `load`, every
|
|
116
|
+
switch of locale or default, every `plural_rule`, and `reset`. A `Text` compares that one Integer and
|
|
117
|
+
never subscribes to `I18n`, so nothing holds on to it.
|
|
118
|
+
|
|
119
|
+
A `Text` built before any table loads shows the [missing-key](localization.md#missing-keys)
|
|
120
|
+
answer. After a `load`, its next read shows the translation.
|
|
121
|
+
|
|
122
|
+
A translation whose placeholders differ from the declared names also goes to the
|
|
123
|
+
missing policy. Under `:key` the `Text` shows its key, and a callable receives
|
|
124
|
+
the key and the chain. Under `:raise` it raises `I18n::VariableMismatch` in
|
|
125
|
+
either direction: for a `%{name}` the `Text` does not declare, and for a
|
|
126
|
+
declared name the translation never prints. A plural whose `Text` lacks `:count`
|
|
127
|
+
raises it too.
|
|
128
|
+
|
|
129
|
+
### `Text.literal` and `Text.computed`
|
|
130
|
+
|
|
131
|
+
`Text.literal(string)` shows `string` in every locale and never consults `I18n`.
|
|
132
|
+
`Text.computed(*names) { |**keywords| ... }` shows what its block returns. The
|
|
133
|
+
block runs when a keyword or `I18n.generation` changes, and never on an unchanged
|
|
134
|
+
read. A block that calls `I18n.t` therefore follows the language:
|
|
135
|
+
`examples/pathfinding` assembles its status line this way from four keys, two of
|
|
136
|
+
them plurals. Both answer
|
|
137
|
+
`with`, `to_s` and `to_str` like any `Text`, and both ignore `scope=`.
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
require 'rgame'
|
|
141
|
+
|
|
142
|
+
lives = RGame::Engine::Text.computed(:lives) { |lives:| "Lives: #{lives}" }
|
|
143
|
+
lives.with(lives: 3) # => "Lives: 3"
|
|
144
|
+
RGame::Engine::Text.literal('Ada').to_s # => "Ada"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Use a `Text` instead of working around the cop.** `Game/NoInterpolationInHotPath`
|
|
148
|
+
refuses `"Score: #{score}"` in a draw, and a `Text` built in `initialize` is the
|
|
149
|
+
answer. `examples/sound` shows it: a play counter on screen costs one render per
|
|
150
|
+
press and none in the frames between. A value that changes *every* frame, such
|
|
151
|
+
as an FPS counter, gains nothing from a cache. `RGame::Engine::DebugOverlay`
|
|
152
|
+
draws its digits one by one from cached single-character strings.
|
|
153
|
+
|
|
154
|
+
**Text chosen by state is a table of `Text`s**, such as
|
|
155
|
+
`{ true => Text.new('state.fullscreen'), false => Text.new('state.windowed') }.freeze`.
|
|
156
|
+
It selects a `Text` and builds nothing. A `Text` holds no reference to a game, so
|
|
157
|
+
it can live in a constant. A `Text.computed` made at the top level of a file
|
|
158
|
+
cannot: its block keeps that file's local variables alive, a `game` among them. A
|
|
159
|
+
value that **never changes and is not words**, such as an
|
|
160
|
+
id, belongs in an ivar built in `initialize`.
|
|
52
161
|
|
|
53
162
|
## `Pool` — reuse, don't allocate
|
|
54
163
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
list is empty.
|
|
164
|
+
**`RGame::Engine::Pool` (`rgame/engine/pool`) recycles short-lived objects of one
|
|
165
|
+
kind**, so steady-state spawning allocates nothing. Bullets, particles and
|
|
166
|
+
transient enemies are typical. `acquire` takes an object from a free list, and
|
|
167
|
+
calls the factory block only when the list is empty.
|
|
59
168
|
|
|
60
169
|
```ruby
|
|
61
170
|
pool = RGame::Engine::Pool.new { Bullet.new } # factory builds a blank object
|
|
@@ -65,20 +174,21 @@ pool.each { |bullet| bullet.update(dt) }
|
|
|
65
174
|
pool.reclaim_if(&:dead?) # sweep dead → free list, once per frame
|
|
66
175
|
```
|
|
67
176
|
|
|
68
|
-
The factory builds a *blank* object
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
iterating with `each
|
|
73
|
-
and `each` expose the live set for update
|
|
177
|
+
The factory builds a *blank* object. The caller re-initialises it after
|
|
178
|
+
`acquire`, usually through a `reset` method on the pooled class.
|
|
179
|
+
`examples/pooling` shows this on a node. `reclaim_if` sweeps the active list once
|
|
180
|
+
and moves every object the block marks dead onto the free list. Call it *after*
|
|
181
|
+
iterating with `each`; never change the active list mid-iteration. `active`,
|
|
182
|
+
`size`, `empty?` and `each` expose the live set for update and draw.
|
|
74
183
|
|
|
75
184
|
## `Path` — a walkable polyline
|
|
76
185
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
accessors
|
|
186
|
+
**`RGame::Engine::Path` (`rgame/engine/path`) is an ordered polyline** that an
|
|
187
|
+
entity walks along: a road, a patrol route, a track. It is pure data. It holds the
|
|
188
|
+
waypoints and precomputed segment lengths, so a follower allocates nothing at
|
|
189
|
+
runtime. It stores waypoints flat (`x0, y0, x1, y1, …`) in one array and returns
|
|
190
|
+
them through scalar accessors. Neither construction nor traversal creates an
|
|
191
|
+
object per waypoint.
|
|
82
192
|
|
|
83
193
|
```ruby
|
|
84
194
|
path = RGame::Engine::Path.new([[0, 0], [100, 0], [100, 100]]) # ≥ 2 waypoints, in walk order
|
|
@@ -90,21 +200,115 @@ path.length # total length
|
|
|
90
200
|
path.distance_to(x, y) # shortest distance from a point to the polyline
|
|
91
201
|
```
|
|
92
202
|
|
|
93
|
-
A follower ([`Components::PathFollow`](components.md#pathfollow)) reads segments
|
|
94
|
-
and interpolates itself; Path never returns a coordinate pair.
|
|
95
|
-
far is this point from the road"
|
|
96
|
-
|
|
203
|
+
A follower ([`Components::PathFollow`](components.md#pathfollow)) reads segments
|
|
204
|
+
by index and interpolates itself; `Path` never returns a coordinate pair.
|
|
205
|
+
`distance_to` answers "how far is this point from the road" with scalar maths and
|
|
206
|
+
no allocation. A level can use it to keep objects off or away from the road.
|
|
207
|
+
|
|
208
|
+
## `NavGrid` — routes over a tile grid
|
|
209
|
+
|
|
210
|
+
**`RGame::Engine::NavGrid` (`rgame/engine/nav_grid`) finds the cheapest route from
|
|
211
|
+
one cell to another.** The search runs in C, as a
|
|
212
|
+
[`Util::RouteSearch`](values.md#rgameutilroutesearch). In a tile scene you do not
|
|
213
|
+
build one: [`TileWorld#nav_grid`](components.md#tileworld) returns one over the
|
|
214
|
+
map's solid tiles.
|
|
215
|
+
|
|
216
|
+
You build a `NavGrid` in one of two ways. Passing both or neither raises
|
|
217
|
+
`ArgumentError`.
|
|
218
|
+
|
|
219
|
+
- **From a callable**: `NavGrid.new(width:, height:, solid:)`.
|
|
220
|
+
`solid.call(col, row)` returns true for a solid cell. `NavGrid` calls it once per
|
|
221
|
+
cell, copies the answers into its own grid, and never calls it again. A later
|
|
222
|
+
change behind the callable stays invisible.
|
|
223
|
+
- **Over a shared grid**: `NavGrid.new(grid:)`, with a
|
|
224
|
+
[`Util::SolidGrid`](values.md#rgameutilsolidgrid). Nothing is copied, so a cell
|
|
225
|
+
changed in that grid changes for the `NavGrid` at once. `TileWorld` builds its
|
|
226
|
+
grid this way. Use this form for solidity that will change. A
|
|
227
|
+
[`TileBlockers`](internals.md#tileblockers--the-tile-grid-as-a-blocker-source)
|
|
228
|
+
is built over the same grid, so routing and collision read one store.
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
require 'rgame'
|
|
232
|
+
|
|
233
|
+
rows = [
|
|
234
|
+
'........',
|
|
235
|
+
'###..###',
|
|
236
|
+
'........'
|
|
237
|
+
]
|
|
238
|
+
grid = RGame::Engine::NavGrid.new(width: 8, height: 3,
|
|
239
|
+
solid: ->(col, row) { rows[row][col] == '#' })
|
|
240
|
+
|
|
241
|
+
grid.walkable?(3, 1) # => true
|
|
242
|
+
grid.find(0, 0, 7, 2) # => [[0, 0], [1, 0], [2, 0], [3, 0], [4, 1], [4, 2], [5, 2], [6, 2], [7, 2]]
|
|
243
|
+
grid.find(0, 0, 0, 1) # => nil — the goal is solid
|
|
244
|
+
grid.reachable?(0, 0, 7, 2) # => true
|
|
245
|
+
grid.region(0, 0) # => 0 — an Integer label; nil for a solid or off-grid cell
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
- **A route is a list of cells, both ends included**, as `[[col, row], ...]`. When
|
|
249
|
+
start equals goal, the route has one cell. Turning a cell into a point to walk to
|
|
250
|
+
depends on the node's collider, which the grid knows nothing about. So `NavGrid`
|
|
251
|
+
never speaks pixels. [`Components::Navigator`](components.md#navigator) does that
|
|
252
|
+
and walks the result.
|
|
253
|
+
- **An unreachable goal returns `nil`.** That covers a solid or off-grid end, and a
|
|
254
|
+
goal in another region. `nil` is an ordinary answer, not an error.
|
|
255
|
+
- **Moves go 8 ways at octile cost**: 1 straight, √2 diagonal. A diagonal move
|
|
256
|
+
requires both orthogonal neighbours to be open, so a route never cuts the corner
|
|
257
|
+
of a solid cell. The search returns a cheapest route. It breaks ties
|
|
258
|
+
deterministically, so the same query always returns the same route.
|
|
259
|
+
- **`NavGrid` relabels connected regions whenever the grid has changed.** A goal
|
|
260
|
+
in another region is the slowest answer for a search, which must exhaust the
|
|
261
|
+
start's region first. Region labels turn it into a lookup. Relabelling a 120x90
|
|
262
|
+
map takes about 45 µs, on the first query after a change.
|
|
263
|
+
- **Regions and routes follow a shared grid.** Wall a region in two through the
|
|
264
|
+
`SolidGrid`, and the next `find` across the wall returns `nil`. Open it again and
|
|
265
|
+
the route goes through, with nothing rebuilt:
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
store = RGame::Util::SolidGrid.build(8, 3) { |col, row| rows[row][col] == '#' }
|
|
269
|
+
shared = RGame::Engine::NavGrid.new(grid: store)
|
|
270
|
+
store.set_solid(3, 1, true)
|
|
271
|
+
store.set_solid(4, 1, true)
|
|
272
|
+
shared.find(0, 0, 7, 2) # => nil — the gap is closed
|
|
273
|
+
shared.reachable?(0, 0, 7, 2) # => false
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
- **Coordinates are Integers.** A `Float` or `nil` names no cell and raises
|
|
277
|
+
`TypeError`. An Integer outside the grid, however large, is treated like any
|
|
278
|
+
other cell outside it.
|
|
279
|
+
- **Search on demand, never per frame.** A search allocates its result. It keeps
|
|
280
|
+
its working buffers between searches, so never search one `NavGrid` from two
|
|
281
|
+
threads at once. A corner-to-corner route across a 60x40 town takes under
|
|
282
|
+
0.1 ms. The slowest of 200 random routes on a 120x90 map takes about 1 ms.
|
|
283
|
+
|
|
284
|
+
### What routing does not cover
|
|
285
|
+
|
|
286
|
+
- **Maps that change at runtime, in a tile scene.** A shared `SolidGrid` changes
|
|
287
|
+
under a `NavGrid` and a `TileBlockers` at once, as above. `TileWorld` keeps its
|
|
288
|
+
grid private, though, and a
|
|
289
|
+
[`Navigator`](components.md#navigator) already walking learns nothing of a change.
|
|
290
|
+
- **Replanning around moving actors.** `find` knows only the grid. It cannot treat a
|
|
291
|
+
cell as blocked for one query, and a `Navigator` held by another actor waits
|
|
292
|
+
instead of replanning.
|
|
293
|
+
- **Crowds.** Many navigators can share one `TileWorld#nav_grid`. Each `go_to` costs
|
|
294
|
+
a search plus smoothing, well under a millisecond on a 60x40 map. Nothing
|
|
295
|
+
coordinates the walkers.
|
|
296
|
+
- **Avoidance.** Nothing steers a walker around another. Of the blocker sources,
|
|
297
|
+
only `TileBlockers` answers `travel?`.
|
|
298
|
+
- **Flow fields.** A search answers one start and one goal. Nothing computes the
|
|
299
|
+
distance from every cell to a shared goal.
|
|
300
|
+
- **Weighted terrain.** Every open cell costs the same.
|
|
301
|
+
- **Colliders larger than a tile.** `Navigator#go_to` raises for one.
|
|
97
302
|
|
|
98
303
|
## `Timer` — paced periodic events
|
|
99
304
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
per-frame path.
|
|
305
|
+
**`RGame::Engine::Timer` (`rgame/engine/timer`) accumulates time; its owner decides
|
|
306
|
+
what each interval means.** Use it for periodic events that no input drives: a
|
|
307
|
+
spawner that emits an enemy every N seconds, a turret's fire rate, a wave clock.
|
|
308
|
+
The split lets one class serve two patterns. "Act automatically" consumes every
|
|
309
|
+
ready interval. "Stay loaded until allowed" checks `ready?` but calls `consume`
|
|
310
|
+
only when acting, so a turret without a target keeps its shot ready. `Timer` is
|
|
311
|
+
pure and allocates nothing, so it runs on the per-frame path.
|
|
108
312
|
|
|
109
313
|
```ruby
|
|
110
314
|
@spawn_timer = RGame::Engine::Timer.new(0.8) # built once, off the hot path
|
|
@@ -118,19 +322,20 @@ def on_update(dt)
|
|
|
118
322
|
end
|
|
119
323
|
```
|
|
120
324
|
|
|
121
|
-
`consume` carries the overshoot forward
|
|
122
|
-
|
|
123
|
-
span several intervals, loop:
|
|
325
|
+
`consume` carries the overshoot forward instead of zeroing it, so a long-running
|
|
326
|
+
cadence does not drift. `reset` drops accumulated time, for example after you
|
|
327
|
+
change `interval`. When one step may span several intervals, loop:
|
|
328
|
+
`while timer.ready? do …; timer.consume end`.
|
|
124
329
|
|
|
125
|
-
For a node that should tick
|
|
126
|
-
[`Components::Timer`](components.md#timer)
|
|
127
|
-
update tick
|
|
128
|
-
you poll `ready
|
|
330
|
+
For a node that should tick on its own, use
|
|
331
|
+
[`Components::Timer`](components.md#timer). It owns a `Timer`, runs in the node's
|
|
332
|
+
update tick so nothing can forget to drive it, and emits `on_timeout` instead of
|
|
333
|
+
making you poll `ready?` and `consume`.
|
|
129
334
|
|
|
130
335
|
## `Camera` — follow a point, clamp to the world
|
|
131
336
|
|
|
132
|
-
|
|
133
|
-
scrolling view
|
|
337
|
+
**`RGame::Engine::Camera` (`rgame/engine/camera`) holds the follow-and-clamp maths
|
|
338
|
+
for a scrolling view.** It splits the work into two calls:
|
|
134
339
|
|
|
135
340
|
```ruby
|
|
136
341
|
camera = RGame::Engine::Camera.new(world_width: map.pixel_width, world_height: map.pixel_height)
|
|
@@ -139,26 +344,92 @@ camera.resolve(view_width, view_height) # at draw: the offset for *this* viewpor
|
|
|
139
344
|
camera.x, camera.y # the resolved offset
|
|
140
345
|
```
|
|
141
346
|
|
|
142
|
-
`center_on` records the target
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
347
|
+
`center_on` records the target, and `target_x` and `target_y` read it back.
|
|
348
|
+
`resolve` computes the offset, clamped so the
|
|
349
|
+
view never shows past the world's edges. `world_width` and `world_height` are
|
|
350
|
+
optional and writable: a scene sets them when it loads a map. Left `nil`, the
|
|
351
|
+
camera is unbounded and follows its target exactly. With bounds, the target drifts
|
|
352
|
+
off centre near a corner.
|
|
353
|
+
|
|
354
|
+
**The viewport size is an argument, not state**, because one camera can be drawn
|
|
355
|
+
through viewports of different sizes. A half-width viewport clamps
|
|
356
|
+
differently from a full-width one, and the difference shows near a world edge.
|
|
357
|
+
|
|
358
|
+
**A camera belongs to a player**
|
|
359
|
+
([`RGame::Engine::Player#camera`](input.md#players-seats-and-joining)), not to a
|
|
360
|
+
scene, because a scene may have any number of viewers. You never call `resolve`
|
|
361
|
+
yourself. `Viewports` resolves each view's camera against that view's size before
|
|
362
|
+
the frame is drawn. A
|
|
363
|
+
[`CameraFollow`](components.md#camerafollow) component on the followed node points
|
|
364
|
+
the camera, and a [`WorldView`](scene_graph.md#view-transforms-and-the-camera)
|
|
365
|
+
applies it. See `examples/scroll_map` for one camera and `examples/split_screen`
|
|
366
|
+
for two.
|
|
367
|
+
|
|
368
|
+
## Making a character that collides
|
|
369
|
+
|
|
370
|
+
**A character stopped by the world *and* by other characters is four components
|
|
371
|
+
on a plain node.** Nothing connects them by hand:
|
|
147
372
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
373
|
+
```ruby
|
|
374
|
+
require 'rgame'
|
|
375
|
+
|
|
376
|
+
node = RGame::Engine::Node2D.new(x: 240, y: 320)
|
|
377
|
+
node.add_component(RGame::Engine::Components::AnimatedSprite.new(sheet: 'hero.json'))
|
|
378
|
+
node.add_component(RGame::Engine::Components::FeetCollider.new(width: 10, height: 8, layer: :hero))
|
|
379
|
+
node.add_component(RGame::Engine::Components::CharacterBody.new(speed: 60, blocked_by: %i[tiles hero npc]))
|
|
380
|
+
node.add_component(RGame::Engine::Components::PlayerController.new)
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Each line does one job:
|
|
384
|
+
|
|
385
|
+
- The [sprite](components.md#animatedsprite) gives the node its dimensions, and
|
|
386
|
+
reads the body's intent back as a facing.
|
|
387
|
+
- The [`FeetCollider`](components.md#feetcollider) derives a small box at the
|
|
388
|
+
bottom of those dimensions. It is the node's **only** shape.
|
|
389
|
+
- The [`CharacterBody`](components.md#characterbody) names what that shape may not
|
|
390
|
+
pass through.
|
|
391
|
+
- The [controller](components.md#playercontroller) writes an intent each step, and
|
|
392
|
+
the body turns it into a move.
|
|
393
|
+
|
|
394
|
+
An NPC is the same four lines, with a
|
|
395
|
+
[`WanderController`](components.md#wandercontroller) and `layer: :npc`. The hero's
|
|
396
|
+
`blocked_by` names `:npc`, and the NPC's names `:hero`. The two stop each other
|
|
397
|
+
without knowing what the other is. A crowd of NPCs that all declare `:npc` works,
|
|
398
|
+
because a body is never stopped by its own collider.
|
|
399
|
+
|
|
400
|
+
The scene must mount what those names refer to: a
|
|
401
|
+
[`TileWorld`](components.md#tileworld) for `:tiles`, and a
|
|
402
|
+
[`CollisionWorld`](components.md#collisionworld) for the two layer names. **A name
|
|
403
|
+
with nothing behind it raises at attach**, naming what is missing. An actor that
|
|
404
|
+
silently walked through walls would look like a collision bug, with its cause in a
|
|
405
|
+
scene file three directories away.
|
|
406
|
+
|
|
407
|
+
Three things need no remembering:
|
|
408
|
+
|
|
409
|
+
- **Component order does not matter.** The feet box needs the size the sprite
|
|
410
|
+
gives the node. So the collider builds it on first read, not at construction.
|
|
411
|
+
- **The node has one shape, not two.** The body resolves the collider's box. The
|
|
412
|
+
rectangle a fence stops is the rectangle that reports contacts, and
|
|
413
|
+
`collider.box =` retunes both.
|
|
414
|
+
- **Reacting needs no second mechanism.** `body.on_blocked { |by| ... }` fires once
|
|
415
|
+
when something starts stopping the body: a villager, a solid tile or the world's
|
|
416
|
+
edge. A blocked pair ends up *touching*, not overlapping, so `on_hit` is the wrong
|
|
417
|
+
signal. See
|
|
418
|
+
[Blocking and overlapping](systems.md#blocking-and-overlapping-are-two-reports-and-a-pair-gets-one-of-them).
|
|
419
|
+
|
|
420
|
+
[Collision: two indexes, one resolver](systems.md#collision-two-indexes-one-resolver)
|
|
421
|
+
describes the architecture behind those four lines.
|
|
154
422
|
|
|
155
423
|
## `CollisionBox` — an actor's feet box
|
|
156
424
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
[`
|
|
161
|
-
|
|
425
|
+
**`RGame::Engine::CollisionBox` (`rgame/engine/collision_box`) is a character's
|
|
426
|
+
collision rectangle.** It stores an offset and a size **relative to the sprite's
|
|
427
|
+
top-left origin**, independent of the sprite's size. A 32×32 sprite can therefore
|
|
428
|
+
carry a small box at its feet. A [`BoxCollider`](components.md#boxcollider) holds
|
|
429
|
+
one. [`FeetCollider`](components.md#feetcollider) builds this shape from the node's
|
|
430
|
+
dimensions, so a character rarely constructs one by hand. The collision code
|
|
431
|
+
resolves the box, not the sprite, against whatever the body declared: solid tiles,
|
|
432
|
+
other actors, the world's edge.
|
|
162
433
|
|
|
163
434
|
```ruby
|
|
164
435
|
box = RGame::Engine::CollisionBox.bottom_anchored(
|
|
@@ -167,43 +438,39 @@ box = RGame::Engine::CollisionBox.bottom_anchored(
|
|
|
167
438
|
box.aabb(x, y) # => [x + offset_x, y + offset_y, width, height]
|
|
168
439
|
```
|
|
169
440
|
|
|
170
|
-
`bottom_anchored`
|
|
171
|
-
`
|
|
172
|
-
|
|
173
|
-
## `RGame::Engine::I18n` — localization
|
|
441
|
+
`bottom_anchored` covers the common feet box. For anything else, the constructor
|
|
442
|
+
takes `width:` and `height:`, plus optional `offset_x:` and `offset_y:`, both
|
|
443
|
+
defaulting to 0.
|
|
174
444
|
|
|
175
|
-
`
|
|
176
|
-
|
|
177
|
-
fallback locale, and pluralization. It is a **global module** (like the signal
|
|
178
|
-
dispatcher), so `t` is reachable anywhere without wiring. YAML is its only dependency.
|
|
445
|
+
`CollisionBox` also holds rectangle geometry, as class methods over plain numbers.
|
|
446
|
+
A per-frame path can call them without building a box:
|
|
179
447
|
|
|
180
448
|
```ruby
|
|
181
|
-
RGame::Engine::
|
|
182
|
-
RGame::Engine::
|
|
183
|
-
RGame::Engine::I18n.default = :en # fallback when the current locale lacks a key
|
|
184
|
-
RGame::Engine::I18n.locale = :de
|
|
185
|
-
|
|
186
|
-
RGame::Engine::I18n.t("menu.title") # dotted key, resolved in :de then :en
|
|
187
|
-
RGame::Engine::I18n.t(:greeting, name: "Ada") # => "Hello, Ada" from %{name}
|
|
188
|
-
RGame::Engine::I18n.t(:apples, count: 3) # pluralized: { one:, other:, zero? }
|
|
449
|
+
RGame::Engine::CollisionBox.overlap?(x1, y1, w1, h1, x2, y2, w2, h2) # rect vs rect
|
|
450
|
+
RGame::Engine::CollisionBox.overlap_circle?(x, y, w, h, cx, cy, r) # rect vs circle
|
|
189
451
|
```
|
|
190
452
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
453
|
+
**Both tests are half-open**: a shape spans `[x, x + w)`. Shapes that share only
|
|
454
|
+
an edge, or a circle that exactly grazes a box, do not overlap. That is also why
|
|
455
|
+
blocking and contact are separate reports. A blocked pair ends up exactly
|
|
456
|
+
touching, so it does not overlap, and `on_hit` does not fire.
|
|
457
|
+
`CircleCollider.overlap?` agrees, so contact means the same for every pair of
|
|
458
|
+
shapes. A grid needs this convention. Pieces on neighbouring squares border each
|
|
459
|
+
other constantly, and an inclusive test would report each as a contact. The
|
|
460
|
+
broadphase buckets by the same convention, so the two never disagree.
|
|
196
461
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
462
|
+
A [`BoxCollider`](components.md#boxcollider) component is a `CollisionBox` plus a
|
|
463
|
+
registration in the scene's [`CollisionWorld`](components.md#collisionworld). These
|
|
464
|
+
two methods are its narrowphase. [`FeetCollider`](components.md#feetcollider) adds
|
|
465
|
+
the `bottom_anchored` arithmetic, computed from the node's own dimensions.
|
|
200
466
|
|
|
201
467
|
## `AudioBus` — decoupled audio facts
|
|
202
468
|
|
|
203
|
-
|
|
204
|
-
emits audio *facts*
|
|
205
|
-
|
|
206
|
-
sound.
|
|
469
|
+
**`RGame::Engine::AudioBus` (`rgame/engine/audio_bus`) is a global audio bus.**
|
|
470
|
+
Gameplay emits audio *facts* on it, such as "play this sound" or "play this
|
|
471
|
+
music", separate from playback. An `AudioDirector` subscribes and turns the facts
|
|
472
|
+
into sound. The bus is a module, not an instance, so any node reaches it without
|
|
473
|
+
wiring.
|
|
207
474
|
|
|
208
475
|
```ruby
|
|
209
476
|
RGame::Engine::AudioBus.play_sound(:boom)
|
|
@@ -211,34 +478,16 @@ RGame::Engine::AudioBus.play_music(:theme)
|
|
|
211
478
|
RGame::Engine::AudioBus.stop_music
|
|
212
479
|
```
|
|
213
480
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
the
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
`
|
|
223
|
-
`
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
```ruby
|
|
228
|
-
Point = RGame::Engine::Resettable.define(:x, :y)
|
|
229
|
-
p = Point.new(3, 4)
|
|
230
|
-
p.x # => 3 (read-only; no x= setter)
|
|
231
|
-
p.reset(5, 6) # overwrite in place, allocation-free → self
|
|
232
|
-
|
|
233
|
-
Vel = RGame::Engine::Resettable.define(:dx, :dy, keyword_init: true)
|
|
234
|
-
Vel.new(dx: 1, dy: 0).reset(dx: 2, dy: 0)
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
That single in-place `reset` is the only mutability a [`Pool`](#pool--reuse-dont-allocate)
|
|
238
|
-
needs: acquire a recycled instance and `reset` it, without exposing the per-field setters
|
|
239
|
-
a `Struct` would. Methods are generated fixed-arity with direct ivar assignment (as
|
|
240
|
-
Struct/Data do), so `reset` is allocation-free and recycling stays zero-allocation in
|
|
241
|
-
steady state — including the `keyword_init: true` form, which generates named parameters
|
|
242
|
-
(`reset(x:, y:)`) rather than a `**kwargs` splat (the one form that would build a Hash per
|
|
243
|
-
call). Reach for this over a mutable `Struct` whenever a value object is pool-recycled; see
|
|
244
|
-
the Style notes in `CLAUDE.md`.
|
|
481
|
+
**`RGame::Game` subscribes a director when it starts, and calls `unsubscribe`
|
|
482
|
+
when the loop ends.** A game does neither. The release matters. The bus is a
|
|
483
|
+
module, so it holds its listeners until someone removes them. A listener holds the
|
|
484
|
+
director, the audio device, and the asset manager the device resolves paths
|
|
485
|
+
through. That manager holds the `App` it loads images for, window included. A
|
|
486
|
+
single game never notices, because its `App` lives as long as the process. A
|
|
487
|
+
process that runs two games does notice.
|
|
488
|
+
|
|
489
|
+
`play_sound`, `play_music` and `stop_music` form the gameplay API. Under each sits
|
|
490
|
+
an [`RGame::Engine::Signal`](signals.md) (`on_play_sound`, `on_play_music`,
|
|
491
|
+
`on_stop_music`), which the director subscribes to. The engine only emits facts
|
|
492
|
+
and never names an audio device. The bus therefore stays in the engine layer, and
|
|
493
|
+
playback stays in `RGame::Core`.
|