const renderLayers = {
foreground: 1 << 0,
};
// Height, in world units, up from the bottom edge of the camera's fixed
// vertical world units that the ember fountain sits at, so it stays in view
// regardless of resolution or aspect ratio.
const fountainHeightFromBottom = DEMO_VERTICAL_WORLD_UNITS * 0.12;
export const createParticlesGame = async (): Promise<Game> => {
const { game, world, renderContext, time } = createGame('demo-game');
createCamera(world, {
isStatic: true,
cullingMask: renderLayers.foreground,
verticalWorldUnits: DEMO_VERTICAL_WORLD_UNITS,
});
const random = new Random();
const cursorEffects = await createCursorEffects(
world,
renderContext,
renderLayers.foreground,
);
const fountainPosition = new Vector2(
0,
-DEMO_VERTICAL_WORLD_UNITS / 2 + fountainHeightFromBottom,
);
await createEmberFountain(
world,
renderContext,
renderLayers.foreground,
fountainPosition,
);
world.addSystem(createAmbientEmitterEcsSystem());
world.addSystem(createParticleEcsSystem(time, random));
world.addSystem(createParticlePositionEcsSystem(time));
world.addSystem(createLifetimeTrackingEcsSystem(time));
world.addSystem(createAgeScaleEcsSystem());
world.addSystem(createRemoveFromWorldEcsSystem());
// Particles only update their local transform, so the transform system
// needs to run before the camera/render systems to resolve it to the world
// transform the renderer reads.
world.addSystem(createTransformEcsSystem());
world.addSystem(createCameraEcsSystem(time));
world.addSystem(createRenderEcsSystem(renderContext));
// The camera is static at the world origin with a zoom of 1 (see
// `createCamera` above), so screen coordinates can be converted to world
// coordinates directly, once scaled by the camera's pixels-per-unit.
const toWorldPosition = (event: MouseEvent): Vector2 => {
const canvasBounds = renderContext.canvas.getBoundingClientRect();
const screenPosition = new Vector2(
event.clientX - canvasBounds.left,
event.clientY - canvasBounds.top,
);
const pixelsPerUnit = calculatePixelsPerUnit(
renderContext.height,
DEMO_VERTICAL_WORLD_UNITS,
);
return screenToWorldSpace(
screenPosition,
Vector2.zero,
1,
renderContext.width,
renderContext.height,
pixelsPerUnit,
);
};
let isDragging = false;
renderContext.canvas.addEventListener('mousedown', (event: MouseEvent) => {
isDragging = true;
cursorEffects.setCursorPosition(toWorldPosition(event));
cursorEffects.triggerSparkBurst();
});
renderContext.canvas.addEventListener('mousemove', (event: MouseEvent) => {
if (!isDragging) {
return;
}
cursorEffects.setCursorPosition(toWorldPosition(event));
cursorEffects.continueSmokeTrail();
});
const stopDragging = (): void => {
isDragging = false;
};
renderContext.canvas.addEventListener('mouseup', stopDragging);
renderContext.canvas.addEventListener('mouseleave', stopDragging);
return game;
};
This demo showcases the particle system using Kenney's particle pack. A fountain of embers at the bottom continuously emits and drifts upward on its own, click anywhere to burst a shower of sparks, and hold and drag the mouse to paint a trail of smoke. The fountain and the spark burst both use a one-off burst (emitDurationSeconds of 0), while the smoke trail spreads its particles out continuously for as long as the mouse is held, and grows instead of shrinking as it fades. The spark and smoke emitters live on the same entity as two named emitters, the same pattern used for running several independent effects, like an attack and a footstep, off one entity.