Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

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.

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 adds three.quarks to its own dependencies, at the version @spawnite/engine names as its peer, so a published page runs the engine’s own copy and a burst’s color and count reach the effect.

  • An effect from the shared assets. spawnite add asset <id>, or the MCP’s add_asset, copies it into the game as src/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’s list_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.

An effect you made goes into the shared assets, so any game adds it with spawnite add asset:

  1. Put the effect’s .json in a folder, with each sprite it names by a file name, such as spark.png, beside it.
  2. Write its row in the folder’s catalog.json, with the category effects, as Publish an asset shows.
  3. Run spawnite assets publish <file>, or the MCP’s publish_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.

A burst can name three values, and the effect keeps its own for the next burst:

  • color is the colour every particle starts with.
  • count is how many particles each of the effect’s bursts throws.
  • yaw turns 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.

<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.

<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.

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.

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.

Sparks where the player clicks the ground, in src/scenes/Sparks.tsx of a game that ran spawnite add asset bolt-burst:

src/scenes/Sparks.tsx
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 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:

packages/engine/test/outside/Coin.tsx
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;
}

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.