VFX: Tap Burst
Four built-in particle-effect presets — Explosion, Firework, Lightning,
Turbulence — pre-placed at the world origin and cycled on every screen
tap. Each tap stops the previous burst (with VFXStopBehavior.StopEmittingAndClear),
swaps in the next preset, and calls .play() on its VisualEffect
component. A 2D status label echoes the current preset name.

What you'll build
- Four pre-built VFX SceneObjects via
add_builtin_object— each with a different particle preset ("Explosion Particles","Firework Particles","Lightning Particles","Turbulence Particles"). All four start hidden (visible: falsein the inspector — equivalent tosetEnabledInHierarchy(false)). - A scaled-up
Transform.localScale = (5, 5, 5)on each VFX so the particles read at a reasonable size against the default camera-feed perspective. - A
TitleTextand aStatusText2D HUD that mirrors the active preset name. - A
GameControllerempty SceneObject hostingTapBurst— the script that subscribes to globalEventType.Touchand cycles the presets.
Open the demo
Unzip and open in Effect House (5.9.0+). The opening scene contains:
- Camera — default 3D perspective camera at
(0, 0, 40)looking at the world origin. - 2D Camera — auto-created by the first 2D Text.
- VfxExplosion / VfxFirework / VfxLightning / VfxTurbulence —
four particle-effect SceneObjects at world origin. Each holds a
VisualEffectcomponent with its built-inVisualEffectAssetpre-wired by the correspondingadd_builtin_objectpreset; we don't author a custom asset. - TitleText — "VFX Tap Burst" at the top.
- StatusText — "Tap anywhere" →
Playing: <preset-name>yellow label. - HintText — short usage hint at the bottom.
- GameController — empty SceneObject hosting
TapBurst.
Read the script
TapBurst.ts
@component()
export class TapBurst extends APJS.BasicScriptComponent {
// The four VFX SceneObjects to cycle through, in display order. Each has
// a VisualEffect component pre-configured with a different particle preset
// (Explosion / Firework / Lightning / Turbulence). All start hidden
// (`visible: false` in the inspector); the script enables one at a time.
@serializeProperty vfxObjects: APJS.SceneObject[] = [];
// Status label that shows the current preset name.
@serializeProperty statusText!: APJS.SceneObject;
private statusComp!: APJS.Text;
private vfxComps: APJS.VisualEffect[] = [];
private touchCallback!: (e: APJS.IEvent) => void;
private cycleIndex: number = -1; // -1 = nothing playing yet
private inited: boolean = false;
// Display names paired index-for-index with vfxObjects[]. Keep them as
// a `static readonly` array so the inspector doesn't need a second
// wired field for labels.
private static readonly PRESET_NAMES = [
"Explosion", "Firework", "Lightning", "Turbulence",
];
onUpdate(_dt: number): void {
if (this.inited) return;
if (!this.statusText || !this.vfxObjects || this.vfxObjects.length === 0) return;
this.statusComp = this.statusText.getComponent("Text") as APJS.Text;
// Cache each VFX object's VisualEffect component up-front so the tap
// handler doesn't getComponent() on the hot path.
for (const obj of this.vfxObjects) {
if (!obj) continue;
const vfx = obj.getComponent("VisualEffect") as APJS.VisualEffect;
if (vfx) this.vfxComps.push(vfx);
}
this.touchCallback = (event: APJS.IEvent) => {
const t = event.args[0] as APJS.TouchData;
if (t.phase !== APJS.TouchPhase.Began) return;
this.fireNextBurst();
};
APJS.EventManager.getGlobalEmitter()
.on(APJS.EventType.Touch, this.touchCallback, this);
this.inited = true;
console.log("[TapBurst] ready — " + this.vfxObjects.length + " presets wired");
}
onDestroy(): void {
if (this.touchCallback) {
APJS.EventManager.getGlobalEmitter()
.off(APJS.EventType.Touch, this.touchCallback, this);
}
}
private fireNextBurst(): void {
// Hide the currently-playing burst and clear its remaining particles.
// VFXStopBehavior.StopEmittingAndClear (the default for stop()) is the
// strict "hide + clean up immediately" mode — that's what we want here.
// The alternative, StopEmitting, keeps existing particles drifting
// until they expire naturally; useful when you want a graceful tail.
if (this.cycleIndex >= 0) {
const prev = this.vfxObjects[this.cycleIndex];
const prevVfx = this.vfxComps[this.cycleIndex];
if (prevVfx) prevVfx.stop(APJS.VFXStopBehavior.StopEmittingAndClear);
if (prev) prev.setEnabledInHierarchy(false);
}
// Advance to the next preset.
this.cycleIndex = (this.cycleIndex + 1) % this.vfxObjects.length;
const next = this.vfxObjects[this.cycleIndex];
const nextVfx = this.vfxComps[this.cycleIndex];
if (next) next.setEnabledInHierarchy(true);
if (nextVfx) nextVfx.play();
const name = TapBurst.PRESET_NAMES[this.cycleIndex] || ("Preset " + this.cycleIndex);
this.statusComp.text = "Playing: " + name;
console.log("[TapBurst] play " + name);
}
}
The VFX-namespace calls of interest:
VisualEffectis the runtime component every particle preset ships with. Look it up at runtime viaobj.getComponent("VisualEffect") as APJS.VisualEffect.VisualEffect.play()starts emission. Callingplay()again on an already-emitting effect re-triggers it — useful for one-shot bursts like Explosion / Firework where each tap should spawn a fresh batch.VisualEffect.stop(behavior?)halts emission. The single argumentbehavioris aVFXStopBehaviorenum:StopEmittingAndClear(default) — stop + immediately delete every particle currently on screen. Strict cleanup.StopEmitting— stop new emissions but let already-spawned particles finish their lifetime naturally. Use for graceful tails (a firework that finishes its sparkle even after you stop the trigger).
VisualEffectAssetis the data side of the effect — the particle graph, gradient curves, lifetime distributions, etc. The 10 built-in presets each ship with a pre-authored asset; anadd_builtin_object("<Preset> Particles")creates a SceneObject with the VisualEffect already pointing at that asset.- The 10 built-in presets:
Basic Particles— neutral starting point.Physics Particles— particles that interact with colliders.Fire Particles,Rain Particles,Snow Particles— weather/elemental.Explosion Particles,Firework Particles,Lightning Particles— one-shot dramatic effects.Fog Particles,Turbulence Particles— continuous ambient.
SceneObject.setEnabledInHierarchy(true|false)is the visibility toggle in script. The DSL equivalent isset_object({visible: false}). Hidden VFX objects don't render particles even ifplay()is called — flip the flag before the play() call.
Customize
On GameController → TapBurst:
vfxObjects— drop in any number of VFX SceneObjects. The cycle wraps modulo the array length, so 2 presets cycle A→B→A→B, 6 presets cycle through all 6.PRESET_NAMESis astatic readonlyarray in the script. Edit it to match a re-orderedvfxObjects[]so the status text stays accurate.
In the editor, on each VFX SceneObject:
Transform.localScale— bigger = bigger particle field. The demo uses(5, 5, 5)so the particles read clearly against the default camera-feed framing. Drop to(1, 1, 1)for a tight burst, push to(20, 20, 20)for a massive cone.Transform.localPosition— move the burst origin. The demo keeps everything at(0, 0, 0)so the particles emit from the scene's center; offsetting (e.g.(0, 5, -5)) moves the burst off-axis.
Suggestions for further play:
- Replace the cycle-on-every-tap with a long-press pattern: use
the Events & Input tutorial's
GestureType.LongTapto keep a continuous-emission preset (Turbulence / Fog) playing while held, andGestureType.Dropto call.stop(StopEmitting)for a graceful tail. - Bind a single VFX's emission to the BGM beat: combine with the
Audio tutorial's
BeatDetectorandplay()the burst on each detected beat. - Move the VFX origin to follow a finger drag — translate
gestureInfo.endPointfrom normalized 0-1 viewport coordinates to world coordinates viaCamera.viewportPointToRay(see the Physics 3D tutorial for the ray-cast pattern), then setlocalPositionon each tap.
What you learned
This tutorial used:
VisualEffect—.play(),.stop(behavior)with explicitVFXStopBehavior, runtime component lookup viagetComponent("VisualEffect").VFXStopBehavior—StopEmittingAndClear(default; immediate) vsStopEmitting(let particles finish).- The
add_builtin_objectpreset family — 10 ready-to-use particle systems whoseVisualEffect.assetis pre-wired; you build with them, not for them. setEnabledInHierarchy(true|false)— the visibility gate; flip beforeplay()so hidden effects don't try to render.@serializeProperty SceneObject[]for the cycle list — wired in the inspector, mapped to component references in the lazy-initonUpdate.
Read the full VisualEffect reference, the VisualEffectAsset reference, the VfxStopBehavior reference, and the VFX namespace overview.
For the touch-event subscription pattern used here, see the Events & Input tutorial. For hit-tested taps on individual UI elements (instead of "tap anywhere"), see TouchUtils.