Effects in 3D
Particles, trails, decals, words and sprites in 3D, and which for which moment.
Every action in a 3D game answers with an effect: a hit sparks, a coin bursts, a death explodes, a spell glows. The engine has them built
in. They come with Render3D(), need no files, follow the player's Graphics setting by themselves, and are one line each. The full
reference is "3D effects" in API.md.
The lines you need
ctx.effects3d.burst('explosion', enemy); // once, at an entity or [x, y, z]
ctx.effects3d.burst('sparks', hit.point, { direction: hit.normal });
ctx.effects3d.decal('scorch', hit.point, { normal: hit.normal, size: 2 });
ctx.feel.particles.burst('sparks', target); // the same call works in 2D
ctx.feel.floatText('-12', target, { color: '#ff4040' }); // damage numbers, drawn on top
world.spawn([Transform({ position: [0, 0, 0] }), Particles3D({ preset: 'fire' })]); // a looping emitter
world.spawn([Transform(), Trail3D.sword(), ...Mesh.box([0.1, 1.2, 0.05], blade)]); // a ribbon behind a node
node.effect('levelUp'); // in a script
Presets: fire, smoke, sparks, explosion, magic, dust, rain, snow, confetti, splash, heal, hit, muzzleFlash,
coinBurst, levelUp. Decal looks: scorch, footprint, splat, hole, shadow, crack, ring, skid. Trails:
Trail3D.sword(), .kart(), .projectile(), .magic().
Which effect for which moment
| Moment | Effect |
|---|---|
| A hit lands | hit at the target, feel.floatText the damage, feel.hitStop(60) for a big one |
| A bullet hits a wall | sparks with direction: normal, and a hole decal (size: 0.15) |
| Something blows up | explosion, a scorch decal under it, feel.shake(0.6) |
| A pickup | coinBurst (or heal for health) at the player |
| A jump lands | dust at the feet |
| A goal, a win, a level | confetti or levelUp |
| A gun fires | muzzleFlash on the gun's node (it points down the node's -z) |
| A torch, a campfire, a burning wreck | Particles3D({ preset: 'fire' }) on a node (smoke for embers that have gone out) |
| Weather | Particles3D({ preset: 'rain' }) on the camera or the player, so it falls around them |
| A sword swing, a kart's skids, a fast shot | Trail3D.sword(), Trail3D.kart() on a wheel, Trail3D.projectile() |
| Props on the low tier (no real shadows there) | a shadow decal under each (Decal3D, size their width) |
| A name or a price in the world | Label3D({ text: 'SHOP' }) |
Make one of your own
Tweak a preset before writing one: Particles3D({ preset: 'fire', color: '#53e0ff' }) is blue fire, scale: 2 is twice as big,
amount: 0.5 half as many, settings: { look: 'star' } changes every layer. For a new effect, define it once and use it by name:
// @run-fx3d
import { createGame, Transform } from '@onceworlds/engine';
import { Render3D, Particles3D, effectsOf } from '@onceworlds/engine/modules';
const game = createGame({ headless: true, modules: [Render3D()] });
game.start();
await game.ready;
const effects = effectsOf(game.world);
effects.define('shieldHit', {
layers: [
{ count: 1, speed: 0, life: 0.25, size: 1.2, sizeEnd: 2, color: '#53e0ff', look: 'ring', glow: 2 },
{ count: 18, shape: 'sphere', radius: 0.3, speed: [2, 4], life: [0.2, 0.4], size: 0.08, color: '#bff4ff', look: 'spark', glow: 2 },
],
});
effects.burst('shieldHit', [0, 1, 0]);
game.world.spawn([Transform(), Particles3D({ preset: 'fire', color: '#53e0ff', scale: 0.5 })]);
game.run(0.2);
expect(effects.count).toBeGreaterThan(10);
expect(game.errors).toEqual([]);
A layer needs life and size, and count (a burst) or rate (a second). The rest have good defaults: it starts at a point,
flies up at 1 m/s, glows (blend: 'add'), fades out over the second half of its life. blend: 'normal' for smoke, dust and
anything that should cover rather than glow; facing: 'velocity' for streaks; facing: 'flat' for rings on the ground. Mistakes
throw at define, saying what to write.
Rules of thumb
- Effects are cosmetic: fire them on every page from what each page sees (an event, a collision), never replicate them.
- One-shot effects as bursts (
effects3d.burst), lasting ones as components (Particles3Don the node they belong to). Don't spawn an entity per particle. - Glow wants bloom: a camera with
PostFx.polished()makesglowabove 1 shine. - Phones: the low tier keeps 800 particles in all and makes 0.4 of each burst, smoothly thinning past 70% of the cap. Design big
moments for the high tier and they still read on a phone; don't raise
amountto make up for the low tier. - Effects follow Reduce motion (half as many, no shake) and freeze in a hit-stop. Nothing to do.
- In Build: the 3D node kinds
Particles3D(pick a preset in the Inspector),Trail3D,Decal3D,Label3DandSprite3Dplay in the 3D view; View > Play Effects turns them off.