Showcase: Physarum network
A Physarum (slime-mold) transport network: tens of thousands of agents deposit a pheromone trail, sense their own trail ahead, and steer toward it. Positive feedback condenses diffuse pheromone into sharp branching veins; decay prunes the unused ones; the network continuously rewires itself. It is among the gentlest of the showcases in terms of engine footprint, so it’s the best place to learn the shared pattern.
What the engine provides here
Section titled “What the engine provides here”Narrowly: GameLoop + CoroutineManager (@flare-engine/core), SeededRNG + clamp/lerp
(@flare-engine/math), an optional spark ParticlePool (@flare-engine/particles), and the
bloom/aberration finish (@flare-engine/postfx). The entire simulation — the agents and the
pheromone field — is your own framework-free code. No ECS, no physics package. That’s the point:
the engine makes a heavy custom sim run deterministically and glow; it doesn’t run it.
Data-oriented agents
Section titled “Data-oriented agents”With tens of thousands of agents, per-entity objects would thrash the garbage collector. Store
them as a structure of arrays — parallel Float32Arrays — instead:
const px = new Float32Array(count); // x positionconst py = new Float32Array(count); // y positionconst heading = new Float32Array(count); // direction, radiansScatter them reproducibly with the engine’s SeededRNG so a given seed always yields the same
network — invaluable when you’re tuning:
import { SeededRNG } from "@flare-engine/math";
const rng = new SeededRNG(seed);for (let i = 0; i < count; i++) { px[i] = rng.float(0, width); py[i] = rng.float(0, height); heading[i] = rng.float(0, Math.PI * 2);}The pheromone field as a pure module
Section titled “The pheromone field as a pure module”physarum/field.ts is a TrailField — a coarse scalar grid with no engine or CanvasKit imports,
unit-tested with bun test. It exposes a handful of allocation-free operations:
senseAt(x, y)— bilinear sample of the field (what an agent “smells”).deposit(x, y, amount)— additive drop where an agent stands.step()— one diffuse + decay tick (below).toRGBA(...)— pack the whole grid into an RGBA byte buffer for the renderer.
The Jones rule
Section titled “The Jones rule”Each fixed step, every agent reads three sensors — front, front-left, front-right — turns toward the brightest, adds a little chaos, then steps forward (with toroidal wrap) and deposits:
function stepAgents(field: TrailField, dt: number): void { for (let i = 0; i < count; i++) { const a = heading[i]; const f = field.senseAt(px[i] + Math.cos(a) * SENSE, py[i] + Math.sin(a) * SENSE); const l = field.senseAt(px[i] + Math.cos(a - SENSE_ANGLE) * SENSE, /* ... */); const r = field.senseAt(px[i] + Math.cos(a + SENSE_ANGLE) * SENSE, /* ... */); heading[i] = decideTurn(a, f, l, r) + (rng.float(-1, 1) * chaos); px[i] = wrap(px[i] + Math.cos(heading[i]) * SPEED, width); py[i] = wrap(py[i] + Math.sin(heading[i]) * SPEED, height); field.deposit(px[i], py[i], DEPOSIT); }}decideTurn and senseAt are pure functions with their own tests — the rule’s correctness is
verified without ever touching a canvas.
Allocation-free diffuse + decay
Section titled “Allocation-free diffuse + decay”The network’s life is in the field’s step(): a separable 3×3 box blur (a horizontal pass
then a vertical pass into a ping-pong buffer — far cheaper than a 2D kernel) followed by a
multiplicative decay and a clamp. Reusing two preallocated buffers means zero allocation per
frame:
step(): void { boxBlurH(this.buf, this.tmp, this.w, this.h); // horizontal pass → tmp boxBlurV(this.tmp, this.buf, this.w, this.h); // vertical pass → buf for (let i = 0; i < this.buf.length; i++) { this.buf[i] = clamp(this.buf[i] * this.decay, 0, this.max); // prune + stabilize }}Decay is what makes it loop forever with no reset: unused veins fade, active ones are continuously re-fed, so the network is always rewiring.
Driving it on the loop
Section titled “Driving it on the loop”Run the agents and the field on the fixed step so behavior is frame-rate-independent; let the
CoroutineManager choreography ride the variable step:
loop.onFixedUpdate.add((dt) => { stepAgents(field, dt); field.step(); });loop.onUpdate.add((dt) => coro.update(dt));Living choreography
Section titled “Living choreography”A few nutrient sources walk a seed → branch → migrate → reknit cycle and emit into the
field, so the web grows veins toward food (the famous Tokyo-rail result). Each wait is gated on
the network’s live density, never a clock:
function* nutrients() { placeSeed(); yield* waitUntil(() => density() > seedThreshold); // wait for a core to form branch(); yield* waitUntil(() => density() > branchThreshold); migrate(); // move the food; veins chase it yield* waitUntil(() => reknit());}Rendering: the field is the image
Section titled “Rendering: the field is the image”The key rendering move: don’t draw cells. Pack the entire field into one RGBA buffer with a
palette-binned ramp, build one GPU texture with kit.MakeImage, and blit it upscaled with a
single drawImageRectOptions call. Nutrient nodes, a sparse agent shimmer, and ignition sparks
ride on top; then the shared surface composites bloom + aberration:
draw(sim: PhysarumSim): void { const sc = this.surface.sceneCanvas(); this.surface.fadeTrail(BG, 0.2); const bytes = sim.field.toRGBA(this.scratch); // grid → RGBA, palette ramp const img = this.kit.MakeImage(this.imageInfo, bytes, this.rowBytes); sc.drawImageRectOptions(img, srcRect, dstRectUpscaled, /* nearest */, paintPlus); img.delete(); drawNutrients(sc); drawAgentShimmer(sc); this.surface.composite(sim.glow()); // engine bloom + aberration → present}- Agents as parallel
Float32Arrays, scattered withSeededRNG. - A pure, bun-tested
TrailField: sense, deposit, separable blur + decay,toRGBA. - The Jones sense-rotate-move rule on
loop.onFixedUpdate. - Density-gated nutrient choreography on a
CoroutineManager. - Render the field as one
MakeImageblit; finish with the engine’s post-FX surface.
The engine carried the loop, the RNG, an optional spark pool, and the glow. Everything that makes it Physarum is the pure field + agent code — yours to keep and to unit-test.