Skip to main content

VisualEffect

Component that controls a visual effect instance (playback, seed, camera binding, and exposed slot values).

TypeNameInterface Description
VariablesaliveParticleCounts: number[]

Function: Current alive particle counts for each system in the effect.

Variablesasset: VisualEffectAsset

Function: The VFX profile asset used by this effect.

VariablesenableAliveParticleCount: boolean

Function: Enable or disable tracking of alive particle counts.

VariablesisEmitting: boolean

Function: Whether the effect is currently spawning new particles. Becomes false after any stop() call regardless of the stop behavior. While isEmitting is false but isPlaying is still true, existing particles continue to simulate until they expire naturally.

VariablesisPlaying: boolean

Function: Whether the VFX system is currently running (i.e. particles are still being simulated and rendered). Remains true after stop(StopEmitting) until all existing particles have naturally expired. Becomes false only after stop(StopEmittingAndClear).

Functionsconstructor()

Functionsemit(): void

Function: Emits a burst immediately. Use this for one-shot burst behavior when the effect is already playing. Has no effect if isEmitting is false (i.e. after stop() has been called).

Functionspause(): void

Function: Pause the visual effect playback.

Functionsplay(): void

Function: Start or resume the visual effect playback.

Functionsreset(): void

Function: Reset the visual effect to its initial state.

FunctionssetStartSeed(seed: number): void

Function: Set the random seed for the VFX simulation.

Parameters

seed: Random seed value

Functionsstop(behavior?: VFXStopBehavior): void

Function: Stop the visual effect playback.

Parameters

behavior: Stop behavior, default is StopEmittingAndClear, StopEmittingAndClear will stop the effect and clear all particles, StopEmitting will stop the effect but keep existing particles.

Examples

constructor()

let obj = new APJS.VisualEffect();

emit(): void

export class NewScriptComponent extends APJS.BasicScriptComponent {
......
onUpdate(deltaTime: number) {
if (this.visualEffectComponent.isPlaying && triggerBurstOnce) {
this.visualEffectComponent.emit();
}
}
}

stop(behavior?: VFXStopBehavior): void

export class NewScriptComponent extends APJS.BasicScriptComponent {
......
onUpdate(deltaTime: number) {
if (this.visualEffectComponent.isEmitting && otherCondition) {
this.visualEffectComponent.stop(APJS.VFXStopBehavior.StopEmitting);
} else if (this.visualEffectComponent.isPlaying && otherCondition) {
this.visualEffectComponent.stop(); // StopEmittingAndClear by default
}
}
}

Use Case

Example 1 — Control VisualEffect particle system: play, stop, modify Lifetime, swap MainTexture for color/shape change

@component()
export class VFXPlayStopControl extends APJS.BasicScriptComponent {
@serializeProperty
texHolder!: APJS.SceneObject;

private vfx!: APJS.VisualEffect;
private initialized = false;

onUpdate(dt: number): void {
if (!this.initialized) {
const v = this.getSceneObject().getComponent("VisualEffect") as APJS.VisualEffect;
if (!v) return;
this.vfx = v;
this.initialized = true;
}
}

// Call from game logic to trigger a short particle burst
triggerBurst(lifetime: number): void {
this.vfx.stop();
this.vfx.asset.setFloat("Lifetime", lifetime);
this.vfx.play();
}

// Swap particle texture for different color/shape
swapTexture(): void {
if (!this.texHolder) return;
const img = this.texHolder.getComponent("Image") as APJS.Image;
if (img && img.texture) {
this.vfx.stop();
this.vfx.asset.setTexture("MainTexture", img.texture);
this.vfx.play();
}
}

// Adjust velocity range for spread
setSpread(min: APJS.Vector3f, max: APJS.Vector3f): void {
const a = this.vfx.asset;
if (a.hasVectorKey("Min")) a.setVector("Min", min);
if (a.hasVectorKey("Max")) a.setVector("Max", max);
}

stopEffect(): void {
this.vfx.stop(APJS.VFXStopBehavior.StopEmitting);
}
}

Example 2 — Game juice combo — on tap: camera shake + particle burst + sound effect + scale bounce + score fly-up, all firing simultaneously from one triggerJuice() method

@component()
export class GameJuiceCombo extends APJS.BasicScriptComponent {
@serializeProperty
camera!: APJS.SceneObject;
@serializeProperty
target!: APJS.SceneObject;
@serializeProperty
burstVFX!: APJS.SceneObject;
@serializeProperty
sfxPlayer!: APJS.SceneObject;
@serializeProperty
flyText!: APJS.SceneObject;

private camTransform!: APJS.Transform;
private originalCamPos!: APJS.Vector3f;
private targetTransform!: APJS.Transform;
private baseScale!: APJS.Vector3f;
private vfx!: APJS.VisualEffect;
private audio!: APJS.AudioComponent;
private textComp!: APJS.Text;
private textST!: APJS.ScreenTransform;

// Shake state
private shakeTimer = 0;
private shakeDuration = 0.3;
private shakeIntensity = 0.5;

// Scale bounce state (manual — no Tween component needed)
private bounceTimer = -1;
private bounceDuration = 0.3;
private bounceScale = 1.3;

// Fly-up state
private flyTimer = -1;
private flyDuration = 0.8;
private flyStartY = 0;

private score = 0;
private initialized = false;
private touchCallback!: (event: APJS.IEvent) => void;

// RecordStart: reset score + cancel all in-flight juice animations + restore camera/target
// pose + stop audio. onUpdate accumulators (timers) are event-driven (set by triggerJuice
// taps), so no RecordEnd needed (path: RecordStart resets, no further accumulation between).
// See GameState §"RecordStart / RecordEnd Lifecycle".
private onRecordStart = (_event: APJS.IEvent) => {
if (!this.initialized) return;
this.score = 0;
this.shakeTimer = 0;
this.bounceTimer = -1;
this.flyTimer = -1;
if (this.camTransform && this.originalCamPos) {
this.camTransform.localPosition = this.originalCamPos.clone();
}
if (this.targetTransform && this.baseScale) {
this.targetTransform.localScale = this.baseScale.clone();
}
if (this.flyText) this.flyText.setEnabledInHierarchy(false);
if (this.audio) this.audio.stop();
if (this.vfx) this.vfx.stop();
};

onStart(): void {
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.RecordStart, this.onRecordStart);
}

