plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
3c9dcee
— Peter John
2026-11-02T20:04:16+05:30
T9: Scene on trait Actor
- README.md +19 -7
- plum-examples/scene.plum +154 -42
- plum-game/web-test/BENCHMARK.md +15 -0
- plum-game/web-test/fixtures/stress_scene.plum +75 -28
- plum-game/web-test/fixtures/stress_world.plum +3 -3
- plum-std/game/Scene.plum +360 -288
README.md
CHANGED
|
@@ -1080,22 +1080,34 @@ Games run in the browser on Plum's own WebGPU renderer (`plum-game/web/plum_brid
|
|
|
1080
1080
|
|
|
1081
1081
|
`plum-examples/game.plum` is a small arcade game: steer a ship with the arrow keys or WASD, collect orbs, dodge mines (Enter restarts). It draws shapes, text, additive orb glow (`blendMode(Blend.Add)`) and an animated sprite sheet (`plum-examples/assets/sprites.png`, generated by `plum-examples/tools/make_sprites.py`) and plays music and sound effects (`plum-examples/assets/*.wav`, generated by `plum-examples/tools/make_sounds.py`); both generators live outside the asset dir so they are not shipped. For a bigger game built on this engine see the Knights and Merchants port in the sibling repo `../kam_plum` (`./run.sh` there). Its rules are plain functions with `test` blocks, so `plum test plum-examples` covers them without a browser.
|
|
1082
1082
|
|
|
1083
|
-
**`std/game/Scene`** is a scene-graph library written in Plum (see [`plum-examples/scene.plum`](plum-examples/scene.plum)): a tree of
|
|
1083
|
+
**`std/game/Scene`** is a scene-graph library written in Plum (see [`plum-examples/scene.plum`](plum-examples/scene.plum)): a tree of actors under a `Stage`, with parent-relative position, rotation, scale and colour, per-actor `act`, `draw` and `hit` behaviour, a camera and hit testing; its demo renders a minimap into a texture with `textureCreate` and `renderTarget`. Use it for UI and other hierarchical scenes, where moving a parent should move everything in it. It is libGDX Scene2D-style subclassing on a [trait](#traits): `Actor` is a trait holding the position, size, origin, scale, rotation, colour, visibility, children and culling fields, and `Group`, `Sprite`, `Rect`, `Circle` and `Text` are records embedding it. A game adds its own kinds the same way and overrides what it needs:
|
|
1084
1084
|
|
|
1085
|
-
`
|
|
1085
|
+
```plum
|
|
1086
|
+
record Walker(Actor, speed: Float) =
|
|
1087
|
+
fun act(self, dt: Float) =
|
|
1088
|
+
self.x = self.x + self.speed * dt
|
|
1089
|
+
fun draw(self, m: Affine, alpha: Float) =
|
|
1090
|
+
drawRectBox(m, alpha, self.width, self.height, self.color)
|
|
1091
|
+
|
|
1092
|
+
stage.root.add(Walker(x: 0.0, y: 0.0, width: 8.0, height: 8.0, originX: 0.0, originY: 0.0, scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.Red, visible: true, children: List[Actor](), parent: null, scratch: List[Actor](), cull: false, cullX: 0.0, cullY: 0.0, cullW: 0.0, cullH: 0.0, speed: 30.0))
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
The default `act` acts the children (an override that keeps children calls `self.actChildren(dt)`), the default `draw` paints nothing and the default `hit(p)` tests the width by height box. Compare actors with `same(a, b)`, not `==` (a trait value is a new wrapper each time a record is converted). Every trait field is read and written through a dispatch over the program's actor kinds, so a hot loop that reads fields from outside the trait (a y-sort key over 20,000 actors) costs several times a closure-based node ([`BENCHMARK.md`](plum-game/web-test/BENCHMARK.md)); code inside the trait's default methods reads them directly. For a large world, keep the game objects in flat records and lists instead (for example `enum Entity = Unit | House`) and draw them directly through `std/game/Game`.
|
|
1096
|
+
|
|
1097
|
+
`std/game/Scene` follows libGDX Scene2D for draw order and culling. An actor's z-index is its position in its parent's child list (draw order is list order, hit testing goes in reverse):
|
|
1086
1098
|
|
|
1087
1099
|
```plum
|
|
1088
|
-
|
|
1100
|
+
a.zIndex() # index among its siblings, -1 without a parent
|
|
1089
|
-
|
|
1101
|
+
a.setZIndex(0) # move within the parent (clamped to 0..length-1)
|
|
1090
|
-
|
|
1102
|
+
a.toFront() # last child: drawn on top, hit first
|
|
1091
|
-
|
|
1103
|
+
a.toBack() # first child: drawn first
|
|
1092
1104
|
|
|
1093
1105
|
world.sortChildren(|a, b| a.y < b.y) # stable, in place, no allocation once warm
|
|
1094
1106
|
world.setCullingArea(cam.x, cam.y, viewW, viewH) # in world's own coordinates
|
|
1095
1107
|
world.clearCullingArea()
|
|
1096
1108
|
```
|
|
1097
1109
|
|
|
1098
|
-
`sortChildren(before)` is a natural merge sort with a scratch list kept on the
|
|
1110
|
+
`sortChildren(before)` is a natural merge sort with a scratch list kept on the actor: a list that is already sorted costs n - 1 comparisons and a nearly sorted one (a y-sorted layer re-sorted every frame) about 2n, with O(n log n) as the worst case; equal keys keep their order, so ties do not flicker. `setCullingArea` makes a group skip each direct child whose box (position and size, scaled about its origin; rotation ignored) lies outside the rectangle, in both `draw` and `hit`, with the child's whole subtree; children with no size (plain groups) are never culled. In [`plum-game/web-test/BENCHMARK.md`](plum-game/web-test/BENCHMARK.md) a 20,000-sprite y-sorted layer costs 1.15 ms per frame with culling and 4.20 ms without on the earlier closure-based `Node` (2.09 ms with culling on `trait Actor`, which pays a dispatch per field read in the sort).
|
|
1099
1111
|
|
|
1100
1112
|
A layered world (a layer recipe for a large map) is a few groups under the stage root, drawn in order: `terrain` (static, culled), `world` (y-sorted every frame, culled), `effects` (culled) and `ui` (not culled, in screen space with a second `Stage` or camera). Each frame: `world.sortChildren(|a, b| a.y < b.y)`, then `setCullingArea` on `terrain`, `world` and `effects` from the camera view.
|
|
1101
1113
|
|
plum-examples/scene.plum
CHANGED
|
@@ -36,47 +36,88 @@ primitive Layout =
|
|
|
36
36
|
WalkMin = 500.0
|
|
37
37
|
WalkMax = 1300.0
|
|
38
38
|
|
|
39
|
+
# Actor kinds of this game: `record X(Actor, ...)` embeds the scene's `Actor` trait, so
|
|
40
|
+
# an X goes wherever an Actor is expected and its own `act` and `draw` run in the tree.
|
|
41
|
+
|
|
42
|
+
# A group that turns `speed` radians per second and carries its children round.
|
|
43
|
+
record Orbit(Actor, speed: Float) =
|
|
44
|
+
|
|
45
|
+
fun create(speed: Float) -> Orbit =
|
|
46
|
+
Orbit(
|
|
47
|
+
x: 0.0, y: 0.0, width: 0.0, height: 0.0, originX: 0.0, originY: 0.0,
|
|
48
|
+
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
49
|
+
children: List[Actor](), parent: null, scratch: List[Actor](),
|
|
50
|
+
cull: false, cullX: 0.0, cullY: 0.0, cullW: 0.0, cullH: 0.0,
|
|
51
|
+
speed: speed)
|
|
52
|
+
|
|
53
|
+
fun act(self, dt: Float) =
|
|
54
|
+
self.rotation = self.rotation + self.speed * dt
|
|
55
|
+
self.actChildren(dt)
|
|
56
|
+
|
|
57
|
+
# A sprite that turns on its own axis, `speed` radians per second.
|
|
58
|
+
record Spinner(Actor, speed: Float, tex: Int, srcX: Float, srcY: Float, srcW: Float, srcH: Float) =
|
|
59
|
+
|
|
60
|
+
fun create(sheet: Int, sx: Float, sy: Float, size: Float, speed: Float) -> Spinner =
|
|
61
|
+
Spinner(
|
|
62
|
+
x: 0.0, y: 0.0, width: size, height: size, originX: size / 2.0, originY: size / 2.0,
|
|
63
|
+
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
64
|
+
children: List[Actor](), parent: null, scratch: List[Actor](),
|
|
65
|
+
cull: false, cullX: 0.0, cullY: 0.0, cullW: 0.0, cullH: 0.0,
|
|
66
|
+
speed: speed, tex: sheet, srcX: sx, srcY: sy, srcW: Layout.Cell, srcH: Layout.Cell)
|
|
67
|
+
|
|
68
|
+
fun act(self, dt: Float) =
|
|
69
|
+
self.rotation = self.rotation + self.speed * dt
|
|
70
|
+
self.actChildren(dt)
|
|
71
|
+
|
|
72
|
+
fun draw(self, m: Affine, alpha: Float) =
|
|
73
|
+
drawSpriteFrame(m, alpha, self.width, self.height, self.color, self.tex, self.srcX, self.srcY, self.srcW, self.srcH)
|
|
74
|
+
|
|
75
|
+
# A walker: moves along x at `pace` and bobs on a wave of its x.
|
|
76
|
+
record Walker(Actor, pace: Float, phase: Float, tex: Int, srcX: Float, srcY: Float, srcW: Float, srcH: Float) =
|
|
77
|
+
|
|
78
|
+
fun create(sheet: Int, sx: Float, pace: Float, phase: Float) -> Walker =
|
|
79
|
+
Walker(
|
|
80
|
+
x: 0.0, y: 0.0, width: 24.0, height: 24.0, originX: 0.0, originY: 0.0,
|
|
81
|
+
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
82
|
+
children: List[Actor](), parent: null, scratch: List[Actor](),
|
|
83
|
+
cull: false, cullX: 0.0, cullY: 0.0, cullW: 0.0, cullH: 0.0,
|
|
84
|
+
pace: pace, phase: phase, tex: sheet, srcX: sx, srcY: Layout.Cell, srcW: Layout.Cell, srcH: Layout.Cell)
|
|
85
|
+
|
|
86
|
+
fun act(self, dt: Float) =
|
|
87
|
+
nx := self.x + dt * self.pace
|
|
88
|
+
if nx > Layout.WalkMax
|
|
89
|
+
nx = 0.0 - Layout.WalkMin
|
|
90
|
+
if nx < 0.0 - Layout.WalkMin
|
|
91
|
+
nx = Layout.WalkMax
|
|
92
|
+
self.x = nx
|
|
93
|
+
self.y = 510.0 + 50.0 * Math.sin(nx / 90.0 + self.phase)
|
|
94
|
+
|
|
95
|
+
fun draw(self, m: Affine, alpha: Float) =
|
|
96
|
+
drawSpriteFrame(m, alpha, self.width, self.height, self.color, self.tex, self.srcX, self.srcY, self.srcW, self.srcH)
|
|
97
|
+
|
|
39
98
|
# Everything `update` and `draw` need. `bodies` and `names` are parallel (every
|
|
40
99
|
# planet and moon, each planet followed by its moons), as are `halos` (the glow shown when selected).
|
|
41
100
|
record Sky(
|
|
42
101
|
stage: Stage,
|
|
43
|
-
bodies: List[
|
|
102
|
+
bodies: List[Actor],
|
|
44
|
-
halos: List[
|
|
103
|
+
halos: List[Actor],
|
|
45
104
|
names: List[Str],
|
|
46
|
-
sun:
|
|
105
|
+
sun: Spinner,
|
|
47
|
-
label:
|
|
106
|
+
label: Text,
|
|
48
|
-
crowd:
|
|
107
|
+
crowd: Group,
|
|
49
|
-
front:
|
|
108
|
+
front: Actor?,
|
|
50
109
|
selected: Int,
|
|
51
110
|
clock: Float,
|
|
52
111
|
paused: Bool,
|
|
53
112
|
map: Int)
|
|
54
113
|
|
|
55
|
-
# Gives the node a behaviour that turns it `speed` radians per second.
|
|
56
|
-
fun spun(n: Node, speed: Float) -> Node =
|
|
57
|
-
turn := |m, dt|
|
|
58
|
-
m.rotation = m.rotation + speed * dt
|
|
59
|
-
n.setAct(turn)
|
|
60
|
-
|
|
61
|
-
# A walker's behaviour: it moves along x at `speed` and bobs on a wave of its x.
|
|
62
|
-
fun walking(n: Node, pace: Float, phase: Float) -> Node =
|
|
63
|
-
step: fn(Node, Float) := |m, dt|
|
|
64
|
-
nx := m.x + dt * pace
|
|
65
|
-
if nx > Layout.WalkMax
|
|
66
|
-
nx = 0.0 - Layout.WalkMin
|
|
67
|
-
if nx < 0.0 - Layout.WalkMin
|
|
68
|
-
nx = Layout.WalkMax
|
|
69
|
-
_ := m.at(nx, 510.0 + 50.0 * Math.sin(nx / 90.0 + phase))
|
|
70
|
-
n.setAct(step)
|
|
71
|
-
|
|
72
114
|
# A body sprite and its (hidden) halo in a group `slot` centred on the group's own
|
|
73
115
|
# origin. The caller places the slot; the sprite spins on its own axis.
|
|
74
|
-
fun addBody(sky: Sky, sheet: Int, name: Str, row: Int, frame: Int, size: Float, spin: Float) ->
|
|
116
|
+
fun addBody(sky: Sky, sheet: Int, name: Str, row: Int, frame: Int, size: Float, spin: Float) -> Actor =
|
|
75
|
-
slot :=
|
|
117
|
+
slot := Group.create()
|
|
76
|
-
halo :=
|
|
118
|
+
halo := Circle.create(size * 0.8, Color(r: 1.0, g: 0.9, b: 0.5, a: 0.35)).at(0.0 - size * 0.8, 0.0 - size * 0.8)
|
|
77
119
|
halo.visible = false
|
|
78
|
-
body :=
|
|
120
|
+
body := Spinner.create(sheet, Float(frame) * Layout.Cell, Float(row) * Layout.Cell, size, spin).at(0.0 - size / 2.0, 0.0 - size / 2.0)
|
|
79
|
-
body = spun(body, spin)
|
|
80
121
|
slot.add(halo)
|
|
81
122
|
slot.add(body)
|
|
82
123
|
sky.halos.push(halo)
|
|
@@ -86,28 +127,32 @@ fun addBody(sky: Sky, sheet: Int, name: Str, row: Int, frame: Int, size: Float,
|
|
|
86
127
|
|
|
87
128
|
fun newSky(sheet: Int) -> Sky =
|
|
88
129
|
stage := Stage.create()
|
|
89
|
-
backdrop :=
|
|
130
|
+
backdrop := Rect.create(Layout.Width, Layout.Height, Color.rgb(0.04, 0.05, 0.1))
|
|
90
131
|
stage.root.add(backdrop)
|
|
91
132
|
for i := range 40
|
|
92
|
-
star :=
|
|
133
|
+
star := Circle.create(1.0 + Float(i % 3) * 0.5, Color(r: 0.7, g: 0.8, b: 1.0, a: 0.4)).at(Float({i * 7919} % 800), Float({i * 104729} % 600))
|
|
93
134
|
stage.root.add(star)
|
|
94
|
-
sun :=
|
|
135
|
+
sun := Spinner.create(sheet, 0.0, Layout.Cell, 96.0, 0.3)
|
|
136
|
+
sun.x = Layout.CenterX - 48.0
|
|
137
|
+
sun.y = Layout.CenterY - 48.0
|
|
95
|
-
label :=
|
|
138
|
+
label := Text.create("Click a planet", 24.0, Color.White)
|
|
139
|
+
label.x = 16.0
|
|
140
|
+
label.y = 12.0
|
|
96
141
|
sky := Sky(
|
|
97
|
-
stage: stage, bodies: List[
|
|
142
|
+
stage: stage, bodies: List[Actor](), halos: List[Actor](), names: List[Str](),
|
|
98
|
-
sun: sun, label: label, crowd:
|
|
143
|
+
sun: sun, label: label, crowd: Group.create(), front: null, selected: -1, clock: 0.0, paused: false, map: -1)
|
|
99
144
|
# faint orbit rings, added first so every body draws (and is hit) above them
|
|
100
145
|
for i := range Layout.Planets
|
|
101
146
|
r := 90.0 + Float(i) * 60.0
|
|
102
|
-
stage.root.add(
|
|
147
|
+
stage.root.add(Circle.create(r, Color(r: 1.0, g: 1.0, b: 1.0, a: 0.04)).at(Layout.CenterX - r, Layout.CenterY - r))
|
|
103
148
|
planetNames := List("Ember", "Cinder", "Ashen", "Glacier")
|
|
104
149
|
moons := List(0, 1, 2, 1)
|
|
105
150
|
for i := range Layout.Planets
|
|
106
|
-
orbit :=
|
|
151
|
+
orbit := Orbit.create(0.9 / Float(1 + i)).at(Layout.CenterX, Layout.CenterY)
|
|
107
152
|
planet := addBody(sky, sheet, planetNames.at(i), 2, i, 40.0, 1.5).at(90.0 + Float(i) * 60.0, 0.0)
|
|
108
153
|
orbit.add(planet)
|
|
109
154
|
for m := range moons.at(i)
|
|
110
|
-
moonOrbit :=
|
|
155
|
+
moonOrbit := Orbit.create(2.0 + Float(m))
|
|
111
156
|
moon := addBody(sky, sheet, "{planetNames.at(i)} moon {m + 1}", 1, m, 18.0, 3.0).at(36.0 + Float(m) * 16.0, 0.0)
|
|
112
157
|
moonOrbit.add(moon)
|
|
113
158
|
planet.add(moonOrbit)
|
|
@@ -117,8 +162,8 @@ fun newSky(sheet: Int) -> Sky =
|
|
|
117
162
|
speed := 20.0 + Float(i % 7) * 9.0
|
|
118
163
|
if i % 2 == 0
|
|
119
164
|
speed = 0.0 - speed
|
|
120
|
-
walker :=
|
|
165
|
+
walker := Walker.create(sheet, Float(i % 3) * Layout.Cell, speed, Float(i)).at(Layout.WalkMax - 1800.0 + Float(i * 61 % 1800), 510.0)
|
|
121
|
-
sky.crowd.add(
|
|
166
|
+
sky.crowd.add(walker)
|
|
122
167
|
stage.root.add(sky.crowd)
|
|
123
168
|
stage.root.add(label)
|
|
124
169
|
sky
|
|
@@ -132,9 +177,9 @@ fun fit(sky: Sky, sw: Float, sh: Float) =
|
|
|
132
177
|
sky.crowd.setCullingArea(sky.stage.camX, sky.stage.camY, sw / zoom, sh / zoom)
|
|
133
178
|
|
|
134
179
|
# The index of a body (or its halo) in `bodies`, or -1.
|
|
135
|
-
fun indexOf(sky: Sky, n:
|
|
180
|
+
fun indexOf(sky: Sky, n: Actor) -> Int =
|
|
136
181
|
for i := range sky.bodies.length()
|
|
137
|
-
if sky.bodies.at(i)
|
|
182
|
+
if same(sky.bodies.at(i), n) || same(sky.halos.at(i), n)
|
|
138
183
|
return i
|
|
139
184
|
-1
|
|
140
185
|
|
|
@@ -155,7 +200,7 @@ fun clickAt(sky: Sky, x: Float, y: Float) =
|
|
|
155
200
|
if n := sky.stage.hit(x, y)
|
|
156
201
|
i = indexOf(sky, n)
|
|
157
202
|
if p := n.parent
|
|
158
|
-
if p == sky.crowd
|
|
203
|
+
if p.children == sky.crowd.children
|
|
159
204
|
n.toFront()
|
|
160
205
|
sky.front = n
|
|
161
206
|
if i == sky.selected
|
|
@@ -226,6 +271,38 @@ export fun draw(sky: Sky) =
|
|
|
226
271
|
|
|
227
272
|
# --- tests ---
|
|
228
273
|
|
|
274
|
+
# Test probes (the std library keeps its own tests free of extra Actor kinds).
|
|
275
|
+
# A group that turns by dt each act and acts its children too.
|
|
276
|
+
record ProbeSpinner(Actor) =
|
|
277
|
+
|
|
278
|
+
fun create() -> ProbeSpinner =
|
|
279
|
+
ProbeSpinner(
|
|
280
|
+
x: 0.0, y: 0.0, width: 0.0, height: 0.0, originX: 0.0, originY: 0.0,
|
|
281
|
+
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
282
|
+
children: List[Actor](), parent: null, scratch: List[Actor](),
|
|
283
|
+
cull: false, cullX: 0.0, cullY: 0.0, cullW: 0.0, cullH: 0.0)
|
|
284
|
+
|
|
285
|
+
fun act(self, dt: Float) =
|
|
286
|
+
self.rotation = self.rotation + dt
|
|
287
|
+
self.actChildren(dt)
|
|
288
|
+
|
|
289
|
+
# A walker: moves along x in its own `act` and counts the frames it draws itself in.
|
|
290
|
+
record ProbeWalker(Actor, speed: Float, drawn: Int) =
|
|
291
|
+
|
|
292
|
+
fun create(speed: Float) -> ProbeWalker =
|
|
293
|
+
ProbeWalker(
|
|
294
|
+
x: 0.0, y: 0.0, width: 10.0, height: 10.0, originX: 0.0, originY: 0.0,
|
|
295
|
+
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
296
|
+
children: List[Actor](), parent: null, scratch: List[Actor](),
|
|
297
|
+
cull: false, cullX: 0.0, cullY: 0.0, cullW: 0.0, cullH: 0.0,
|
|
298
|
+
speed: speed, drawn: 0)
|
|
299
|
+
|
|
300
|
+
fun act(self, dt: Float) =
|
|
301
|
+
self.x = self.x + self.speed * dt
|
|
302
|
+
|
|
303
|
+
fun draw(self, m: Affine, alpha: Float) =
|
|
304
|
+
self.drawn = self.drawn + 1
|
|
305
|
+
|
|
229
306
|
fun near(a: Float, b: Float) -> Bool =
|
|
230
307
|
{a - b}.abs() < 0.01
|
|
231
308
|
|
|
@@ -323,3 +400,38 @@ test "clicking a walker brings it to the front, even after the next sort"
|
|
|
323
400
|
assert w.zIndex() == 59
|
|
324
401
|
tick(sky, 0.1)
|
|
325
402
|
assert w.zIndex() == 59
|
|
403
|
+
|
|
404
|
+
test "act runs behaviours depth first, including invisible actors"
|
|
405
|
+
stage := Stage.create()
|
|
406
|
+
g := ProbeSpinner.create()
|
|
407
|
+
stage.root.add(g)
|
|
408
|
+
k := ProbeSpinner.create()
|
|
409
|
+
g.add(k)
|
|
410
|
+
g.visible = false
|
|
411
|
+
stage.act(0.5)
|
|
412
|
+
stage.act(0.25)
|
|
413
|
+
assert near(g.rotation, 0.75)
|
|
414
|
+
assert near(k.rotation, 0.75)
|
|
415
|
+
assert stage.root.rotation == 0.0
|
|
416
|
+
|
|
417
|
+
test "walker_record: a Walker in a Stage moves via its own act and draws via its own draw"
|
|
418
|
+
stage := Stage.create()
|
|
419
|
+
w := ProbeWalker.create(4.0)
|
|
420
|
+
stage.root.add(w)
|
|
421
|
+
stage.root.add(Group.create())
|
|
422
|
+
stage.act(0.5)
|
|
423
|
+
stage.act(0.5)
|
|
424
|
+
assert near(w.x, 4.0)
|
|
425
|
+
assert w.drawn == 0
|
|
426
|
+
stage.root.drawTree(Affine.Identity, 1.0)
|
|
427
|
+
stage.root.drawTree(Affine.Identity, 1.0)
|
|
428
|
+
assert w.drawn == 2
|
|
429
|
+
w.visible = false
|
|
430
|
+
stage.root.drawTree(Affine.Identity, 1.0)
|
|
431
|
+
assert w.drawn == 2
|
|
432
|
+
w.visible = true
|
|
433
|
+
# a trait value reaches the same record
|
|
434
|
+
a: Actor := w
|
|
435
|
+
a.act(1.0)
|
|
436
|
+
assert near(w.x, 8.0)
|
|
437
|
+
assert same({stage.hit(8.5, 5.0) ?: crash "miss"}, w)
|
plum-game/web-test/BENCHMARK.md
CHANGED
|
@@ -112,3 +112,18 @@ Fixture `stress_world.plum`: one world layer with 20,000 sprites in a 4000x2300
|
|
|
112
112
|
| stress_scene, 5.8k nodes (reference) | 1.71 | 1.9 | 584 | 0.060 | 1.251 | 0.261 |
|
|
113
113
|
|
|
114
114
|
With culling the frame costs about what the 2,000 visible sprites cost, not what the 20,000 nodes cost: 1.15 ms against 4.20 ms without it (3.7x), and below the 5k-node `stress_scene` (1.71 ms), which draws every node. Culling removes draw (2.92 -> 0.52 ms) and render (0.69 -> 0.09 ms, 20,000 against about 2,000 sprites reaching the batcher); what stays is `update` at about 0.42 ms, which is the y-sort's O(n) scan over all 20,000 children (n - 1 comparisons through the closure, about 20 ns each, plus the merge of the few displaced sprites) and the 500 walkers. So the remaining cost grows with total nodes through the sort and the `act` walk, but at a fraction of a draw, and the draw side now scales with what is on screen.
|
|
115
|
+
|
|
116
|
+
## Scene on `trait Actor` vs closure `Node`
|
|
117
|
+
|
|
118
|
+
`std/game/Scene` moved from one `Node` record with `act: fn(Node, Float)` closures to `trait Actor` with records embedding it (`Group`, `Sprite`, ... and the game's own, such as `Walker`). Same machine and method as above (headed, gpu per-call, `--uncapped --measure 10 --frames-wait 6`, two runs per cell). The fixtures were ported 1:1 (same node counts and motion); in `stress_scene` each node kind became a record with its own `act` and `draw`.
|
|
119
|
+
|
|
120
|
+
| fixture | version | avg ms (2 runs) | p95 | update | draw | render |
|
|
121
|
+
|---|---|---|---|---|---|---|
|
|
122
|
+
| stress_scene, 5.8k nodes | closure `Node` | 1.60, 1.62 | 1.7-1.8 | 0.055 | 1.19-1.20 | 0.24 |
|
|
123
|
+
| stress_scene, 5.8k nodes | `trait Actor` | 1.69, 1.69 | 1.8 | 0.039 | 1.27 | 0.26 |
|
|
124
|
+
| stress_world, 20k nodes, culling on | closure `Node` | 1.09, 1.09 | 1.2 | 0.421 | 0.495 | 0.082 |
|
|
125
|
+
| stress_world, 20k nodes, culling on | `trait Actor` | 2.09, 2.12 | 2.3-2.4 | 1.29-1.31 | 0.59 | 0.09 |
|
|
126
|
+
|
|
127
|
+
- `stress_scene`: +5% (1.60 -> 1.69 ms), at the edge of noise. The per-node closure call became a trait dispatch, which is cheaper (`update` 0.055 -> 0.039 ms); `draw` grew by 0.08 ms because the draw walk reads child fields through the trait.
|
|
128
|
+
- `stress_world`: +92% (1.09 -> 2.09 ms), above the 10% threshold. All of it is `update`, which here is the y-sort: `sortChildren(|a, b| a.y < b.y)` makes 2 x 19,999 `a.y` reads per frame and every read of a trait field is a dispatch (`__get_y`) over the program's actor kinds, about 30-40 ns each against about 10 ns for a direct field read. Without the sort `update` is 0.01 ms. The cost scales with the number of actor kinds in the program: dropping two unused test-only kinds took `update` from 1.65 to 1.29 ms, which is why the Scene tests that need their own `act`/`draw` records live in `plum-examples/scene.plum` and not in `std/game/Scene.plum`. Draw is +0.1 ms (culling and transforms run as trait default methods, so they read fields directly, one dispatch per child).
|
|
129
|
+
- Reading the trait's fields from outside it (a game's sort key, hit tests on `.x`/`.y`) is the price of the trait. Default methods run on the concrete record and read its fields directly, which is why `local()`, `outside()`, `drawTree()` and `hitTree()` are trait methods rather than free functions.
|
plum-game/web-test/fixtures/stress_scene.plum
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# The stress scene of stress.plum built as a Scene2D-like scene graph (std/game/Scene):
|
|
2
2
|
# 5000 sprites in 50 slowly rotating groups, 500 rects, 200 circles and 50 texts
|
|
3
3
|
# with a changing number, drawn through a camera. Every node moves in its own
|
|
4
|
-
# `act`
|
|
4
|
+
# `act`, so each frame runs ~5800 trait-dispatched acts and a composed-transform draw.
|
|
5
5
|
module stress_scene
|
|
6
6
|
import std/Str
|
|
7
7
|
import std/Int
|
|
@@ -26,31 +26,78 @@ record Demo(sheet: Int, stage: Stage)
|
|
|
26
26
|
fun hue(i: Int) -> Color =
|
|
27
27
|
Color(r: Float(i % 7) / 7.0, g: Float(i % 5) / 5.0, b: 0.4 + Float(i % 3) / 5.0, a: 0.8)
|
|
28
28
|
|
|
29
|
+
# The actor kinds of this scene: records embedding the Actor trait, each with its own `act`.
|
|
30
|
+
record SpinGroup(Actor) =
|
|
31
|
+
fun create() -> SpinGroup =
|
|
32
|
+
SpinGroup(
|
|
33
|
+
x: 0.0, y: 0.0, width: 0.0, height: 0.0, originX: 75.0, originY: 85.0,
|
|
34
|
+
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
35
|
+
children: List[Actor](), parent: null, scratch: List[Actor](),
|
|
36
|
+
cull: false, cullX: 0.0, cullY: 0.0, cullW: 0.0, cullH: 0.0)
|
|
29
|
-
fun
|
|
37
|
+
fun act(self, dt: Float) =
|
|
30
|
-
|
|
38
|
+
self.rotation = self.rotation + 0.004
|
|
39
|
+
self.actChildren(dt)
|
|
31
40
|
|
|
32
41
|
# Turns the sprite; its frame follows the angle, advancing every 6 frames.
|
|
42
|
+
record Dancer(Actor, tex: Int, srcX: Float) =
|
|
43
|
+
fun create(tex: Int, srcX: Float) -> Dancer =
|
|
44
|
+
Dancer(
|
|
45
|
+
x: 0.0, y: 0.0, width: 16.0, height: 16.0, originX: 8.0, originY: 8.0,
|
|
46
|
+
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
47
|
+
children: List[Actor](), parent: null, scratch: List[Actor](),
|
|
48
|
+
cull: false, cullX: 0.0, cullY: 0.0, cullW: 0.0, cullH: 0.0,
|
|
49
|
+
tex: tex, srcX: srcX)
|
|
33
|
-
fun
|
|
50
|
+
fun act(self, dt: Float) =
|
|
34
|
-
|
|
51
|
+
self.rotation = self.rotation + 0.02
|
|
35
|
-
|
|
52
|
+
self.srcX = Float(Int(self.rotation * 50.0) / 6 % 4 * 16)
|
|
53
|
+
fun draw(self, m: Affine, alpha: Float) =
|
|
54
|
+
drawSpriteFrame(m, alpha, self.width, self.height, self.color, self.tex, self.srcX, 0.0, 16.0, 16.0)
|
|
36
55
|
|
|
56
|
+
record DriftRect(Actor) =
|
|
57
|
+
fun create(c: Color) -> DriftRect =
|
|
58
|
+
DriftRect(
|
|
59
|
+
x: 0.0, y: 0.0, width: 12.0, height: 8.0, originX: 0.0, originY: 0.0,
|
|
60
|
+
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: c, visible: true,
|
|
61
|
+
children: List[Actor](), parent: null, scratch: List[Actor](),
|
|
62
|
+
cull: false, cullX: 0.0, cullY: 0.0, cullW: 0.0, cullH: 0.0)
|
|
37
|
-
fun
|
|
63
|
+
fun act(self, dt: Float) =
|
|
38
|
-
|
|
64
|
+
self.x = self.x + 2.0
|
|
39
|
-
|
|
65
|
+
if self.x > Float(Counts.WorldW)
|
|
40
|
-
|
|
66
|
+
self.x = self.x - Float(Counts.WorldW)
|
|
67
|
+
fun draw(self, m: Affine, alpha: Float) =
|
|
68
|
+
drawRectBox(m, alpha, self.width, self.height, self.color)
|
|
41
69
|
|
|
70
|
+
record DriftCircle(Actor) =
|
|
71
|
+
fun create(c: Color) -> DriftCircle =
|
|
72
|
+
DriftCircle(
|
|
73
|
+
x: 0.0, y: 0.0, width: 12.0, height: 12.0, originX: 0.0, originY: 0.0,
|
|
74
|
+
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: c, visible: true,
|
|
75
|
+
children: List[Actor](), parent: null, scratch: List[Actor](),
|
|
76
|
+
cull: false, cullX: 0.0, cullY: 0.0, cullW: 0.0, cullH: 0.0)
|
|
42
|
-
fun
|
|
77
|
+
fun act(self, dt: Float) =
|
|
43
|
-
|
|
78
|
+
self.x = self.x + 3.0
|
|
44
|
-
|
|
79
|
+
self.y = self.y + 1.0
|
|
45
|
-
|
|
80
|
+
if self.x > Float(Counts.WorldW)
|
|
46
|
-
|
|
81
|
+
self.x = self.x - Float(Counts.WorldW)
|
|
47
|
-
|
|
82
|
+
if self.y > Float(Counts.WorldH)
|
|
48
|
-
|
|
83
|
+
self.y = self.y - Float(Counts.WorldH)
|
|
84
|
+
fun draw(self, m: Affine, alpha: Float) =
|
|
85
|
+
drawDisc(m, alpha, self.width / 2.0, self.color)
|
|
49
86
|
|
|
50
|
-
# `
|
|
87
|
+
# `n` is the number shown.
|
|
88
|
+
record CountText(Actor, n: Int, text: Str) =
|
|
89
|
+
fun create(n: Int) -> CountText =
|
|
90
|
+
CountText(
|
|
91
|
+
x: 0.0, y: 0.0, width: 60.0, height: 14.0, originX: 0.0, originY: 0.0,
|
|
92
|
+
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
93
|
+
children: List[Actor](), parent: null, scratch: List[Actor](),
|
|
94
|
+
cull: false, cullX: 0.0, cullY: 0.0, cullW: 0.0, cullH: 0.0,
|
|
95
|
+
n: n, text: "n {n}")
|
|
51
|
-
fun
|
|
96
|
+
fun act(self, dt: Float) =
|
|
52
|
-
|
|
97
|
+
self.n = self.n + 1
|
|
53
|
-
|
|
98
|
+
self.text = "n {self.n}"
|
|
99
|
+
fun draw(self, m: Affine, alpha: Float) =
|
|
100
|
+
drawLabel(m, alpha, self.text, 14.0, self.color)
|
|
54
101
|
|
|
55
102
|
export fun init() -> Demo =
|
|
56
103
|
sheet := textureLoad("sprites.png")
|
|
@@ -58,21 +105,21 @@ export fun init() -> Demo =
|
|
|
58
105
|
stage.camScale = 0.8
|
|
59
106
|
per := Counts.Sprites / Counts.Groups
|
|
60
107
|
for gi := range Counts.Groups
|
|
61
|
-
g :=
|
|
108
|
+
g := SpinGroup.create().at(Float(gi % 10 * 160), Float(gi / 10 * 180))
|
|
62
109
|
for j := range per
|
|
63
110
|
i := gi * per + j
|
|
111
|
+
s := Dancer.create(sheet, Float(i % 4 * 16))
|
|
64
|
-
s
|
|
112
|
+
s.x = Float({j * j * 13 + j * 37} % 150)
|
|
113
|
+
s.y = Float({j * j * 29 + j * 53} % 170)
|
|
65
114
|
s.rotation = Float(i)
|
|
66
115
|
g.add(s)
|
|
67
116
|
stage.root.add(g)
|
|
68
117
|
for i := range Counts.Rects
|
|
69
|
-
stage.root.add(
|
|
118
|
+
stage.root.add(DriftRect.create(hue(i)).at(Float(i * 61 % Counts.WorldW), Float(i * 29 % Counts.WorldH)))
|
|
70
119
|
for i := range Counts.Circles
|
|
71
|
-
stage.root.add(
|
|
120
|
+
stage.root.add(DriftCircle.create(hue(i + 3)).at(Float(i * 83 % Counts.WorldW), Float(i * 47 % Counts.WorldH)))
|
|
72
121
|
for i := range Counts.Texts
|
|
73
|
-
|
|
122
|
+
stage.root.add(CountText.create(i).at(Float(i * 30 % Counts.WorldW), Float(i * 17 % Counts.WorldH)))
|
|
74
|
-
t.tex = i
|
|
75
|
-
stage.root.add(t)
|
|
76
123
|
Demo(sheet: sheet, stage: stage)
|
|
77
124
|
|
|
78
125
|
export fun update(g: Demo, dt: Float) =
|
plum-game/web-test/fixtures/stress_world.plum
CHANGED
|
@@ -23,17 +23,17 @@ primitive World =
|
|
|
23
23
|
ViewH = 720.0
|
|
24
24
|
Culling = 1
|
|
25
25
|
|
|
26
|
-
record Demo(sheet: Int, stage: Stage, layer:
|
|
26
|
+
record Demo(sheet: Int, stage: Stage, layer: Group, frame: Int)
|
|
27
27
|
|
|
28
28
|
fun spriteAt(i: Int) -> Vec2 =
|
|
29
29
|
Vec2(x: Float({i * 7919 + 13} % World.Width), y: Float({i * i * 31 / 7 + i * 1013} % World.Height))
|
|
30
30
|
|
|
31
31
|
fun build(sheet: Int) -> Demo =
|
|
32
32
|
stage := Stage.create()
|
|
33
|
-
layer :=
|
|
33
|
+
layer := Group.create()
|
|
34
34
|
for i := range World.Sprites
|
|
35
35
|
p := spriteAt(i)
|
|
36
|
-
layer.add(
|
|
36
|
+
layer.add(Sprite.create(sheet, Float(i % 4 * 16), 0.0, 16.0, 16.0, 16.0, 16.0).at(p.x, p.y))
|
|
37
37
|
stage.root.add(layer)
|
|
38
38
|
Demo(sheet: sheet, stage: stage, layer: layer, frame: 0)
|
|
39
39
|
|
plum-std/game/Scene.plum
CHANGED
|
@@ -8,20 +8,31 @@ import std/game/Vec2
|
|
|
8
8
|
import std/game/Color
|
|
9
9
|
import std/game/Game
|
|
10
10
|
|
|
11
|
-
# A Scene2D-like scene graph: a tree of
|
|
11
|
+
# A Scene2D-like scene graph: a tree of actors under a `Stage`. `Actor` is a trait;
|
|
12
|
+
# every kind of actor is a `record` that embeds it (`Group`, `Sprite`, `Rect`,
|
|
13
|
+
# `Circle`, `Text` here, your own `record Walker(Actor, speed: Float)` in a game).
|
|
12
|
-
# position, size, origin (the pivot of its rotation and scale),
|
|
14
|
+
# An actor has a position, size, origin (the pivot of its rotation and scale),
|
|
13
|
-
# (radians, clockwise), colour, visibility and children. Children
|
|
15
|
+
# scale, rotation (radians, clockwise), colour, visibility and children. Children
|
|
14
|
-
# their parent's coordinates, so moving, turning or scaling a group
|
|
16
|
+
# are placed in their parent's coordinates, so moving, turning or scaling a group
|
|
15
|
-
# everything in it. Colour alpha multiplies down the tree and an invisible
|
|
17
|
+
# moves everything in it. Colour alpha multiplies down the tree and an invisible
|
|
16
|
-
# hides its whole subtree.
|
|
18
|
+
# actor hides its whole subtree.
|
|
19
|
+
#
|
|
20
|
+
# A record overrides the default methods `act(dt)` (its behaviour), `draw(m, alpha)`
|
|
21
|
+
# (what it paints, with its stage transform `m`) and `hit(p)` (whether a point in
|
|
22
|
+
# its own coordinates is on it). The default `act` runs the children's `act`; an
|
|
23
|
+
# override that has children calls `self.actChildren(dt)` itself.
|
|
17
24
|
#
|
|
18
25
|
# stage := Stage.create()
|
|
19
|
-
# ring :=
|
|
26
|
+
# ring := Group.create()
|
|
20
27
|
# stage.root.add(ring)
|
|
21
|
-
# ring.add(
|
|
28
|
+
# ring.add(Rect.create(40.0, 20.0, Color.Red).at(10.0, 5.0))
|
|
22
|
-
# stage.act(dt) # runs every
|
|
29
|
+
# stage.act(dt) # runs every actor's `act`, depth first
|
|
23
30
|
# stage.draw() # draws through `std/game/Game`
|
|
24
31
|
#
|
|
32
|
+
# Actors are compared with `same(a, b)` (a trait value is a fresh wrapper each time
|
|
33
|
+
# it is converted, so `==` would not tell). A record built for the tree sets every
|
|
34
|
+
# trait field; copy `Group.create()` for the list.
|
|
35
|
+
#
|
|
25
36
|
# The transform maths (`Affine`, `localMatrix`, `worldMatrix`, `flatten`,
|
|
26
37
|
# `hitNode`) never calls the host, so it runs under `plum test`.
|
|
27
38
|
#
|
|
@@ -31,14 +42,6 @@ import std/game/Game
|
|
|
31
42
|
# scale, since the host has no rotated shapes. Non-uniform scale under rotation
|
|
32
43
|
# (skew) is not supported for drawing; hit testing handles it exactly.
|
|
33
44
|
|
|
34
|
-
# What a node draws.
|
|
35
|
-
primitive NodeKind =
|
|
36
|
-
Group = 0
|
|
37
|
-
Sprite = 1
|
|
38
|
-
Rect = 2
|
|
39
|
-
Circle = 3
|
|
40
|
-
Text = 4
|
|
41
|
-
|
|
42
45
|
# A 2D affine transform: x' = a*x + c*y + e, y' = b*x + d*y + f.
|
|
43
46
|
struct Affine(a: Float, b: Float, c: Float, d: Float, e: Float, f: Float) =
|
|
44
47
|
|
|
@@ -80,91 +83,77 @@ struct Affine(a: Float, b: Float, c: Float, d: Float, e: Float, f: Float) =
|
|
|
80
83
|
fun rotation(self) -> Float =
|
|
81
84
|
Math.atan2(self.b, self.a)
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
trait Actor(
|
|
84
87
|
x: Float, y: Float, width: Float, height: Float,
|
|
85
88
|
originX: Float, originY: Float,
|
|
86
89
|
scaleX: Float, scaleY: Float,
|
|
87
90
|
rotation: Float,
|
|
88
91
|
color: Color,
|
|
89
92
|
visible: Bool,
|
|
90
|
-
children: List[
|
|
93
|
+
children: List[Actor],
|
|
91
|
-
parent:
|
|
94
|
+
parent: Actor?,
|
|
92
|
-
kind: Int,
|
|
93
|
-
tex: Int, srcX: Float, srcY: Float, srcW: Float, srcH: Float,
|
|
94
|
-
|
|
95
|
+
scratch: List[Actor],
|
|
95
|
-
acting: Bool,
|
|
96
|
-
act: fn(Node, Float),
|
|
97
|
-
cull: Bool, cullX: Float, cullY: Float, cullW: Float, cullH: Float
|
|
96
|
+
cull: Bool, cullX: Float, cullY: Float, cullW: Float, cullH: Float) =
|
|
98
|
-
|
|
97
|
+
|
|
99
|
-
|
|
100
|
-
fun make(kind: Int, w: Float, h: Float, c: Color) -> Node =
|
|
101
|
-
Node(
|
|
102
|
-
x: 0.0, y: 0.0, width: w, height: h, originX: 0.0, originY: 0.0,
|
|
103
|
-
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: c, visible: true,
|
|
104
|
-
children: List[Node](), parent: null, kind: kind,
|
|
105
|
-
tex: -1, srcX: 0.0, srcY: 0.0, srcW: 0.0, srcH: 0.0, text: "", size: 0.0,
|
|
106
|
-
acting: false, act: noAct,
|
|
107
|
-
|
|
98
|
+
# This actor's behaviour. The default acts the children; an override that keeps
|
|
108
|
-
|
|
109
|
-
#
|
|
99
|
+
# children calls `self.actChildren(dt)` too.
|
|
110
|
-
fun
|
|
100
|
+
fun act(self, dt: Float) =
|
|
101
|
+
self.actChildren(dt)
|
|
102
|
+
|
|
111
|
-
|
|
103
|
+
fun actChildren(self, dt: Float) =
|
|
112
|
-
|
|
104
|
+
i := 0
|
|
105
|
+
while i < self.children.length()
|
|
106
|
+
self.children.at(i).act(dt)
|
|
107
|
+
i = i + 1
|
|
108
|
+
|
|
113
|
-
#
|
|
109
|
+
# Paints this actor only, given its stage transform `m` and accumulated alpha.
|
|
114
|
-
fun sprite(tex: Int, sx: Float, sy: Float, sw: Float, sh: Float, w: Float, h: Float) -> Node =
|
|
115
|
-
|
|
110
|
+
# The default paints nothing (a group).
|
|
116
|
-
n.tex = tex
|
|
117
|
-
n.srcX = sx
|
|
118
|
-
n.srcY = sy
|
|
119
|
-
n.srcW = sw
|
|
120
|
-
n.srcH = sh
|
|
121
|
-
n
|
|
122
|
-
|
|
123
|
-
fun
|
|
111
|
+
fun draw(self, m: Affine, alpha: Float) =
|
|
124
|
-
|
|
112
|
+
return
|
|
125
|
-
|
|
113
|
+
|
|
126
|
-
#
|
|
114
|
+
# Whether `p`, a point in this actor's own coordinates, is on it: inside its
|
|
115
|
+
# width by height box. A group with no size is never hit itself.
|
|
127
|
-
fun
|
|
116
|
+
fun hit(self, p: Vec2) -> Bool =
|
|
128
|
-
Node.make(NodeKind.Circle, r * 2.0, r * 2.0, c)
|
|
129
|
-
|
|
130
|
-
|
|
117
|
+
self.width > 0.0 && self.height > 0.0 && p.x >= 0.0 && p.x <= self.width && p.y >= 0.0 && p.y <= self.height
|
|
131
|
-
|
|
118
|
+
|
|
132
|
-
n := Node.make(NodeKind.Text, size * 0.6 * Float(s.length()), size, c)
|
|
133
|
-
n.text = s
|
|
134
|
-
n.size = size
|
|
135
|
-
n
|
|
136
|
-
|
|
137
|
-
fun at(self, x: Float, y: Float) ->
|
|
119
|
+
fun at(self, x: Float, y: Float) -> Actor =
|
|
138
120
|
self.x = x
|
|
139
121
|
self.y = y
|
|
140
122
|
self
|
|
141
123
|
|
|
142
|
-
fun setOrigin(self, ox: Float, oy: Float) ->
|
|
124
|
+
fun setOrigin(self, ox: Float, oy: Float) -> Actor =
|
|
143
125
|
self.originX = ox
|
|
144
126
|
self.originY = oy
|
|
145
127
|
self
|
|
146
128
|
|
|
147
|
-
fun centerOrigin(self) ->
|
|
129
|
+
fun centerOrigin(self) -> Actor =
|
|
148
130
|
self.originX = self.width / 2.0
|
|
149
131
|
self.originY = self.height / 2.0
|
|
150
132
|
self
|
|
151
133
|
|
|
134
|
+
# The index of `c` among this actor's children, -1 if it is not one.
|
|
152
|
-
fun
|
|
135
|
+
fun childIndex(self, c: Actor) -> Int =
|
|
136
|
+
for i := range self.children.length()
|
|
137
|
+
if self.children.at(i).children == c.children
|
|
153
|
-
|
|
138
|
+
return i
|
|
154
|
-
self.acting = true
|
|
155
|
-
|
|
139
|
+
-1
|
|
156
140
|
|
|
157
141
|
# Appends a child (taking it from its previous parent) so it draws after its siblings.
|
|
158
|
-
fun add(self, child:
|
|
142
|
+
fun add(self, child: Actor) =
|
|
159
143
|
if p := child.parent
|
|
160
|
-
p.
|
|
144
|
+
p.detach(child)
|
|
161
145
|
child.parent = self
|
|
162
146
|
self.children.push(child)
|
|
163
147
|
|
|
148
|
+
fun detach(self, child: Actor) =
|
|
149
|
+
i := self.childIndex(child)
|
|
150
|
+
if i >= 0
|
|
151
|
+
self.children.removeAt(i)
|
|
152
|
+
|
|
164
|
-
fun remove(self, child:
|
|
153
|
+
fun remove(self, child: Actor) =
|
|
165
154
|
if p := child.parent
|
|
166
|
-
if p == self
|
|
167
|
-
|
|
155
|
+
if p.children == self.children
|
|
156
|
+
self.detach(child)
|
|
168
157
|
child.parent = null
|
|
169
158
|
|
|
170
159
|
fun clear(self) =
|
|
@@ -172,15 +161,13 @@ record Node(
|
|
|
172
161
|
self.children.at(i).parent = null
|
|
173
162
|
self.children.clear()
|
|
174
163
|
|
|
175
|
-
# The
|
|
164
|
+
# The actor's index among its siblings (its draw order), -1 without a parent.
|
|
176
165
|
fun zIndex(self) -> Int =
|
|
177
166
|
if p := self.parent
|
|
178
|
-
for i := range p.children.length()
|
|
179
|
-
|
|
167
|
+
return p.childIndex(self)
|
|
180
|
-
return i
|
|
181
168
|
-1
|
|
182
169
|
|
|
183
|
-
# Moves the
|
|
170
|
+
# Moves the actor to index `i` of its parent's children (clamped to the valid
|
|
184
171
|
# range), shifting the siblings in between. Without a parent it does nothing.
|
|
185
172
|
fun setZIndex(self, i: Int) =
|
|
186
173
|
if p := self.parent
|
|
@@ -192,13 +179,14 @@ record Node(
|
|
|
192
179
|
dst = p.children.length() - 1
|
|
193
180
|
d := p.children.data
|
|
194
181
|
base := p.children.start
|
|
182
|
+
me: Actor := self
|
|
195
183
|
while src < dst
|
|
196
184
|
d.set(base + src, d.get(base + src + 1))
|
|
197
185
|
src = src + 1
|
|
198
186
|
while src > dst
|
|
199
187
|
d.set(base + src, d.get(base + src - 1))
|
|
200
188
|
src = src - 1
|
|
201
|
-
d.set(base + dst,
|
|
189
|
+
d.set(base + dst, me)
|
|
202
190
|
|
|
203
191
|
# Draws last (on top) and is hit first.
|
|
204
192
|
fun toFront(self) =
|
|
@@ -216,9 +204,9 @@ record Node(
|
|
|
216
204
|
# is already sorted costs n - 1 comparisons, a nearly sorted one about 2n (one
|
|
217
205
|
# scan to find the runs, one to confirm the merge) plus the distance each
|
|
218
206
|
# displaced child moves; the worst case is O(n log n). The merge buffer
|
|
219
|
-
# is the
|
|
207
|
+
# is the actor's own `scratch` list, which keeps its capacity between calls, so
|
|
220
208
|
# sorting every frame allocates nothing once it has grown.
|
|
221
|
-
fun sortChildren(self, before: fn(
|
|
209
|
+
fun sortChildren(self, before: fn(Actor, Actor) -> Bool) =
|
|
222
210
|
n := self.children.length()
|
|
223
211
|
d := self.children.data
|
|
224
212
|
base := self.children.start
|
|
@@ -238,8 +226,8 @@ record Node(
|
|
|
238
226
|
i = b
|
|
239
227
|
runs = {runs + 1} / 2
|
|
240
228
|
|
|
241
|
-
# Culls this
|
|
229
|
+
# Culls this actor's direct children against the rectangle (x, y, w, h), in this
|
|
242
|
-
#
|
|
230
|
+
# actor's own coordinates: a child whose box (position and size, scaled about its
|
|
243
231
|
# origin; rotation ignored) misses it is neither drawn nor hit, with its whole
|
|
244
232
|
# subtree. A child with no size (a plain group) is never culled.
|
|
245
233
|
fun setCullingArea(self, x: Float, y: Float, w: Float, h: Float) =
|
|
@@ -252,19 +240,188 @@ record Node(
|
|
|
252
240
|
fun clearCullingArea(self) =
|
|
253
241
|
self.cull = false
|
|
254
242
|
|
|
243
|
+
# The transform into the parent's coordinates: translate to the position,
|
|
244
|
+
# rotate and scale about the origin.
|
|
245
|
+
fun local(self) -> Affine =
|
|
246
|
+
cs := Math.cos(self.rotation)
|
|
247
|
+
sn := Math.sin(self.rotation)
|
|
248
|
+
a := cs * self.scaleX
|
|
249
|
+
b := sn * self.scaleX
|
|
250
|
+
c := 0.0 - sn * self.scaleY
|
|
251
|
+
d := cs * self.scaleY
|
|
252
|
+
Affine(
|
|
253
|
+
a: a, b: b, c: c, d: d,
|
|
254
|
+
e: self.x + self.originX - {a * self.originX + c * self.originY},
|
|
255
|
+
f: self.y + self.originY - {b * self.originX + d * self.originY})
|
|
256
|
+
|
|
257
|
+
# Whether this actor's box (in its parent's coordinates) lies outside the
|
|
258
|
+
# rectangle (ax, ay, aw, ah). An actor with no size is never outside.
|
|
259
|
+
fun outside(self, ax: Float, ay: Float, aw: Float, ah: Float) -> Bool =
|
|
260
|
+
if self.width == 0.0 && self.height == 0.0
|
|
261
|
+
return false
|
|
262
|
+
w := self.width * self.scaleX
|
|
263
|
+
h := self.height * self.scaleY
|
|
264
|
+
x := self.x + self.originX - self.originX * self.scaleX
|
|
265
|
+
y := self.y + self.originY - self.originY * self.scaleY
|
|
266
|
+
if w < 0.0
|
|
267
|
+
x = x + w
|
|
268
|
+
w = 0.0 - w
|
|
269
|
+
if h < 0.0
|
|
270
|
+
y = y + h
|
|
271
|
+
h = 0.0 - h
|
|
272
|
+
x > ax + aw || x + w < ax || y > ay + ah || y + h < ay
|
|
273
|
+
|
|
274
|
+
# Whether this actor culls its child `c`.
|
|
275
|
+
fun culls(self, c: Actor) -> Bool =
|
|
276
|
+
self.cull && c.outside(self.cullX, self.cullY, self.cullW, self.cullH)
|
|
277
|
+
|
|
278
|
+
# Draws this actor and then its children, given the parent's stage transform.
|
|
279
|
+
fun drawTree(self, parent: Affine, alpha: Float) =
|
|
280
|
+
if !self.visible
|
|
281
|
+
return
|
|
282
|
+
m := parent * self.local()
|
|
283
|
+
a := alpha * self.color.a
|
|
284
|
+
self.draw(m, a)
|
|
285
|
+
n := self.children.length()
|
|
286
|
+
for i := range n
|
|
287
|
+
kid := self.children.at(i)
|
|
288
|
+
if !self.culls(kid)
|
|
289
|
+
kid.drawTree(m, a)
|
|
290
|
+
|
|
291
|
+
fun collect(self, parent: Affine, alpha: Float, out: List[Drawn]) =
|
|
292
|
+
if !self.visible
|
|
293
|
+
return
|
|
294
|
+
m := parent * self.local()
|
|
295
|
+
a := alpha * self.color.a
|
|
296
|
+
me: Actor := self
|
|
297
|
+
out.push(Drawn(node: me, m: m, alpha: a))
|
|
298
|
+
n := self.children.length()
|
|
299
|
+
for i := range n
|
|
300
|
+
kid := self.children.at(i)
|
|
301
|
+
if !self.culls(kid)
|
|
302
|
+
kid.collect(m, a, out)
|
|
303
|
+
|
|
304
|
+
# The topmost visible actor under `p`, a point in the parent's coordinates:
|
|
305
|
+
# children before the actor itself, later siblings before earlier ones.
|
|
306
|
+
fun hitTree(self, p: Vec2) -> Actor? =
|
|
307
|
+
if !self.visible
|
|
308
|
+
return null
|
|
309
|
+
q := self.local().inverse().apply(p)
|
|
310
|
+
n := self.children.length()
|
|
311
|
+
for i := range n
|
|
312
|
+
child := self.children.at(n - 1 - i)
|
|
313
|
+
if !self.culls(child)
|
|
314
|
+
if found := child.hitTree(q)
|
|
315
|
+
return found
|
|
316
|
+
if self.hit(q)
|
|
317
|
+
me: Actor := self
|
|
318
|
+
return me
|
|
319
|
+
null
|
|
320
|
+
|
|
255
|
-
# A point in this
|
|
321
|
+
# A point in this actor's coordinates, in the stage's.
|
|
256
322
|
fun localToStage(self, p: Vec2) -> Vec2 =
|
|
323
|
+
me: Actor := self
|
|
257
|
-
worldMatrix(
|
|
324
|
+
worldMatrix(me).apply(p)
|
|
258
325
|
|
|
259
|
-
# A point in the stage's coordinates, in this
|
|
326
|
+
# A point in the stage's coordinates, in this actor's.
|
|
260
327
|
fun stageToLocal(self, p: Vec2) -> Vec2 =
|
|
328
|
+
me: Actor := self
|
|
261
|
-
worldMatrix(
|
|
329
|
+
worldMatrix(me).inverse().apply(p)
|
|
330
|
+
|
|
331
|
+
# Whether two actor values are the same actor (they share the children list).
|
|
332
|
+
fun same(a: Actor, b: Actor) -> Bool =
|
|
333
|
+
a.children == b.children
|
|
334
|
+
|
|
335
|
+
# An actor that only holds children.
|
|
336
|
+
record Group(Actor) =
|
|
337
|
+
|
|
338
|
+
fun create() -> Group =
|
|
339
|
+
Group(
|
|
340
|
+
x: 0.0, y: 0.0, width: 0.0, height: 0.0, originX: 0.0, originY: 0.0,
|
|
341
|
+
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
342
|
+
children: List[Actor](), parent: null, scratch: List[Actor](),
|
|
343
|
+
cull: false, cullX: 0.0, cullY: 0.0, cullW: 0.0, cullH: 0.0)
|
|
344
|
+
|
|
345
|
+
# A texture region (srcX, srcY, srcW, srcH in texture pixels) drawn width by height.
|
|
346
|
+
record Sprite(Actor, tex: Int, srcX: Float, srcY: Float, srcW: Float, srcH: Float) =
|
|
347
|
+
|
|
348
|
+
fun create(tex: Int, sx: Float, sy: Float, sw: Float, sh: Float, w: Float, h: Float) -> Sprite =
|
|
349
|
+
Sprite(
|
|
350
|
+
x: 0.0, y: 0.0, width: w, height: h, originX: 0.0, originY: 0.0,
|
|
351
|
+
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
352
|
+
children: List[Actor](), parent: null, scratch: List[Actor](),
|
|
353
|
+
cull: false, cullX: 0.0, cullY: 0.0, cullW: 0.0, cullH: 0.0,
|
|
354
|
+
tex: tex, srcX: sx, srcY: sy, srcW: sw, srcH: sh)
|
|
355
|
+
|
|
356
|
+
fun draw(self, m: Affine, alpha: Float) =
|
|
357
|
+
drawSpriteFrame(m, alpha, self.width, self.height, self.color, self.tex, self.srcX, self.srcY, self.srcW, self.srcH)
|
|
358
|
+
|
|
359
|
+
record Rect(Actor) =
|
|
360
|
+
|
|
361
|
+
fun create(w: Float, h: Float, c: Color) -> Rect =
|
|
362
|
+
Rect(
|
|
363
|
+
x: 0.0, y: 0.0, width: w, height: h, originX: 0.0, originY: 0.0,
|
|
364
|
+
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: c, visible: true,
|
|
365
|
+
children: List[Actor](), parent: null, scratch: List[Actor](),
|
|
366
|
+
cull: false, cullX: 0.0, cullY: 0.0, cullW: 0.0, cullH: 0.0)
|
|
367
|
+
|
|
368
|
+
fun draw(self, m: Affine, alpha: Float) =
|
|
369
|
+
drawRectBox(m, alpha, self.width, self.height, self.color)
|
|
370
|
+
|
|
371
|
+
record Circle(Actor) =
|
|
372
|
+
|
|
373
|
+
# A circle of radius r whose bounding box has its top-left at the actor's position.
|
|
374
|
+
fun create(r: Float, c: Color) -> Circle =
|
|
375
|
+
Circle(
|
|
376
|
+
x: 0.0, y: 0.0, width: r * 2.0, height: r * 2.0, originX: 0.0, originY: 0.0,
|
|
377
|
+
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: c, visible: true,
|
|
378
|
+
children: List[Actor](), parent: null, scratch: List[Actor](),
|
|
379
|
+
cull: false, cullX: 0.0, cullY: 0.0, cullW: 0.0, cullH: 0.0)
|
|
380
|
+
|
|
381
|
+
fun draw(self, m: Affine, alpha: Float) =
|
|
382
|
+
drawDisc(m, alpha, self.width / 2.0, self.color)
|
|
383
|
+
|
|
384
|
+
record Text(Actor, text: Str, size: Float) =
|
|
385
|
+
|
|
386
|
+
# Text with its top-left at the actor's position; the size is an estimate of the extent.
|
|
387
|
+
fun create(s: Str, size: Float, c: Color) -> Text =
|
|
388
|
+
Text(
|
|
389
|
+
x: 0.0, y: 0.0, width: size * 0.6 * Float(s.length()), height: size, originX: 0.0, originY: 0.0,
|
|
390
|
+
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: c, visible: true,
|
|
391
|
+
children: List[Actor](), parent: null, scratch: List[Actor](),
|
|
392
|
+
cull: false, cullX: 0.0, cullY: 0.0, cullW: 0.0, cullH: 0.0,
|
|
393
|
+
text: s, size: size)
|
|
394
|
+
|
|
395
|
+
fun draw(self, m: Affine, alpha: Float) =
|
|
396
|
+
drawLabel(m, alpha, self.text, self.size, self.color)
|
|
397
|
+
|
|
398
|
+
# Draws a `w` by `h` rectangle at the transform `m`, for your own rect-like records.
|
|
399
|
+
fun drawRectBox(m: Affine, alpha: Float, w: Float, h: Float, color: Color) =
|
|
400
|
+
sx := w * m.scaleX()
|
|
401
|
+
sy := h * m.scaleY()
|
|
402
|
+
c := m.apply(Vec2(x: w / 2.0, y: h / 2.0))
|
|
403
|
+
drawRect(c.x - sx / 2.0, c.y - sy / 2.0, sx, sy, color.withAlpha(alpha))
|
|
404
|
+
|
|
405
|
+
# Draws a circle of radius `r` whose bounding box has its top-left at the transform `m`.
|
|
406
|
+
fun drawDisc(m: Affine, alpha: Float, r: Float, color: Color) =
|
|
407
|
+
c := m.apply(Vec2(x: r, y: r))
|
|
408
|
+
drawCircle(c.x, c.y, r * m.scaleX(), color.withAlpha(alpha))
|
|
409
|
+
|
|
410
|
+
# Draws text with its top-left at the transform `m`.
|
|
411
|
+
fun drawLabel(m: Affine, alpha: Float, s: Str, size: Float, color: Color) =
|
|
412
|
+
p := m.apply(Vec2(x: 0.0, y: 0.0))
|
|
413
|
+
drawText(s, p.x, p.y, size * m.scaleY(), color.withAlpha(alpha))
|
|
414
|
+
|
|
415
|
+
# Draws a texture region `w` by `h` at the transform `m`: for your own sprite-like records.
|
|
416
|
+
fun drawSpriteFrame(m: Affine, alpha: Float, w: Float, h: Float, color: Color, tex: Int, srcX: Float, srcY: Float, srcW: Float, srcH: Float) =
|
|
417
|
+
sx := w * m.scaleX()
|
|
418
|
+
sy := h * m.scaleY()
|
|
419
|
+
c := m.apply(Vec2(x: w / 2.0, y: h / 2.0))
|
|
420
|
+
drawTextureEx(tex, srcX, srcY, srcW, srcH, c.x - sx / 2.0, c.y - sy / 2.0, sx, sy, m.rotation(), false, color.withAlpha(alpha))
|
|
262
421
|
|
|
263
|
-
fun noAct(n: Node, dt: Float) =
|
|
264
|
-
n.x = n.x
|
|
265
422
|
|
|
266
423
|
# The end of the ascending run of `d` starting at `i` (exclusive), within n.
|
|
267
|
-
fun sortRunEnd(d: Array[
|
|
424
|
+
fun sortRunEnd(d: Array[Actor], base: Int, i: Int, n: Int, before: fn(Actor, Actor) -> Bool) -> Int =
|
|
268
425
|
j := i + 1
|
|
269
426
|
while j < n && !before(d.get(base + j), d.get(base + j - 1))
|
|
270
427
|
j = j + 1
|
|
@@ -273,7 +430,7 @@ fun sortRunEnd(d: Array[Node], base: Int, i: Int, n: Int, before: fn(Node, Node)
|
|
|
273
430
|
# Merges the sorted runs [lo, a) and [a, b) of `d`, stably. The left elements that
|
|
274
431
|
# already precede the first right one and the right elements that already follow
|
|
275
432
|
# the last left one are found by binary search and left alone.
|
|
276
|
-
fun sortMerge(scratch: List[
|
|
433
|
+
fun sortMerge(scratch: List[Actor], d: Array[Actor], base: Int, lo: Int, a: Int, b: Int, before: fn(Actor, Actor) -> Bool) =
|
|
277
434
|
first := d.get(base + a)
|
|
278
435
|
l := lo
|
|
279
436
|
h := a
|
|
@@ -318,136 +475,57 @@ fun sortMerge(scratch: List[Node], d: Array[Node], base: Int, lo: Int, a: Int, b
|
|
|
318
475
|
i = i + 1
|
|
319
476
|
o = o + 1
|
|
320
477
|
|
|
321
|
-
|
|
478
|
+
|
|
322
|
-
# outside the parent's culling area.
|
|
323
|
-
fun culled(parent: Node, c: Node) -> Bool =
|
|
324
|
-
if !parent.cull || c.width == 0.0 && c.height == 0.0
|
|
325
|
-
return false
|
|
326
|
-
w := c.width * c.scaleX
|
|
327
|
-
h := c.height * c.scaleY
|
|
328
|
-
x := c.x + c.originX - c.originX * c.scaleX
|
|
329
|
-
y := c.y + c.originY - c.originY * c.scaleY
|
|
330
|
-
if w < 0.0
|
|
331
|
-
x = x + w
|
|
332
|
-
w = 0.0 - w
|
|
333
|
-
if h < 0.0
|
|
334
|
-
y = y + h
|
|
335
|
-
h = 0.0 - h
|
|
336
|
-
x > parent.cullX + parent.cullW || x + w < parent.cullX || y > parent.cullY + parent.cullH || y + h < parent.cullY
|
|
337
|
-
|
|
338
|
-
# The
|
|
479
|
+
# The actor's transform into its parent's coordinates.
|
|
339
|
-
# rotate and scale about the origin.
|
|
340
|
-
fun localMatrix(n:
|
|
480
|
+
fun localMatrix(n: Actor) -> Affine =
|
|
341
|
-
cs := Math.cos(n.rotation)
|
|
342
|
-
sn := Math.sin(n.rotation)
|
|
343
|
-
a := cs * n.scaleX
|
|
344
|
-
b := sn * n.scaleX
|
|
345
|
-
c := 0.0 - sn * n.scaleY
|
|
346
|
-
d := cs * n.scaleY
|
|
347
|
-
|
|
481
|
+
n.local()
|
|
348
|
-
|
|
482
|
+
|
|
349
|
-
e: n.x + n.originX - {a * n.originX + c * n.originY},
|
|
350
|
-
f: n.y + n.originY - {b * n.originX + d * n.originY})
|
|
351
|
-
|
|
352
|
-
# The
|
|
483
|
+
# The actor's transform into stage coordinates.
|
|
353
|
-
fun worldMatrix(n:
|
|
484
|
+
fun worldMatrix(n: Actor) -> Affine =
|
|
354
485
|
if p := n.parent
|
|
355
|
-
worldMatrix(p) *
|
|
486
|
+
worldMatrix(p) * n.local()
|
|
356
487
|
else
|
|
357
|
-
|
|
488
|
+
n.local()
|
|
358
489
|
|
|
359
|
-
# One entry of the draw list: a visible
|
|
490
|
+
# One entry of the draw list: a visible actor, its stage transform and its alpha.
|
|
360
|
-
record Drawn(node:
|
|
491
|
+
record Drawn(node: Actor, m: Affine, alpha: Float)
|
|
361
|
-
|
|
362
|
-
fun collect(n: Node, parent: Affine, alpha: Float, out: List[Drawn]) =
|
|
363
|
-
if !n.visible
|
|
364
|
-
return
|
|
365
|
-
m := parent * localMatrix(n)
|
|
366
|
-
a := alpha * n.color.a
|
|
367
|
-
out.push(Drawn(node: n, m: m, alpha: a))
|
|
368
|
-
for i := range n.children.length()
|
|
369
|
-
kid := n.children.at(i)
|
|
370
|
-
if !culled(n, kid)
|
|
371
|
-
collect(kid, m, a, out)
|
|
372
492
|
|
|
373
493
|
# What `draw` would draw, in order: a parent before its children, siblings in
|
|
374
494
|
# list order, invisible and culled subtrees left out.
|
|
375
|
-
fun flatten(root:
|
|
495
|
+
fun flatten(root: Actor) -> List[Drawn] =
|
|
376
496
|
out := List[Drawn]()
|
|
377
|
-
collect(
|
|
497
|
+
root.collect(Affine.Identity, 1.0, out)
|
|
378
498
|
out
|
|
379
499
|
|
|
380
|
-
fun drawNode(n:
|
|
500
|
+
fun drawNode(n: Actor, parent: Affine, alpha: Float) =
|
|
381
|
-
if !n.visible
|
|
382
|
-
return
|
|
383
|
-
m := parent * localMatrix(n)
|
|
384
|
-
a := alpha * n.color.a
|
|
385
|
-
if n.kind == NodeKind.Sprite
|
|
386
|
-
sx := n.width * m.scaleX()
|
|
387
|
-
sy := n.height * m.scaleY()
|
|
388
|
-
c := m.apply(Vec2(x: n.width / 2.0, y: n.height / 2.0))
|
|
389
|
-
drawTextureEx(n.tex, n.srcX, n.srcY, n.srcW, n.srcH, c.x - sx / 2.0, c.y - sy / 2.0, sx, sy, m.rotation(), false, n.color.withAlpha(a))
|
|
390
|
-
else if n.kind == NodeKind.Rect
|
|
391
|
-
sx := n.width * m.scaleX()
|
|
392
|
-
sy := n.height * m.scaleY()
|
|
393
|
-
c := m.apply(Vec2(x: n.width / 2.0, y: n.height / 2.0))
|
|
394
|
-
drawRect(c.x - sx / 2.0, c.y - sy / 2.0, sx, sy, n.color.withAlpha(a))
|
|
395
|
-
else if n.kind == NodeKind.Circle
|
|
396
|
-
r := n.width / 2.0
|
|
397
|
-
c := m.apply(Vec2(x: r, y: r))
|
|
398
|
-
drawCircle(c.x, c.y, r * m.scaleX(), n.color.withAlpha(a))
|
|
399
|
-
else if n.kind == NodeKind.Text
|
|
400
|
-
p := m.apply(Vec2(x: 0.0, y: 0.0))
|
|
401
|
-
drawText(n.text, p.x, p.y, n.size * m.scaleY(), n.color.withAlpha(a))
|
|
402
|
-
for i := range n.children.length()
|
|
403
|
-
kid := n.children.at(i)
|
|
404
|
-
if !culled(n, kid)
|
|
405
|
-
|
|
501
|
+
n.drawTree(parent, alpha)
|
|
406
|
-
|
|
502
|
+
|
|
407
|
-
fun actNode(n:
|
|
503
|
+
fun actNode(n: Actor, dt: Float) =
|
|
408
|
-
|
|
504
|
+
n.act(dt)
|
|
409
|
-
f := n.act
|
|
410
|
-
f(n, dt)
|
|
411
|
-
i := 0
|
|
412
|
-
while i < n.children.length()
|
|
413
|
-
actNode(n.children.at(i), dt)
|
|
414
|
-
i = i + 1
|
|
415
505
|
|
|
416
|
-
# The topmost visible
|
|
506
|
+
# The topmost visible actor under `p`, a point in the actor's parent's coordinates.
|
|
417
|
-
# children before the node itself, later siblings before earlier ones. A node is
|
|
418
|
-
# hit inside its width by height box; a group with no size is never hit itself.
|
|
419
|
-
fun hitNode(n:
|
|
507
|
+
fun hitNode(n: Actor, p: Vec2) -> Actor? =
|
|
420
|
-
if !n.visible
|
|
421
|
-
|
|
508
|
+
n.hitTree(p)
|
|
422
|
-
|
|
509
|
+
|
|
423
|
-
for i := range n.children.length()
|
|
424
|
-
child := n.children.at(n.children.length() - 1 - i)
|
|
425
|
-
if !culled(n, child)
|
|
426
|
-
if found := hitNode(child, q)
|
|
427
|
-
return found
|
|
428
|
-
if n.width > 0.0 && n.height > 0.0 && q.x >= 0.0 && q.x <= n.width && q.y >= 0.0 && q.y <= n.height
|
|
429
|
-
return n
|
|
430
|
-
null
|
|
431
|
-
|
|
432
|
-
# A root
|
|
510
|
+
# A root actor plus a camera (the world point at the screen's top-left and the
|
|
433
511
|
# pixels per world unit, as in `setCamera`).
|
|
434
|
-
record Stage(root:
|
|
512
|
+
record Stage(root: Actor, camX: Float, camY: Float, camScale: Float) =
|
|
435
513
|
|
|
436
514
|
fun create() -> Stage =
|
|
437
|
-
Stage(root:
|
|
515
|
+
Stage(root: Group.create(), camX: 0.0, camY: 0.0, camScale: 1.0)
|
|
438
516
|
|
|
439
|
-
# Runs every
|
|
517
|
+
# Runs every actor's `act`, parents before children, in insertion order.
|
|
440
518
|
fun act(self, dt: Float) =
|
|
441
|
-
|
|
519
|
+
self.root.act(dt)
|
|
442
520
|
|
|
443
521
|
fun draw(self) =
|
|
444
522
|
setCamera(self.camX, self.camY, self.camScale)
|
|
445
|
-
|
|
523
|
+
self.root.drawTree(Affine.Identity, 1.0)
|
|
446
524
|
resetCamera()
|
|
447
525
|
|
|
448
|
-
# The topmost visible
|
|
526
|
+
# The topmost visible actor at a stage point.
|
|
449
|
-
fun hit(self, x: Float, y: Float) ->
|
|
527
|
+
fun hit(self, x: Float, y: Float) -> Actor? =
|
|
450
|
-
|
|
528
|
+
self.root.hitTree(Vec2(x: x, y: y))
|
|
451
529
|
|
|
452
530
|
# A screen pixel in stage coordinates under this camera.
|
|
453
531
|
fun screenToStage(self, sx: Float, sy: Float) -> Vec2 =
|
|
@@ -461,23 +539,20 @@ fun sceneNear(a: Float, b: Float) -> Bool =
|
|
|
461
539
|
fun sceneNearV(p: Vec2, x: Float, y: Float) -> Bool =
|
|
462
540
|
sceneNear(p.x, x) && sceneNear(p.y, y)
|
|
463
541
|
|
|
464
|
-
fun sceneKids(n: Int) ->
|
|
542
|
+
fun sceneKids(n: Int) -> Group =
|
|
465
|
-
g :=
|
|
543
|
+
g := Group.create()
|
|
466
544
|
for i := range n
|
|
467
|
-
g.add(
|
|
545
|
+
g.add(Rect.create(1.0, 1.0, Color.Red).at(Float(i), 0.0))
|
|
468
546
|
g
|
|
469
547
|
|
|
470
|
-
fun sceneOrder(g:
|
|
548
|
+
fun sceneOrder(g: Actor) -> Str =
|
|
471
549
|
s := ""
|
|
472
550
|
for i := range g.children.length()
|
|
473
551
|
s = "{s}{Int(g.children.at(i).x)},"
|
|
474
552
|
s
|
|
475
553
|
|
|
476
|
-
fun sceneSpin(n: Node, dt: Float) =
|
|
477
|
-
n.rotation = n.rotation + dt
|
|
478
|
-
|
|
479
|
-
test "
|
|
554
|
+
test "an actor translates; rotation and scale pivot about the origin"
|
|
480
|
-
n :=
|
|
555
|
+
n := Rect.create(10.0, 10.0, Color.Red).at(100.0, 50.0)
|
|
481
556
|
assert sceneNearV(localMatrix(n).apply(Vec2(x: 0.0, y: 0.0)), 100.0, 50.0)
|
|
482
557
|
n.originX = 5.0
|
|
483
558
|
n.originY = 5.0
|
|
@@ -489,11 +564,11 @@ test "a node translates; rotation and scale pivot about the origin"
|
|
|
489
564
|
assert sceneNearV(localMatrix(n).apply(Vec2(x: 10.0, y: 10.0)), 115.0, 60.0)
|
|
490
565
|
|
|
491
566
|
test "matrices compose, and inverse undoes them"
|
|
492
|
-
g :=
|
|
567
|
+
g := Group.create().at(100.0, 0.0)
|
|
493
568
|
g.rotation = Float.HalfPi
|
|
494
569
|
g.scaleX = 2.0
|
|
495
570
|
g.scaleY = 2.0
|
|
496
|
-
k :=
|
|
571
|
+
k := Rect.create(10.0, 10.0, Color.Red).at(10.0, 0.0)
|
|
497
572
|
g.add(k)
|
|
498
573
|
w := worldMatrix(k)
|
|
499
574
|
assert sceneNearV(w.apply(Vec2(x: 0.0, y: 0.0)), 100.0, 20.0)
|
|
@@ -506,112 +581,109 @@ test "matrices compose, and inverse undoes them"
|
|
|
506
581
|
assert sceneNearV(k.stageToLocal(Vec2(x: 80.0, y: 40.0)), 10.0, 10.0)
|
|
507
582
|
|
|
508
583
|
test "add reparents, remove and clear detach"
|
|
509
|
-
a :=
|
|
584
|
+
a := Group.create()
|
|
510
|
-
b :=
|
|
585
|
+
b := Group.create()
|
|
511
|
-
k :=
|
|
586
|
+
k := Group.create()
|
|
512
587
|
a.add(k)
|
|
513
588
|
assert a.children.length() == 1
|
|
514
589
|
b.add(k)
|
|
515
590
|
assert a.children.length() == 0
|
|
516
591
|
assert b.children.length() == 1
|
|
517
|
-
assert {k.parent ?: crash "no parent"}
|
|
592
|
+
assert same({k.parent ?: crash "no parent"}, b)
|
|
518
593
|
b.remove(k)
|
|
519
594
|
assert b.children.length() == 0
|
|
520
595
|
assert k.parent == null
|
|
521
596
|
b.add(k)
|
|
522
|
-
extra :=
|
|
597
|
+
extra := Group.create()
|
|
523
598
|
b.add(extra)
|
|
524
599
|
b.clear()
|
|
525
600
|
assert b.children.length() == 0
|
|
526
601
|
assert k.parent == null
|
|
527
602
|
|
|
528
603
|
test "draw order is parent first, then children in insertion order"
|
|
529
|
-
root :=
|
|
604
|
+
root := Group.create()
|
|
530
|
-
a :=
|
|
605
|
+
a := Group.create()
|
|
531
606
|
root.add(a)
|
|
532
|
-
b :=
|
|
607
|
+
b := Group.create()
|
|
533
608
|
root.add(b)
|
|
534
|
-
a1 :=
|
|
609
|
+
a1 := Rect.create(1.0, 1.0, Color.Red)
|
|
535
610
|
a.add(a1)
|
|
536
|
-
a2 :=
|
|
611
|
+
a2 := Rect.create(1.0, 1.0, Color.Red)
|
|
537
612
|
a.add(a2)
|
|
538
|
-
b1 :=
|
|
613
|
+
b1 := Rect.create(1.0, 1.0, Color.Red)
|
|
539
614
|
b.add(b1)
|
|
540
615
|
list := flatten(root)
|
|
541
616
|
assert list.length() == 6
|
|
542
|
-
assert list.at(0).node
|
|
617
|
+
assert same(list.at(0).node, root)
|
|
543
|
-
assert list.at(1).node
|
|
618
|
+
assert same(list.at(1).node, a)
|
|
544
|
-
assert list.at(2).node
|
|
619
|
+
assert same(list.at(2).node, a1)
|
|
545
|
-
assert list.at(3).node
|
|
620
|
+
assert same(list.at(3).node, a2)
|
|
546
|
-
assert list.at(4).node
|
|
621
|
+
assert same(list.at(4).node, b)
|
|
547
|
-
assert list.at(5).node
|
|
622
|
+
assert same(list.at(5).node, b1)
|
|
548
623
|
|
|
549
624
|
test "invisible subtrees are skipped and alpha multiplies down"
|
|
550
|
-
root :=
|
|
625
|
+
root := Group.create()
|
|
551
626
|
root.color = Color.White.withAlpha(0.5)
|
|
552
|
-
g :=
|
|
627
|
+
g := Group.create()
|
|
553
628
|
root.add(g)
|
|
554
629
|
g.color = Color.White.withAlpha(0.5)
|
|
555
|
-
k :=
|
|
630
|
+
k := Rect.create(1.0, 1.0, Color.Red)
|
|
556
631
|
g.add(k)
|
|
557
|
-
hidden :=
|
|
632
|
+
hidden := Group.create()
|
|
558
633
|
root.add(hidden)
|
|
559
634
|
hidden.visible = false
|
|
560
|
-
hidden.add(
|
|
635
|
+
hidden.add(Rect.create(1.0, 1.0, Color.Red))
|
|
561
636
|
list := flatten(root)
|
|
562
637
|
assert list.length() == 3
|
|
563
638
|
assert sceneNear(list.at(2).alpha, 0.25)
|
|
564
639
|
g.visible = false
|
|
565
640
|
assert flatten(root).length() == 1
|
|
566
641
|
|
|
567
|
-
test "hit finds the topmost
|
|
642
|
+
test "hit finds the topmost actor through rotated and scaled groups"
|
|
568
|
-
root :=
|
|
643
|
+
root := Group.create()
|
|
569
|
-
g :=
|
|
644
|
+
g := Group.create().at(100.0, 0.0)
|
|
570
645
|
root.add(g)
|
|
571
646
|
g.rotation = Float.HalfPi
|
|
572
647
|
g.scaleX = 2.0
|
|
573
648
|
g.scaleY = 2.0
|
|
574
|
-
low :=
|
|
649
|
+
low := Rect.create(30.0, 30.0, Color.Red)
|
|
575
650
|
g.add(low)
|
|
576
|
-
top :=
|
|
651
|
+
top := Rect.create(10.0, 10.0, Color.Blue).at(10.0, 0.0)
|
|
577
652
|
g.add(top)
|
|
578
653
|
stage := Stage.create()
|
|
579
654
|
stage.root.add(g)
|
|
580
|
-
assert {stage.hit(90.0, 30.0) ?: crash "miss"}
|
|
655
|
+
assert same({stage.hit(90.0, 30.0) ?: crash "miss"}, top)
|
|
581
|
-
assert {stage.hit(70.0, 10.0) ?: crash "miss"}
|
|
656
|
+
assert same({stage.hit(70.0, 10.0) ?: crash "miss"}, low)
|
|
582
|
-
assert {stage.hit(60.0, 30.0) ?: crash "miss"}
|
|
657
|
+
assert same({stage.hit(60.0, 30.0) ?: crash "miss"}, low)
|
|
583
658
|
assert stage.hit(10.0, 10.0) == null
|
|
584
659
|
assert stage.hit(150.0, 30.0) == null
|
|
585
660
|
top.visible = false
|
|
586
|
-
assert {stage.hit(90.0, 30.0) ?: crash "miss"}
|
|
661
|
+
assert same({stage.hit(90.0, 30.0) ?: crash "miss"}, low)
|
|
587
662
|
g.visible = false
|
|
588
663
|
assert stage.hit(90.0, 30.0) == null
|
|
589
664
|
|
|
590
665
|
test "hit prefers later siblings and a child over its parent"
|
|
591
666
|
stage := Stage.create()
|
|
592
|
-
a :=
|
|
667
|
+
a := Rect.create(50.0, 50.0, Color.Red)
|
|
593
668
|
stage.root.add(a)
|
|
594
|
-
b :=
|
|
669
|
+
b := Rect.create(50.0, 50.0, Color.Blue).at(25.0, 0.0)
|
|
595
670
|
stage.root.add(b)
|
|
596
|
-
inner :=
|
|
671
|
+
inner := Rect.create(10.0, 10.0, Color.Green)
|
|
597
672
|
b.add(inner)
|
|
598
|
-
assert {stage.hit(10.0, 10.0) ?: crash "miss"}
|
|
673
|
+
assert same({stage.hit(10.0, 10.0) ?: crash "miss"}, a)
|
|
599
|
-
assert {stage.hit(30.0, 5.0) ?: crash "miss"}
|
|
674
|
+
assert same({stage.hit(30.0, 5.0) ?: crash "miss"}, inner)
|
|
600
|
-
assert {stage.hit(60.0, 40.0) ?: crash "miss"}
|
|
675
|
+
assert same({stage.hit(60.0, 40.0) ?: crash "miss"}, b)
|
|
601
|
-
assert {stage.hit(40.0, 10.0) ?: crash "miss"}
|
|
676
|
+
assert same({stage.hit(40.0, 10.0) ?: crash "miss"}, b)
|
|
602
677
|
|
|
603
|
-
test "act runs
|
|
678
|
+
test "the default act runs the children's act, invisible ones included"
|
|
604
679
|
stage := Stage.create()
|
|
605
|
-
g :=
|
|
680
|
+
g := Group.create()
|
|
606
681
|
stage.root.add(g)
|
|
607
|
-
|
|
682
|
+
g.add(Rect.create(1.0, 1.0, Color.Red))
|
|
608
|
-
g.add(k)
|
|
609
683
|
g.visible = false
|
|
610
684
|
stage.act(0.5)
|
|
611
|
-
stage.act(0.25)
|
|
612
|
-
assert sceneNear(g.rotation, 0.75)
|
|
613
|
-
assert sceneNear(k.rotation, 0.75)
|
|
614
685
|
assert stage.root.rotation == 0.0
|
|
686
|
+
assert g.children.length() == 1
|
|
615
687
|
|
|
616
688
|
test "the screen maps to the stage through the camera"
|
|
617
689
|
stage := Stage.create()
|
|
@@ -622,11 +694,11 @@ test "the screen maps to the stage through the camera"
|
|
|
622
694
|
assert sceneNearV(p, 120.0, 60.0)
|
|
623
695
|
|
|
624
696
|
test "sizes of sprites, circles and text"
|
|
625
|
-
assert
|
|
697
|
+
assert Circle.create(5.0, Color.Red).width == 10.0
|
|
626
|
-
s :=
|
|
698
|
+
s := Sprite.create(3, 16.0, 0.0, 16.0, 16.0, 32.0, 32.0)
|
|
627
699
|
assert s.tex == 3
|
|
628
|
-
assert s.
|
|
700
|
+
assert s.width == 32.0
|
|
629
|
-
assert
|
|
701
|
+
assert Text.create("ab", 10.0, Color.White).height == 10.0
|
|
630
702
|
|
|
631
703
|
test "setZIndex, toFront and toBack reorder the siblings"
|
|
632
704
|
g := sceneKids(4)
|
|
@@ -646,36 +718,36 @@ test "setZIndex, toFront and toBack reorder the siblings"
|
|
|
646
718
|
assert sceneOrder(g) == "3,2,0,1,"
|
|
647
719
|
b.setZIndex(-5)
|
|
648
720
|
assert sceneOrder(g) == "1,3,2,0,"
|
|
649
|
-
assert
|
|
721
|
+
assert Group.create().zIndex() == -1
|
|
650
|
-
|
|
722
|
+
Group.create().toFront()
|
|
651
723
|
assert g.children.length() == 4
|
|
652
724
|
|
|
653
725
|
test "sortChildren is stable and keeps ties in order"
|
|
654
|
-
g :=
|
|
726
|
+
g := Group.create()
|
|
655
727
|
for i := range 10
|
|
656
|
-
k :=
|
|
728
|
+
k := Rect.create(1.0, 1.0, Color.Red).at(Float(i), Float(i * 7 % 3))
|
|
657
729
|
g.add(k)
|
|
658
730
|
g.sortChildren(|a, b| a.y < b.y)
|
|
659
731
|
assert sceneOrder(g) == "0,3,6,9,1,4,7,2,5,8,"
|
|
660
732
|
g.sortChildren(|a, b| a.y < b.y)
|
|
661
733
|
assert sceneOrder(g) == "0,3,6,9,1,4,7,2,5,8,"
|
|
662
734
|
|
|
663
|
-
test "sortChildren sorts 5000 random
|
|
735
|
+
test "sortChildren sorts 5000 random actors and updates hit order"
|
|
664
|
-
g :=
|
|
736
|
+
g := Group.create()
|
|
665
737
|
for i := range 5000
|
|
666
|
-
g.add(
|
|
738
|
+
g.add(Rect.create(1.0, 1.0, Color.Red).at(Float(i), Float({i * 7919 + 13} % 5003)))
|
|
667
739
|
g.sortChildren(|a, b| a.y < b.y)
|
|
668
740
|
assert g.children.length() == 5000
|
|
669
741
|
for i := range 4999
|
|
670
742
|
assert g.children.at(i).y <= g.children.at(i + 1).y
|
|
671
|
-
assert {g.children.at(10).parent ?: crash "orphan"}
|
|
743
|
+
assert same({g.children.at(10).parent ?: crash "orphan"}, g)
|
|
672
744
|
|
|
673
745
|
test "sorting a nearly sorted list takes about n comparisons and reuses the scratch buffer"
|
|
674
|
-
g :=
|
|
746
|
+
g := Group.create()
|
|
675
747
|
for i := range 5000
|
|
676
|
-
g.add(
|
|
748
|
+
g.add(Rect.create(1.0, 1.0, Color.Red).at(0.0, Float(i)))
|
|
677
749
|
c := List(0)
|
|
678
|
-
less: fn(
|
|
750
|
+
less: fn(Actor, Actor) -> Bool := |a, b|
|
|
679
751
|
_ := c.set(0, c.at(0) + 1)
|
|
680
752
|
a.y < b.y
|
|
681
753
|
g.sortChildren(less)
|
|
@@ -698,22 +770,22 @@ test "sorting a nearly sorted list takes about n comparisons and reuses the scra
|
|
|
698
770
|
_ := c.set(0, 0)
|
|
699
771
|
g.children.clear()
|
|
700
772
|
for i := range 5000
|
|
701
|
-
g.add(
|
|
773
|
+
g.add(Rect.create(1.0, 1.0, Color.Red).at(0.0, Float({i * 7919 + 13} % 5003)))
|
|
702
774
|
g.sortChildren(less)
|
|
703
775
|
assert c.at(0) < 130000
|
|
704
776
|
|
|
705
777
|
test "culling skips children outside the area in flatten and draw order"
|
|
706
|
-
g :=
|
|
778
|
+
g := Group.create()
|
|
707
|
-
inside :=
|
|
779
|
+
inside := Rect.create(10.0, 10.0, Color.Red).at(20.0, 20.0)
|
|
708
|
-
outside :=
|
|
780
|
+
outside := Rect.create(10.0, 10.0, Color.Red).at(500.0, 20.0)
|
|
709
781
|
g.add(inside)
|
|
710
782
|
g.add(outside)
|
|
711
|
-
outside.add(
|
|
783
|
+
outside.add(Rect.create(5.0, 5.0, Color.Red))
|
|
712
784
|
assert flatten(g).length() == 4
|
|
713
785
|
g.setCullingArea(0.0, 0.0, 100.0, 100.0)
|
|
714
786
|
list := flatten(g)
|
|
715
787
|
assert list.length() == 2
|
|
716
|
-
assert list.at(1).node
|
|
788
|
+
assert same(list.at(1).node, inside)
|
|
717
789
|
g.setCullingArea(0.0, 100.0, 100.0, 100.0)
|
|
718
790
|
assert flatten(g).length() == 1
|
|
719
791
|
g.clearCullingArea()
|
|
@@ -721,24 +793,24 @@ test "culling skips children outside the area in flatten and draw order"
|
|
|
721
793
|
|
|
722
794
|
test "hit does not return culled children, and a scaled child near the edge is kept"
|
|
723
795
|
stage := Stage.create()
|
|
724
|
-
k :=
|
|
796
|
+
k := Rect.create(10.0, 10.0, Color.Red).at(105.0, 5.0)
|
|
725
797
|
stage.root.add(k)
|
|
726
798
|
stage.root.setCullingArea(0.0, 0.0, 100.0, 100.0)
|
|
727
799
|
assert stage.hit(107.0, 7.0) == null
|
|
728
800
|
k.scaleX = 4.0
|
|
729
801
|
k.originX = 10.0
|
|
730
|
-
assert {stage.hit(90.0, 7.0) ?: crash "miss"}
|
|
802
|
+
assert same({stage.hit(90.0, 7.0) ?: crash "miss"}, k)
|
|
731
803
|
assert flatten(stage.root).length() == 2
|
|
732
804
|
k.scaleX = 1.0
|
|
733
805
|
k.originX = 0.0
|
|
734
806
|
assert stage.hit(107.0, 7.0) == null
|
|
735
807
|
stage.root.clearCullingArea()
|
|
736
|
-
assert {stage.hit(107.0, 7.0) ?: crash "miss"}
|
|
808
|
+
assert same({stage.hit(107.0, 7.0) ?: crash "miss"}, k)
|
|
737
809
|
|
|
738
810
|
test "a child without a size is never culled"
|
|
739
|
-
g :=
|
|
811
|
+
g := Group.create()
|
|
740
|
-
slot :=
|
|
812
|
+
slot := Group.create().at(900.0, 900.0)
|
|
741
813
|
g.add(slot)
|
|
742
|
-
slot.add(
|
|
814
|
+
slot.add(Rect.create(10.0, 10.0, Color.Red).at(-890.0, -890.0))
|
|
743
815
|
g.setCullingArea(0.0, 0.0, 100.0, 100.0)
|
|
744
816
|
assert flatten(g).length() == 3
|