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.
Files changed (253) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +144 -1
  3. data/README.md +67 -65
  4. data/docs/api/README.md +86 -82
  5. data/docs/api/app.md +125 -68
  6. data/docs/api/assets.md +203 -165
  7. data/docs/api/audio.md +130 -89
  8. data/docs/api/cli.md +259 -0
  9. data/docs/api/components.md +1032 -265
  10. data/docs/api/drawing.md +160 -160
  11. data/docs/api/examples.md +263 -0
  12. data/docs/api/game.md +149 -50
  13. data/docs/api/images.md +39 -39
  14. data/docs/api/input.md +226 -148
  15. data/docs/api/internals.md +241 -62
  16. data/docs/api/localization.md +285 -0
  17. data/docs/api/scene_graph.md +397 -244
  18. data/docs/api/signals.md +81 -76
  19. data/docs/api/systems.md +186 -73
  20. data/docs/api/text.md +58 -41
  21. data/docs/api/tile_maps.md +237 -0
  22. data/docs/api/toolbox.md +395 -146
  23. data/docs/api/ui.md +883 -54
  24. data/docs/api/values.md +216 -33
  25. data/examples/assets/README.md +322 -0
  26. data/examples/assets/blip.ogg +0 -0
  27. data/examples/assets/glyphs.json +5 -0
  28. data/examples/assets/glyphs.png +0 -0
  29. data/examples/assets/hero.json +12 -0
  30. data/examples/assets/hero.png +0 -0
  31. data/examples/assets/icons.json +13 -0
  32. data/examples/assets/icons.png +0 -0
  33. data/examples/assets/music.ogg +0 -0
  34. data/examples/assets/skills.json +10 -0
  35. data/examples/assets/skills.png +0 -0
  36. data/examples/assets/tileset.png +0 -0
  37. data/examples/assets/tileset.tsx +65 -0
  38. data/examples/assets/town.tmx +26 -0
  39. data/examples/assets/ui.json +11 -0
  40. data/examples/assets/ui.png +0 -0
  41. data/examples/collision/locales/en.yml +8 -0
  42. data/examples/collision/main.rb +316 -0
  43. data/examples/collision_tiles/locales/en.yml +9 -0
  44. data/examples/collision_tiles/main.rb +274 -0
  45. data/examples/fullscreen/locales/en.yml +10 -0
  46. data/examples/fullscreen/main.rb +216 -0
  47. data/examples/game_menu/locales/en.yml +8 -0
  48. data/examples/game_menu/main.rb +170 -0
  49. data/examples/input_glyphs/locales/en.yml +14 -0
  50. data/examples/input_glyphs/main.rb +213 -0
  51. data/examples/jump_topdown/locales/en.yml +9 -0
  52. data/examples/jump_topdown/main.rb +178 -0
  53. data/examples/localization/locales/de.yml +12 -0
  54. data/examples/localization/locales/en.yml +13 -0
  55. data/examples/localization/main.rb +158 -0
  56. data/examples/menu_navigation/locales/en.yml +23 -0
  57. data/examples/menu_navigation/main.rb +365 -0
  58. data/examples/music/locales/en.yml +7 -0
  59. data/examples/music/main.rb +134 -0
  60. data/examples/pathfinding/locales/en.yml +17 -0
  61. data/examples/pathfinding/main.rb +298 -0
  62. data/examples/pooling/locales/en.yml +7 -0
  63. data/examples/pooling/main.rb +259 -0
  64. data/examples/quick_wheel/locales/en.yml +16 -0
  65. data/examples/quick_wheel/main.rb +184 -0
  66. data/examples/radial_menu/locales/en.yml +16 -0
  67. data/examples/radial_menu/main.rb +184 -0
  68. data/examples/save_load/locales/en.yml +11 -0
  69. data/examples/save_load/main.rb +207 -0
  70. data/examples/save_load_ids/locales/en.yml +11 -0
  71. data/examples/save_load_ids/main.rb +322 -0
  72. data/examples/scroll_map/locales/en.yml +4 -0
  73. data/examples/scroll_map/main.rb +140 -0
  74. data/examples/signals/locales/en.yml +6 -0
  75. data/examples/signals/main.rb +278 -0
  76. data/examples/skill_bar/locales/en.yml +14 -0
  77. data/examples/skill_bar/main.rb +159 -0
  78. data/examples/sound/locales/en.yml +6 -0
  79. data/examples/sound/main.rb +122 -0
  80. data/examples/split_screen/locales/en.yml +9 -0
  81. data/examples/split_screen/main.rb +304 -0
  82. data/examples/sprite/locales/en.yml +8 -0
  83. data/examples/sprite/main.rb +180 -0
  84. data/examples/timer/locales/en.yml +12 -0
  85. data/examples/timer/main.rb +273 -0
  86. data/examples/velocity/locales/en.yml +6 -0
  87. data/examples/velocity/main.rb +196 -0
  88. data/examples/walk/locales/en.yml +4 -0
  89. data/examples/walk/main.rb +99 -0
  90. data/exe/rgame +9 -0
  91. data/ext/rgame_core/app/app.c +33 -3
  92. data/ext/rgame_core/app/locale.c +67 -0
  93. data/ext/rgame_core/app/locale.h +28 -0
  94. data/ext/rgame_core/audio/audio.c +39 -2
  95. data/ext/rgame_core/example.rb +0 -49
  96. data/ext/rgame_core/extconf.rb +0 -125
  97. data/ext/rgame_core/include/rgame/core.h +38 -2
  98. data/ext/rgame_core/ruby/audio_ext.c +10 -5
  99. data/ext/rgame_core/ruby/core_ext.c +30 -7
  100. data/ext/rgame_core/ruby/core_ext.h +3 -0
  101. data/ext/rgame_core/ruby/locale_ext.c +44 -0
  102. data/ext/rgame_core/ruby/recording_ext.c +1 -1
  103. data/ext/rgame_core/ruby/renderer_ext.c +20 -20
  104. data/ext/rgame_util/extconf.rb +2 -20
  105. data/ext/rgame_util/route_search.c +305 -0
  106. data/ext/rgame_util/route_search.h +86 -0
  107. data/ext/rgame_util/route_search_ext.c +150 -0
  108. data/ext/rgame_util/solid_grid.c +58 -0
  109. data/ext/rgame_util/solid_grid.h +49 -0
  110. data/ext/rgame_util/solid_grid_ext.c +161 -0
  111. data/ext/rgame_util/tile_sweep.c +164 -0
  112. data/ext/rgame_util/tile_sweep.h +62 -0
  113. data/ext/rgame_util/tile_sweep_ext.c +155 -0
  114. data/ext/rgame_util/util_ext.c +3 -0
  115. data/ext/rgame_util/util_ext.h +15 -0
  116. data/lib/rgame/boot.rb +0 -10
  117. data/lib/rgame/cli/new_project.rb +139 -0
  118. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  119. data/lib/rgame/cli/templates/README.md.tt +93 -0
  120. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  121. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  122. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  123. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  124. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  125. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  126. data/lib/rgame/cli/templates/rspec.tt +2 -0
  127. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  128. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  129. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  130. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  131. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  132. data/lib/rgame/cli.rb +66 -0
  133. data/lib/rgame/core/app.rb +6 -44
  134. data/lib/rgame/core/asset_manager.rb +13 -31
  135. data/lib/rgame/core/audio.rb +37 -16
  136. data/lib/rgame/core/font.rb +0 -3
  137. data/lib/rgame/core/locale.rb +22 -0
  138. data/lib/rgame/core/nine_slice.rb +0 -21
  139. data/lib/rgame/core/renderer.rb +6 -63
  140. data/lib/rgame/core/sprite_sheet.rb +0 -3
  141. data/lib/rgame/core/tile_map_renderer.rb +2 -19
  142. data/lib/rgame/core/ui_atlas.rb +28 -13
  143. data/lib/rgame/core.rb +1 -8
  144. data/lib/rgame/engine/actor_blockers.rb +131 -0
  145. data/lib/rgame/engine/animation_set.rb +1 -0
  146. data/lib/rgame/engine/audio_director.rb +36 -6
  147. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  148. data/lib/rgame/engine/camera.rb +3 -3
  149. data/lib/rgame/engine/circle_collider.rb +4 -2
  150. data/lib/rgame/engine/collision_box.rb +26 -1
  151. data/lib/rgame/engine/collision_system.rb +110 -22
  152. data/lib/rgame/engine/component.rb +34 -10
  153. data/lib/rgame/engine/components/action_trigger.rb +0 -1
  154. data/lib/rgame/engine/components/animated_sprite.rb +27 -25
  155. data/lib/rgame/engine/components/box_collider.rb +99 -0
  156. data/lib/rgame/engine/components/camera_follow.rb +6 -5
  157. data/lib/rgame/engine/components/character_body.rb +20 -61
  158. data/lib/rgame/engine/components/circle_collider.rb +47 -11
  159. data/lib/rgame/engine/components/collision_world.rb +159 -31
  160. data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
  161. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  162. data/lib/rgame/engine/components/hop.rb +76 -0
  163. data/lib/rgame/engine/components/identity.rb +73 -0
  164. data/lib/rgame/engine/components/mover.rb +285 -0
  165. data/lib/rgame/engine/components/navigator.rb +145 -0
  166. data/lib/rgame/engine/components/path_follow.rb +123 -31
  167. data/lib/rgame/engine/components/player_controller.rb +5 -2
  168. data/lib/rgame/engine/components/pool.rb +1 -1
  169. data/lib/rgame/engine/components/screen_wrap.rb +33 -11
  170. data/lib/rgame/engine/components/sprite.rb +12 -6
  171. data/lib/rgame/engine/components/targeting.rb +9 -11
  172. data/lib/rgame/engine/components/thrust_controller.rb +1 -1
  173. data/lib/rgame/engine/components/tile_world.rb +41 -13
  174. data/lib/rgame/engine/components/timer.rb +1 -1
  175. data/lib/rgame/engine/components/velocity.rb +23 -7
  176. data/lib/rgame/engine/components/wander_controller.rb +6 -2
  177. data/lib/rgame/engine/components/world.rb +133 -0
  178. data/lib/rgame/engine/contact_set.rb +74 -0
  179. data/lib/rgame/engine/culling.rb +4 -6
  180. data/lib/rgame/engine/debug_overlay.rb +5 -11
  181. data/lib/rgame/engine/i18n/plural.rb +45 -0
  182. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  183. data/lib/rgame/engine/i18n/template.rb +59 -0
  184. data/lib/rgame/engine/i18n.rb +276 -51
  185. data/lib/rgame/engine/input/action_mapper.rb +0 -26
  186. data/lib/rgame/engine/input/actions.rb +2 -8
  187. data/lib/rgame/engine/input/input_map.rb +39 -21
  188. data/lib/rgame/engine/nav_grid.rb +87 -0
  189. data/lib/rgame/engine/node2d.rb +248 -133
  190. data/lib/rgame/engine/path.rb +4 -6
  191. data/lib/rgame/engine/players.rb +6 -13
  192. data/lib/rgame/engine/presentation.rb +171 -0
  193. data/lib/rgame/engine/scene/scene_stack.rb +4 -4
  194. data/lib/rgame/engine/sealed_privates.rb +54 -0
  195. data/lib/rgame/engine/spatial_hash.rb +51 -19
  196. data/lib/rgame/engine/text.rb +194 -0
  197. data/lib/rgame/engine/tile_blockers.rb +63 -0
  198. data/lib/rgame/engine/tile_map.rb +3 -5
  199. data/lib/rgame/engine/tile_map_layer.rb +0 -2
  200. data/lib/rgame/engine/tileset.rb +2 -4
  201. data/lib/rgame/engine/timer.rb +2 -2
  202. data/lib/rgame/engine/ui/button.rb +248 -0
  203. data/lib/rgame/engine/ui/column.rb +20 -0
  204. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  205. data/lib/rgame/engine/ui/menu.rb +246 -71
  206. data/lib/rgame/engine/ui/navigation.rb +57 -0
  207. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  208. data/lib/rgame/engine/ui/option_button.rb +163 -0
  209. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  210. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  211. data/lib/rgame/engine/ui/pointing.rb +146 -0
  212. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  213. data/lib/rgame/engine/ui/ring.rb +55 -0
  214. data/lib/rgame/engine/ui/row.rb +21 -0
  215. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  216. data/lib/rgame/engine/ui/stack.rb +58 -0
  217. data/lib/rgame/engine/ui/stepping.rb +93 -0
  218. data/lib/rgame/engine/ui/text_button.rb +59 -0
  219. data/lib/rgame/engine/viewports.rb +2 -5
  220. data/lib/rgame/engine/world_view.rb +5 -4
  221. data/lib/rgame/engine.rb +31 -25
  222. data/lib/rgame/game.rb +99 -27
  223. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  224. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  225. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  226. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  227. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  228. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  229. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  230. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  231. data/lib/rgame/rubocop/default.yml +39 -0
  232. data/lib/rgame/rubocop/plugin.rb +45 -0
  233. data/lib/rgame/rubocop.rb +11 -0
  234. data/lib/rgame/util/color.rb +20 -24
  235. data/lib/rgame/util/controls.rb +14 -28
  236. data/lib/rgame/util/route_search.rb +27 -0
  237. data/lib/rgame/util/save_file.rb +107 -0
  238. data/lib/rgame/util/solid_grid.rb +37 -0
  239. data/lib/rgame/util/tensor.rb +0 -9
  240. data/lib/rgame/util/tile_sweep.rb +36 -0
  241. data/lib/rgame/util/z.rb +4 -14
  242. data/lib/rgame/util.rb +4 -3
  243. data/lib/rgame/version.rb +1 -1
  244. data/lib/rgame.rb +0 -15
  245. metadata +167 -12
  246. data/lib/rgame/engine/actor.rb +0 -53
  247. data/lib/rgame/engine/body.rb +0 -49
  248. data/lib/rgame/engine/cached_label.rb +0 -33
  249. data/lib/rgame/engine/input/player_controller.rb +0 -14
  250. data/lib/rgame/engine/matrix.rb +0 -32
  251. data/lib/rgame/engine/resettable.rb +0 -67
  252. data/lib/rgame/engine/tile_collision.rb +0 -78
  253. data/lib/rgame/engine/ui/menu_item.rb +0 -84