onUpdate(dt: number): void {
if (!this.initialized) {
if (!this.camera || !this.target || !this.burstVFX || !this.sfxPlayer || !this.flyText) return;
this.camTransform = this.camera.getComponent("Transform") as APJS.Transform;
this.originalCamPos = this.camTransform.localPosition.clone();
this.targetTransform = this.target.getComponent("Transform") as APJS.Transform;
this.baseScale = this.targetTransform.localScale.clone();
this.vfx = this.burstVFX.getComponent("VisualEffect") as APJS.VisualEffect;
this.audio = this.sfxPlayer.getComponent("AudioComponent") as APJS.AudioComponent;
this.textComp = this.flyText.getComponent("Text") as APJS.Text;
this.textST = this.flyText.getComponent("ScreenTransform") as APJS.ScreenTransform;
this.vfx.stop();
this.touchCallback = (event: APJS.IEvent) => {
const touch = event.args[0] as APJS.TouchData;
if (touch.phase === APJS.TouchPhase.Began) {
this.triggerJuice();
}
};
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.Touch, this.touchCallback, this);
this.initialized = true;
}
this.updateShake(dt);
this.updateBounce(dt);
this.updateFlyUp(dt);
}

private triggerJuice(): void {
this.score += 10;
// 1. Screen shake
this.shakeTimer = this.shakeDuration;
// 2. Particle burst
this.vfx.stop();
this.vfx.asset.setFloat("Lifetime", 0.5);
this.vfx.play();
// 3. Sound effect
this.audio.stop();
this.audio.loopCount = 1;
this.audio.play();
// 4. Scale bounce
this.bounceTimer = 0;
// 5. Score fly-up
this.showFlyUp("+" + this.score);
}

private updateShake(dt: number): void {
if (this.shakeTimer <= 0) return;
this.shakeTimer -= dt;
if (this.shakeTimer <= 0) {
this.camTransform.localPosition = this.originalCamPos.clone();
return;
}
const decay = this.shakeTimer / this.shakeDuration;
const ox = (Math.random() - 0.5) * 2 * this.shakeIntensity * decay;
const oy = (Math.random() - 0.5) * 2 * this.shakeIntensity * decay;
this.camTransform.localPosition = new APJS.Vector3f(
this.originalCamPos.x + ox,
this.originalCamPos.y + oy,
this.originalCamPos.z
);
}

// Manual scale bounce: pop up then ease back to base scale
private updateBounce(dt: number): void {
if (this.bounceTimer < 0) return;
this.bounceTimer += dt;
const p = this.bounceTimer / this.bounceDuration;
if (p >= 1) {
this.targetTransform.localScale = this.baseScale.clone();
this.bounceTimer = -1;
return;
}
// Sine ease-out: fast pop up, smooth settle back
// p=0 → scale=bounceScale (max), p=1 → scale=1 (base)
const factor = 1 + (this.bounceScale - 1) * Math.cos(p * Math.PI * 0.5);
this.targetTransform.localScale = new APJS.Vector3f(
this.baseScale.x * factor,
this.baseScale.y * factor,
this.baseScale.z * factor
);
}

private showFlyUp(text: string): void {
this.flyText.setEnabledInHierarchy(true);
this.textComp.text = text;
this.flyStartY = 100;
this.textST.anchoredPosition = new APJS.Vector2f(0, this.flyStartY);
this.textST.scale = new APJS.Vector2f(1, 1);
this.flyTimer = 0;
}

private updateFlyUp(dt: number): void {
if (this.flyTimer < 0) return;
this.flyTimer += dt;
const p = this.flyTimer / this.flyDuration;
if (p >= 1) {
this.flyText.setEnabledInHierarchy(false);
this.flyTimer = -1;
return;
}
this.textST.anchoredPosition = new APJS.Vector2f(0, this.flyStartY + p * 150);
const s = 1 - p * 0.4;
this.textST.scale = new APJS.Vector2f(s, s);
}

onDestroy(): void {
if (this.touchCallback) {
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.Touch, this.touchCallback, this);
}
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.RecordStart, this.onRecordStart);
}
}
Copyright © 2026 TikTok. All rights reserved.
About TikTokHelp CenterCareersContactLegalTerms of ServicePrivacy PolicyCookies