VisualEffect
Component that controls a visual effect instance (playback, seed, camera binding, and exposed slot values).
| Type | Name | Interface Description |
|---|---|---|
| Variables | aliveParticleCounts: number[] | • Function: Current alive particle counts for each system in the effect. |
| Variables | asset: VisualEffectAsset | • Function: The VFX profile asset used by this effect. |
| Variables | enableAliveParticleCount: boolean | • Function: Enable or disable tracking of alive particle counts. |
| Variables | isEmitting: boolean | • Function: Whether the effect is currently spawning new particles. Becomes |
| Variables | isPlaying: boolean | • Function: Whether the VFX system is currently running (i.e. particles are still being simulated and rendered). Remains |
| Functions | constructor() | |
| Functions | emit(): void | • Function: Emits a burst immediately. Use this for one-shot burst behavior when the effect is already playing. Has no effect if |
| Functions | pause(): void | • Function: Pause the visual effect playback. |
| Functions | play(): void | • Function: Start or resume the visual effect playback. |
| Functions | reset(): void | • Function: Reset the visual effect to its initial state. |
| Functions | setStartSeed(seed: number): void | • Function: Set the random seed for the VFX simulation. Parameters • |
| Functions | stop(behavior?: VFXStopBehavior): void | • Function: Stop the visual effect playback. Parameters • |
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);
}
}