rgame 0.4.0-x86_64-linux-gnu
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 +7 -0
- data/CHANGELOG.md +251 -0
- data/LICENSE +26 -0
- data/README.md +325 -0
- data/docs/api/README.md +224 -0
- data/docs/api/app.md +249 -0
- data/docs/api/assets.md +463 -0
- data/docs/api/audio.md +249 -0
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1112 -0
- data/docs/api/drawing.md +398 -0
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +219 -0
- data/docs/api/images.md +118 -0
- data/docs/api/input.md +476 -0
- data/docs/api/internals.md +289 -0
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +539 -0
- data/docs/api/signals.md +147 -0
- data/docs/api/systems.md +231 -0
- data/docs/api/text.md +133 -0
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +493 -0
- data/docs/api/ui.md +927 -0
- data/docs/api/values.md +316 -0
- 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/lib/rgame/boot.rb +3 -0
- 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 +46 -0
- data/lib/rgame/core/asset_manager.rb +206 -0
- data/lib/rgame/core/audio.rb +145 -0
- data/lib/rgame/core/font.rb +46 -0
- data/lib/rgame/core/gamepad.rb +55 -0
- data/lib/rgame/core/image.rb +55 -0
- data/lib/rgame/core/input.rb +71 -0
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +142 -0
- data/lib/rgame/core/recording.rb +54 -0
- data/lib/rgame/core/renderer.rb +354 -0
- data/lib/rgame/core/sprite_sheet.rb +105 -0
- data/lib/rgame/core/tile_map_renderer.rb +172 -0
- data/lib/rgame/core/ui_atlas.rb +101 -0
- data/lib/rgame/core/virtual_gamepad.rb +26 -0
- data/lib/rgame/core.rb +18 -0
- data/lib/rgame/core_ext.so +0 -0
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +50 -0
- data/lib/rgame/engine/animator.rb +44 -0
- data/lib/rgame/engine/audio_bus.rb +24 -0
- data/lib/rgame/engine/audio_director.rb +59 -0
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +78 -0
- data/lib/rgame/engine/circle_collider.rb +34 -0
- data/lib/rgame/engine/collision_box.rb +59 -0
- data/lib/rgame/engine/collision_system.rb +132 -0
- data/lib/rgame/engine/component.rb +64 -0
- data/lib/rgame/engine/components/action_trigger.rb +40 -0
- data/lib/rgame/engine/components/animated_sprite.rb +71 -0
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +45 -0
- data/lib/rgame/engine/components/character_body.rb +50 -0
- data/lib/rgame/engine/components/circle_collider.rb +80 -0
- data/lib/rgame/engine/components/collision_world.rb +231 -0
- data/lib/rgame/engine/components/despawn_offscreen.rb +42 -0
- 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 +176 -0
- data/lib/rgame/engine/components/player_controller.rb +27 -0
- data/lib/rgame/engine/components/pool.rb +53 -0
- data/lib/rgame/engine/components/screen_wrap.rb +49 -0
- data/lib/rgame/engine/components/sprite.rb +47 -0
- data/lib/rgame/engine/components/targeting.rb +52 -0
- data/lib/rgame/engine/components/thrust_controller.rb +65 -0
- data/lib/rgame/engine/components/tile_world.rb +109 -0
- data/lib/rgame/engine/components/timer.rb +75 -0
- data/lib/rgame/engine/components/velocity.rb +43 -0
- data/lib/rgame/engine/components/wander_controller.rb +64 -0
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +45 -0
- data/lib/rgame/engine/debug_overlay.rb +111 -0
- 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 +322 -0
- data/lib/rgame/engine/input/action_mapper.rb +100 -0
- data/lib/rgame/engine/input/actions.rb +92 -0
- data/lib/rgame/engine/input/input_map.rb +196 -0
- data/lib/rgame/engine/layout.rb +82 -0
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +555 -0
- data/lib/rgame/engine/path.rb +76 -0
- data/lib/rgame/engine/player.rb +69 -0
- data/lib/rgame/engine/player_layer.rb +70 -0
- data/lib/rgame/engine/players.rb +205 -0
- data/lib/rgame/engine/pool.rb +51 -0
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +87 -0
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/signal.rb +75 -0
- data/lib/rgame/engine/spatial_hash.rb +116 -0
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +147 -0
- data/lib/rgame/engine/tile_map_layer.rb +82 -0
- data/lib/rgame/engine/tileset.rb +99 -0
- data/lib/rgame/engine/timer.rb +51 -0
- data/lib/rgame/engine/ui/button.rb +248 -0
- data/lib/rgame/engine/ui/column.rb +20 -0
- data/lib/rgame/engine/ui/icon_button.rb +93 -0
- data/lib/rgame/engine/ui/menu.rb +290 -0
- data/lib/rgame/engine/ui/navigation.rb +57 -0
- data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
- data/lib/rgame/engine/ui/option_button.rb +163 -0
- data/lib/rgame/engine/ui/panel_button.rb +32 -0
- data/lib/rgame/engine/ui/panel_menu.rb +36 -0
- data/lib/rgame/engine/ui/pointing.rb +146 -0
- data/lib/rgame/engine/ui/radial_menu.rb +85 -0
- data/lib/rgame/engine/ui/ring.rb +55 -0
- data/lib/rgame/engine/ui/row.rb +21 -0
- data/lib/rgame/engine/ui/shape_style.rb +102 -0
- data/lib/rgame/engine/ui/stack.rb +58 -0
- data/lib/rgame/engine/ui/stepping.rb +93 -0
- data/lib/rgame/engine/ui/text_button.rb +59 -0
- data/lib/rgame/engine/view.rb +76 -0
- data/lib/rgame/engine/viewports.rb +171 -0
- data/lib/rgame/engine/world_view.rb +71 -0
- data/lib/rgame/engine.rb +86 -0
- data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
- data/lib/rgame/fonts/OFL.txt +102 -0
- data/lib/rgame/game.rb +271 -0
- 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 +23 -0
- data/lib/rgame/util/controls.rb +169 -0
- 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 +3 -0
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +123 -0
- data/lib/rgame/util.rb +10 -0
- data/lib/rgame/util_ext.so +0 -0
- data/lib/rgame/version.rb +12 -0
- data/lib/rgame.rb +5 -0
- data/licenses/SDL2/LICENSE.txt +18 -0
- metadata +309 -0
data/docs/api/drawing.md
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# Drawing
|
|
2
|
+
|
|
3
|
+
Everything on screen goes through `RGame::Core::Renderer`. **In an
|
|
4
|
+
`RGame::Game`, you never build one.** `Game` builds it and passes it to every
|
|
5
|
+
node's `on_draw(renderer, view)`. The examples on this page build one on a plain
|
|
6
|
+
`App` instead, to show the calls without a scene graph:
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
require 'rgame'
|
|
10
|
+
require 'rgame/core'
|
|
11
|
+
|
|
12
|
+
class MyGame < RGame::Core::App
|
|
13
|
+
Color = RGame::Util::Color
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
super(width: 800, height: 600, caption: 'demo')
|
|
17
|
+
@renderer = RGame::Core::Renderer.new(self)
|
|
18
|
+
@hero = RGame::Core::Image.new(self, 'hero.png')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def draw
|
|
22
|
+
@renderer.rect(40, 40, 160, 100, color: Color.new(224, 64, 64))
|
|
23
|
+
@renderer.circle(620, 110, 70, color: Color.new(64, 96, 224))
|
|
24
|
+
@renderer.image(@hero, 400, 300, angle: 45)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
MyGame.new.run
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Know two rules before anything else.
|
|
32
|
+
|
|
33
|
+
**Draw only inside `draw`**, or a node's `on_draw`. A drawing call from `update`
|
|
34
|
+
or from a constructor raises. The frame is not open then, so the call would vanish without a trace.
|
|
35
|
+
A loud failure beats an invisible one. `renderer.drawing?` returns whether a frame
|
|
36
|
+
is open.
|
|
37
|
+
|
|
38
|
+
**The renderer draws nothing immediately.** It collects calls, sorts the frame
|
|
39
|
+
and sends it to the GPU once, after `draw` returns. So call order does not decide
|
|
40
|
+
what ends up on top; the scene tree does. See "Draw order" below.
|
|
41
|
+
|
|
42
|
+
## Coordinates, colours and z
|
|
43
|
+
|
|
44
|
+
| | |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Origin | Top-left. x grows right, y grows **down**. |
|
|
47
|
+
| Angles | Degrees. A **positive angle turns clockwise** on screen. |
|
|
48
|
+
| `z:` | Where this call sits among **this node's own** drawing. −512…511. |
|
|
49
|
+
| `color:` | `nil` (white), `[r, g, b]`, `[r, g, b, a]`, or a `RGame::Util::Color`. |
|
|
50
|
+
|
|
51
|
+
**`z:` is an offset inside the current layer**, not a global number. It puts a
|
|
52
|
+
node's panel under its label and its shadow under its sprite. It cannot reach
|
|
53
|
+
anything else. A value outside −512…511 raises.
|
|
54
|
+
|
|
55
|
+
Shapes default to `50`, text to `10` and images to `0`. A debug box or a health
|
|
56
|
+
bar without a `z:` therefore lands on top of *that node's* sprite. Calls with
|
|
57
|
+
equal z keep their call order. Without that rule, two sprites on one layer could
|
|
58
|
+
swap places from frame to frame, and players would see flicker.
|
|
59
|
+
|
|
60
|
+
## Draw order
|
|
61
|
+
|
|
62
|
+
**The renderer orders a frame in three steps, coarsest first.** A drawing call
|
|
63
|
+
passes a number only for the last step.
|
|
64
|
+
|
|
65
|
+
1. **The band**: `:world` (the default), `:hud`, `:overlay` or `:debug`.
|
|
66
|
+
Everything in one band lies under everything in the next.
|
|
67
|
+
2. **The slot.** The traversal walks the scene tree depth-first, siblings in `z`
|
|
68
|
+
order. Each node takes the next slot in its band when the walk reaches it.
|
|
69
|
+
Draw order is therefore **tree order**. A node's subtree forms one unbroken
|
|
70
|
+
run and cannot straddle a sibling.
|
|
71
|
+
3. **The offset**: the `z:` above, inside one node's slot.
|
|
72
|
+
|
|
73
|
+
A scene graph arranges all of this. `RGame::Engine::Node2D#draw` opens a layer per
|
|
74
|
+
node, so a game sets `z` on nodes and a `band` on the few that start one. See
|
|
75
|
+
[scene_graph.md](scene_graph.md), "Draw order".
|
|
76
|
+
|
|
77
|
+
### Opening a layer by hand
|
|
78
|
+
|
|
79
|
+
Code that draws outside the scene tree opens its own layer. The debug overlay, a
|
|
80
|
+
spec and a script all do:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
renderer.layered(:hud) do
|
|
84
|
+
renderer.nine_slice(:panel, x, y, w, h)
|
|
85
|
+
renderer.text(score, x + 8, y + 6, z: 1) # above this layer's own panel
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`layered` takes the next slot in that band and measures every `z:` inside the
|
|
90
|
+
block from it. Afterwards it restores the previous base, even when the block
|
|
91
|
+
raises. Nesting *replaces* the base; it does not add to it. A node's slot depends
|
|
92
|
+
on where the traversal reached it, not on its ancestors' picks. Outside any block
|
|
93
|
+
the base is 0, so a bare script gets exactly the z it passes.
|
|
94
|
+
|
|
95
|
+
`renderer.layer` returns the base in effect.
|
|
96
|
+
|
|
97
|
+
### Why bands exist
|
|
98
|
+
|
|
99
|
+
A frame holds three kinds of content:
|
|
100
|
+
|
|
101
|
+
- **World**: inside a `WorldView`, drawn once per viewport, under a camera.
|
|
102
|
+
- **A player's own screen space**: their HUD and menu, drawn once and clipped to
|
|
103
|
+
their viewport (`PlayerLayer`).
|
|
104
|
+
- **Global screen space**: a cutscene or a results panel, drawn once across the
|
|
105
|
+
whole window.
|
|
106
|
+
|
|
107
|
+
The world is a different *space* from the other two, and the tree enforces that:
|
|
108
|
+
`WorldView` draws its subtree once per viewport. The other two share one space,
|
|
109
|
+
and the band tells them apart.
|
|
110
|
+
|
|
111
|
+
Two viewports may interleave in the sort without harm. Their commands carry
|
|
112
|
+
different clips and land on different pixels. Order *within* one viewport
|
|
113
|
+
matters, and drawing a HUD after the world does not put it on top. Its band
|
|
114
|
+
does. Bands lie `2**40` apart and a `z:` spans 1024, so no offset can carry a
|
|
115
|
+
call into the next band. See `RGame::Util::Z`.
|
|
116
|
+
|
|
117
|
+
### Colours and allocation
|
|
118
|
+
|
|
119
|
+
**Passing a `Color` allocates nothing.** A `Color` is a frozen value, so every
|
|
120
|
+
sprite can share the same one. Passing an array allocates a colour on every
|
|
121
|
+
call. That is fine at setup and wasteful sixty times a second:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
RED = RGame::Util::Color.new(224, 64, 64) # once
|
|
125
|
+
|
|
126
|
+
def draw
|
|
127
|
+
@renderer.rect(10, 10, 50, 50, color: RED) # every frame, allocation-free
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Shapes
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
renderer.rect(x, y, width, height, z: 50, color: nil)
|
|
135
|
+
renderer.quad(x1, y1, x2, y2, x3, y3, x4, y4, z: 50, color: nil)
|
|
136
|
+
renderer.triangle(x1, y1, x2, y2, x3, y3, z: 50, color: nil)
|
|
137
|
+
renderer.line(x1, y1, x2, y2, thickness: 1.0, z: 50, color: nil)
|
|
138
|
+
renderer.circle(cx, cy, radius, z: 50, color: nil, segments: 64)
|
|
139
|
+
renderer.debug_box(x, y, width, height, z: 50)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
A **quad** takes its four points in loop order: top-left, top-right,
|
|
143
|
+
bottom-right, bottom-left for a rectangle. Points in Z order give an hourglass.
|
|
144
|
+
|
|
145
|
+
A **line** has real thickness, because the renderer draws it as a quad. Drivers
|
|
146
|
+
may ignore OpenGL's own line width above one pixel.
|
|
147
|
+
|
|
148
|
+
A **circle** is a fan of triangles in one batch. It needs no cached texture and
|
|
149
|
+
no configuration. Adjust `segments:` if 64 is too many or too few.
|
|
150
|
+
|
|
151
|
+
`debug_box` draws a translucent red rectangle to show a collision box. A scene
|
|
152
|
+
can ask for one without choosing a debug colour.
|
|
153
|
+
|
|
154
|
+
## Images
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
renderer.image(image, cx, cy, angle: 0, scale: 1, z: 0, color: nil)
|
|
158
|
+
renderer.image_at(image, x, y, scale_x: 1, scale_y: 1, z: 0, color: nil)
|
|
159
|
+
renderer.background(image, x = 0, y = 0, z: 0, color: nil)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Each method anchors the image differently:
|
|
163
|
+
|
|
164
|
+
- `image` **centres** the image on the position and rotates it about that
|
|
165
|
+
centre. Use it for sprites.
|
|
166
|
+
- `image_at` places the **top-left** corner and scales each axis separately. Use
|
|
167
|
+
it for tiles, nine-slice corners and sheet frames.
|
|
168
|
+
- `background` is `image_at` at natural size.
|
|
169
|
+
|
|
170
|
+
### Mirroring
|
|
171
|
+
|
|
172
|
+
**A negative scale on `image_at` mirrors the image inside the same rectangle.**
|
|
173
|
+
It does not move the image:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
renderer.image_at(frame, x, y, scale_x: facing_left ? -1 : 1)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Both calls cover the same pixels; only the picture is reversed. `(x, y)` stays
|
|
180
|
+
the top-left corner whatever the scale's sign, so a mirrored sprite stays put.
|
|
181
|
+
Mirroring about the anchor would shift the image one width to the left. Every
|
|
182
|
+
flipped draw would then have to add that width back.
|
|
183
|
+
|
|
184
|
+
A scale of `0` draws nothing.
|
|
185
|
+
|
|
186
|
+
`color:` tints the image by multiplying its pixels. White leaves the image
|
|
187
|
+
unchanged, and a colour with alpha fades it.
|
|
188
|
+
|
|
189
|
+
[Images](images.md) covers loading files and slicing tiles; [Assets](assets.md#sprite-sheets) covers sprite sheets.
|
|
190
|
+
|
|
191
|
+
**Only the app that loaded an image can draw it.** A GPU texture belongs to one
|
|
192
|
+
window's OpenGL context. Drawing another app's image would sample nothing and
|
|
193
|
+
paint a plain white rectangle, so the renderer raises `ArgumentError` instead. A
|
|
194
|
+
one-window game never meets this.
|
|
195
|
+
|
|
196
|
+
## Drawing by id
|
|
197
|
+
|
|
198
|
+
**Game logic names an asset; it does not hold one.** The scene layer may hold
|
|
199
|
+
`RGame::Util` values but no `RGame::Core` handle. A Symbol or a path is the only
|
|
200
|
+
thing a node *can* carry.
|
|
201
|
+
|
|
202
|
+
An id is normally a **root-relative path**. The renderer resolves it through the
|
|
203
|
+
app's [asset manager](assets.md) and remembers the result:
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
renderer.sprite('hero.json', row, col, x, y, flip_x: false, z: 0)
|
|
207
|
+
renderer.image('space.png', cx, cy, angle: 0, scale: 1)
|
|
208
|
+
renderer.background('space.png')
|
|
209
|
+
renderer.tilemap('map/island.tmx', layer, cull_x, cull_y, cull_w, cull_h, elapsed: 0.0) # draws in world coordinates
|
|
210
|
+
renderer.nine_slice(:panel, x, y, width, height, z: 0, tint: nil)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Paths need no setup. `Renderer.new(app)` uses the app's own manager.
|
|
214
|
+
`Renderer.new(app, assets: other)` uses a different one.
|
|
215
|
+
|
|
216
|
+
### Registering
|
|
217
|
+
|
|
218
|
+
`register_*` binds an id to an object you choose, and takes priority over the
|
|
219
|
+
asset manager. Use it for what a path cannot name: an id that is not a file, and
|
|
220
|
+
an object the game built itself.
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
renderer.register_image(:space, app.assets.image('space.png'))
|
|
224
|
+
renderer.register_sheet(:hero, app.assets.sheet('hero.json'))
|
|
225
|
+
renderer.register_tilemap(:level1, app.assets.tilemap('island.tmx'))
|
|
226
|
+
renderer.register_nine_slice(:panel, atlas.nine_slices[:panel])
|
|
227
|
+
renderer.register_ui_atlas(atlas) # every element under its own name
|
|
228
|
+
|
|
229
|
+
renderer.image(:space, 100, 100)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Nine-slices must be registered.** Their ids name an *element of an atlas*, not
|
|
233
|
+
a file, so an asset manager has nothing to resolve.
|
|
234
|
+
|
|
235
|
+
### How the renderer resolves an id
|
|
236
|
+
|
|
237
|
+
| Given | |
|
|
238
|
+
|---|---|
|
|
239
|
+
| An `Image` | drawn directly — `#image`, `#image_at` and `#background` all take one |
|
|
240
|
+
| A registered id | the registered object |
|
|
241
|
+
| A `String` | resolved through the asset manager, then remembered |
|
|
242
|
+
| A `Symbol` that is not registered | `KeyError`, naming the id and the type |
|
|
243
|
+
| `nil` | `TypeError` |
|
|
244
|
+
|
|
245
|
+
The renderer never offers a Symbol to the asset manager, because only a String
|
|
246
|
+
can be a path. A mistyped Symbol therefore raises "no sheet registered for
|
|
247
|
+
`:heor`". A broken *file* raises its own `LoadError` naming the file. The two
|
|
248
|
+
errors point at two different fixes.
|
|
249
|
+
|
|
250
|
+
The renderer resolves each id once and keeps the answer. Per-frame drawing
|
|
251
|
+
neither resolves again nor allocates a lookup key.
|
|
252
|
+
|
|
253
|
+
## Transform blocks
|
|
254
|
+
|
|
255
|
+
Each block applies to everything drawn inside it and undoes itself afterwards,
|
|
256
|
+
even when the block raises.
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
renderer.translated(dx, dy) { ... }
|
|
260
|
+
renderer.rotated(angle, pivot_x, pivot_y) { ... }
|
|
261
|
+
renderer.scaled(sx, sy = sx) { ... }
|
|
262
|
+
renderer.clipped(x, y, width, height) { ... }
|
|
263
|
+
renderer.layered(band) { ... } # see "Draw order" above
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Blocks nest and compose in the order you open them:
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
renderer.translated(-camera.x, -camera.y) do # world space -> screen space
|
|
270
|
+
renderer.rotated(ship.angle, ship.x, ship.y) do
|
|
271
|
+
renderer.image(hull, ship.x, ship.y)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**A camera is a `translated` block.** Because the offset applies at draw time,
|
|
277
|
+
the same world can be drawn twice under two different offsets. That is
|
|
278
|
+
split-screen.
|
|
279
|
+
|
|
280
|
+
`rotated(0, …)`, `translated(0, 0)` and `scaled(1)` cost nothing. They skip the
|
|
281
|
+
transform and run the block, so unrotated drawing pays nothing.
|
|
282
|
+
|
|
283
|
+
**Inside a scene graph you rarely open a transform block yourself.** The examples
|
|
284
|
+
on this page drive the renderer from an `App`, in window coordinates. For a
|
|
285
|
+
`Node2D`, the traversal pushes the node's transform before it calls `on_draw`. A
|
|
286
|
+
node therefore draws at *its own* origin, and passing its position would apply it
|
|
287
|
+
twice. See [Scene graph](scene_graph.md#drawing-happens-in-local-space).
|
|
288
|
+
|
|
289
|
+
### Clipping and split-screen
|
|
290
|
+
|
|
291
|
+
**A clip narrows.** A nested clip intersects with its parent, so a child never
|
|
292
|
+
draws outside the region its parent allowed. Two clipped blocks make a split
|
|
293
|
+
screen:
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
def draw
|
|
297
|
+
@renderer.clipped(0, 0, 400, 600) do
|
|
298
|
+
@renderer.translated(-@player_one.x, -@player_one.y) { draw_world }
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
@renderer.clipped(400, 0, 400, 600) do
|
|
302
|
+
@renderer.translated(400 - @player_two.x, -@player_two.y) { draw_world }
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**A game does not write that.**
|
|
308
|
+
[`RGame::Engine::WorldView`](scene_graph.md#view-transforms-and-the-camera) does it
|
|
309
|
+
once per active player, with the layout's rectangles and each player's camera.
|
|
310
|
+
Call `clipped` directly for a region of your own, such as a minimap or a
|
|
311
|
+
scrolling list.
|
|
312
|
+
|
|
313
|
+
## Recordings: bake once, replay cheaply
|
|
314
|
+
|
|
315
|
+
**`record` bakes a block of drawing, and a replay costs one call per texture.** A
|
|
316
|
+
tile layer holds a few thousand quads that stay the same once the level loads, so
|
|
317
|
+
it is the typical case:
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
def draw
|
|
321
|
+
@ground ||= @renderer.record do
|
|
322
|
+
@tiles.each { |tile| @renderer.image(tile.image, tile.x, tile.y) }
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
@ground.draw(-@camera.x, -@camera.y)
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Baking draws nothing; the block's output goes into the recording, not the frame.
|
|
330
|
+
`record` must run inside `draw` like every other call. The example therefore
|
|
331
|
+
bakes on the first frame, not in `initialize`.
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
baked.draw(x = 0, y = 0, z: 0, color: nil)
|
|
335
|
+
baked.batch_count # GL calls one replay costs
|
|
336
|
+
baked.vertex_count # vertices baked in
|
|
337
|
+
baked.width # the size of what was baked, with #height
|
|
338
|
+
baked.empty?
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**A recording bakes in positions, texture coordinates, colours and any
|
|
342
|
+
transforms inside the block.** The transform in effect at replay applies on top.
|
|
343
|
+
A baked layer scrolls under a camera without a rebuild, and one recording can be
|
|
344
|
+
stamped in several places:
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
5.times { |i| @bush.draw(i * 120, 300) }
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**`color:` tints the replay.** The renderer multiplies each recorded colour by
|
|
351
|
+
it, so you can fade a whole baked layer at once.
|
|
352
|
+
|
|
353
|
+
**A recording cannot contain a clip.** Clipping happens at rasterisation, so a
|
|
354
|
+
clip captured in one place would be wrong everywhere else the recording is drawn.
|
|
355
|
+
Pushing a clip inside a `record` block raises. Clip the replay instead:
|
|
356
|
+
|
|
357
|
+
```ruby
|
|
358
|
+
@renderer.clipped(0, 0, 400, 600) { @ground.draw(-@camera.x, -@camera.y) }
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Recordings do not nest. A block that raises leaves no half-built recording
|
|
362
|
+
behind. A recording keeps its baked images alive, so dropping a sprite sheet
|
|
363
|
+
after baking does not free its texture.
|
|
364
|
+
|
|
365
|
+
## Testing what a scene draws
|
|
366
|
+
|
|
367
|
+
**Treat the renderer as an interface, not a class your game names.** Game logic
|
|
368
|
+
receives a renderer and calls its methods. A headless spec passes a recording
|
|
369
|
+
fake instead and asserts on the calls. rgame's own suite uses `FakeRenderer`
|
|
370
|
+
from `spec/support/`:
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
renderer = FakeRenderer.new
|
|
374
|
+
health_bar.on_draw(renderer, nil)
|
|
375
|
+
|
|
376
|
+
expect(renderer.calls_to(:rect).map(&:args)).to eq([[10, 10, 64, 8]])
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
The fake also records recordings, and keeps two questions apart: what was baked,
|
|
380
|
+
and where it was replayed.
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
ground = renderer.record { ... } # => a FakeRecording
|
|
384
|
+
|
|
385
|
+
expect(ground.calls.size).to eq(tiles.size) # baked once, not per frame
|
|
386
|
+
expect(ground.draws.map(&:args)).to eq([[-camera.x, -camera.y]])
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
These specs run with no window, no GPU and no clock. One shared contract,
|
|
390
|
+
`spec/support/shared_examples/a_renderer.rb`, checks both the fake and the real
|
|
391
|
+
renderer. A fake that drifted from the real renderer would keep the suite green
|
|
392
|
+
while the game stopped drawing.
|
|
393
|
+
|
|
394
|
+
## Text
|
|
395
|
+
|
|
396
|
+
`renderer.text(string, x, y)` draws a line of text, and `text_width` measures
|
|
397
|
+
one. Both take a String or an [`Engine::Text`](toolbox.md#text--the-string-a-node-draws). [Text](text.md) covers fonts, the shipped default and the characters it
|
|
398
|
+
covers.
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
`examples/` holds one small program per concept. Each is a single `main.rb` that
|
|
4
|
+
runs on its own:
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
ruby examples/walk/main.rb
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
**An example answers "how do I do *X*".** The complete games under
|
|
11
|
+
`test_projects/` answer that poorly. An example shows one concept in one file,
|
|
12
|
+
and its header comment explains the concept at length. This page is the index;
|
|
13
|
+
the file is the long version.
|
|
14
|
+
|
|
15
|
+
Every example has an acceptance test as well. A scripted input track lives at
|
|
16
|
+
`tools/drive/examples/<name>.rb`. The harness feeds that track to the unmodified
|
|
17
|
+
program and reports what the game asked for:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
ruby tools/drive_test_project.rb examples/walk/main.rb --ticks 240
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The examples' assets live under `examples/assets/` and are redistributable, so
|
|
24
|
+
every example runs from a fresh clone. Most examples need no art at all.
|
|
25
|
+
|
|
26
|
+
## Movement and drawing
|
|
27
|
+
|
|
28
|
+
### walk
|
|
29
|
+
|
|
30
|
+
A player-controlled sprite: the smallest complete game. It is a plain node with
|
|
31
|
+
three components, and no component knows about the others.
|
|
32
|
+
|
|
33
|
+
**Uses:** `Node2D`, `Components::AnimatedSprite`, `Components::CharacterBody`,
|
|
34
|
+
`Components::PlayerController`, `InputMap.default`.
|
|
35
|
+
|
|
36
|
+
### sprite
|
|
37
|
+
|
|
38
|
+
One frame drawn at a node, with no animation. Most things in a game look like
|
|
39
|
+
this. The draw passes no position and no angle, because the node's transform
|
|
40
|
+
already applies.
|
|
41
|
+
|
|
42
|
+
**Uses:** `Components::Sprite`, `Image#subimage`, `renderer.register_image`.
|
|
43
|
+
|
|
44
|
+
### velocity
|
|
45
|
+
|
|
46
|
+
The second way a node moves. A character has an *intent*. A rock has a velocity,
|
|
47
|
+
and a component integrates it, spin included.
|
|
48
|
+
|
|
49
|
+
**Uses:** `Components::Velocity`, `Components::ScreenWrap`, `Components::World`,
|
|
50
|
+
`Components::WorldBounds`.
|
|
51
|
+
|
|
52
|
+
## The world
|
|
53
|
+
|
|
54
|
+
### scroll_map
|
|
55
|
+
|
|
56
|
+
A Tiled map larger than the window, scrolled by a player. No call pans the
|
|
57
|
+
camera. A component on a node points the camera, so scrolling is walking.
|
|
58
|
+
|
|
59
|
+
**Uses:** the `:tilemap` asset loader, `Components::TileWorld`, `TileMapLayer`,
|
|
60
|
+
`WorldView`, `Camera`, `Components::CameraFollow`.
|
|
61
|
+
|
|
62
|
+
### collision
|
|
63
|
+
|
|
64
|
+
Object-to-object collision. A scene-scoped system pairs up shapes each step and
|
|
65
|
+
tells both sides they overlapped. It never learns what either object is.
|
|
66
|
+
|
|
67
|
+
**Uses:** `Components::CollisionWorld`, `Components::CircleCollider`,
|
|
68
|
+
`Components::BoxCollider`, `Components::Velocity`, `Engine::Text`.
|
|
69
|
+
|
|
70
|
+
### collision_tiles
|
|
71
|
+
|
|
72
|
+
A character against a grid of solid tiles, sliding along a wall while walking
|
|
73
|
+
diagonally. East of the start, a spiky ball stops the hero and costs a life. One
|
|
74
|
+
feet box is thus stopped by two indexes: the map's grid and the broadphase.
|
|
75
|
+
`blocked_by: %i[tiles spike]` is the only place the difference shows. The life is
|
|
76
|
+
spent in `on_blocked`, because a blocked pair ends up touching, not overlapping.
|
|
77
|
+
`on_hit` never fires for it.
|
|
78
|
+
|
|
79
|
+
**Uses:** `Components::TileWorld`, `Components::CollisionWorld`,
|
|
80
|
+
`Components::FeetCollider`, `Components::CharacterBody` with
|
|
81
|
+
`blocked_by: %i[tiles spike]` and `on_blocked`, `Components::CameraFollow`,
|
|
82
|
+
`Engine::Text`.
|
|
83
|
+
|
|
84
|
+
### jump_topdown
|
|
85
|
+
|
|
86
|
+
A hop in a top-down view. "Up" on screen is north, so a jump cannot move the
|
|
87
|
+
character. The sprite rises along `Hop`'s parabola. The feet box, the shadow and
|
|
88
|
+
the camera stay on the ground. A hop at the fence therefore does not clear it,
|
|
89
|
+
because the part that collides never leaves the ground. The game decides what a
|
|
90
|
+
hop may cross, through `airborne?`.
|
|
91
|
+
|
|
92
|
+
**Uses:** `Components::Hop`, `Node2D#elevation`, `Components::AnimatedSprite`,
|
|
93
|
+
`Components::FeetCollider`, `Components::CharacterBody`, `Components::TileWorld`,
|
|
94
|
+
`Components::CameraFollow`, `InputMap.default.merge`.
|
|
95
|
+
|
|
96
|
+
### pathfinding
|
|
97
|
+
|
|
98
|
+
Pick a tile, and the hero works out how to get there. Small dots show the route
|
|
99
|
+
the search found, one per tile. Lines show the route the hero walks. The
|
|
100
|
+
navigator pulls the route tight, keeping each line straight as long as the hero's
|
|
101
|
+
feet box fits.
|
|
102
|
+
|
|
103
|
+
**Uses:** `Components::Navigator`, `Components::TileWorld#nav_grid`,
|
|
104
|
+
`Components::AnimatedSprite`, `Components::ActionTrigger`,
|
|
105
|
+
`Components::CameraFollow`, `Engine::Text.computed` over `I18n.t` with plurals.
|
|
106
|
+
|
|
107
|
+
## Structure
|
|
108
|
+
|
|
109
|
+
### signals
|
|
110
|
+
|
|
111
|
+
Declaring your own signal. A pressure plate announces that it was pressed, and
|
|
112
|
+
does nothing more. The door and the lamp connect to it; the plate never names
|
|
113
|
+
them.
|
|
114
|
+
|
|
115
|
+
**Uses:** `Signal::DSL`, `Signal.define`, `Components::ActionTrigger`, the
|
|
116
|
+
connect handle.
|
|
117
|
+
|
|
118
|
+
### timer
|
|
119
|
+
|
|
120
|
+
Periodic behaviour that no input drives: a spawn cadence and a one-shot, with two
|
|
121
|
+
cadences on one node.
|
|
122
|
+
|
|
123
|
+
**Uses:** `Components::Timer` (repeating and `repeating: false`, and `as:`),
|
|
124
|
+
`Engine::Timer`.
|
|
125
|
+
|
|
126
|
+
### pooling
|
|
127
|
+
|
|
128
|
+
Spawning many things without building any of them. The allocation count on
|
|
129
|
+
screen makes the case.
|
|
130
|
+
|
|
131
|
+
**Uses:** `Components::Pool`, `Engine::Pool`, `Components::DespawnOffscreen`,
|
|
132
|
+
`Components::Timer`, `Engine::Text`.
|
|
133
|
+
|
|
134
|
+
## UI
|
|
135
|
+
|
|
136
|
+
### game_menu
|
|
137
|
+
|
|
138
|
+
A menu that opens over a running world. Pausing belongs to a node, so only the
|
|
139
|
+
hero stops while the villagers walk on.
|
|
140
|
+
|
|
141
|
+
**Uses:** `PlayerLayer`, `UI::PanelMenu`, `UI::PanelButton`, `UI::Menu#open` /
|
|
142
|
+
`#close`, `Node2D#paused`, `renderer.nine_slice`.
|
|
143
|
+
|
|
144
|
+
### menu_navigation
|
|
145
|
+
|
|
146
|
+
Several screens (title, settings, back) and settings that change something real
|
|
147
|
+
and survive a restart. It contrasts pushing a scene with replacing one.
|
|
148
|
+
|
|
149
|
+
**Uses:** `Scene::SceneStack`, `UI::OptionButton`, `Util::SaveFile`,
|
|
150
|
+
`RGame::Game`'s fullscreen, scale mode and volume.
|
|
151
|
+
|
|
152
|
+
### radial_menu
|
|
153
|
+
|
|
154
|
+
Choosing by pointing. Eight icons sit on a wheel, focused by the direction of the
|
|
155
|
+
stick or the arrow keys. A released stick selects nothing, so pressing A at rest
|
|
156
|
+
never picks what the stick passed on its way back.
|
|
157
|
+
|
|
158
|
+
**Uses:** `UI::RadialMenu`, `UI::IconButton` on a disc `UI::ShapeStyle`,
|
|
159
|
+
`ui_radial_x` / `ui_radial_y`, and a UI atlas's `images` (`icons.json`).
|
|
160
|
+
|
|
161
|
+
### quick_wheel
|
|
162
|
+
|
|
163
|
+
The same eight icons, on a wheel held open by Tab or the left shoulder button.
|
|
164
|
+
Releasing the button chooses. A stick released a moment before the button still
|
|
165
|
+
chooses; a stick at rest chooses nothing. The world drifts at a quarter speed
|
|
166
|
+
while the wheel is open.
|
|
167
|
+
|
|
168
|
+
**Uses:** `UI::RadialMenu` with `trigger:`, `UI::Pointing`'s grace window,
|
|
169
|
+
`UI::Menu#on_opened` / `#on_closed`, `InputMap.default.merge`.
|
|
170
|
+
|
|
171
|
+
### skill_bar
|
|
172
|
+
|
|
173
|
+
A tool bar with five tools in a row. Left and right step through them and Enter
|
|
174
|
+
uses one. The number keys use a tool directly, without moving the focus. Holding
|
|
175
|
+
a number uses its tool once. Pressing a tool's number and Enter together also
|
|
176
|
+
uses it once.
|
|
177
|
+
|
|
178
|
+
**Uses:** `UI::Row`, `UI::Stepping` taking its axis from the layout,
|
|
179
|
+
`UI::Button`'s `hotkey:` and `activate_on: :press`, captioned `UI::IconButton`s
|
|
180
|
+
on a disc `UI::ShapeStyle`, `InputMap.default.merge`, and a UI atlas's `images`
|
|
181
|
+
(`skills.json`).
|
|
182
|
+
|
|
183
|
+
## Audio
|
|
184
|
+
|
|
185
|
+
### sound
|
|
186
|
+
|
|
187
|
+
A sound effect fired by a button, and the path it travels. A node may not name
|
|
188
|
+
the audio device, so it emits a fact and a director plays it.
|
|
189
|
+
|
|
190
|
+
**Uses:** `Core::Sample`, `Engine::AudioBus`, `Engine::AudioDirector`,
|
|
191
|
+
`Engine::Text`.
|
|
192
|
+
|
|
193
|
+
### music
|
|
194
|
+
|
|
195
|
+
The other kind of sound: one streamed voice. You can stop it and ask whether it
|
|
196
|
+
plays, and starting it again does not restart it.
|
|
197
|
+
|
|
198
|
+
**Uses:** `Core::Song`, `AudioBus.play_music` / `.stop_music`,
|
|
199
|
+
`Engine::AudioDirector`.
|
|
200
|
+
|
|
201
|
+
## Players and input
|
|
202
|
+
|
|
203
|
+
### split_screen
|
|
204
|
+
|
|
205
|
+
Two players in one world. `WorldView` draws the world once per viewport, through
|
|
206
|
+
that viewport's camera. The world never knows how often it is drawn.
|
|
207
|
+
|
|
208
|
+
**Uses:** `Game.new(players: 2)`, `Engine::Players`, `Engine::WorldView`,
|
|
209
|
+
`Engine::PlayerLayer`, `Engine::Camera`, `Components::CameraFollow`,
|
|
210
|
+
`input_owner`.
|
|
211
|
+
|
|
212
|
+
### input_glyphs
|
|
213
|
+
|
|
214
|
+
Prompts that match the device in the player's hands. They switch between
|
|
215
|
+
keyboard and controller mid-session. Nothing listens for a plugged-in pad; using
|
|
216
|
+
the pad takes the seat.
|
|
217
|
+
|
|
218
|
+
**Uses:** `Controls.gamepad?`, `InputMap#button_for`, `Engine::Players` with
|
|
219
|
+
`on_unassigned_input` defaulting to `:takeover`, `renderer.sprite`.
|
|
220
|
+
|
|
221
|
+
## The window
|
|
222
|
+
|
|
223
|
+
### fullscreen
|
|
224
|
+
|
|
225
|
+
Opening fullscreen, switching while the game runs, and all four scale modes with
|
|
226
|
+
the layout following each.
|
|
227
|
+
|
|
228
|
+
**Uses:** `RGame::Game.new(fullscreen:)`, `App#fullscreen?` / `#fullscreen=`,
|
|
229
|
+
`RGame::Game#scale_mode=`, the `view` a node is drawn with,
|
|
230
|
+
`InputMap.default.merge`.
|
|
231
|
+
|
|
232
|
+
## Language
|
|
233
|
+
|
|
234
|
+
### localization
|
|
235
|
+
|
|
236
|
+
The same screen in English and German. A `Text` with a `count` picks its plural
|
|
237
|
+
form, and one with a variable shows the current locale. German lacks one key, so
|
|
238
|
+
that line falls back to English. The player's language choice overrides the
|
|
239
|
+
operating system's and is saved.
|
|
240
|
+
|
|
241
|
+
**Uses:** `Engine::I18n`, `Engine::Text` and `Text.literal`, `UI::Menu`'s
|
|
242
|
+
`scope:`, `RGame::Game.new(locales:)`, `RGame::Core.preferred_locales`,
|
|
243
|
+
`Util::SaveFile`.
|
|
244
|
+
|
|
245
|
+
## Persistence
|
|
246
|
+
|
|
247
|
+
### save_load
|
|
248
|
+
|
|
249
|
+
Writing game state to disk and restoring it. The tree is not saved: a scene is a
|
|
250
|
+
recipe, and a save file is state. The variable holding a singular thing restores
|
|
251
|
+
it, and array order restores a flock.
|
|
252
|
+
|
|
253
|
+
**Uses:** `Util::SaveFile`, the `examples/walk` composition with a
|
|
254
|
+
`WanderController`.
|
|
255
|
+
|
|
256
|
+
### save_load_ids
|
|
257
|
+
|
|
258
|
+
The case `save_load` leaves out: a collection whose members can be lost, and one
|
|
259
|
+
saved object that refers to another. A reference forces ids; a changing
|
|
260
|
+
collection alone does not.
|
|
261
|
+
|
|
262
|
+
**Uses:** `Components::Identity`, `Util::SaveFile`, a save of records rather
|
|
263
|
+
than positions.
|