Class: RGame::Engine::Node2D

Inherits:
Object
  • Object
show all
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.

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Signal::DSL

signal

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_angleObject (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_bandObject (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_ownerObject (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_xObject (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_yObject (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

#angleObject

Returns the value of attribute angle.



12
13
14
# File 'lib/rgame/engine/node2d.rb', line 12

def angle
  @angle
end

#bandObject

Returns the value of attribute band.



19
20
21
# File 'lib/rgame/engine/node2d.rb', line 19

def band
  @band
end

#childrenObject (readonly)

Returns the value of attribute children.



19
20
21
# File 'lib/rgame/engine/node2d.rb', line 19

def children
  @children
end

#componentsObject (readonly)

Returns the value of attribute components.



19
20
21
# File 'lib/rgame/engine/node2d.rb', line 19

def components
  @components
end

#contextObject



211
212
213
# File 'lib/rgame/engine/node2d.rb', line 211

def context
  @context ||= root.context
end

#heightObject

Returns the value of attribute height.



12
13
14
# File 'lib/rgame/engine/node2d.rb', line 12

def height
  @height
end

#input_ownerObject

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

#parentObject

Returns the value of attribute parent.



12
13
14
# File 'lib/rgame/engine/node2d.rb', line 12

def parent
  @parent
end

#pausedObject

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

#sceneObject

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_orderObject

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

#widthObject

Returns the value of attribute width.



12
13
14
# File 'lib/rgame/engine/node2d.rb', line 12

def width
  @width
end

#xObject

Returns the value of attribute x.



12
13
14
# File 'lib/rgame/engine/node2d.rb', line 12

def x
  @x
end

#yObject

Returns the value of attribute y.



12
13
14
# File 'lib/rgame/engine/node2d.rb', line 12

def y
  @y
end

#zObject

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.

Raises:

  • (ArgumentError)


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_treeObject

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_treeObject

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

Returns:

  • (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

Returns:

  • (Boolean)


293
# File 'lib/rgame/engine/node2d.rb', line 293

def in_tree? = @in_tree

#on_addObject



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_removeObject



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_freeObject

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

#rootObject

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_freedObject

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