Scene: Spawn and Fade Cubes
A staggered reveal that fans six pre-built cubes out into two cluster
parents — LeftCluster and RightCluster — one cube every quarter-second.
Each reveal flips setEnabledInHierarchy(true), reassigns
SceneObject.parent to the chosen cluster, randomizes the cube's
Transform.localPosition and localEulerAngles, and tweens the local
scale up from zero. Tap anywhere to reset and replay with a fresh
random configuration.

What you'll build
- A scene with a hidden
CubeContainerparent holding six colored cubes, plus two empty cluster parentsLeftClusterandRightClusterat(±1.5, 0, 0). - A
CubeStaggeredSpawnerscript that runs onCubeContainer, holds the six cubes as aSceneObject[]@serializePropertyarray, and reveals them on a stagger timer. - Each reveal exercises three core Scene APIs:
SceneObject.parent(runtime reparenting),SceneObject.setEnabledInHierarchy(visibility), andSceneObject.getTransform()(component lookup wrapper) forlocalPositionandlocalEulerAnglesmutation. - A global
APJS.EventType.Touchlistener resets every cube to its home parent and re-runs the stagger.
Open the demo
Unzip and open in Effect House (5.9.0+). The opening scene contains:
- Camera — at
(0, 1, 8)with an 8° downward pitch. - Directional Light / Environment Light — defaults.
- CubeContainer — empty SceneObject at the origin, parent of all six
cubes at scene start. Hosts the
CubeStaggeredSpawnerscript. - Cube_Red, Cube_Orange, Cube_Yellow, Cube_Green, Cube_Blue,
Cube_Purple — six 0.8-unit cubes (scale
0.1on a base-8 Cube primitive), each with its own Standard PBR material. All six start hidden so the script'ssetEnabledInHierarchy(true)reveal reads as a real animation. - LeftCluster — empty SceneObject at
(-1.5, 0, 0). Receives some of the cubes via runtime reparenting. - RightCluster — empty SceneObject at
(+1.5, 0, 0). Receives the rest.
Read the scripts
CubeStaggeredSpawner.ts
Lives on CubeContainer. Six wired @serializeProperty references
(cubes, leftCluster, rightCluster, homeContainer) plus three
tunables. The onUpdate body advances the stagger timer, ticks any
in-flight scale fades, and (on first tick) installs the touch reset
handler.
interface FadeEntry {
obj: APJS.SceneObject;
t: number;
finalScale: APJS.Vector3f;
}
@component()
export class CubeStaggeredSpawner extends APJS.BasicScriptComponent {
// The 6 cubes that will fan out, randomly assigned to a cluster on each spawn.
@serializeProperty cubes: APJS.SceneObject[] = [];
// Two alternative parents the cubes get reparented to at spawn time.
@serializeProperty leftCluster!: APJS.SceneObject;
@serializeProperty rightCluster!: APJS.SceneObject;
// Where cubes return to on reset (typically the GameObject this script lives on).
@serializeProperty homeContainer!: APJS.SceneObject;
// Seconds between successive cube reveals.
@serializeProperty staggerSeconds: number = 0.25;
// Seconds the scale fade-in takes per cube.
@serializeProperty fadeSeconds: number = 0.4;
// Local-space spread within a cluster, in world units.
@serializeProperty spread: number = 0.6;
// Internal state.
private nextSpawnIndex: number = 0;
private nextSpawnAt: number = 0;
private elapsed: number = 0;
private fading: FadeEntry[] = [];
private touchCallback!: (event: APJS.IEvent) => void;
private inited: boolean = false;
onUpdate(dt: number): void {
// Lazy init — @serializeProperty references are null in onStart.
if (!this.inited) {
if (!this.cubes || this.cubes.length === 0) return;
if (!this.leftCluster || !this.rightCluster || !this.homeContainer) return;
this.touchCallback = (event: APJS.IEvent) => {
const t = event.args[0] as APJS.TouchData;
if (t.phase === APJS.TouchPhase.Began) this.resetAll();
};
APJS.EventManager.getGlobalEmitter()
.on(APJS.EventType.Touch, this.touchCallback, this);
// Hide every cube on entry so the stagger reveal reads as a real animation.
for (let i = 0; i < this.cubes.length; i++) {
const c = this.cubes[i];
if (c) c.setEnabledInHierarchy(false);
}
this.inited = true;
}
this.elapsed += dt;
// Reveal one cube at each stagger tick until they're all out.
while (
this.nextSpawnIndex < this.cubes.length &&
this.elapsed >= this.nextSpawnAt
) {
this.spawnOne(this.nextSpawnIndex);
this.nextSpawnIndex++;
this.nextSpawnAt += this.staggerSeconds;
}
// Tick scale-tweens.
for (let i = this.fading.length - 1; i >= 0; i--) {
const f = this.fading[i];
f.t += dt / this.fadeSeconds;
const k = Math.min(f.t, 1);
const tr = f.obj.getTransform();
tr.localScale = new APJS.Vector3f(
f.finalScale.x * k,
f.finalScale.y * k,
f.finalScale.z * k,
);
if (k >= 1) this.fading.splice(i, 1);
}
}
onDestroy(): void {
if (this.touchCallback) {
APJS.EventManager.getGlobalEmitter()
.off(APJS.EventType.Touch, this.touchCallback, this);
}
}
private spawnOne(index: number): void {
const cube = this.cubes[index];
if (!cube) return;
// Pick a cluster — alternate left/right with a random nudge so the
// result reads as choreographed but not rigid.
const goLeft =
(index % 2 === 0) ? Math.random() < 0.7 : Math.random() < 0.3;
const cluster = goLeft ? this.leftCluster : this.rightCluster;
// Runtime reparenting: SceneObject.parent setter reassigns the
// transform under a new parent. Both clusters live at the scene
// root with their own world position, so we get instant relocation.
cube.parent = cluster;
// Position relative to the new parent (random spot inside ±spread).
const tr = cube.getTransform();
const half = this.spread * 0.5;
tr.localPosition = new APJS.Vector3f(
(Math.random() - 0.5) * this.spread,
(Math.random() - 0.5) * this.spread,
(Math.random() - 0.5) * half,
);
// Random Euler tilt — Transform.localEulerAngles reads/writes degrees,
// not radians.
tr.localEulerAngles = new APJS.Vector3f(
Math.random() * 360,
Math.random() * 360,
Math.random() * 360,
);
// Reveal + scale-tween from 0 to the final scale.
cube.setEnabledInHierarchy(true);
const final = new APJS.Vector3f(0.1, 0.1, 0.1);
tr.localScale = new APJS.Vector3f(0, 0, 0);
this.fading.push({ obj: cube, t: 0, finalScale: final });
}
private resetAll(): void {
// Hide every cube, return to the home container, clear the queue.
for (let i = 0; i < this.cubes.length; i++) {
const c = this.cubes[i];
if (!c) continue;
c.setEnabledInHierarchy(false);
c.parent = this.homeContainer;
}
this.fading.length = 0;
this.nextSpawnIndex = 0;
this.nextSpawnAt = this.staggerSeconds;
this.elapsed = 0;
}
}
The Scene-namespace calls of interest:
SceneObject.parentis a getter/setter pair. Assigning a new parent reassigns the object's transform to the new parent — local position and rotation are interpreted relative to the new parent's transform after the reassignment. Settingparent = nullwould detach the object back to the scene root.SceneObject.setEnabledInHierarchy(boolean)flips the effective visibility — the object and every descendant rendered or hidden in one call. Compare againstSceneObject.enabled(just this object) andSceneObject.visible(read-only effective state, accounting for ancestors).SceneObject.getTransform()is a typed shortcut forgetComponent("Transform") as APJS.Transform. The longer form works too; the helper just saves a cast and a runtimenullguard for the common case.Transform.localPositionis aVector3fsetter. Assigning a freshVector3f(x, y, z)is the canonical way to teleport an object; mutating components of the existing vector does not re-trigger the underlying transform update.Transform.localEulerAnglesis aVector3fof degrees (not radians). The script feedsMath.random() * 360directly so the tutorial sees the raw degree-space.@serializeProperty cubes: APJS.SceneObject[]exposes a typed array reference field to the inspector. Wiring uses[{guid, type: "SceneObject"}, ...]— bare GUID strings are rejected.
Customize
On CubeContainer → CubeStaggeredSpawner:
staggerSeconds(default0.25) — gap between successive reveals. Larger values turn the demo into a slower drumroll.fadeSeconds(default0.4) — duration of the scale tween per cube.0removes the tween (snap visible).spread(default0.6) — bounding-box edge length each cube can randomly land within, relative to its assigned cluster.
Tunables on the cubes themselves (set via set_component on each
Transform):
localScale— change the per-cube target size. The script reads the inspector value as the spawn-tween destination.localPosition/localEulerAngles— adjust the initial layout the cubes return to on reset (when they're parented back toCubeContainer).
Suggestions for further play:
- Replace the alternating-cluster heuristic with a third
centerClusterparent, and havespawnOnepick uniformly across all three. - Drive the scale tween with
Vector3f.lerp(zeroVec, finalVec, k)(the Math tutorial demonstrates this) instead of the explicit component multiplication used here. - Add a per-cube
OrbitController(see the Math tutorial) that runs while the cube is enabled, so each revealed cube starts orbiting its cluster.
What you learned
This tutorial used:
SceneObject.parent— runtime reparenting; assignment moves the transform under the new parent.SceneObject.setEnabledInHierarchy(boolean)— toggling effective visibility for an object and its descendants in one call.SceneObject.getTransform()— the typed shortcut forgetComponent("Transform").Transform.localPositionandTransform.localEulerAngles— Vector3f-valued setters on a Transform;localEulerAnglesis in degrees.@serializePropertyfor arrays (SceneObject[]) and single references (SceneObject) — wiring uses{guid, type: "SceneObject"}.
Read the full SceneObject reference, the Transform reference, the Component reference, and the Scene namespace overview.