@@ -1,73 +1,196 @@
1
1
  # Scene graph
2
2
 
3
- The engine builds a game out of a tree of nodes a classic scene graph. A node
4
- holds state, logic and drawing for one game object; nesting nodes builds up whole
5
- scenes. Everything is pure Ruby: nodes draw through the renderer interface and
6
- read input from a per-frame snapshot, never naming a graphics library at all.
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. (The `2D` in the
11
- name leaves room for a future 3D node; today everything is 2D.) A node carries:
10
+ `RGame::Engine::Node2D` (`engine/node2d`) is the basic building block. A node
11
+ carries:
12
12
 
13
- - a **transform** — relative `x`, `y`, `angle` plus `width`/`height`, and a `z`
14
- 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 automatically when it is added).
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 declare
20
- and emit signals without opting in. See [Signals](signals.md).
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
- A node is driven in three phases, run in this order every frame:
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
- 1. `control(actions)` read intent, both from the player (the `actions`
27
- snapshot) and from AI/scripted controllers.
28
- 2. `update(dt)` advance game logic and physics over the timestep `dt`.
29
- 3. `draw(renderer, view)` render the current visual state into `view`, the
30
- viewport being drawn.
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 **settles the node itself first its components, then its own hook —
33
- and only then descends into the children**. So you override the hook, not the
34
- phase itself:
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 this node is being drawn into its rectangle, and the camera (if
41
- any) it is seen through. Most nodes ignore it and just draw. Two things need it: laying
42
- out against the edges of *this* region rather than the whole window
43
- (`view.x`, `view.width`), and culling (`view.visible?(x, y, w, h)`), which stops being an
44
- optimisation once the world is drawn once per player. See
45
- [Viewports](#viewports-and-views).
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
- Self-before-subtree keeps the transform flowing downward: a component or hook
48
- that moves the node does so before its children resolve their origin from it (see
49
- [Absolute position](#absolute-position)).
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
- Because the traversal recurses into children for you, **never re-implement child
52
- iteration** add children with `add_node` and let the tree drive them.
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
- ### Absolute position
165
+ ### Drawing happens in local space
55
166
 
56
- `x`/`y` are **relative to the parent**. At the start of each phase a node
57
- resolves its absolute position by accumulating onto the parent's origin
58
- (`abs_x = parent.abs_x + x`, and likewise for `y`, with the parent's rotation
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
- **`z` is not among them, and there is no `abs_z`.** Depth is decided by where
64
- the traversal reaches a node, not by summing what its ancestors picked — see
65
- "Draw order" below.
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` says where it sits among its **siblings**, and nowhere else. The
70
- tree is drawn depth-first with siblings in `z` order, so:
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 of them may be built out of as many
79
- child nodes as it likes: **a subtree is atomic**, so no part of `clouds` can end
80
- up behind `birds`, and no part of `birds` in front of `clouds`.
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. `z` is never added to anything and never reaches
83
- the renderer, so its magnitude means nothing `1` and `1_000_000` behave
84
- identically if they are the only two children and negatives are ordinary.
85
- Equal `z` keeps the order the nodes were added in.
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 **band** overrules all of it. `band:` is `:world` (the default), `:hud`,
88
- `:overlay` or `:debug`, and it is inherited down the tree like `input_owner`:
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, and nothing a node passes as `z:` can cross the gap. `WorldView` declares
97
- `:world` and `PlayerLayer` declares `:hud`, so most games never name a band at
98
- all; a node that must escape the band it inherits says so with `band:`, which is
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
- The engine turns all of this into the single number the renderer sorts on:
102
- `Node2D#draw` opens a layer per node, taking the next slot in its band. See
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
- `control` is handed an input **source**, not one player's snapshot a
108
- [`RGame::Engine::Players`](input.md) registry, or a bare `Actions` when there is
109
- only ever one answer. Each node asks the source for the actions of whichever
110
- player owns it, and hands its components and its own `on_control` that plain
111
- `Actions`.
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
- Ownership is `input_owner`, and it is **inherited down the tree exactly like the
114
- transform**, resolved onto `abs_input_owner` alongside `abs_x`/`abs_y`:
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; a tree that names nobody
121
- anywhere reads the primary player. That is what keeps single-player free of
122
- ceremony — no game that has one player ever mentions this.
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
- Because the *source* descends rather than the resolved snapshot, two subtrees in
125
- one traversal can read two different controllers, while a component still sees
126
- the `control(actions)` it always did.
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
- > It is `input_owner` rather than `player` because `@player` is what a game's own
129
- > scene usually calls its hero node, and rather than `controller` because
130
- > `Actor#controller` already means the thing producing movement intent.
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's transform is its place in the **world**. A *view* transform is different: it
135
- maps that world onto the screen (a camera), and it must wrap a whole subtree's draw
136
- without being baked into any node's position. So `draw` calls a `draw_children` step a
137
- subclass can override to wrap the subtree in a renderer transform.
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: a node is either inside a
142
- `WorldView` or it is not, and that decides what its coordinates mean and how
143
- many times it is drawn.
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 inherited down the tree, declared by `WorldView` and
147
- `PlayerLayer` but it decides *what covers what* rather than what coordinates
148
- mean. See [Drawing](drawing.md#draw-order).
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
- They are not the same partition. All screen-space content is one *space* and is
151
- drawn once; the bands subdivide it by what should cover what.
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
- `RGame::Engine::WorldView` is that subclass, and it is where **world space begins**.
154
- Its children draw at their own world origin and never know about a camera; the node
155
- draws them **once per active viewport**, clipping to that viewport's rectangle and
156
- translating by its camera:
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 distinction
164
- is what separates a HUD from the world, and where it goes is the game's choice — nothing
165
- is imposed above the game's own root.
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. Cameras belong to players
168
- (`RGame::Engine::Player#camera`), and the node asks
169
- `node.system(RGame::Engine::Viewports)` which viewports exist, so the same subtree serves
170
- one player or four with nothing below it changing. A camera owned by a node *inside* the
171
- world could not do that it would force the world to know how many times it is drawn.
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` still run once per node per tick
174
- however many players are watching, which is what keeps simulation cost independent of
175
- player count and what makes the standing "draw renders state" rule load-bearing rather
176
- than stylistic: a `draw` with a side effect now runs once per player.
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
- See `examples/15_tiled_world`.
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 holding how the screen is divided;
183
- `RGame::Engine::Layout` is the pure arithmetic behind it, and a `RGame::Engine::View` is
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
- `screen_for` is the same rectangle that player's world view is drawn into, so a
194
- HUD laid out at (10, 10) lands ten pixels inside the region the world beneath it
195
- occupies. It is **nil** when they have nowhere to draw: an empty seat has no
196
- viewport, and while the split is collapsed nobody owns a half of the screen
197
- a cutscene is everyone looking at one thing, so something that must stay on
198
- screen through it belongs in the global overlay band instead.
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`, plus two things nodes actually use:
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
- **Views are reused, not rebuilt.** `Viewports` mutates one per viewport each frame, the
209
- way `ActionMapper` reuses its `Actions` — building fresh ones would allocate every frame.
210
- Hold the player or the viewports, never a `View`.
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
- **`Layout`** answers only "given a count and a window, where does each one go", with no
213
- state and no anchors: one viewport gets the window, two get a row each, three or four
214
- share a 2x2 grid. Edges are computed as `(i * total) / count`, so the rects tile exactly
215
- and no seam is left down the middle of an odd-sized window.
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
- `RGame::Engine::PlayerLayer` is the node for it: its subtree is drawn **once**,
220
- clipped to that player's viewport and translated to its corner, in screen space.
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
- That is the third kind of content a frame holds. The world is drawn once per
228
- viewport under a camera (`WorldView`), a global overlay once across the whole
229
- window (anything else in the tree), and this once per player inside their own
230
- region.
231
-
232
- It is also where the `:hud` band comes from: `PlayerLayer` declares it, so
233
- everything under here draws over everything in the world without any of it
234
- saying so.
235
-
236
- **Children are positioned relative to the layer**, so a node at (10, 10) is ten
237
- pixels inside *that player's* region wherever the layout put it, and the same
238
- HUD class serves either player unchanged. Lay out against the far edge with the
239
- view's **size** — `view.width - margin`. `view.x` and `view.y` are where the
240
- region sits on the window and are the clip's business, not a layout origin;
241
- adding them would offset a second time.
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 that player: an empty seat,
249
- or anybody while the split is collapsed.
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
- `solo!` collapses to one screen-wide view for a cutscene, or anywhere the world should
259
- be seen through a single camera. **The camera is required**: promoting one player's would
260
- silently give everyone else their view, and choosing what is on screen is what a cutscene
261
- is for. Point an ordinary `Camera` however you like (a `CameraFollow` on a cutscene actor
262
- works) and hand it over.
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 **deferred**, like `queue_free`: they record a request and it takes effect on the
265
- next tick. This system is reachable from anywhere including a `draw`, and a `draw` runs
266
- once per view, so applying immediately would tear the frame it was requested in.
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 a results screen, a pause panel usually wants no collapse at all:
269
- draw it in screen space, outside any `WorldView`, with `band: :overlay` so it covers
270
- the whole window over whatever the players are seeing, HUDs included.
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 a piece of behaviour you attach to a
275
- node instead of baking it into a subclass. A component knows its owning `node`,
276
- and like nodes it extends the signal DSL.
277
-
278
- - `add_component(component, as: nil)` attaches one in a **named slot** and back-links
279
- it to the node. The slot defaults to the component's class, so by default a node
280
- still holds **at most one component per class** a taken slot raises. Pass a name
281
- (`add_component(Timer.new, as: :spawn)`) when a node needs several of one type.
282
- - `get_component(key)` looks a component up by its slot: a class (matched by ancestry,
283
- so a base class finds a subclass instance) or a Symbol name. A class lookup **raises
284
- if it is ambiguous** several components share that type so name them and look up
285
- by name.
286
- - `remove_component(key)` detaches and unlinks the component in that slot (class or
287
- name), returning it (or `nil` if the slot is empty).
288
-
289
- A component mirrors the node's three phases — `control(actions)`, `update(dt)`,
290
- `draw(renderer)` and the node drives its components in each phase, before its
291
- own hook and before its children. It also has the two tree-lifecycle hooks below
292
- (`on_attach`/`on_detach`).
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, and conflating them is a classic source of bugs
297
- (it is why mature engines split Godot's `_init`/`_ready`, Unity's `Awake`/`OnEnable`,
298
- Unreal's constructor/`BeginPlay`):
299
-
300
- 1. **Construction** (`initialize`) the node and its components exist, but the node
301
- is **not yet in the live tree**. It has no resolved anchors: `root`/`scene` (below)
302
- don't point anywhere useful, and shared systems aren't reachable. Build children
303
- and attach components here; do **not** look anything up across the tree.
304
- 2. **Entering the tree** when the node becomes live, the engine runs a depth-first
305
- cascade that fires, in order: each component's `on_attach`, then the node's
306
- `on_add`, then the same for every child. **This is where anchors and systems are
307
- available**, so it's where a component registers with a shared system. Leaving the
308
- tree runs the mirror cascade children first, then `on_remove`, then component
309
- `on_detach` to release those registrations.
310
-
311
- The engine drives this; you never call it. The relevant calls are `enter_tree` /
312
- `exit_tree` (and `in_tree?`), fired automatically:
313
-
314
- - `add_node` enters the child immediately **iff** the parent is already live;
315
- otherwise the child waits and is entered when its ancestor enters. So a node tree
316
- assembled in `initialize` (before it's mounted) comes alive all at once when it
317
- is. `remove_node` exits the subtree the same way.
318
- - `add_component` / `remove_component` fire `on_attach` / `on_detach` immediately when
319
- the host node is already live (otherwise attach happens during the node's entry).
320
- - `SceneStack#push` / `pop` enter/exit a scene; the platform enters the root once at
321
- boot (`RGame::Game#start`).
322
-
323
- **The split is load-bearing for the `on_add`/`initialize` divide:** put cross-tree
324
- lookups (anchors, systems, sibling components) in `on_add` / `on_attach`, never in
325
- `initialize`. The engine guarantees the anchors are wired before those hooks run, so
326
- you can't accidentally read them too early.
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 it being threaded through
331
- constructors, both **resolved by walking parents** (never cached, so they can't go
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` the top-most node (a node with no parent is its own root). Home for
335
- global, program-lifetime systems.
336
- - `scene` the nearest enclosing scene node (marked as a boundary by `SceneStack`).
337
- Home for scene-lifetime systems.
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 just a `Component` living on one of those anchor nodes; nodes find one
340
- with `node.system(SomeSystem)` (scene scope first, then the global root). See
341
- [Systems & shared resources](systems.md) for the scoping model and worked examples.
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 just one node, while its owner is in a menu
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` and so does everything under it,
351
- because a subtree is only ever reached through its parent. **It still draws.**
352
- Pausing is about time, not visibility, which is what lets a frozen world sit
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
- It is a property of a *node* rather than of the world on purpose. "Pause the
356
- world" is `world_view.paused = true` with no new concept, and the same flag
357
- stops one player's character while they browse a menu without touching the
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
- There is no `abs_paused` to go with `abs_input_owner`: ownership has to be
361
- resolved because a node needs to know whose input it reads even when its parent
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
- `draw` still resolves the transform, so a paused node under an ancestor that is
365
- still moving is drawn where it now is rather than where it was when it stopped.
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 detaches itself or a sibling mid-tick would mutate a parent's `children`
370
- while that list is being iterated the classic scene-graph footgun. So removal is
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; `freed?` reports the mark. The node stays in
374
- the tree and keeps ticking until the sweep.
375
- - `sweep_freed` detaches every marked node, depth-first, running the normal leave-tree
376
- cascade (`on_remove` / `on_detach`) on each. It runs from a safe point **outside** the
377
- tick — the platform loop flushes it once per step, after `update`.
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
- Because it's deferred, any component or hook can call `node.queue_free` from inside
380
- `update` without corrupting the traversal. Container components that hold nodes off the
381
- normal child list (e.g. `SceneStack`) override `Component#sweep_freed` to forward the
382
- sweep into the subtree they own.
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 later re-added comes back
385
- alive. This is what lets a pool recycle nodes: a despawned (freed) node is returned to
386
- its pool, and re-acquiring it and `add_node`-ing it revives it cleanly.
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.