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.

Direct Known Subclasses

CameraView

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) ⇒ Node2D

Returns a new instance of Node2D.



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/rgame/engine/node2d.rb', line 16

def initialize(x: 0, y: 0, z: 0, angle: 0, width: 0, height: 0)
  @x = x
  @y = y
  @z = z
  @angle = angle
  @width = width
  @height = height
  @children = []
  @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.



14
15
16
# File 'lib/rgame/engine/node2d.rb', line 14

def abs_angle
  @abs_angle
end

#abs_xObject (readonly)

Returns the value of attribute abs_x.



14
15
16
# File 'lib/rgame/engine/node2d.rb', line 14

def abs_x
  @abs_x
end

#abs_yObject (readonly)

Returns the value of attribute abs_y.



14
15
16
# File 'lib/rgame/engine/node2d.rb', line 14

def abs_y
  @abs_y
end

#abs_zObject (readonly)

Returns the value of attribute abs_z.



14
15
16
# File 'lib/rgame/engine/node2d.rb', line 14

def abs_z
  @abs_z
end

#angleObject

Returns the value of attribute angle.



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

def angle
  @angle
end

#childrenObject (readonly)

Returns the value of attribute children.



14
15
16
# File 'lib/rgame/engine/node2d.rb', line 14

def children
  @children
end

#componentsObject (readonly)

Returns the value of attribute components.



14
15
16
# File 'lib/rgame/engine/node2d.rb', line 14

def components
  @components
end

#contextObject



110
111
112
# File 'lib/rgame/engine/node2d.rb', line 110

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

#parentObject

Returns the value of attribute parent.



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

def parent
  @parent
end

#sceneObject

The nearest enclosing scene node, marked as a boundary by SceneStack (#scene= self). Scene-lifetime systems live on it as components.



106
107
108
# File 'lib/rgame/engine/node2d.rb', line 106

def scene
  @scene || @parent&.scene
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.



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

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)


71
72
73
74
75
76
77
78
79
80
# File 'lib/rgame/engine/node2d.rb', line 71

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



32
33
34
35
36
37
38
39
40
# File 'lib/rgame/engine/node2d.rb', line 32

def add_node(node)
  @children << node
  node.parent = self
  # 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

#control(actions) ⇒ Object

updates input, both from player (readings actions) as well as AI-driven node control. This run first in a game tick Each phase settles this node first (components, then the node's own hook), then descends into children. Self-before-subtree keeps the transform flowing downward: a component or hook that moves this node does so before children resolve their origin from it.



126
127
128
129
130
131
# File 'lib/rgame/engine/node2d.rb', line 126

def control(actions)
  resolve_origin
  @components.each { it.control(actions) }
  on_control(actions)
  @children.each { it.control(actions) }
end

#draw(renderer) ⇒ Object

update visual game state, drawing the node. This runs last in a game tick



145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/rgame/engine/node2d.rb', line 145

def draw(renderer)
  resolve_origin
  # 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)
  else
    renderer.rotated(abs_angle * 180.0 / Math::PI, abs_x, abs_y) { draw_content(renderer) }
  end
  draw_children(renderer)
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.



191
192
193
194
195
196
197
198
199
# File 'lib/rgame/engine/node2d.rb', line 191

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.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).



203
204
205
206
207
208
209
210
# File 'lib/rgame/engine/node2d.rb', line 203

def exit_tree
  return unless @in_tree

  @children.each(&:exit_tree)
  on_remove
  @components.each(&:on_detach)
  @in_tree = false
end

#freed?Boolean

Returns:

  • (Boolean)


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

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.



54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/rgame/engine/node2d.rb', line 54

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)


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

def in_tree? = @in_tree

#on_addObject



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

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.



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

def on_control(actions); end

#on_draw(renderer) ⇒ Object



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

def on_draw(renderer); end

#on_removeObject



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

def on_remove; end

#on_update(dt) ⇒ Object



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

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.



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

def queue_free = @freed = true

#remove_component(key) ⇒ Object



82
83
84
85
86
87
88
89
90
91
# File 'lib/rgame/engine/node2d.rb', line 82

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



42
43
44
45
46
47
# File 'lib/rgame/engine/node2d.rb', line 42

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.



100
101
102
# File 'lib/rgame/engine/node2d.rb', line 100

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.



173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/rgame/engine/node2d.rb', line 173

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.



115
116
117
# File 'lib/rgame/engine/node2d.rb', line 115

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



136
137
138
139
140
141
# File 'lib/rgame/engine/node2d.rb', line 136

def update(dt)
  resolve_origin
  @components.each { it.update(dt) }
  on_update(dt)
  @children.each { it.update(dt) }
end