rgame 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +94 -0
- data/README.md +130 -233
- data/docs/api/README.md +116 -69
- data/docs/api/assets.md +11 -12
- data/docs/api/components.md +58 -34
- data/docs/api/drawing.md +77 -9
- data/docs/api/game.md +34 -13
- data/docs/api/input.md +232 -51
- data/docs/api/scene_graph.md +242 -15
- data/docs/api/systems.md +20 -0
- data/docs/api/toolbox.md +19 -15
- data/docs/api/ui.md +98 -0
- data/docs/api/values.md +32 -0
- data/ext/README.md +6 -5
- data/ext/rgame_core/app/app.c +182 -8
- data/ext/rgame_core/audio/audio.c +74 -0
- data/ext/rgame_core/example.rb +17 -6
- data/ext/rgame_core/extconf.rb +52 -24
- data/ext/rgame_core/graphics/canvas.c +45 -4
- data/ext/rgame_core/graphics/canvas.h +65 -10
- data/ext/rgame_core/graphics/clip.c +22 -13
- data/ext/rgame_core/include/rgame/core.h +113 -3
- data/ext/rgame_core/input/gamepad.c +57 -3
- data/ext/rgame_core/ruby/core_ext.c +16 -0
- data/ext/rgame_core/ruby/renderer_ext.c +23 -0
- data/ext/rgame_util/color_ext.c +12 -3
- data/lib/rgame/core/app.rb +2 -0
- data/lib/rgame/core/input.rb +35 -41
- data/lib/rgame/core/recording.rb +3 -1
- data/lib/rgame/core/renderer.rb +76 -28
- data/lib/rgame/core/tile_map_renderer.rb +84 -55
- data/lib/rgame/engine/camera.rb +55 -10
- data/lib/rgame/engine/component.rb +11 -1
- data/lib/rgame/engine/components/animated_sprite.rb +9 -3
- data/lib/rgame/engine/components/camera_follow.rb +44 -0
- data/lib/rgame/engine/components/character_body.rb +25 -4
- data/lib/rgame/engine/components/sprite.rb +11 -1
- data/lib/rgame/engine/components/tile_world.rb +31 -18
- data/lib/rgame/engine/culling.rb +47 -0
- data/lib/rgame/engine/debug_overlay.rb +20 -9
- data/lib/rgame/engine/input/action_mapper.rb +101 -21
- data/lib/rgame/engine/input/actions.rb +69 -12
- data/lib/rgame/engine/input/input_map.rb +178 -0
- data/lib/rgame/engine/layout.rb +82 -0
- data/lib/rgame/engine/node2d.rb +205 -36
- data/lib/rgame/engine/player.rb +69 -0
- data/lib/rgame/engine/player_layer.rb +70 -0
- data/lib/rgame/engine/players.rb +212 -0
- data/lib/rgame/engine/scene/scene_stack.rb +25 -3
- data/lib/rgame/engine/spatial_hash.rb +17 -4
- data/lib/rgame/engine/tile_map_layer.rb +84 -0
- data/lib/rgame/engine/ui/menu.rb +115 -0
- data/lib/rgame/engine/ui/menu_item.rb +84 -0
- data/lib/rgame/engine/view.rb +76 -0
- data/lib/rgame/engine/viewports.rb +174 -0
- data/lib/rgame/engine/world_view.rb +70 -0
- data/lib/rgame/engine.rb +13 -1
- data/lib/rgame/game.rb +81 -11
- data/lib/rgame/util/controls.rb +117 -41
- data/lib/rgame/util/z.rb +133 -0
- data/lib/rgame/util.rb +1 -0
- data/lib/rgame/version.rb +1 -1
- metadata +26 -11
- data/lib/rgame/engine/camera_view.rb +0 -28
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# Where each viewport goes on screen: pure rectangle arithmetic, and nothing
|
|
6
|
+
# else.
|
|
7
|
+
#
|
|
8
|
+
# Layout.rects(2, 640, 480) # => [[0, 0, 640, 240], [0, 240, 640, 240]]
|
|
9
|
+
#
|
|
10
|
+
# No state, no anchors, no lifetime — so it is specced on its own with no
|
|
11
|
+
# tree and no window, which is the point of separating it from Viewports.
|
|
12
|
+
# That system holds *which* mode is current and *who* is playing; this only
|
|
13
|
+
# answers "given a count and a window, where do they go".
|
|
14
|
+
#
|
|
15
|
+
# ## Rects tile exactly
|
|
16
|
+
#
|
|
17
|
+
# Edges are computed as `(i * total) / count` rather than by multiplying a
|
|
18
|
+
# rounded size, so three rows of a 481-pixel window are 161, 160 and 160 and
|
|
19
|
+
# the last one still ends exactly at 481. Dividing first leaves a seam at the
|
|
20
|
+
# bottom of the screen that nothing draws into — a one-pixel line that looks
|
|
21
|
+
# like a rendering bug and is really a rounding one.
|
|
22
|
+
module Layout
|
|
23
|
+
# A window's worth of rows, columns or cells for `count` viewports.
|
|
24
|
+
#
|
|
25
|
+
# Two players get rows rather than columns because halving the height of a
|
|
26
|
+
# landscape window leaves each view landscape, while halving the width
|
|
27
|
+
# leaves two tall slots that fit a 2D scene badly. Three and four share a
|
|
28
|
+
# 2x2 grid, with the fourth cell left empty for three.
|
|
29
|
+
# Forwards its block with `&block` rather than re-yielding through
|
|
30
|
+
# `{ |*args| yield(*args) }`, which builds an Array per yield. Passing a
|
|
31
|
+
# block straight on allocates nothing — measured, not assumed; see the
|
|
32
|
+
# allocation example in layout_spec.rb.
|
|
33
|
+
def self.each_rect(count, width, height, &)
|
|
34
|
+
return if count <= 0
|
|
35
|
+
return yield(0, 0, 0, width, height) if count == 1
|
|
36
|
+
return each_row(count, width, height, &) if count == 2
|
|
37
|
+
|
|
38
|
+
each_cell(count, 2, 2, width, height, &)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The same, as an Array of `[x, y, width, height]`. For specs and setup;
|
|
42
|
+
# the per-frame path uses #each_rect, which allocates nothing.
|
|
43
|
+
def self.rects(count, width, height)
|
|
44
|
+
result = []
|
|
45
|
+
each_rect(count, width, height) { |_i, x, y, w, h| result << [x, y, w, h] }
|
|
46
|
+
result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# `count` full-width rows, stacked.
|
|
50
|
+
def self.each_row(count, width, height)
|
|
51
|
+
count.times do |i|
|
|
52
|
+
top = edge(i, count, height)
|
|
53
|
+
yield(i, 0, top, width, edge(i + 1, count, height) - top)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# `count` full-height columns, side by side.
|
|
58
|
+
def self.each_column(count, width, height)
|
|
59
|
+
count.times do |i|
|
|
60
|
+
left = edge(i, count, width)
|
|
61
|
+
yield(i, left, 0, edge(i + 1, count, width) - left, height)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The first `count` cells of a `cols` x `rows` grid, filled left to right,
|
|
66
|
+
# top to bottom.
|
|
67
|
+
def self.each_cell(count, cols, rows, width, height)
|
|
68
|
+
count.times do |i|
|
|
69
|
+
col = i % cols
|
|
70
|
+
row = i / cols
|
|
71
|
+
left = edge(col, cols, width)
|
|
72
|
+
top = edge(row, rows, height)
|
|
73
|
+
yield(i, left, top, edge(col + 1, cols, width) - left, edge(row + 1, rows, height) - top)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The i-th boundary of `count` even divisions of `total`. Multiplying
|
|
78
|
+
# before dividing is what makes the divisions tile with no seam.
|
|
79
|
+
def self.edge(index, count, total) = (index * total) / count
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/rgame/engine/node2d.rb
CHANGED
|
@@ -9,18 +9,111 @@ module RGame
|
|
|
9
9
|
class Node2D
|
|
10
10
|
extend Engine::Signal::DSL
|
|
11
11
|
|
|
12
|
-
attr_accessor :x, :y, :
|
|
12
|
+
attr_accessor :x, :y, :angle, :width, :height, :parent
|
|
13
13
|
attr_writer :scene, :context
|
|
14
|
-
attr_reader :children, :components, :abs_x, :abs_y, :abs_z, :abs_angle
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
# Insertion order among siblings, the tie-breaker for equal `z`. Engine
|
|
16
|
+
# bookkeeping, set by the parent's #add_node the way `parent` is — not for
|
|
17
|
+
# game code, and meaningless on a node with no parent.
|
|
18
|
+
attr_accessor :sibling_order
|
|
19
|
+
attr_reader :children, :components, :abs_x, :abs_y, :abs_angle, :abs_input_owner, :z,
|
|
20
|
+
:band, :abs_band
|
|
21
|
+
|
|
22
|
+
# Where this node sits among its **siblings**, and nowhere else.
|
|
23
|
+
#
|
|
24
|
+
# The tree is drawn depth-first with siblings in `z` order, so a node's
|
|
25
|
+
# whole subtree is drawn before or after a sibling's whole subtree —
|
|
26
|
+
# never interleaved with it. Clouds over birds over people is three
|
|
27
|
+
# children of one node at `z` 2, 1 and 0, and each of them may be built
|
|
28
|
+
# out of as many parts as it likes without any of those parts escaping.
|
|
29
|
+
#
|
|
30
|
+
# Only the *comparison* matters. `z` is never added to anything and never
|
|
31
|
+
# reaches the renderer, so its magnitude means nothing: 1 and 1_000_000
|
|
32
|
+
# behave identically if they are the only two children, and a negative is
|
|
33
|
+
# ordinary. Equal `z` keeps the order the nodes were added in.
|
|
34
|
+
#
|
|
35
|
+
# This is deliberately unlike the additive relative z it replaces
|
|
36
|
+
# (`abs_z = parent.abs_z + z`), where a node at z 2 with a child at z 5
|
|
37
|
+
# resolved to 7 and overtook a sibling at 4 — some of a node's parts in
|
|
38
|
+
# front of something the node itself was behind. See RGame::Util::Z.
|
|
39
|
+
def z=(value)
|
|
40
|
+
@z = value
|
|
41
|
+
@parent&.children_unsorted!
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Which band this node and everything under it draws in — `:world` (the
|
|
45
|
+
# default), `:hud`, `:overlay` or `:debug`. A band beats every `z` in the
|
|
46
|
+
# tree: nothing in `:world` can draw over anything in `:hud`.
|
|
47
|
+
#
|
|
48
|
+
# Inherited like `input_owner`, and normally set by a node that exists to
|
|
49
|
+
# mark one: WorldView is `:world`, PlayerLayer is `:hud`. Setting it
|
|
50
|
+
# directly is the escape hatch — a node inside the world that must draw
|
|
51
|
+
# over the HUD says `band: :overlay` and does, still clipped to whatever
|
|
52
|
+
# its ancestors allowed. That is explicit and named, which is the whole
|
|
53
|
+
# difference from the Integer bases this replaces.
|
|
54
|
+
def band=(value)
|
|
55
|
+
Util::Z.band!(value) unless value.nil?
|
|
56
|
+
@band = value
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Whose input drives this node: an RGame::Engine::Player, or nil.
|
|
60
|
+
#
|
|
61
|
+
# Inherited down the tree exactly like the transform. Set it on a node and
|
|
62
|
+
# its whole subtree reads that player, so `ship.input_owner = players[1]`
|
|
63
|
+
# is all it takes for everything under the ship to answer to player two. A
|
|
64
|
+
# node that sets none inherits its parent's, and a tree that sets none
|
|
65
|
+
# anywhere reads the primary player — which is why single player needs no
|
|
66
|
+
# mention of this at all.
|
|
67
|
+
#
|
|
68
|
+
# **Not `player`**, deliberately, and not `controller` either. `@player` is
|
|
69
|
+
# what a game's own code calls its hero node (`examples/15_tiled_world`
|
|
70
|
+
# does), so an `attr_accessor :player` here would quietly claim that ivar
|
|
71
|
+
# out from under every scene that has one — which it did, and the symptom
|
|
72
|
+
# was the input system being handed a Node2D. `controller` is taken too:
|
|
73
|
+
# Actor#controller is the thing that produces movement intent, a different
|
|
74
|
+
# idea entirely. This name says exactly what it decides and collides with
|
|
75
|
+
# neither.
|
|
76
|
+
attr_accessor :input_owner
|
|
77
|
+
|
|
78
|
+
# A paused node skips `control` and `update` — and so does everything
|
|
79
|
+
# under it, because a subtree is only ever reached through its parent.
|
|
80
|
+
# It still **draws**: pausing is about time, not visibility, which is what
|
|
81
|
+
# lets a frozen world sit under a cutscene overlay that keeps animating.
|
|
82
|
+
#
|
|
83
|
+
# world_view.paused = true # the world stops; the overlay above it does not
|
|
84
|
+
#
|
|
85
|
+
# There is no `abs_paused` to go with `abs_input_owner`. Ownership has to
|
|
86
|
+
# be resolved because a node needs to know whose input it reads even when
|
|
87
|
+
# its parent claims nobody; pausing needs no resolution at all, because a
|
|
88
|
+
# paused node simply never descends.
|
|
89
|
+
attr_accessor :paused
|
|
90
|
+
|
|
91
|
+
def initialize(x: 0, y: 0, z: 0, angle: 0, width: 0, height: 0, input_owner: nil,
|
|
92
|
+
band: nil)
|
|
93
|
+
@input_owner = input_owner
|
|
94
|
+
@paused = false
|
|
17
95
|
@x = x
|
|
18
96
|
@y = y
|
|
19
97
|
@z = z
|
|
98
|
+
self.band = band
|
|
20
99
|
@angle = angle
|
|
21
100
|
@width = width
|
|
22
101
|
@height = height
|
|
102
|
+
# Resolved by #resolve_origin at the top of every phase. Seeded here so
|
|
103
|
+
# a node that has not been driven yet reads as being at the origin
|
|
104
|
+
# rather than as nil — which is the same answer resolve_origin gives an
|
|
105
|
+
# unparented node, and saves every reader of abs_* from a NoMethodError
|
|
106
|
+
# on a node built but not yet ticked.
|
|
107
|
+
@abs_x = @abs_y = @abs_angle = 0
|
|
108
|
+
@abs_input_owner = @input_owner
|
|
109
|
+
@abs_band = @band || Util::Z::DEFAULT
|
|
23
110
|
@children = []
|
|
111
|
+
# Siblings are drawn in `z` order, and in insertion order within one
|
|
112
|
+
# `z`. Ruby's sort is not stable, so insertion order is carried as a
|
|
113
|
+
# number rather than relied on — the same reason the C draw queue
|
|
114
|
+
# compares (z, order) instead of trusting qsort.
|
|
115
|
+
@child_seq = 0
|
|
116
|
+
@children_sorted = true
|
|
24
117
|
@components = []
|
|
25
118
|
@component_slots = {} # slot (Class by default, or a Symbol name) => component
|
|
26
119
|
@parent = nil
|
|
@@ -32,6 +125,8 @@ module RGame
|
|
|
32
125
|
def add_node(node)
|
|
33
126
|
@children << node
|
|
34
127
|
node.parent = self
|
|
128
|
+
node.sibling_order = (@child_seq += 1)
|
|
129
|
+
@children_sorted = false
|
|
35
130
|
# Defer the entered-tree cascade until this node is itself live; otherwise it
|
|
36
131
|
# fires when an ancestor enters (see #enter_tree). This is the construct-vs-enter
|
|
37
132
|
# split — a node built inside another node's initialize is not yet in the tree.
|
|
@@ -39,6 +134,12 @@ module RGame
|
|
|
39
134
|
node
|
|
40
135
|
end
|
|
41
136
|
|
|
137
|
+
# A child was added, or one changed its `z`, so the child order is stale.
|
|
138
|
+
# The sort is deferred to the next traversal rather than done here, so
|
|
139
|
+
# building a scene of a thousand nodes costs one sort rather than a
|
|
140
|
+
# thousand. Called by the engine; a game only ever assigns `z`.
|
|
141
|
+
def children_unsorted! = @children_sorted = false
|
|
142
|
+
|
|
42
143
|
def remove_node(node)
|
|
43
144
|
node.exit_tree if @in_tree
|
|
44
145
|
@children.delete(node)
|
|
@@ -123,38 +224,70 @@ module RGame
|
|
|
123
224
|
# transform flowing downward: a component or hook that moves this node
|
|
124
225
|
# does so before children resolve their origin from it.
|
|
125
226
|
|
|
126
|
-
|
|
227
|
+
# `input` is an input *source*, not one player's snapshot: an
|
|
228
|
+
# RGame::Engine::Players registry, or a bare Actions when there is only
|
|
229
|
+
# ever one answer (which is what a spec usually passes).
|
|
230
|
+
#
|
|
231
|
+
# Each node asks the source for the actions of whichever player owns it,
|
|
232
|
+
# and hands its components and its own hook that plain Actions. So a
|
|
233
|
+
# component never learns there is more than one player — `control(actions)`
|
|
234
|
+
# means the same thing it always did — while two subtrees under one tick
|
|
235
|
+
# can read two different controllers.
|
|
236
|
+
#
|
|
237
|
+
# The source is what descends, not the resolved snapshot, because
|
|
238
|
+
# ownership can change further down.
|
|
239
|
+
def control(input)
|
|
240
|
+
return if @paused
|
|
241
|
+
|
|
127
242
|
resolve_origin
|
|
243
|
+
actions = input.actions_for(@abs_input_owner)
|
|
128
244
|
@components.each { it.control(actions) }
|
|
129
245
|
on_control(actions)
|
|
130
|
-
|
|
246
|
+
children_in_order.each { it.control(input) }
|
|
131
247
|
end
|
|
132
248
|
|
|
133
249
|
# update game logic and physics (might become two calls with
|
|
134
250
|
# time, but for now works in one step). This runs second in a
|
|
135
251
|
# game tick
|
|
136
252
|
def update(dt)
|
|
253
|
+
return if @paused
|
|
254
|
+
|
|
137
255
|
resolve_origin
|
|
138
256
|
@components.each { it.update(dt) }
|
|
139
257
|
on_update(dt)
|
|
140
|
-
|
|
258
|
+
children_in_order.each { it.update(dt) }
|
|
141
259
|
end
|
|
142
260
|
|
|
143
261
|
# update visual game state, drawing the node. This runs last in
|
|
144
262
|
# a game tick
|
|
145
|
-
|
|
263
|
+
# `view` is the viewport being drawn into: its rectangle, and the camera
|
|
264
|
+
# (if any) it is seen through. Every node gets it, because a node cannot
|
|
265
|
+
# otherwise know where the edges of its own region are — a HUD laying out
|
|
266
|
+
# against the whole window is wrong the moment the window is one player's
|
|
267
|
+
# half of it — and because culling needs it once the world is drawn more
|
|
268
|
+
# than once. Most nodes ignore it and simply draw.
|
|
269
|
+
def draw(renderer, view)
|
|
146
270
|
resolve_origin
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
271
|
+
# This node's own drawing goes in its own layer: the renderer hands out
|
|
272
|
+
# the next slot in the node's band, and every `z:` the node passes is an
|
|
273
|
+
# offset inside it. Because the traversal takes slots in the order it
|
|
274
|
+
# reaches nodes, draw order *is* tree order — and because a slot is
|
|
275
|
+
# narrow, nothing a node draws can reach past itself. The node never
|
|
276
|
+
# asks for this and cannot forget it; see RGame::Util::Z.
|
|
277
|
+
renderer.layered(@abs_band) do
|
|
278
|
+
# Draw this node's own visuals oriented by its absolute angle, then descend.
|
|
279
|
+
# Children resolve their own world transform (resolve_origin already baked this
|
|
280
|
+
# node's rotation into their abs_x/abs_y), so they draw in flat world space and
|
|
281
|
+
# must NOT be nested inside this node's rotation — nesting would apply that
|
|
282
|
+
# rotation to them a second time. Unrotated nodes skip the wrapper entirely.
|
|
283
|
+
if abs_angle.zero?
|
|
284
|
+
draw_content(renderer, view)
|
|
285
|
+
else
|
|
286
|
+
renderer.rotated(abs_angle * 180.0 / Math::PI, abs_x, abs_y) { draw_content(renderer, view) }
|
|
287
|
+
end
|
|
156
288
|
end
|
|
157
|
-
|
|
289
|
+
# Outside the block: a child takes a slot of its own, after this one.
|
|
290
|
+
draw_children(renderer, view)
|
|
158
291
|
end
|
|
159
292
|
|
|
160
293
|
def in_tree? = @in_tree
|
|
@@ -195,7 +328,7 @@ module RGame
|
|
|
195
328
|
@freed = false # revive: a pooled node reacquired after death re-enters here
|
|
196
329
|
@components.each(&:on_attach)
|
|
197
330
|
on_add
|
|
198
|
-
|
|
331
|
+
children_in_order.each(&:enter_tree)
|
|
199
332
|
end
|
|
200
333
|
|
|
201
334
|
# Leaving-tree cascade: mirror of #enter_tree (children first, then this
|
|
@@ -203,7 +336,7 @@ module RGame
|
|
|
203
336
|
def exit_tree
|
|
204
337
|
return unless @in_tree
|
|
205
338
|
|
|
206
|
-
|
|
339
|
+
children_in_order.each(&:exit_tree)
|
|
207
340
|
on_remove
|
|
208
341
|
@components.each(&:on_detach)
|
|
209
342
|
@in_tree = false
|
|
@@ -216,7 +349,7 @@ module RGame
|
|
|
216
349
|
|
|
217
350
|
def on_control(actions); end
|
|
218
351
|
def on_update(dt); end
|
|
219
|
-
def on_draw(renderer); end
|
|
352
|
+
def on_draw(renderer, view); end
|
|
220
353
|
def on_add; end
|
|
221
354
|
def on_remove; end
|
|
222
355
|
|
|
@@ -224,35 +357,72 @@ module RGame
|
|
|
224
357
|
|
|
225
358
|
# This node's own drawing: its components and its draw hook, in that order.
|
|
226
359
|
# Wrapped in renderer.rotated by #draw when the node carries an absolute angle.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
360
|
+
# hot-path
|
|
361
|
+
def draw_content(renderer, view)
|
|
362
|
+
@components.each { it.draw(renderer, view) }
|
|
363
|
+
on_draw(renderer, view)
|
|
230
364
|
end
|
|
231
365
|
|
|
232
366
|
# Draw the child subtrees. Its own method so a node can wrap the whole subtree's
|
|
233
|
-
# draw in a transform
|
|
234
|
-
#
|
|
235
|
-
def draw_children(renderer)
|
|
236
|
-
|
|
367
|
+
# draw in a transform without each child knowing about it.
|
|
368
|
+
# hot-path
|
|
369
|
+
def draw_children(renderer, view)
|
|
370
|
+
children_in_order.each { it.draw(renderer, view) }
|
|
237
371
|
end
|
|
238
372
|
|
|
239
|
-
#
|
|
240
|
-
#
|
|
373
|
+
# The children, in the order every phase visits them: by `z`, then by when
|
|
374
|
+
# they were added. Sorted lazily — a scene that never touches `z` after
|
|
375
|
+
# building sorts once and then pays one boolean per phase.
|
|
376
|
+
# hot-path
|
|
377
|
+
def children_in_order
|
|
378
|
+
sort_children unless @children_sorted
|
|
379
|
+
@children
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Ruby's sort is not stable, so the insertion counter is compared
|
|
383
|
+
# explicitly. Without it two same-z siblings would swap places between
|
|
384
|
+
# frames, which reads on screen as flicker rather than as a sort problem.
|
|
385
|
+
def sort_children
|
|
386
|
+
@children_sorted = true
|
|
387
|
+
# Nothing to order, and the overwhelmingly common case for a leaf or a
|
|
388
|
+
# node with one visual — worth skipping before touching the array.
|
|
389
|
+
return if @children.size < 2
|
|
390
|
+
|
|
391
|
+
@children.sort! do |a, b|
|
|
392
|
+
order = a.z <=> b.z
|
|
393
|
+
order.zero? ? a.sibling_order <=> b.sibling_order : order
|
|
394
|
+
end
|
|
395
|
+
end
|
|
241
396
|
|
|
242
397
|
# Resolve this node's absolute transform from the parent origin passed down by the
|
|
243
|
-
# traversal. Relative x/y/
|
|
244
|
-
#
|
|
245
|
-
#
|
|
398
|
+
# traversal. Relative x/y/angle accumulate, so a nested Node offsets and rotates
|
|
399
|
+
# its whole subtree: a child's local (x, y) is rotated by the parent's accumulated
|
|
400
|
+
# angle before being added to the parent's origin.
|
|
401
|
+
#
|
|
402
|
+
# `z` is **not** among them, and that is the point: depth is decided by
|
|
403
|
+
# where the traversal reaches a node, not by summing what its ancestors
|
|
404
|
+
# picked. See #z= and RGame::Util::Z.
|
|
246
405
|
# TODO: Do not recalculate every time, but use a @dirty flag
|
|
247
|
-
# TODO: Handle z better - offset not by Z of the parent, but their
|
|
248
|
-
# depth
|
|
249
406
|
def resolve_origin
|
|
250
407
|
if @parent.nil?
|
|
251
|
-
@abs_x = @abs_y =
|
|
408
|
+
@abs_x = @abs_y = 0
|
|
252
409
|
@abs_angle = 0 # root pinned to identity, like its position
|
|
410
|
+
@abs_input_owner = @input_owner
|
|
411
|
+
@abs_band = @band || Util::Z::DEFAULT
|
|
253
412
|
return
|
|
254
413
|
end
|
|
255
414
|
|
|
415
|
+
# Ownership accumulates the same way the transform does: this node's own
|
|
416
|
+
# if it has one, otherwise whatever it inherits. Resolved here rather
|
|
417
|
+
# than walked on demand so it costs one assignment per phase, and so it
|
|
418
|
+
# is equally available in update and draw — a HUD node drawing in its
|
|
419
|
+
# player's corner wants the same answer `control` used.
|
|
420
|
+
@abs_input_owner = @input_owner || @parent.abs_input_owner
|
|
421
|
+
# The band inherits the same way. A node that declares one overrides it
|
|
422
|
+
# for its whole subtree, which is the only way out of a band and is
|
|
423
|
+
# spelled with a name rather than a number.
|
|
424
|
+
@abs_band = @band || @parent.abs_band
|
|
425
|
+
|
|
256
426
|
pa = @parent.abs_angle
|
|
257
427
|
if pa.zero? # fast path: parent unrotated -> plain translation, no trig
|
|
258
428
|
@abs_x = @parent.abs_x + @x
|
|
@@ -263,7 +433,6 @@ module RGame
|
|
|
263
433
|
@abs_x = @parent.abs_x + (@x * cos) - (@y * sin)
|
|
264
434
|
@abs_y = @parent.abs_y + (@x * sin) + (@y * cos)
|
|
265
435
|
end
|
|
266
|
-
@abs_z = @parent.abs_z + @z
|
|
267
436
|
@abs_angle = pa + @angle
|
|
268
437
|
end
|
|
269
438
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# One person playing: their device, their bindings, their camera, and their
|
|
6
|
+
# own corner of the screen.
|
|
7
|
+
#
|
|
8
|
+
# player = Player.new(id: 0, device: Controls.gamepad(0))
|
|
9
|
+
# player.actions.held?(:fire) # what they are doing this tick
|
|
10
|
+
# player.camera # where they are looking
|
|
11
|
+
# player.ui # their HUD and menus, in screen space
|
|
12
|
+
#
|
|
13
|
+
# ## Why the player owns these and the scene does not
|
|
14
|
+
#
|
|
15
|
+
# A world has one simulation and any number of viewers. Two players share
|
|
16
|
+
# every NPC and every tile, but not a camera, not a set of bindings, and not
|
|
17
|
+
# a menu — so those belong to the viewer. Putting the camera on the scene
|
|
18
|
+
# works exactly until there are two of them, and putting it on a node in the
|
|
19
|
+
# world is worse: it forces the world to know how many times it is drawn.
|
|
20
|
+
#
|
|
21
|
+
# This is the model Unreal calls a LocalPlayer and Unity spreads across
|
|
22
|
+
# PlayerInput plus a camera.
|
|
23
|
+
#
|
|
24
|
+
# ## The action *names* are shared, the bindings are not
|
|
25
|
+
#
|
|
26
|
+
# Every player reads `:fire`. What triggers it is per player: one on Space,
|
|
27
|
+
# one on a pad's X button, and the same InputMap can serve both because a
|
|
28
|
+
# device only answers for its own kind of input. Each player gets their own
|
|
29
|
+
# ActionMapper, so their edge queries are independent — one player's press
|
|
30
|
+
# cannot consume another's.
|
|
31
|
+
class Player
|
|
32
|
+
Controls = RGame::Util::Controls
|
|
33
|
+
|
|
34
|
+
attr_reader :id, :camera, :ui, :mapper
|
|
35
|
+
attr_accessor :name
|
|
36
|
+
|
|
37
|
+
# `device` may be nil, meaning "nobody is driving this player yet" — a
|
|
38
|
+
# seat waiting for a controller. Polling one reads as nothing held rather
|
|
39
|
+
# than raising, so a game can show "Player 2: press a button" without a
|
|
40
|
+
# special case.
|
|
41
|
+
def initialize(id: 0, device: Controls::KEYBOARD, input_map: nil, camera: nil)
|
|
42
|
+
@id = id
|
|
43
|
+
@camera = camera || Camera.new
|
|
44
|
+
@mapper = ActionMapper.new(input_map || InputMap.default, device: device)
|
|
45
|
+
@ui = Node2D.new
|
|
46
|
+
@name = nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def device = @mapper.device
|
|
50
|
+
def active? = !@mapper.device.nil?
|
|
51
|
+
|
|
52
|
+
# Reassigning is how a hot-plug lands: the pad that just arrived in a slot
|
|
53
|
+
# becomes this player's, and their bindings and camera carry on unchanged.
|
|
54
|
+
def device=(value)
|
|
55
|
+
@mapper.device = value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# This player's input for the current tick. Set by #poll, and a reused
|
|
59
|
+
# object — hold the Player, never this.
|
|
60
|
+
def actions = @mapper.actions
|
|
61
|
+
|
|
62
|
+
def poll(backend) = @mapper.poll(backend)
|
|
63
|
+
|
|
64
|
+
# What this player's map can answer for. The vocabulary is the game's, so
|
|
65
|
+
# it is the same for every player; the bindings behind it are not.
|
|
66
|
+
def input_map = @mapper.map
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# One player's own corner of the screen.
|
|
6
|
+
#
|
|
7
|
+
# layer = scene.add_node(PlayerLayer.new(player: players[1]))
|
|
8
|
+
# layer.add_node(inventory)
|
|
9
|
+
#
|
|
10
|
+
# Its subtree is drawn **once**, clipped to that player's viewport and
|
|
11
|
+
# translated to its corner, in screen space. That is the third kind of
|
|
12
|
+
# content a frame holds: the world is drawn once per viewport under a
|
|
13
|
+
# camera, a global overlay once across the whole window, and this once per
|
|
14
|
+
# player inside their own region.
|
|
15
|
+
#
|
|
16
|
+
# It is also the `:hud` z band's structural counterpart: everything under
|
|
17
|
+
# here draws above everything in the world, whatever either asked for.
|
|
18
|
+
#
|
|
19
|
+
# ## Its children are positioned relative to it
|
|
20
|
+
#
|
|
21
|
+
# A node at (10, 10) under this layer is ten pixels inside *that player's*
|
|
22
|
+
# region, wherever the layout has put it. The layer's translate is what does
|
|
23
|
+
# that, so the same HUD class serves either player with nothing to configure
|
|
24
|
+
# — which is the point of it being a node rather than a rect a HUD looks up.
|
|
25
|
+
#
|
|
26
|
+
# For laying out against the far edge, use the view's **size**:
|
|
27
|
+
# `view.width - margin`. `view.x` and `view.y` are where the region sits on
|
|
28
|
+
# the window and are the clip's business, not a layout origin; adding them
|
|
29
|
+
# would offset a second time.
|
|
30
|
+
#
|
|
31
|
+
# ## It says whose input its subtree reads
|
|
32
|
+
#
|
|
33
|
+
# `input_owner` is set to that player, and ownership is inherited, so a menu
|
|
34
|
+
# anywhere under here reads their controller and nobody else's. Two players
|
|
35
|
+
# with a menu open at once are independent without either one knowing the
|
|
36
|
+
# other exists — see docs/api/scene_graph.md, "Who a node answers to".
|
|
37
|
+
#
|
|
38
|
+
# ## An empty region draws nothing
|
|
39
|
+
#
|
|
40
|
+
# `Viewports#screen_for` is nil for a seat nobody is in, and for everybody
|
|
41
|
+
# while the split is collapsed — a cutscene is everyone looking at one thing,
|
|
42
|
+
# so per-player UI has no place to be. Either way this draws nothing and
|
|
43
|
+
# needs no guard at the call site.
|
|
44
|
+
class PlayerLayer < Node2D
|
|
45
|
+
# `:hud` is this player's own screen space, above every world slot and
|
|
46
|
+
# below a global overlay. It is inherited, so a menu three nodes down is
|
|
47
|
+
# in it without saying so — which is the point of the band being
|
|
48
|
+
# structural rather than a number each widget carries.
|
|
49
|
+
def initialize(player:, **)
|
|
50
|
+
super(band: :hud, **)
|
|
51
|
+
self.input_owner = player
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Whose layer this is. The same thing as `input_owner`, and stored only
|
|
55
|
+
# there: two fields would be two things to keep in step.
|
|
56
|
+
def player = input_owner
|
|
57
|
+
|
|
58
|
+
def draw(renderer, _view = nil)
|
|
59
|
+
region = system(Viewports).screen_for(player)
|
|
60
|
+
return if region.nil?
|
|
61
|
+
|
|
62
|
+
renderer.clipped(region.x, region.y, region.width, region.height) do
|
|
63
|
+
renderer.translated(region.offset_x, region.offset_y) do
|
|
64
|
+
super(renderer, region)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|