Class: RGame::Engine::Node2D
- Inherits:
-
Object
- Object
- RGame::Engine::Node2D
- Extended by:
- Signal::DSL
- Defined in:
- lib/rgame/engine/node2d.rb
Overview
A node in a scene graph. We currently have only 2D nodes, but the name reflects this should there ever be a 3D space. Nodes are containers for both containers and nodes. They extend the signal DSL to allow for easy signal usage.
Direct Known Subclasses
PlayerLayer, TileMapLayer, UI::Menu, UI::MenuItem, WorldView
Instance Attribute Summary collapse
-
#abs_angle ⇒ Object
readonly
Returns the value of attribute abs_angle.
-
#abs_band ⇒ Object
readonly
Returns the value of attribute abs_band.
-
#abs_input_owner ⇒ Object
readonly
Returns the value of attribute abs_input_owner.
-
#abs_x ⇒ Object
readonly
Returns the value of attribute abs_x.
-
#abs_y ⇒ Object
readonly
Returns the value of attribute abs_y.
-
#angle ⇒ Object
Returns the value of attribute angle.
-
#band ⇒ Object
Returns the value of attribute band.
-
#children ⇒ Object
readonly
Returns the value of attribute children.
-
#components ⇒ Object
readonly
Returns the value of attribute components.
- #context ⇒ Object
-
#height ⇒ Object
Returns the value of attribute height.
-
#input_owner ⇒ Object
Whose input drives this node: an RGame::Engine::Player, or nil.
-
#parent ⇒ Object
Returns the value of attribute parent.
-
#paused ⇒ Object
A paused node skips
controlandupdate— and so does everything under it, because a subtree is only ever reached through its parent. -
#scene ⇒ Object
The nearest enclosing scene node, marked as a boundary by SceneStack (#scene= self).
-
#sibling_order ⇒ Object
Insertion order among siblings, the tie-breaker for equal
z. -
#width ⇒ Object
Returns the value of attribute width.
-
#x ⇒ Object
Returns the value of attribute x.
-
#y ⇒ Object
Returns the value of attribute y.
-
#z ⇒ Object
Returns the value of attribute z.
Instance Method Summary collapse
-
#add_component(component, as: nil) ⇒ Object
Attach a component in a named slot.
- #add_node(node) ⇒ Object
-
#children_unsorted! ⇒ Object
A child was added, or one changed its
z, so the child order is stale. -
#control(input) ⇒ Object
inputis an input source, not one player's snapshot: an RGame::Engine::Players registry, or a bare Actions when there is only ever one answer (which is what a spec usually passes). -
#draw(renderer, view) ⇒ Object
update visual game state, drawing the node.
-
#enter_tree ⇒ Object
Entered-tree cascade: anchors (root/scene) and sibling systems are now reachable, so components attach (register with systems) before this node's own on_add, and the whole subtree enters depth-first.
-
#exit_tree ⇒ Object
Leaving-tree cascade: mirror of #enter_tree (children first, then this node's on_remove, then component on_detach to release registrations).
- #freed? ⇒ Boolean
-
#get_component(key) ⇒ Object
Look a component up by its slot.
- #in_tree? ⇒ Boolean
-
#initialize(x: 0, y: 0, z: 0, angle: 0, width: 0, height: 0, input_owner: nil, band: nil) ⇒ Node2D
constructor
A new instance of Node2D.
- #on_add ⇒ Object
-
#on_control(actions) ⇒ Object
Lifecycle hooks: Subclasses should implement these instead of overwriting the public interface draw/update/add etc.
- #on_draw(renderer, view) ⇒ Object
- #on_remove ⇒ Object
- #on_update(dt) ⇒ Object
-
#queue_free ⇒ Object
Deferred removal (à la Godot's queue_free): mark this node for removal instead of detaching it now.
- #remove_component(key) ⇒ Object
- #remove_node(node) ⇒ Object
-
#root ⇒ Object
The top-most node — a node with no parent is its own root.
-
#sweep_freed ⇒ Object
Detach every node marked by #queue_free, depth-first, from a point outside the update traversal.
-
#system(klass) ⇒ Object
Nearest system of a class: scene scope first, then the global root.
-
#update(dt) ⇒ Object
update game logic and physics (might become two calls with time, but for now works in one step).
Methods included from Signal::DSL
Constructor Details
#initialize(x: 0, y: 0, z: 0, angle: 0, width: 0, height: 0, input_owner: nil, band: nil) ⇒ Node2D
Returns a new instance of Node2D.
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/rgame/engine/node2d.rb', line 91 def initialize(x: 0, y: 0, z: 0, angle: 0, width: 0, height: 0, input_owner: nil, band: nil) @input_owner = input_owner @paused = false @x = x @y = y @z = z self.band = band @angle = angle @width = width @height = height # Resolved by #resolve_origin at the top of every phase. Seeded here so # a node that has not been driven yet reads as being at the origin # rather than as nil — which is the same answer resolve_origin gives an # unparented node, and saves every reader of abs_* from a NoMethodError # on a node built but not yet ticked. @abs_x = @abs_y = @abs_angle = 0 @abs_input_owner = @input_owner @abs_band = @band || Util::Z::DEFAULT @children = [] # Siblings are drawn in `z` order, and in insertion order within one # `z`. Ruby's sort is not stable, so insertion order is carried as a # number rather than relied on — the same reason the C draw queue # compares (z, order) instead of trusting qsort. @child_seq = 0 @children_sorted = true @components = [] @component_slots = {} # slot (Class by default, or a Symbol name) => component @parent = nil @scene = nil @in_tree = false @freed = false end |
Instance Attribute Details
#abs_angle ⇒ Object (readonly)
Returns the value of attribute abs_angle.
19 20 21 |
# File 'lib/rgame/engine/node2d.rb', line 19 def abs_angle @abs_angle end |
#abs_band ⇒ Object (readonly)
Returns the value of attribute abs_band.
19 20 21 |
# File 'lib/rgame/engine/node2d.rb', line 19 def abs_band @abs_band end |
#abs_input_owner ⇒ Object (readonly)
Returns the value of attribute abs_input_owner.
19 20 21 |
# File 'lib/rgame/engine/node2d.rb', line 19 def abs_input_owner @abs_input_owner end |
#abs_x ⇒ Object (readonly)
Returns the value of attribute abs_x.
19 20 21 |
# File 'lib/rgame/engine/node2d.rb', line 19 def abs_x @abs_x end |
#abs_y ⇒ Object (readonly)
Returns the value of attribute abs_y.
19 20 21 |
# File 'lib/rgame/engine/node2d.rb', line 19 def abs_y @abs_y end |
#angle ⇒ Object
Returns the value of attribute angle.
12 13 14 |
# File 'lib/rgame/engine/node2d.rb', line 12 def angle @angle end |
#band ⇒ Object
Returns the value of attribute band.
19 20 21 |
# File 'lib/rgame/engine/node2d.rb', line 19 def band @band end |
#children ⇒ Object (readonly)
Returns the value of attribute children.
19 20 21 |
# File 'lib/rgame/engine/node2d.rb', line 19 def children @children end |
#components ⇒ Object (readonly)
Returns the value of attribute components.
19 20 21 |
# File 'lib/rgame/engine/node2d.rb', line 19 def components @components end |
#context ⇒ Object
211 212 213 |
# File 'lib/rgame/engine/node2d.rb', line 211 def context @context ||= root.context end |
#height ⇒ Object
Returns the value of attribute height.
12 13 14 |
# File 'lib/rgame/engine/node2d.rb', line 12 def height @height end |
#input_owner ⇒ Object
Whose input drives this node: an RGame::Engine::Player, or nil.
Inherited down the tree exactly like the transform. Set it on a node and
its whole subtree reads that player, so ship.input_owner = players[1]
is all it takes for everything under the ship to answer to player two. A
node that sets none inherits its parent's, and a tree that sets none
anywhere reads the primary player — which is why single player needs no
mention of this at all.
Not player, deliberately, and not controller either. @player is
what a game's own code calls its hero node (examples/15_tiled_world
does), so an attr_accessor :player here would quietly claim that ivar
out from under every scene that has one — which it did, and the symptom
was the input system being handed a Node2D. controller is taken too:
Actor#controller is the thing that produces movement intent, a different
idea entirely. This name says exactly what it decides and collides with
neither.
76 77 78 |
# File 'lib/rgame/engine/node2d.rb', line 76 def input_owner @input_owner end |
#parent ⇒ Object
Returns the value of attribute parent.
12 13 14 |
# File 'lib/rgame/engine/node2d.rb', line 12 def parent @parent end |
#paused ⇒ Object
A paused node skips control and update — and so does everything
under it, because a subtree is only ever reached through its parent.
It still draws: pausing is about time, not visibility, which is what
lets a frozen world sit under a cutscene overlay that keeps animating.
world_view.paused = true # the world stops; the overlay above it does not
There is no abs_paused to go with abs_input_owner. Ownership has to
be resolved because a node needs to know whose input it reads even when
its parent claims nobody; pausing needs no resolution at all, because a
paused node simply never descends.
89 90 91 |
# File 'lib/rgame/engine/node2d.rb', line 89 def paused @paused end |
#scene ⇒ Object
The nearest enclosing scene node, marked as a boundary by SceneStack (#scene= self). Scene-lifetime systems live on it as components.
207 208 209 |
# File 'lib/rgame/engine/node2d.rb', line 207 def scene @scene || @parent&.scene end |
#sibling_order ⇒ Object
Insertion order among siblings, the tie-breaker for equal z. Engine
bookkeeping, set by the parent's #add_node the way parent is — not for
game code, and meaningless on a node with no parent.
18 19 20 |
# File 'lib/rgame/engine/node2d.rb', line 18 def sibling_order @sibling_order end |
#width ⇒ Object
Returns the value of attribute width.
12 13 14 |
# File 'lib/rgame/engine/node2d.rb', line 12 def width @width end |
#x ⇒ Object
Returns the value of attribute x.
12 13 14 |
# File 'lib/rgame/engine/node2d.rb', line 12 def x @x end |
#y ⇒ Object
Returns the value of attribute y.
12 13 14 |
# File 'lib/rgame/engine/node2d.rb', line 12 def y @y end |
#z ⇒ Object
Returns the value of attribute z.
19 20 21 |
# File 'lib/rgame/engine/node2d.rb', line 19 def z @z end |
Instance Method Details
#add_component(component, as: nil) ⇒ Object
Attach a component in a named slot. The slot defaults to the component's class, so a
node still holds at most one component per class — until you give them distinct
names: add_component(Timer.new, as: :spawn) / add_component(Timer.new, as: :wave).
A taken slot raises, so an accidental duplicate is still caught.
172 173 174 175 176 177 178 179 180 181 |
# File 'lib/rgame/engine/node2d.rb', line 172 def add_component(component, as: nil) slot = as || component.class raise ArgumentError, "Node already has a component in slot #{slot.inspect}" if @component_slots.key?(slot) @components << component @component_slots[slot] = component component.node = self component.on_attach if @in_tree component end |
#add_node(node) ⇒ Object
125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/rgame/engine/node2d.rb', line 125 def add_node(node) @children << node node.parent = self node.sibling_order = (@child_seq += 1) @children_sorted = false # Defer the entered-tree cascade until this node is itself live; otherwise it # fires when an ancestor enters (see #enter_tree). This is the construct-vs-enter # split — a node built inside another node's initialize is not yet in the tree. node.enter_tree if @in_tree node end |
#children_unsorted! ⇒ Object
A child was added, or one changed its z, so the child order is stale.
The sort is deferred to the next traversal rather than done here, so
building a scene of a thousand nodes costs one sort rather than a
thousand. Called by the engine; a game only ever assigns z.
141 |
# File 'lib/rgame/engine/node2d.rb', line 141 def children_unsorted! = @children_sorted = false |
#control(input) ⇒ Object
input is an input source, not one player's snapshot: an
RGame::Engine::Players registry, or a bare Actions when there is only
ever one answer (which is what a spec usually passes).
Each node asks the source for the actions of whichever player owns it,
and hands its components and its own hook that plain Actions. So a
component never learns there is more than one player — control(actions)
means the same thing it always did — while two subtrees under one tick
can read two different controllers.
The source is what descends, not the resolved snapshot, because ownership can change further down.
239 240 241 242 243 244 245 246 247 |
# File 'lib/rgame/engine/node2d.rb', line 239 def control(input) return if @paused resolve_origin actions = input.actions_for(@abs_input_owner) @components.each { it.control(actions) } on_control(actions) children_in_order.each { it.control(input) } end |
#draw(renderer, view) ⇒ Object
update visual game state, drawing the node. This runs last in
a game tick
view is the viewport being drawn into: its rectangle, and the camera
(if any) it is seen through. Every node gets it, because a node cannot
otherwise know where the edges of its own region are — a HUD laying out
against the whole window is wrong the moment the window is one player's
half of it — and because culling needs it once the world is drawn more
than once. Most nodes ignore it and simply draw.
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 |
# File 'lib/rgame/engine/node2d.rb', line 269 def draw(renderer, view) resolve_origin # This node's own drawing goes in its own layer: the renderer hands out # the next slot in the node's band, and every `z:` the node passes is an # offset inside it. Because the traversal takes slots in the order it # reaches nodes, draw order *is* tree order — and because a slot is # narrow, nothing a node draws can reach past itself. The node never # asks for this and cannot forget it; see RGame::Util::Z. renderer.layered(@abs_band) do # Draw this node's own visuals oriented by its absolute angle, then descend. # Children resolve their own world transform (resolve_origin already baked this # node's rotation into their abs_x/abs_y), so they draw in flat world space and # must NOT be nested inside this node's rotation — nesting would apply that # rotation to them a second time. Unrotated nodes skip the wrapper entirely. if abs_angle.zero? draw_content(renderer, view) else renderer.rotated(abs_angle * 180.0 / Math::PI, abs_x, abs_y) { draw_content(renderer, view) } end end # Outside the block: a child takes a slot of its own, after this one. draw_children(renderer, view) end |
#enter_tree ⇒ Object
Entered-tree cascade: anchors (root/scene) and sibling systems are now reachable, so components attach (register with systems) before this node's own on_add, and the whole subtree enters depth-first. The engine fires this — the user never calls it — so registration can't be forgotten. Idempotent.
324 325 326 327 328 329 330 331 332 |
# File 'lib/rgame/engine/node2d.rb', line 324 def enter_tree return if @in_tree @in_tree = true @freed = false # revive: a pooled node reacquired after death re-enters here @components.each(&:on_attach) on_add children_in_order.each(&:enter_tree) end |
#exit_tree ⇒ Object
Leaving-tree cascade: mirror of #enter_tree (children first, then this node's on_remove, then component on_detach to release registrations).
336 337 338 339 340 341 342 343 |
# File 'lib/rgame/engine/node2d.rb', line 336 def exit_tree return unless @in_tree children_in_order.each(&:exit_tree) on_remove @components.each(&:on_detach) @in_tree = false end |
#freed? ⇒ Boolean
301 |
# File 'lib/rgame/engine/node2d.rb', line 301 def freed? = @freed |
#get_component(key) ⇒ Object
Look a component up by its slot. A Class/Module is matched by ancestry across every
component (so a base class finds a subclass instance); a Symbol names a specific slot
(see #add_component's as:). A class lookup raises when it's ambiguous — two
components share that type — so the caller reaches for the name instead. The scan is
allocation-free, so it's safe to call on the per-frame path.
155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/rgame/engine/node2d.rb', line 155 def get_component(key) return @component_slots[key] unless key.is_a?(Module) found = nil @components.each do |component| next unless component.is_a?(key) raise ArgumentError, "Multiple components match #{key}; look one up by name" if found found = component end found end |
#in_tree? ⇒ Boolean
293 |
# File 'lib/rgame/engine/node2d.rb', line 293 def in_tree? = @in_tree |
#on_add ⇒ Object
353 |
# File 'lib/rgame/engine/node2d.rb', line 353 def on_add; end |
#on_control(actions) ⇒ Object
Lifecycle hooks: Subclasses should implement these instead of overwriting the public interface draw/update/add etc. on_add/on_remove fire when the node enters/leaves the live tree (see #enter_tree), not at construction — so anchors and systems are available inside them.
350 |
# File 'lib/rgame/engine/node2d.rb', line 350 def on_control(actions); end |
#on_draw(renderer, view) ⇒ Object
352 |
# File 'lib/rgame/engine/node2d.rb', line 352 def on_draw(renderer, view); end |
#on_remove ⇒ Object
354 |
# File 'lib/rgame/engine/node2d.rb', line 354 def on_remove; end |
#on_update(dt) ⇒ Object
351 |
# File 'lib/rgame/engine/node2d.rb', line 351 def on_update(dt); end |
#queue_free ⇒ Object
Deferred removal (à la Godot's queue_free): mark this node for removal instead of detaching it now. A node that removes itself or a sibling mid-traversal would mutate the parent's @children while it's being iterated; marking instead and sweeping once after the tick (see #sweep_freed, flushed by the platform loop) keeps removal safe and allocation-free.
300 |
# File 'lib/rgame/engine/node2d.rb', line 300 def queue_free = @freed = true |
#remove_component(key) ⇒ Object
183 184 185 186 187 188 189 190 191 192 |
# File 'lib/rgame/engine/node2d.rb', line 183 def remove_component(key) component = get_component(key) return nil unless component component.on_detach if @in_tree @components.delete(component) @component_slots.delete(@component_slots.key(component)) component.node = nil component end |
#remove_node(node) ⇒ Object
143 144 145 146 147 148 |
# File 'lib/rgame/engine/node2d.rb', line 143 def remove_node(node) node.exit_tree if @in_tree @children.delete(node) node.parent = nil node end |
#root ⇒ Object
The top-most node — a node with no parent is its own root. Global, program-lifetime systems live here as components.
201 202 203 |
# File 'lib/rgame/engine/node2d.rb', line 201 def root @parent ? @parent.root : self end |
#sweep_freed ⇒ Object
Detach every node marked by #queue_free, depth-first, from a point outside the update traversal. Components get a hook too, so a container-style component (e.g. SceneStack) can flush the subtree it owns off the normal child list.
306 307 308 309 310 311 312 313 314 315 316 317 318 |
# File 'lib/rgame/engine/node2d.rb', line 306 def sweep_freed @components.each(&:sweep_freed) i = 0 while i < @children.size child = @children[i] if child.freed? remove_node(child) # detaches + exit_tree; @children shrinks, so don't advance i else child.sweep_freed i += 1 end end end |
#system(klass) ⇒ Object
Nearest system of a class: scene scope first, then the global root.
216 217 218 |
# File 'lib/rgame/engine/node2d.rb', line 216 def system(klass) scene&.get_component(klass) || root.get_component(klass) end |
#update(dt) ⇒ Object
update game logic and physics (might become two calls with time, but for now works in one step). This runs second in a game tick
252 253 254 255 256 257 258 259 |
# File 'lib/rgame/engine/node2d.rb', line 252 def update(dt) return if @paused resolve_origin @components.each { it.update(dt) } on_update(dt) children_in_order.each { it.update(dt) } end |