Particles
<Particles effects> holds the effects a scene plays. Mount one in a scene, inside a World beside its views. Any view in the scene then plays an effect once with useParticles().spawn(effect, { at }), where at is a vector, { x, y, z }, or an entity’s [x, y, z] position, or mounts <Emitter effect> under an object to play it there for as long as the view stays. Nothing loads until a scene mounts one: the particle library is a chunk of its own.
Its props are in ParticlesProps, a burst’s in ParticleBurst, and an emitter’s in EmitterProps.
The engine draws particles with three.quarks. One batched renderer draws every effect that shares a material in one draw call, and steps them all from the frame’s clock, so a paused game holds each particle where it is. A burst removes itself when its last particle dies.
Making an effect
Section titled “Making an effect”An effect is one of the following:
-
A file from the three.quarks editor. Export the effect as JSON and import it in the game. The file holds the emission, the forces, the colour ramps and the sprite.
-
An emitter built in code with the library’s constructors, as
new ParticleSystem({ ... }).emitter. The game addsthree.quarksto its own dependencies, at the version@spawnite/enginenames as its peer, so a published page runs the engine’s own copy and a burst’scolorandcountreach the effect. -
An effect from the shared assets.
spawnite add asset <id>, or the MCP’sadd_asset, copies it into the game assrc/particles/<id>.json, its sprite inside it, and prints the line that imports it and the call that plays it. The game imports its own copy, so a change to it changes no other game.spawnite assets list --kind particle, or the MCP’slist_assets, lists every effect the library holds, creators’ among them, and the asset store shows a picture of each one in the air.
The library holds these five effects from the platform:
| Id | What it plays | How |
|---|---|---|
bolt-burst |
A spell’s impact, 28 white sparks | spawn |
melee-puff |
A blade’s hit, 16 white sparks | spawn |
slash-arc |
A blade’s swing, seven crystal shards thrown down -z | spawn with yaw |
bolt-runes |
Three gold runes orbiting a bolt | Emitter |
bolt-trail |
A violet ribbon behind a bolt | Emitter |
The sparks and the shards draw white: name a color with the burst, or with the Emitter, to tint them.
Sharing an effect
Section titled “Sharing an effect”An effect you made goes into the shared assets, so any game adds it with spawnite add asset:
- Put the effect’s
.jsonin a folder, with each sprite it names by a file name, such asspark.png, beside it. - Write its row in the folder’s
catalog.json, with the categoryeffects, as Publish an asset shows. - Run
spawnite assets publish <file>, or the MCP’spublish_assets.
The command carries each sprite inside the effect, plays it in headless Chromium, and refuses a file three.quarks cannot parse. It also refuses an effect heavy enough to stall a player’s tab: one past 64 objects, 16 emitters, 1,000 particles a second, 100 particles a metre or 5,000 particles alive at once, or emitters that prewarm more than 5 seconds of play, with every emitter’s load summed, or one with a sprite past 4096 pixels a side. The picture it takes of the effect in the air is the effect’s card in the store. The manifest records whether the effect loops, so add asset prints spawn for an effect that plays once and Emitter for one that loops.
Pass every effect the scene plays in effects. Particles parses each one when the scene mounts, because a sprite decodes after the parse returns, and a burst drawn before then is black. A burst asked for while its sprite decodes waits, and plays where it was asked for. spawn throws on an effect that effects does not hold.
Changing a burst
Section titled “Changing a burst”A burst can name three values, and the effect keeps its own for the next burst:
coloris the colour every particle starts with.countis how many particles each of the effect’s bursts throws.yawturns the effect about y before it plays, for one authored pointing down -z, such as the blade’s arc.
For anything else, such as a lifetime or a speed, make a second effect.
Riding an object
Section titled “Riding an object”<Emitter effect> plays an effect under the object it is mounted in, in that object’s space, for as long as it is mounted: a bolt’s runes and trail, a torch’s flame, a companion’s dust. When it unmounts, the emission ends, the particles in the air finish their life where they are, and the effect then leaves the scene. color tints every particle it emits, as a burst’s does. A view that holds a plain three.js object instead calls useParticles().follow(effect, object, color) and stops the handle it returns.
A spell’s look
Section titled “A spell’s look”<SpellLook> plays a spell’s effects at its three moments: in the caster’s hand during the wind-up, riding each of its projectiles, and where each ends. It plays them through Particles, so the scene’s effects holds every one it names.
An effect that rides is authored looping. One in world space with the trail render mode, as bolt-trail.json is, lays its ribbon where the object was and keeps only the head with it.
Checking a burst
Section titled “Checking a burst”Particles exposes { bursts, particles, waiting } to the tools under the page’s particles: the effects playing, bursts and emitters alike, the particles in the air, and the effects that wait for a sprite. spawnite play dump prints them, and spawnite play eval "views.page.particles" reads them.
What the other engines do
Section titled “What the other engines do”Roblox has ParticleEmitter, a component with props, placed in the world. Unity has the Particle System and VFX Graph, Unreal has Niagara, and Godot has GPUParticles3D: each is an effect made in an editor and played from code. Phaser has an emitter configured in code. PlayCanvas has a particle system component.
This engine takes the editor model, because a finished effect has curves and ramps that are slow to write by hand and quick to tune by eye. It keeps code-built effects for an agent, which writes a constructor call well and cannot drive an editor.
Sample
Section titled “Sample”Sparks where the player clicks the ground, in src/scenes/Sparks.tsx of a game that ran spawnite add asset bolt-burst:
import { Particles, useParticles, World } from "@spawnite/engine";import effectBoltBurst from "../particles/bolt-burst.json";
const effects = [effectBoltBurst];
function Ground() { const particles = useParticles(); return ( <mesh rotation={[-Math.PI / 2, 0, 0]} onClick={(event) => particles.spawn(effectBoltBurst, { at: event.point, color: "#ffd166", }) } > <planeGeometry args={[20, 20]} /> <meshStandardMaterial /> </mesh> );}
export function Sparks() { return ( <World map="meadow"> <Particles effects={effects} /> <Ground /> </World> );}A burst from one entity
Section titled “A burst from one entity”A burst that belongs to one entity, such as the sparks where a coin is taken, comes from a view under that entity’s <Entity> that passes { entity: useEntity() } to useEvent. Without it, the view hears every entity’s event, so every coin bursts whenever any coin is taken. The scene’s <Particles effects> holds the sparks the coin is given:
import { Entity, Pickup, TakenEvent, useEntity, useEvent, useFloatingText, useParticles, type ParticleEffect, type Position,} from "@spawnite/engine";
export interface CoinProps { position: Position; /** The burst where the coin is taken, one of the scene's * `<Particles effects>`. */ sparks: ParticleEffect;}
export function Coin({ position, sparks }: CoinProps) { return ( <Entity position={position}> <Pickup reward={1} /> <CoinTaken position={position} sparks={sparks} /> <mesh> <cylinderGeometry args={[0.4, 0.4, 0.1, 24]} /> <meshStandardMaterial color="gold" /> </mesh> </Entity> );}
// Under the coin's Entity, so `useEntity()` is this coin. Without// `{ entity }` the view hears every coin's TakenEvent, and each coin// would burst whenever any coin is taken.function CoinTaken({ position, sparks }: CoinProps) { const floatingText = useFloatingText(); const particles = useParticles(); useEvent( TakenEvent, () => { floatingText.show({ at: position, text: "+1", color: "gold" }); particles.spawn(sparks, { at: position, color: "#ffd166" }); }, { entity: useEntity() }, ); return null;}Where it runs
Section titled “Where it runs”Client. A burst changes nothing in the world; each player’s browser draws its own. Headless and on a room’s server, Particles and Emitter render nothing and spawn does nothing, so a hit’s damage belongs in a system and its sparks in a view that hears the hit with useEvent.