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/scene_graph.md
CHANGED
|
@@ -1,73 +1,196 @@
|
|
|
1
1
|
# Scene graph
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
**A game is a tree of nodes.** A node holds the state, logic and drawing of one
|
|
4
|
+
game object, and nested nodes build whole scenes. The tree is pure Ruby. Nodes
|
|
5
|
+
draw through the renderer interface and read input from a per-tick snapshot. They
|
|
6
|
+
never name a graphics library.
|
|
7
7
|
|
|
8
8
|
## Node2D
|
|
9
9
|
|
|
10
|
-
`RGame::Engine::Node2D` (`engine/node2d`) is the basic building block.
|
|
11
|
-
|
|
10
|
+
`RGame::Engine::Node2D` (`engine/node2d`) is the basic building block. A node
|
|
11
|
+
carries:
|
|
12
12
|
|
|
13
|
-
- a **transform
|
|
14
|
-
that orders it among its siblings;
|
|
15
|
-
- **children
|
|
16
|
-
- **components
|
|
17
|
-
- a **parent
|
|
13
|
+
- a **transform**: `x`, `y` and `angle` relative to its parent, plus `width` and
|
|
14
|
+
`height`, and a `z` that orders it among its siblings;
|
|
15
|
+
- **children**: other nodes nested under it (`add_node`);
|
|
16
|
+
- **components**: reusable pieces of behaviour attached to it (`add_component`);
|
|
17
|
+
- a **parent**: the node it hangs off, set when it is added.
|
|
18
18
|
|
|
19
|
-
Nodes extend the signal DSL (`RGame::Engine::Signal::DSL`), so any subclass can
|
|
20
|
-
and emit signals
|
|
19
|
+
Nodes extend the signal DSL (`RGame::Engine::Signal::DSL`), so any subclass can
|
|
20
|
+
declare and emit signals. See [Signals](signals.md).
|
|
21
|
+
|
|
22
|
+
### Elevation
|
|
23
|
+
|
|
24
|
+
`node.elevation` lifts a node's picture above the ground, in pixels, for a
|
|
25
|
+
top-down view. It defaults to 0, and positive is up the screen.
|
|
26
|
+
|
|
27
|
+
**Elevation is not part of the transform.** `y`, `world_y`, colliders, cameras and
|
|
28
|
+
children all ignore it. A character can therefore leave the ground while its feet
|
|
29
|
+
box and the camera following it stay put. Components that draw the node's
|
|
30
|
+
picture read it: `Components::Sprite` and `Components::AnimatedSprite` draw
|
|
31
|
+
lifted. `Components::Hop` writes it. A node's own `on_draw` is not lifted, so the
|
|
32
|
+
parts that stay on the ground go there:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
require 'rgame'
|
|
36
|
+
|
|
37
|
+
class Hero < RGame::Engine::Node2D
|
|
38
|
+
SHADOW = RGame::Util::Color.rgba(0, 0, 0, 90)
|
|
39
|
+
|
|
40
|
+
def initialize(**)
|
|
41
|
+
super
|
|
42
|
+
add_component(RGame::Engine::Components::AnimatedSprite.new(sheet: 'hero.json'))
|
|
43
|
+
add_component(RGame::Engine::Components::CharacterBody.new(speed: 80))
|
|
44
|
+
add_component(RGame::Engine::Components::Hop.new(peak: 18, duration: 0.5))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A shadow at the feet: drawn in on_draw, so it stays down while the sprite rises.
|
|
48
|
+
def on_draw(renderer, _view)
|
|
49
|
+
renderer.rect(2, 19, 12, 3, color: SHADOW)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
```
|
|
21
53
|
|
|
22
54
|
### The tick: control → update → draw
|
|
23
55
|
|
|
24
|
-
|
|
56
|
+
The engine drives a node in three phases, always in this order:
|
|
57
|
+
|
|
58
|
+
1. `control(actions)` reads intent, from the player (the `actions` snapshot) and
|
|
59
|
+
from AI or scripted controllers.
|
|
60
|
+
2. `update(dt)` advances game logic and physics over the timestep `dt`.
|
|
61
|
+
3. `draw(renderer, view)` renders the current state into `view`, the viewport
|
|
62
|
+
being drawn.
|
|
25
63
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
64
|
+
**The first two run once per simulation tick; `draw` runs once per rendered
|
|
65
|
+
frame.** The counts differ. The loop uses a fixed timestep, so a slow frame runs
|
|
66
|
+
several ticks before it draws. A frame in which nothing advanced skips the draw.
|
|
67
|
+
A `draw` must therefore depend on state alone and never read a clock. See
|
|
68
|
+
[The frame loop](app.md#the-frame-loop).
|
|
31
69
|
|
|
32
|
-
Each phase
|
|
33
|
-
|
|
34
|
-
phase
|
|
70
|
+
**Each phase settles the node first, then descends into its children.** Settling
|
|
71
|
+
means the node's components run, then its own hook. You override the hook, not
|
|
72
|
+
the phase:
|
|
35
73
|
|
|
36
74
|
- `on_control(actions)`
|
|
37
75
|
- `on_update(dt)`
|
|
38
76
|
- `on_draw(renderer, view)`
|
|
39
77
|
|
|
40
|
-
`view` is the viewport
|
|
41
|
-
any
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
78
|
+
`view` is the viewport the node is drawn into: its rectangle and the camera, if
|
|
79
|
+
any. Most nodes ignore it. Two tasks need it. One is laying out against the edges
|
|
80
|
+
of *this* region, not the whole window (`view.x`, `view.width`). The other is
|
|
81
|
+
culling (`view.visible?(x, y, w, h)`), which matters once the world is drawn once
|
|
82
|
+
per player. See [Viewports](#viewports-and-views).
|
|
83
|
+
|
|
84
|
+
The hooks run self before subtree, but no position depends on that order. A world
|
|
85
|
+
position is computed when read, from wherever everything is at that moment. See
|
|
86
|
+
[The two spaces](#the-two-spaces).
|
|
87
|
+
|
|
88
|
+
The traversal recurses into children for you. **Never iterate children
|
|
89
|
+
yourself**: add them with `add_node` and let the tree drive them.
|
|
90
|
+
|
|
91
|
+
**A subclass cannot replace a `Node2D` method whose name starts with `_`.** Those
|
|
92
|
+
methods are the machinery the phases call, such as `_draw_content` and
|
|
93
|
+
`_resolve_inherited`. A subclass method with the same name would take its place
|
|
94
|
+
without warning. So `RGame::Engine::SealedPrivates` raises `NameError` when the
|
|
95
|
+
class loads, naming both methods. `Component` follows the same rule. A non-public method *without*
|
|
96
|
+
the underscore is a seam, meant to be overridden with `super`. `Node2D` has one:
|
|
97
|
+
`draw_children`; see
|
|
98
|
+
[View transforms and the camera](#view-transforms-and-the-camera). Only these two
|
|
99
|
+
classes guard underscored methods; engine subclasses do not.
|
|
100
|
+
|
|
101
|
+
### The two spaces
|
|
102
|
+
|
|
103
|
+
**`x`, `y` and `angle` are relative to the parent.** They are the only position a
|
|
104
|
+
node sets, and the space it lives in. `rel_x`, `rel_y` and `rel_angle` are long
|
|
105
|
+
names for the same three.
|
|
106
|
+
|
|
107
|
+
`world_x`, `world_y` and `world_angle` accumulate that transform over the whole
|
|
108
|
+
ancestry. `world_x` is `parent.world_x + x`, with the parent's rotation applied.
|
|
109
|
+
A node with no parent sits at the origin.
|
|
110
|
+
|
|
111
|
+
`world_x=` and `world_y=` place a node at a world coordinate. They compute the
|
|
112
|
+
local position that puts it there and leave the other coordinate alone. They
|
|
113
|
+
*write `x` and `y`*; they are not a second position. The node still lives in its
|
|
114
|
+
parent's space. The write stays exact under a rotated ancestor, where moving along
|
|
115
|
+
one world axis changes both local coordinates. On a node without a parent they
|
|
116
|
+
change nothing, because that node sits at the origin.
|
|
46
117
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
118
|
+
```ruby
|
|
119
|
+
require 'rgame'
|
|
120
|
+
|
|
121
|
+
root = RGame::Engine::Node2D.new
|
|
122
|
+
container = root.add_node(RGame::Engine::Node2D.new(x: 100, y: 40))
|
|
123
|
+
child = container.add_node(RGame::Engine::Node2D.new(x: 10, y: 5))
|
|
124
|
+
child.world_x = 250
|
|
125
|
+
child.x # => 150
|
|
126
|
+
child.world_x # => 250
|
|
127
|
+
```
|
|
50
128
|
|
|
51
|
-
|
|
52
|
-
|
|
129
|
+
**World coordinates are computed when read, and cached.** Moving a node marks it
|
|
130
|
+
and its whole subtree stale. The next read walks up to the nearest current node
|
|
131
|
+
and recomputes back down. Two properties follow, and the engine relies on both:
|
|
132
|
+
|
|
133
|
+
- **A world position is never stale.** No phase takes a snapshot, so nothing goes
|
|
134
|
+
out of date. Every case answers correctly, at any point in any phase: a node
|
|
135
|
+
that moved, a node whose *ancestor* moved, a node reparented this tick, and a paused
|
|
136
|
+
node under a moving ancestor.
|
|
137
|
+
- **The engine computes nothing for a node nobody asks about.** A frame in which
|
|
138
|
+
nothing moves costs nothing.
|
|
139
|
+
|
|
140
|
+
Two things invalidate a world position. One is writing `x`, `y` or `angle`. The
|
|
141
|
+
other is a new parent through `add_node` or `remove_node`: the same offset from
|
|
142
|
+
somewhere else is still a move.
|
|
143
|
+
|
|
144
|
+
**Which one to use.** Drawing needs neither; see "Drawing happens in local space"
|
|
145
|
+
below. Game logic that reasons about the world reads `world_x`: a distance, a
|
|
146
|
+
collision, a camera target. To move a node, write `x`. Write `world_x=` when the
|
|
147
|
+
destination was decided in world space.
|
|
148
|
+
[`ScreenWrap`](components.md#screenwrap) uses it to put a node on the far edge of
|
|
149
|
+
the world, and a [`Mover`](components.md#mover) to write back a resolved step.
|
|
150
|
+
|
|
151
|
+
**No phase resolves the transform.** Keep that in mind when a spec drives one
|
|
152
|
+
phase and asserts on another's answer. The phases do resolve the *inherited*
|
|
153
|
+
attributes, which are not coordinates:
|
|
154
|
+
|
|
155
|
+
| Phase | Resolves | Because it reads |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| `control` | `abs_input_owner` | whose actions to hand each node |
|
|
158
|
+
| `update` | nothing | it reads neither |
|
|
159
|
+
| `draw` | `abs_band` | to open the node's own layer |
|
|
160
|
+
|
|
161
|
+
**`z` is not among them, and there is no `abs_z`.** A node's depth comes from
|
|
162
|
+
where the traversal reaches it, not from a sum of its ancestors' values. See
|
|
163
|
+
"Draw order" below.
|
|
53
164
|
|
|
54
|
-
###
|
|
165
|
+
### Drawing happens in local space
|
|
55
166
|
|
|
56
|
-
`
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
applied); a node with no parent sits at the origin. Moving a node therefore
|
|
60
|
-
moves its whole subtree. (Dirty-flag caching is noted as future work in the
|
|
61
|
-
source.)
|
|
167
|
+
**A node's `on_draw` never mentions where the node is.** `Node2D#draw` pushes the
|
|
168
|
+
node's transform onto the renderer before the node and its children draw. Inside
|
|
169
|
+
`on_draw`, the origin *is* the node, turned the way the node is turned:
|
|
62
170
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
171
|
+
```ruby
|
|
172
|
+
def on_draw(renderer, _view)
|
|
173
|
+
renderer.rect(0, 0, width, height) # this node's own box, wherever it is
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Passing a position there applies it twice. Both spellings go wrong. `world_x`
|
|
178
|
+
doubles the whole ancestry, camera included. `x` doubles the node's own offset.
|
|
179
|
+
Neither raises. The mistake shows only when the node sits under a parent away from
|
|
180
|
+
the origin. The `Game/DrawInLocalSpace` cop flags both.
|
|
181
|
+
|
|
182
|
+
A **component** drawing for its node runs on the same path and also draws at
|
|
183
|
+
`0, 0`. It may still ask the node for `node.world_x`, for example to cull against
|
|
184
|
+
the camera. That reads another object's coordinate, not its own.
|
|
185
|
+
|
|
186
|
+
The renderer's transform stack makes this work. The same mechanism gives a
|
|
187
|
+
`WorldView` its camera: one `renderer.translated` around a subtree, composed with
|
|
188
|
+
every other.
|
|
66
189
|
|
|
67
190
|
### Draw order
|
|
68
191
|
|
|
69
|
-
A node's `z`
|
|
70
|
-
|
|
192
|
+
**A node's `z` orders it among its siblings, and nowhere else.** The traversal
|
|
193
|
+
draws the tree depth-first, with siblings in `z` order:
|
|
71
194
|
|
|
72
195
|
```ruby
|
|
73
196
|
sky.add_node(Clouds.new(z: 2))
|
|
@@ -75,17 +198,17 @@ sky.add_node(Birds.new(z: 1))
|
|
|
75
198
|
sky.add_node(People.new(z: 0))
|
|
76
199
|
```
|
|
77
200
|
|
|
78
|
-
draws people, then birds, then clouds. Each
|
|
79
|
-
child nodes
|
|
80
|
-
|
|
201
|
+
This draws people, then birds, then clouds. Each may consist of any number of
|
|
202
|
+
child nodes. **A subtree is atomic**, so no part of `clouds` can end up behind
|
|
203
|
+
`birds`, and no part of `birds` in front of `clouds`.
|
|
81
204
|
|
|
82
|
-
Only the comparison matters.
|
|
83
|
-
the renderer, so its magnitude means nothing
|
|
84
|
-
|
|
85
|
-
|
|
205
|
+
Only the comparison matters. The engine never adds `z` to anything and never
|
|
206
|
+
passes it to the renderer, so its magnitude means nothing. `1` and `1_000_000`
|
|
207
|
+
behave the same if they are the only two children, and negatives are ordinary.
|
|
208
|
+
Nodes with equal `z` keep the order they were added in.
|
|
86
209
|
|
|
87
|
-
A
|
|
88
|
-
`:overlay` or `:debug
|
|
210
|
+
**A band overrules all of it.** `band:` is `:world` (the default), `:hud`,
|
|
211
|
+
`:overlay` or `:debug`. Children inherit it, like `input_owner`:
|
|
89
212
|
|
|
90
213
|
```ruby
|
|
91
214
|
scene.add_node(RGame::Engine::PlayerLayer.new(player: player)) # :hud
|
|
@@ -93,95 +216,106 @@ scene.add_node(Cutscene.new(band: :overlay))
|
|
|
93
216
|
```
|
|
94
217
|
|
|
95
218
|
Everything in `:world` draws under everything in `:hud`, whatever either asked
|
|
96
|
-
for
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
the one way out and is explicit.
|
|
219
|
+
for. No `z:` a node passes can cross the gap. `WorldView` declares `:world` and
|
|
220
|
+
`PlayerLayer` declares `:hud`, so most games never name a band. A node that must
|
|
221
|
+
leave its inherited band says so with `band:`, the one explicit way out.
|
|
100
222
|
|
|
101
|
-
|
|
102
|
-
|
|
223
|
+
`Node2D#draw` turns all of this into the single number the renderer sorts on. It
|
|
224
|
+
opens a layer per node, taking the next slot in the node's band. See
|
|
103
225
|
[Drawing](drawing.md#draw-order) and `RGame::Util::Z`.
|
|
104
226
|
|
|
105
227
|
### Who a node answers to
|
|
106
228
|
|
|
107
|
-
|
|
108
|
-
[`RGame::Engine::Players`](input.md) registry, or a bare `Actions` when
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
`
|
|
229
|
+
**`control` receives an input source, not one player's snapshot.** The source is
|
|
230
|
+
a [`RGame::Engine::Players`](input.md) registry, or a bare `Actions` when only one
|
|
231
|
+
answer exists. Each node asks the source for the actions of the player who owns
|
|
232
|
+
it. It then hands that plain `Actions` to its components and its own
|
|
233
|
+
`on_control`.
|
|
112
234
|
|
|
113
|
-
|
|
114
|
-
|
|
235
|
+
`input_owner` sets ownership. **Children inherit it, the way the transform
|
|
236
|
+
accumulates.** `control`, the one phase that reads ownership, resolves it onto
|
|
237
|
+
`abs_input_owner`:
|
|
115
238
|
|
|
116
239
|
```ruby
|
|
117
240
|
ship.input_owner = game.players[1] # the ship and everything under it
|
|
118
241
|
```
|
|
119
242
|
|
|
120
|
-
A node that names nobody inherits its parent's
|
|
121
|
-
anywhere reads the primary player.
|
|
122
|
-
|
|
243
|
+
A node that names nobody inherits its parent's owner. A tree that names nobody
|
|
244
|
+
anywhere reads the primary player. Single-player games therefore never mention
|
|
245
|
+
ownership.
|
|
123
246
|
|
|
124
|
-
|
|
125
|
-
one traversal can read two different controllers, while
|
|
126
|
-
|
|
247
|
+
The *source* descends through the tree, not the resolved snapshot. Two subtrees
|
|
248
|
+
in one traversal can thus read two different controllers, while each component
|
|
249
|
+
still receives a plain `control(actions)`.
|
|
127
250
|
|
|
128
|
-
>
|
|
129
|
-
>
|
|
130
|
-
>
|
|
251
|
+
> The attribute is `input_owner`, not `player`, because a game's scene usually
|
|
252
|
+
> calls its hero node `@player`. It is not `controller` either, because a
|
|
253
|
+
> controller is the component that produces movement intent.
|
|
131
254
|
|
|
132
255
|
### View transforms and the camera
|
|
133
256
|
|
|
134
|
-
A node
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
257
|
+
**A node that owns a view transform overrides `draw` and calls `super` inside
|
|
258
|
+
it.** A node's own transform is its place in its **parent**, and `draw` pushes it
|
|
259
|
+
as the traversal descends. A *view* transform differs. It maps the world onto the
|
|
260
|
+
screen, as a camera does. It belongs to no node in the tree. It must wrap a whole
|
|
261
|
+
subtree's draw, including the subtree root's own drawing.
|
|
262
|
+
|
|
263
|
+
`draw_children` is a separate seam. Override it to wrap or skip the *children's*
|
|
264
|
+
draw while the node still draws itself. `examples/game_menu`'s menu closes by not
|
|
265
|
+
calling `super` from it.
|
|
266
|
+
|
|
267
|
+
`examples/scroll_map` is the smallest program with a camera: a `WorldView`, a map
|
|
268
|
+
under it, and one node the camera follows.
|
|
138
269
|
|
|
139
270
|
### Two words that are easy to confuse
|
|
140
271
|
|
|
141
|
-
**Space** is structural and the tree enforces it
|
|
142
|
-
`WorldView` or
|
|
143
|
-
|
|
272
|
+
**Space** is structural, and the tree enforces it. A node is either inside a
|
|
273
|
+
`WorldView` or not. That decides what its coordinates mean and how often it is
|
|
274
|
+
drawn.
|
|
144
275
|
|
|
145
276
|
**Band** is an ordering partition: `:world`, `:hud`, `:overlay`, `:debug`. It is
|
|
146
|
-
structural too
|
|
147
|
-
|
|
148
|
-
|
|
277
|
+
structural too. Children inherit it, and `WorldView` and `PlayerLayer` declare
|
|
278
|
+
it. But it decides *what covers what*, not what coordinates mean. See
|
|
279
|
+
[Drawing](drawing.md#draw-order).
|
|
149
280
|
|
|
150
|
-
|
|
151
|
-
|
|
281
|
+
The two partitions differ. All screen-space content forms one *space* and draws
|
|
282
|
+
once. Bands subdivide that space by what should cover what.
|
|
152
283
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
284
|
+
**`RGame::Engine::WorldView` is where world space begins.** Its children draw in
|
|
285
|
+
their own local space and never know about a camera. The `WorldView` draws its
|
|
286
|
+
subtree **once per active viewport**. Each time, it clips to that viewport's
|
|
287
|
+
rectangle and translates by its camera. A child drawing at its own origin lands
|
|
288
|
+
wherever that viewport looks:
|
|
157
289
|
|
|
158
290
|
```ruby
|
|
159
291
|
view = scene.add_node(RGame::Engine::WorldView.new)
|
|
160
292
|
view.add_node(player) # world coordinates
|
|
161
293
|
```
|
|
162
294
|
|
|
163
|
-
Everything *outside* a `WorldView` is screen space and draws once. That one
|
|
164
|
-
|
|
165
|
-
|
|
295
|
+
Everything *outside* a `WorldView` is screen space and draws once. That one line
|
|
296
|
+
separates a HUD from the world. The game decides where to draw it; the engine
|
|
297
|
+
imposes nothing above the game's root.
|
|
166
298
|
|
|
167
|
-
A `WorldView` takes no camera
|
|
168
|
-
(`RGame::Engine::Player#camera`)
|
|
169
|
-
`node.system(RGame::Engine::Viewports)` which viewports exist
|
|
170
|
-
one player or four with nothing below it changing. A camera owned by a
|
|
171
|
-
world could not do that
|
|
299
|
+
**A `WorldView` takes no camera.** Cameras belong to players
|
|
300
|
+
(`RGame::Engine::Player#camera`). The `WorldView` asks
|
|
301
|
+
`node.system(RGame::Engine::Viewports)` which viewports exist. So the same subtree
|
|
302
|
+
serves one player or four, with nothing below it changing. A camera owned by a
|
|
303
|
+
node *inside* the world could not do that. The world would have to know how many
|
|
304
|
+
times it is drawn.
|
|
172
305
|
|
|
173
|
-
**Only `draw` multiplies.** `control` and `update`
|
|
174
|
-
however many players
|
|
175
|
-
|
|
176
|
-
|
|
306
|
+
**Only `draw` multiplies.** `control` and `update` run once per node per tick,
|
|
307
|
+
however many players watch. Simulation cost therefore stays independent of player
|
|
308
|
+
count. It also makes the rule that `draw` only renders state essential: a `draw`
|
|
309
|
+
with a side effect runs once per player.
|
|
177
310
|
|
|
178
|
-
|
|
311
|
+
`examples/split_screen` is the smallest program with two viewports: one `Ground`,
|
|
312
|
+
two walkers, a badge each, and a second player who joins mid-session.
|
|
179
313
|
|
|
180
314
|
## Viewports and views
|
|
181
315
|
|
|
182
|
-
`RGame::Engine::Viewports` is a root-scoped system
|
|
183
|
-
`RGame::Engine::Layout`
|
|
184
|
-
one viewport being drawn.
|
|
316
|
+
`RGame::Engine::Viewports` is a root-scoped system that divides the screen.
|
|
317
|
+
`RGame::Engine::Layout` holds the pure arithmetic behind it. A
|
|
318
|
+
`RGame::Engine::View` is one viewport being drawn.
|
|
185
319
|
|
|
186
320
|
```ruby
|
|
187
321
|
viewports = node.system(RGame::Engine::Viewports)
|
|
@@ -190,63 +324,75 @@ viewports.screen # the whole window, no camera — screen space
|
|
|
190
324
|
viewports.screen_for(player) # that player's own region, no camera — their HUD and menus
|
|
191
325
|
```
|
|
192
326
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
viewport
|
|
197
|
-
|
|
198
|
-
|
|
327
|
+
**`screen_for` returns the rectangle that player's world view uses.** A HUD laid
|
|
328
|
+
out at (10, 10) lands ten pixels inside the region of the world beneath it. It
|
|
329
|
+
returns **nil** when the player has nowhere to draw. An empty seat has no
|
|
330
|
+
viewport. While the split is collapsed, nobody owns a part of the screen: a
|
|
331
|
+
cutscene is everyone looking at one thing. Content that must stay on screen
|
|
332
|
+
through a cutscene belongs in the global `:overlay` band.
|
|
199
333
|
|
|
200
334
|
A **`View`** carries `x`, `y`, `width`, `height`, its `camera` (nil in screen
|
|
201
|
-
space) and its `player
|
|
335
|
+
space) and its `player`. Nodes mostly use two more members:
|
|
202
336
|
|
|
203
337
|
| | |
|
|
204
338
|
|---|---|
|
|
205
339
|
| `view.visible?(x, y, w, h)` | is this worth drawing at all |
|
|
206
340
|
| `view.offset_x` / `offset_y` | the translate that maps its contents onto the screen |
|
|
207
341
|
|
|
208
|
-
|
|
209
|
-
way `ActionMapper` reuses its `Actions
|
|
210
|
-
Hold the player or the viewports, never a
|
|
342
|
+
**`Viewports` reuses its views instead of rebuilding them.** It updates one `View`
|
|
343
|
+
per viewport each frame, the way `ActionMapper` reuses its `Actions`. Building
|
|
344
|
+
fresh views would allocate every frame. Hold the player or the viewports, never a
|
|
345
|
+
`View`.
|
|
346
|
+
|
|
347
|
+
**`Layout` answers one question**: given a count and a window, where does each
|
|
348
|
+
viewport go? It keeps no state and no anchors. One viewport gets the window, two
|
|
349
|
+
get a row each, and three or four share a 2x2 grid. It computes edges as
|
|
350
|
+
`(i * total) / count`, so the rectangles tile exactly and an odd-sized window has
|
|
351
|
+
no seam.
|
|
211
352
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
353
|
+
```ruby
|
|
354
|
+
require 'rgame'
|
|
355
|
+
|
|
356
|
+
RGame::Engine::Layout.rects(3, 640, 480) # => [[0, 0, 320, 240], [320, 0, 320, 240], [0, 240, 320, 240]]
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
`Layout.each_rect(count, width, height)` yields `index, x, y, width, height` for
|
|
360
|
+
each viewport and allocates nothing. `rects` returns the same rectangles as an
|
|
361
|
+
Array. The shapes it picks from are public too: `each_row(count, width, height)`,
|
|
362
|
+
`each_column(count, width, height)` and `each_cell(count, cols, rows, width,
|
|
363
|
+
height)`, which fills a grid left to right, top to bottom.
|
|
216
364
|
|
|
217
365
|
### A player's own screen
|
|
218
366
|
|
|
219
|
-
|
|
220
|
-
|
|
367
|
+
**`RGame::Engine::PlayerLayer` draws its subtree once, inside one player's
|
|
368
|
+
region.** It clips to that player's viewport and translates to its corner, in
|
|
369
|
+
screen space.
|
|
221
370
|
|
|
222
371
|
```ruby
|
|
223
372
|
layer = scene.add_node(RGame::Engine::PlayerLayer.new(player: game.players[1]))
|
|
224
373
|
layer.add_node(inventory)
|
|
225
374
|
```
|
|
226
375
|
|
|
227
|
-
|
|
228
|
-
viewport under a camera
|
|
229
|
-
window
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
**It sets `input_owner`**, and ownership is inherited, so a menu anywhere under
|
|
244
|
-
it reads that player's controller and nobody else's. Two players with a menu
|
|
245
|
-
open at once are independent without either knowing the other exists — see
|
|
376
|
+
A frame holds three kinds of content. `WorldView` draws the world once per
|
|
377
|
+
viewport, under a camera. Any other node draws a global overlay once across the
|
|
378
|
+
window. `PlayerLayer` draws once per player, inside that player's region.
|
|
379
|
+
|
|
380
|
+
`PlayerLayer` declares the `:hud` band. Everything under it draws over the world
|
|
381
|
+
without saying so.
|
|
382
|
+
|
|
383
|
+
**Children position themselves relative to the layer.** A node at (10, 10) sits
|
|
384
|
+
ten pixels inside *that player's* region, wherever the layout put it. The same HUD
|
|
385
|
+
class serves any player unchanged. To lay out against the far edge, use the view's
|
|
386
|
+
**size**: `view.width - margin`. `view.x` and `view.y` place the region on the
|
|
387
|
+
window; they belong to the clip, not the layout. Adding them would offset twice.
|
|
388
|
+
|
|
389
|
+
**`PlayerLayer` sets `input_owner`**, and children inherit ownership. A menu
|
|
390
|
+
anywhere under it reads that player's controller and nobody else's. Two players
|
|
391
|
+
can each have a menu open, and neither menu knows about the other. See
|
|
246
392
|
[Who a node answers to](#who-a-node-answers-to).
|
|
247
393
|
|
|
248
|
-
It draws nothing when `screen_for` has no region for
|
|
249
|
-
or
|
|
394
|
+
It draws nothing when `screen_for` has no region for its player: an empty seat,
|
|
395
|
+
or any player while the split is collapsed.
|
|
250
396
|
|
|
251
397
|
### Collapsing the split
|
|
252
398
|
|
|
@@ -255,132 +401,139 @@ node.system(RGame::Engine::Viewports).solo!(cutscene_camera)
|
|
|
255
401
|
node.system(RGame::Engine::Viewports).split!
|
|
256
402
|
```
|
|
257
403
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
404
|
+
**`solo!` collapses the screen to one view through the camera you pass.** Use it
|
|
405
|
+
for a cutscene, or anywhere the world should be seen through one camera. **The
|
|
406
|
+
camera is required.** Promoting one player's camera would silently give everyone
|
|
407
|
+
that player's view, and choosing what is on screen is a cutscene's whole job.
|
|
408
|
+
Point an ordinary `Camera` however you like, for example with a `CameraFollow` on
|
|
409
|
+
a cutscene actor, and pass it in.
|
|
263
410
|
|
|
264
|
-
Both are
|
|
265
|
-
next tick.
|
|
266
|
-
|
|
411
|
+
**Both calls are deferred**, like `queue_free`. They record a request that takes
|
|
412
|
+
effect on the next tick. `solo?` answers for the mode in effect, so it changes on
|
|
413
|
+
that tick too. Any code can reach this system, including a `draw`. A
|
|
414
|
+
`draw` runs once per view, so an immediate change would tear the frame that
|
|
415
|
+
requested it.
|
|
267
416
|
|
|
268
|
-
A full-screen UI
|
|
269
|
-
|
|
270
|
-
the whole window over
|
|
417
|
+
A full-screen UI, such as a results screen or a pause panel, usually needs no
|
|
418
|
+
collapse. Draw it in screen space, outside any `WorldView`, with `band: :overlay`.
|
|
419
|
+
It then covers the whole window over the players' views, HUDs included.
|
|
271
420
|
|
|
272
421
|
## Components
|
|
273
422
|
|
|
274
|
-
`RGame::Engine::Component` (`rgame/engine/component`) is
|
|
275
|
-
node instead of
|
|
276
|
-
and
|
|
277
|
-
|
|
278
|
-
- `add_component(component, as: nil)` attaches
|
|
279
|
-
it to the node. The slot defaults to the component's class, so
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
423
|
+
**A `RGame::Engine::Component` (`rgame/engine/component`) is behaviour you attach
|
|
424
|
+
to a node** instead of building it into a subclass. A component knows its owning
|
|
425
|
+
`node`, and extends the signal DSL like a node.
|
|
426
|
+
|
|
427
|
+
- `add_component(component, as: nil)` attaches a component in a **named slot**
|
|
428
|
+
and links it to the node. The slot defaults to the component's class, so a node
|
|
429
|
+
holds **at most one component per class** by default. A taken slot raises. Pass
|
|
430
|
+
a name when a node needs several of one type:
|
|
431
|
+
`add_component(Timer.new, as: :spawn)`.
|
|
432
|
+
- `get_component(key)` looks a component up by slot. The key is a class, matched
|
|
433
|
+
by ancestry so a base class finds a subclass instance, or a Symbol name. A class
|
|
434
|
+
lookup **raises if it is ambiguous**, when several components share the type.
|
|
435
|
+
Name them and look them up by name.
|
|
436
|
+
- `remove_component(key)` detaches the component in that slot (class or name) and
|
|
437
|
+
returns it, or `nil` if the slot is empty.
|
|
438
|
+
|
|
439
|
+
A component mirrors the node's three phases: `control(actions)`, `update(dt)` and
|
|
440
|
+
`draw(renderer, view)`. In each phase the node drives its components before its
|
|
441
|
+
own hook and before its children. Components also have two tree-lifecycle hooks,
|
|
442
|
+
`on_attach` and `on_detach`, described below.
|
|
293
443
|
|
|
294
444
|
## Lifecycle: constructing vs. entering the tree
|
|
295
445
|
|
|
296
|
-
A node has two distinct moments
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
2. **Entering the tree
|
|
305
|
-
cascade
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
`on_detach` to release
|
|
310
|
-
|
|
311
|
-
The engine drives this; you never call it.
|
|
312
|
-
`
|
|
313
|
-
|
|
314
|
-
- `add_node` enters the child
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
- `add_component`
|
|
319
|
-
the host node is
|
|
320
|
-
- `SceneStack#push`
|
|
321
|
-
boot
|
|
322
|
-
|
|
323
|
-
**
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
446
|
+
**A node has two distinct moments.** Mixing them up causes bugs that are hard to
|
|
447
|
+
trace.
|
|
448
|
+
|
|
449
|
+
1. **Construction** (`initialize`): the node and its components exist, but the
|
|
450
|
+
node is **not yet in the live tree**. Its anchors are unresolved: `root` and
|
|
451
|
+
`scene` (below) point nowhere useful, and shared systems are out of reach.
|
|
452
|
+
Build children and attach components here. Do **not** look anything up across
|
|
453
|
+
the tree.
|
|
454
|
+
2. **Entering the tree**: when the node goes live, the engine runs a depth-first
|
|
455
|
+
cascade. It fires each component's `on_attach`, then the node's `on_add`, then
|
|
456
|
+
the same for every child. **Anchors and systems are available here**, so a
|
|
457
|
+
component registers with a shared system at this point. Leaving the tree runs
|
|
458
|
+
the mirror cascade: children first, then `on_remove`, then each component's
|
|
459
|
+
`on_detach` to release its registrations.
|
|
460
|
+
|
|
461
|
+
The engine drives this; you never call it. It uses `enter_tree`, `exit_tree` and
|
|
462
|
+
`in_tree?`, fired at these points:
|
|
463
|
+
|
|
464
|
+
- `add_node` enters the child at once **only if** the parent is already live.
|
|
465
|
+
Otherwise the child enters when its ancestor does. A tree assembled in
|
|
466
|
+
`initialize` therefore comes alive all at once when it is mounted. `remove_node`
|
|
467
|
+
exits the subtree the same way.
|
|
468
|
+
- `add_component` and `remove_component` fire `on_attach` and `on_detach` at once
|
|
469
|
+
when the host node is live. Otherwise attachment happens when the node enters.
|
|
470
|
+
- `SceneStack#push` and `pop` enter and exit a scene. `RGame::Game#start` enters
|
|
471
|
+
the root once, at boot.
|
|
472
|
+
|
|
473
|
+
**Put cross-tree lookups in `on_add` or `on_attach`, never in `initialize`.** That
|
|
474
|
+
covers anchors, systems and sibling components. The engine wires the anchors
|
|
475
|
+
before those hooks run, so you cannot read them too early.
|
|
476
|
+
|
|
477
|
+
The mirror rule is that *attaching* components belongs in `initialize` or a
|
|
478
|
+
builder. Use `on_add` only when the component's constructor needs the tree. See
|
|
479
|
+
[Where to add a component](components.md#where-to-add-a-component).
|
|
327
480
|
|
|
328
481
|
## Anchors and shared systems
|
|
329
482
|
|
|
330
|
-
Two back-links let any node reach shared state without
|
|
331
|
-
|
|
332
|
-
stale
|
|
483
|
+
Two back-links let any node reach shared state without constructor arguments.
|
|
484
|
+
**The engine resolves both by walking up the parents**, never caching them, so
|
|
485
|
+
they cannot go stale:
|
|
333
486
|
|
|
334
|
-
- `root`
|
|
335
|
-
|
|
336
|
-
- `scene`
|
|
337
|
-
|
|
487
|
+
- `root` is the top-most node; a node without a parent is its own root. Global
|
|
488
|
+
systems that live as long as the program belong there.
|
|
489
|
+
- `scene` is the nearest enclosing scene node, marked as a boundary by
|
|
490
|
+
`SceneStack`. Systems that live as long as a scene belong there.
|
|
338
491
|
|
|
339
|
-
A *system* is
|
|
340
|
-
|
|
341
|
-
[Systems & shared resources](systems.md) for the scoping model and worked
|
|
492
|
+
A *system* is a `Component` on one of those anchor nodes. A node finds one with
|
|
493
|
+
`node.system(SomeSystem)`, which checks the scene first and then the root. See
|
|
494
|
+
[Systems & shared resources](systems.md) for the scoping model and worked
|
|
495
|
+
examples.
|
|
342
496
|
|
|
343
497
|
## Pausing a subtree
|
|
344
498
|
|
|
345
499
|
```ruby
|
|
346
500
|
world_view.paused = true # the world stops; an overlay above it does not
|
|
347
|
-
walker.paused = true # or
|
|
501
|
+
walker.paused = true # or one node, while its owner is in a menu
|
|
348
502
|
```
|
|
349
503
|
|
|
350
|
-
A paused node skips `control` and `update
|
|
351
|
-
because a subtree
|
|
352
|
-
Pausing
|
|
504
|
+
**A paused node skips `control` and `update`, and so does its whole subtree**,
|
|
505
|
+
because the traversal reaches a subtree only through its parent. **It still
|
|
506
|
+
draws.** Pausing concerns time, not visibility. A frozen world can therefore sit
|
|
353
507
|
under a cutscene that keeps animating.
|
|
354
508
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
simulation everyone else is in.
|
|
509
|
+
Pausing belongs to a *node*, not to the world. "Pause the world" is
|
|
510
|
+
`world_view.paused = true`, with no new concept. The same flag stops one player's
|
|
511
|
+
character while they browse a menu, without touching everyone else's simulation.
|
|
359
512
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
claims nobody, while a paused node simply never descends.
|
|
513
|
+
No `abs_paused` exists to match `abs_input_owner`. A node needs its resolved owner
|
|
514
|
+
even when its parent names nobody. A paused node, by contrast, never descends.
|
|
363
515
|
|
|
364
|
-
|
|
365
|
-
|
|
516
|
+
A paused node under a moving ancestor draws where it is now, not where it
|
|
517
|
+
stopped. The engine also culls it against its current position. Neither depends
|
|
518
|
+
on the node running a phase. The traversal pushes the transform as it descends,
|
|
519
|
+
and `world_x` computes itself when read. See [The two spaces](#the-two-spaces).
|
|
366
520
|
|
|
367
521
|
## Deferred free
|
|
368
522
|
|
|
369
|
-
A node that
|
|
370
|
-
|
|
371
|
-
**deferred** (as in Godot's `queue_free`):
|
|
523
|
+
**Removal is deferred.** A node that detached itself or a sibling mid-tick would
|
|
524
|
+
change a parent's `children` while the traversal iterates that list.
|
|
372
525
|
|
|
373
|
-
- `queue_free` marks a node for removal
|
|
374
|
-
the tree and keeps ticking until the sweep.
|
|
375
|
-
- `sweep_freed` detaches every marked node, depth-first,
|
|
376
|
-
cascade (`on_remove` / `on_detach`) on each.
|
|
377
|
-
|
|
526
|
+
- `queue_free` marks a node for removal, and `freed?` reports the mark. The node
|
|
527
|
+
stays in the tree and keeps ticking until the sweep.
|
|
528
|
+
- `sweep_freed` detaches every marked node, depth-first, and runs the normal
|
|
529
|
+
leave-tree cascade (`on_remove` / `on_detach`) on each. The game loop calls it
|
|
530
|
+
once per step, after `update`, outside the traversal.
|
|
378
531
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
sweep into the subtree
|
|
532
|
+
Any component or hook can therefore call `node.queue_free` from inside `update`
|
|
533
|
+
without corrupting the traversal. A component that holds nodes outside the normal
|
|
534
|
+
child list, such as `SceneStack`, overrides `Component#sweep_freed` to pass the
|
|
535
|
+
sweep into the subtree it owns.
|
|
383
536
|
|
|
384
|
-
`enter_tree` clears the freed flag, so a node detached and
|
|
385
|
-
alive.
|
|
386
|
-
|
|
537
|
+
`enter_tree` clears the freed flag, so a node detached and added again comes back
|
|
538
|
+
alive. Pools rely on this. A despawned node returns to its pool, and acquiring it
|
|
539
|
+
and calling `add_node` revives it cleanly.
|