ParticleManager
ParticleManager is a singleton that manages all particle emitters. It uses GPU-instanced rendering via InstancedMesh and Structure-of-Arrays (SoA) particle data for zero-GC per-frame performance.
It is mounted automatically by Game — you do not need to set it up manually.
import { getParticleManager } from "@carverjs/core/systems";How It Works #
On
Gamemount, theParticleFlushinternal component is created, which callsgetParticleManager().tick(delta)every frame at priority -12.Each frame,
tick()runs for each registered emitter:Reads game phase via
useGameStore.getState()— emitters freeze during"paused"/"gameover"/"loading"Emission phase: accumulates fractional particles (stream) or checks burst schedules, spawns new particles from the free-list pool
Update phase: advances age, applies velocity/acceleration/drag/gravity, evaluates over-lifetime curves, kills expired particles (returns to pool)
Write phase: composes instance matrices, writes color/alpha/sprite-frame to GPU buffers, sets
mesh.countto alive count
On
Gameunmount, all emitters are destroyed, GPU resources are disposed, and the singleton is reset.
Zero-GC Architecture #
SoA (Structure-of-Arrays): Particle data stored as 26 pre-allocated
Float32Arrays (position, velocity, age, color, etc.) for cache-friendly iterationFree-list pool: Dead particle indices recycled via a stack — no object allocation in the hot path
Pre-allocated temporaries: Reusable
Matrix4,Vector3,Quaternion,Colorobjects for per-frame computationGPU instancing: One
InstancedMeshper emitter — single draw call regardless of particle count
Frame Priority #
Priority | System
---------|------------------
-50 | InputFlush
-48 | AudioFlush
-45 | Physics (Rapier)
-40 | earlyUpdate
-30 | fixedUpdate
-25 | CollisionFlush
-20 | update
-15 | TweenFlush
-12 | ParticleFlush ← particles update here
-10 | lateUpdate
-5 | GameLoopTick
0 | R3F renderParticles run after tweens (-15) so tween-animated emitter positions are up-to-date. They run before lateUpdate (-10) so late-update code sees current particle state.
API #
getParticleManager() #
Returns the singleton ParticleManager instance. Creates one if it doesn't exist.
destroyParticleManager() #
Disposes all emitters, frees GPU resources, and destroys the singleton. Called automatically when Game unmounts.
Emitter Lifecycle #
| Method | Description |
|---|---|
createEmitter(config) | Create and register a new emitter. Returns a unique ID string |
destroyEmitter(id) | Remove an emitter and dispose its GPU resources |
getEmitter(id) | Get an EmitterInstance by ID for imperative control |
Global Controls #
| Method | Description |
|---|---|
setMode(mode) | Set world mode ("2d" or "3d"). Called automatically by ParticleFlush |
tick(delta) | Advance all emitters by delta seconds. Called by ParticleFlush |
dispose() | Destroy all emitters and free resources |
EmitterInstance Controls #
Each EmitterInstance returned by getEmitter() exposes:
| Method | Description |
|---|---|
start() | Start continuous emission |
stop() | Stop emission (alive particles finish their lifetime) |
clear() | Stop and kill all particles immediately |
reset() | Reset to initial state |
burst(count?) | Emit a burst of particles |
setRate(rate) | Change stream emission rate |
getActiveCount() | Number of alive particles |
isEmitting() | Whether currently emitting |
Preset Registry #
import { getParticlePreset, registerParticlePreset } from "@carverjs/core/systems";
// Get a built-in preset config
const fireConfig = getParticlePreset("fire");
// Register a custom preset
registerParticlePreset("plasma", {
maxParticles: 400,
rate: 80,
shape: { shape: "sphere", radius: 0.3 },
particle: {
speed: [1, 4],
lifetime: [0.5, 1.5],
color: ["#00ffaa", "#0044ff"],
},
blendMode: "additive",
});Usage #
Most game code should use the useParticles hook or <ParticleEmitter> component, which provide auto-cleanup on unmount. Direct manager access is useful for systems-level code:
import { getParticleManager } from "@carverjs/core/systems";
// Direct emitter creation (you manage lifecycle)
const id = getParticleManager().createEmitter({
maxParticles: 200,
rate: 50,
particle: { speed: 5, lifetime: 1, color: "#ff0000" },
blendMode: "additive",
});
const emitter = getParticleManager().getEmitter(id)!;
emitter.burst(100);
// Clean up when done
getParticleManager().destroyEmitter(id);Type Definitions #
See Types for ParticleEmitterConfig, ParticlePreset, EmitterShapeConfig, ParticlePropertyConfig, OverLifetimeConfig, ValueRange, ColorRange, LifetimeCurve, ColorGradient, BurstConfig, SpriteSheetConfig, and ParticleBlendMode.