Skip to main content

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.

VFX Tap Burst demo running in Effect House preview

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: false in the inspector — equivalent to setEnabledInHierarchy(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 TitleText and a StatusText 2D HUD that mirrors the active preset name.
  • A GameController empty SceneObject hosting TapBurst — the script that subscribes to global EventType.Touch and cycles the presets.

Open the demo

↓ tap-burst.zip

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 VisualEffect component with its built-in VisualEffectAsset pre-wired by the corresponding add_builtin_object preset; 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:

  • VisualEffect is the runtime component every particle preset ships with. Look it up at runtime via obj.getComponent("VisualEffect") as APJS.VisualEffect.
  • VisualEffect.play() starts emission. Calling play() 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 argument behavior is a VFXStopBehavior enum:
    • 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).
  • VisualEffectAsset is 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; an add_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 is set_object({visible: false}). Hidden VFX objects don't render particles even if play() is called — flip the flag before the play() call.

Customize

On GameControllerTapBurst:

  • 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_NAMES is a static readonly array in the script. Edit it to match a re-ordered vfxObjects[] 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.LongTap to keep a continuous-emission preset (Turbulence / Fog) playing while held, and GestureType.Drop to call .stop(StopEmitting) for a graceful tail.
  • Bind a single VFX's emission to the BGM beat: combine with the Audio tutorial's BeatDetector and play() the burst on each detected beat.
  • Move the VFX origin to follow a finger drag — translate gestureInfo.endPoint from normalized 0-1 viewport coordinates to world coordinates via Camera.viewportPointToRay (see the Physics 3D tutorial for the ray-cast pattern), then set localPosition on each tap.

What you learned

This tutorial used:

  • VisualEffect.play(), .stop(behavior) with explicit VFXStopBehavior, runtime component lookup via getComponent("VisualEffect").
  • VFXStopBehaviorStopEmittingAndClear (default; immediate) vs StopEmitting (let particles finish).
  • The add_builtin_object preset family — 10 ready-to-use particle systems whose VisualEffect.asset is pre-wired; you build with them, not for them.
  • setEnabledInHierarchy(true|false) — the visibility gate; flip before play() 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-init onUpdate.

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.

Copyright © 2026 TikTok. All rights reserved.
About TikTokHelp CenterCareersContactLegalTerms of ServicePrivacy PolicyCookies