@anarkisti/korpi
v0.4.0
Published
A 2.5D pixel game engine: composable systems in a world that is a function of time and seed, drawn into one depth raster
Maintainers
Readme
korpi
A 2.5D pixel game engine. The world is 3D, in metres, and a function of time, seed and recorded input, so any moment can be shown, scrubbed forward or back, and tested; painters draw it through a view into one raster with a depth per pixel. Its systems (bodies, liquids, fire, air, weather, light, matter that breaks) meet through shared contracts, so a game composes effects from them rather than scripting each one.
yarn add @anarkisti/korpi0.x: the interface may change in any minor version until 1.0.0.
import { wallOf } from "@anarkisti/korpi/masonry";
import { wallPainter } from "@anarkisti/korpi/masonry/paint";
import { presenter, raster } from "@anarkisti/korpi/paint";
import { obliqueView } from "@anarkisti/korpi/view";
const wall = wallOf({ spec, seed: 1, at: { x: 0, y: 3.75, z: 0 } });
const paint = wallPainter(wall);
const view = obliqueView({ w: 320, h: 180, horizon: 150 });
const scene = raster(320, 180);
const screen = presenter();
const frame = (t: number) => {
paint.paint(scene, view, t);
screen.present(ctx, scene, 0, 0, 3);
};Subpaths
| | |
| --- | --- |
| core | hashes and seeded streams, easing, packed colours, world units, memo and scratch, Input/Cue/Inspect |
| clock | a world's calendar: seasons, values eased between them, the shuttle's rate |
| motion | the air every mover reads (gusts crossing from upwind, thermals, the lee of walls), springs, falls |
| sky | days with the sun and the moon, sky colours, weather spells, snow cover, snow and rain in a world volume |
| light | what lights the world: the open sky, cover, lamps and whatever glows, in metres; a lamp may throw shadows, light what faces it more, shine as a spot, through a mask or out of a housing |
| light/paint | a frame lit in one pass, every pixel at the world point drawn there, shadows looked for across its depths |
| sky/paint | the sky, sun and moon, falling snow and rain at their depths, snow lying on ledges and ground |
| paint | rasters with depth and glow, pens, brushes, raster ops, 5×7 text, present |
| view | the oblique front view, a camera (pan in metres, whole-number zoom) |
| shapes | solids through any view: triangles, quads, boxes, a floor, an upright slice of a cone (frustum) |
| plants | what grows, of any kind it is given (Species, ConkKind, ShrubKind, ClimberKind, FlowerKind, GrassKind: how it grows, what it is made of, how it looks): trees laid down from a seed over a whole life (pruned by recorded cuts: what grew past one goes, and the tree grows back from behind it) and swaying in the air, shrubs, climbers, grass and flowers, moss; a stand's trees living, dying and coming back in their slots, a meadow's flowers following the light |
| flora | content, not engine: the plants of a northern wood, its trees (TREES), conks (CONKS), shrubs, climbers, flowers and grass (SHRUBS, CLIMBERS, FLOWERS, GRASS), for a game to take or write its own beside |
| plants/paint | bark, leaves, needles, blossom, fruit, bracket fungi, the soft plants and moss, in a plant's own pixels; a stand and a meadow painted posed |
| sprites | dab sprites standing in the world at their depth, or painted through any pen (@anarkisti/dab, an optional peer, reads them) |
| model | a thing in three dimensions as voxels, a dab sprite in depth (its grid the front, its slices behind): drawn at any turn, each voxel at its own depth and made of what the sprite says (dab's materials); its volume (the air it closes in with it) and outline for physics; carved, what is left when some is gone |
| model/world | models as things in a physics world (thingsOf): dropped, each a body that sinks or floats by its weight and volume; settled in water, a thin one lies down on its side and rocks; in a fire (fuel, burnt), burning in chunks by what it is made of, charring and losing what burns through; struck or blasted, breaking by its material, glass first, lumps coming away as pieces, a kind's own look kept from the blows landed on it (marking); debris; planks, fixed boards a blow wears through, what nothing holds coming loose |
| posed | paintings that move: painted once at rest, part by part, and every part put back where its pose has it each frame |
| masonry | a wall laid from a seed that wears, is knocked and comes down, every fall and rest worked out once; wallOf places it in the world |
| masonry/paint | the wall's face, its stones and their dust, painted into the depth raster |
| actor | what moves itself: a body walking, jumping, falling and gliding under a parasail in the world's air, on a fixed tick; an actor that logs what it is told and reads any moment of its past back, cut to a branch or forked to try a plan |
| session | world time and the actor in it: as the clock runs, at a shuttle's rate, or only while the controller asks; control taken in the past starts a branch, the old future kept to redo; a preview of a plan changes nothing |
| physics | a world of rigid bodies (circles, capsules) stepped at a fixed rate, each at its depths, snapshots to go back to; a ground that may dip, and water on it or in a tub over it holding up what floats (Ground), drops lying together holding it up too (drops); a body in the water kept awake, a sleeping one woken as water rises round it, pushed by what is not a body |
| fluid | liquids as drops among a physics world's bodies: falling, running off, pooling, soaking in, into a Pool (a pond); pushing back on what moves them, and lying together in a hollow, water to float in (surfaceAt) |
| fire | what burns by its material (wood, leaves, grass, moss, rubber, vinyl, plastic; materialNamed for a sprite's names): heat, catching, flames climbing (what burns within half a metre one blaze, its flame as tall as all of it), water boiling off, puffs given off |
| smoke | smoke and steam as puffs, where each is a function of its moment, as black as the soot in it |
| water | water lying and running over the ground as a shallow-water field (fieldOf): finding its level, spilling over a brim, spreading and soaking in, only its wet cells stepped; the vessels that hold it, a pond's shore from a seed, its bed a bowl, or a tub (tubOf); its water (waterOf) one with the drops and rain falling in, its level from what it holds, spilling at the brim, rippling where things meet it |
| water/paint | the field's water, each pixel drawn once at its depth, still stretches flat, and a glass tub (paintField, paintGlass); a pond's water laid into the floor, ruffled by the wind, and what its mirror is given (pondGround) |
| fluid/paint, fire/paint, smoke/paint | drops, flames and the heat haze, smoke |
| gpu | a frame on WebGL2: the CPU's raster laid in, posed paintings drawn there, glass and passes over them |
| decals | marks laid flat on surfaces or on bodies (holes, slashes, scorches), kept within a box, fading out of a life; still ones baked once |
| window, window/paint | panes in a frame under a transom, rain running down them; a pane cracks where struck in a web from each blow and shatters past what it takes, teeth left in its frame, its shards falling |
| neon, neon/paint | neon signs on @glowbox/neon's pixel layouts: lit tube by tube, struck when switched on, mirrored for a window, a tube broken by a blow going dark |
| testing | checks for world systems: deterministic, causal, order-free, isolated, silent backwards; fingerprints |
| bench | the workbench: mountBench(node, { units }), plain DOM; stage.lap(name) times a draw's passes |
Content is not shipped: src/examples composes the engine on the bench (a forest, a TV shop, and
the city it looks out on: a skyline and a street from a seed, src/examples/city).
Frames
A frame is painted into one raster (a colour, a depth and a light a pixel) and presented once.
Where there is WebGL2 it can be the GPU's instead (gpu: glFrameOf(canvas)):
- The CPU paints what it paints into the raster, as before; the raster is laid into the GPU frame
at its depths, only the band of rows it painted anything in going up. A backdrop painted
ahead (a sky,
layerOf) is sentunderinstead, going up only when it steps. - Posed paintings are drawn by the GPU. A pen made with
{ gpu: frame.sink }sends them there (Pen.gpu, carried throughshiftedandstained): each painting's pixels go up once, its parts' moves each frame; cracks, seams and holes asposeddraws them. - Translucent pixels go to the raster's glass (
Raster.glass), laid over what the GPU drew by depth: pens,stampand the falling rain all put them there. - A painter that reads or reworks the finished frame takes the
GpuSinkand sends a pass instead (GpuPass: a fragment shader over a box): the pond's mirror, the heat haze. - A frame lit by
light/paint'sresolve(its shelter a function of a world point, worked out on the CPU) is drawn on the CPU: its posed paintings are not sent to the GPU, which only presents it. - Nothing is read back but a pixel's depth asked for between frames (
depthAt, where a tap lands): waiting on the GPU for a frame costs a frame.
A frame goes begin, layer(raster), paintings() (posed, their holes, the passes under the
glass), glass(glass) (and the passes over it), present(zoom) (or its depths). The CPU frame
stays the reference that tests and fingerprints draw; the GPU's is the same within float32
rounding, a few pixels a frame. On the bench, u switches between them, and a unit that lights
its frame says lit.
What made what faster, and what did not, is in PERF.md.
Costs
Whole frames on Apple M1: the CPU's part of a frame, mean and 95th percentile, ms, drawn
on the CPU and on the GPU (u on the bench); what the GPU then took to draw it (the frame
takes the longer of the two); and the costliest passes.
Chromium 149 (headless):
| scene | size | cpu | gpu | gpu draws | costliest passes | | --- | --- | --- | --- | --- | --- | | actor · a run and a glide | 320×180 | 0.4 (0.6) | 0.7 (1.1) | 1.0 (1.1) | draw 0.7 | | blast · one going off | 320×180 | 0.7 (1.0) | 0.8 (1.1) | 1.4 (1.7) | draw 0.8 | | camera · a laptop following | 480×300 | 0.6 (0.8) | 1.3 (1.4) | 1.3 (1.3) | draw 1.3 | | decals · marks everywhere | 320×180 | 0.2 (0.2) | 0.4 (0.4) | 0.8 (0.8) | draw 0.3 | | fire · a fire laid and lit | 320×180 | 0.6 (0.7) | 1.1 (1.3) | 2.2 (2.6) | draw 1.0 | | fire · a torch along the row | 320×180 | 1.4 (1.5) | 1.4 (1.5) | 1.9 (2.3) | draw 1.3 | | fluid · the tub runs over | 320×180 | 0.8 (1.0) | 1.3 (1.5) | 1.0 (1.0) | draw 1.3 | | forest · a big fire, large | 816×785 | 27.9 (32.6) | 19.9 (23.2) | 9.3 (10.1) | smoke 6.3 · world 3.5 · flames 3.2 | | forest · a big fire | 480×270 | 19.9 (22.9) | 15.5 (18.7) | 7.3 (7.6) | smoke 3.9 · world 3.1 · flames 2.6 | | forest · clear | 480×270 | 7.7 (8.7) | 3.7 (4.4) | 9.6 (11.3) | sway 1.2 · trees 0.6 · rustle 0.4 | | forest · hacked | 480×270 | 7.5 (8.5) | 5.9 (7.0) | 8.8 (9.5) | world 1.3 · sway 1.0 · pieces 0.8 | | light · a lamp and a lantern at night | 320×180 | 7.3 (7.6) | 7.3 (7.8) | 1.1 (1.4) | draw 7.3 | | meadow · a canopy closing | 320×180 | 0.9 (1.6) | 1.3 (1.9) | 2.4 (3.5) | draw 1.2 | | meadow · deep shade | 320×180 | 0.6 (0.8) | 1.0 (1.3) | 1.5 (1.7) | draw 1.0 | | neon · signs switched on | 320×180 | 0.5 (0.5) | 0.9 (1.0) | 1.0 (1.0) | draw 0.9 | | physics · a heap settling | 320×180 | 0.4 (0.4) | 0.7 (0.7) | 0.9 (1.0) | draw 0.7 | | physics · dropped into the tank | 320×180 | 0.4 (0.5) | 0.8 (0.8) | 1.0 (1.0) | draw 0.7 | | planks · hacked through | 320×180 | 0.7 (0.8) | 0.8 (0.9) | 0.8 (1.0) | draw 0.8 | | pond · rain in a tub | 320×180 | 1.7 (1.9) | 2.4 (2.5) | 1.3 (1.4) | draw 2.3 | | pond · rain | 320×180 | 2.2 (2.4) | 2.2 (2.5) | 1.9 (2.7) | draw 2.1 | | sky · dusk | 320×180 | 0.4 (0.5) | 0.8 (0.9) | 1.6 (1.8) | draw 0.7 | | skyline · a city in the rain | 320×180 | 2.3 (2.5) | 3.0 (3.2) | 1.4 (1.6) | draw 2.9 | | smoke · a hot fire | 320×180 | 1.4 (1.6) | 1.4 (1.5) | 1.3 (1.5) | draw 1.3 | | stand · six trees, large | 1000×800 | 6.3 (8.1) | 5.9 (6.9) | 4.3 (4.5) | draw 5.2 | | stand · six trees | 320×180 | 5.8 (7.5) | 5.2 (6.5) | 2.7 (3.1) | draw 5.1 | | storm · a storm, large | 1000×800 | 6.4 (14.9) | 6.4 (15.8) | 3.4 (3.5) | draw 6.3 | | street · under fire | 320×180 | 9.6 (10.1) | 3.2 (3.4) | 3.7 (4.0) | draw 3.2 | | things · a tyre and a duck lit | 320×180 | 2.3 (2.6) | 1.7 (1.9) | 1.9 (2.2) | draw 1.6 | | things · struck and blasted | 320×180 | 0.5 (1.1) | 0.7 (1.4) | 0.8 (0.9) | draw 0.7 | | trees · an old oak | 320×180 | 0.3 (0.3) | 0.6 (0.8) | 1.9 (2.0) | draw 0.5 | | tv shop · a burst and a slash | 320×180 | 6.2 (7.2) | 3.1 (4.4) | 3.2 (4.5) | street 1.2 · tvs 1.0 · signs 0.4 | | tv shop · hacked apart | 320×180 | 2.8 (4.0) | 3.3 (4.6) | 1.2 (1.3) | tvs 1.3 · street 1.0 · planks 0.5 | | tv shop · nine TVs | 320×180 | 5.9 (7.1) | 2.7 (4.2) | 3.1 (3.3) | street 1.0 · tvs 0.8 · signs 0.4 | | tv shop · out across the road | 320×180 | 6.3 (7.5) | 3.0 (4.6) | 3.2 (3.3) | street 1.2 · tvs 0.8 · planks 0.4 | | wall · an hour on | 320×180 | 0.5 (0.5) | 0.9 (0.9) | 0.9 (1.0) | draw 0.8 | | water · filling over | 320×180 | 1.7 (1.9) | 2.2 (2.4) | 1.2 (1.3) | draw 2.2 | | weather · rain | 320×180 | 0.7 (0.8) | 0.9 (1.1) | 1.4 (1.5) | draw 0.8 | | window · a pane cracked, one shattered | 320×180 | 0.7 (0.8) | 1.2 (1.2) | 1.1 (1.0) | draw 1.1 |
Safari 27.0.1:
| scene | size | cpu | gpu | gpu draws | costliest passes | | --- | --- | --- | --- | --- | --- | | forest · a big fire, large | 816×785 | 31.9 (37.5) | 18.9 (23.8) | 8.3 (9.4) | smoke 5.7 · world 4.0 · flames 3.0 | | forest · a big fire | 480×270 | 23.7 (28.3) | 14.7 (19.3) | 6.7 (7.8) | world 3.9 · smoke 3.5 · flames 2.6 | | forest · clear | 480×270 | 7.0 (7.4) | 3.3 (3.8) | 8.9 (10.2) | trees 0.8 · sway 0.8 · rustle 0.4 | | forest · hacked | 480×270 | 12.3 (13.1) | 4.7 (5.4) | 8.5 (10.1) | world 1.0 · sway 0.8 · pieces 0.7 | | pond · rain | 320×180 | 3.8 (4.0) | 1.4 (1.6) | 1.3 (1.9) | draw 1.4 | | smoke · a hot fire | 320×180 | 1.3 (1.4) | 1.7 (1.8) | 0.9 (1.2) | draw 1.7 | | stand · six trees, large | 1000×800 | 6.0 (6.8) | 5.5 (6.4) | 4.1 (4.9) | draw 5.2 | | stand · six trees | 320×180 | 6.0 (7.2) | 5.3 (6.1) | 2.3 (3.3) | draw 5.2 | | trees · an old oak | 320×180 | 0.3 (0.3) | 0.8 (1.2) | 2.1 (3.1) | draw 0.7 | | wall · an hour on | 320×180 | 0.3 (0.4) | 0.6 (0.6) | 0.7 (1.2) | draw 0.5 |
yarn bench:frame plays the scenes the units name (Unit.scenes) and times their frames;
yarn bench:frame:heavy adds the heavy ones (a big fire, a hacked forest, large frames), and
-t picks scenes by name. yarn bench:frame:safari runs them in Safari itself, through
safaridriver: it opens a window of its own, and wants Safari's Settings ▸ Developer ▸ Allow
remote automation on, once.
The pieces alone, on an Apple M4 Pro, Node 26, at 320×180:
| | mean | p99 | | --- | --- | --- | | bake the reference wall (219 blocks) | 25 ms | 38 ms | | paint a frame of its blast | 0.53 ms | 1.3 ms | | paint it still | 0.37 ms | 0.47 ms | | light a frame (open and covered, 7 lamps) | 0.51 ms | 0.57 ms | | a frame of sky and snow (120 flakes) | 0.15 ms | 0.20 ms | | pose a sapling (12 pieces), and going over | 19 µs, 26 µs | 27 µs, 38 µs | | a frame of a six-tree stand, posed | 1.6 ms | 2.6 ms | | lay down a birch's life; read a tree at an age | 0.11 ms; 0.10 ms | | | a frame of a 21-spot meadow | 0.04 ms | | | a walking deer, drawn from cache | 5 µs | 6 µs | | the wind at a point | 0.1 µs | 0.2 µs | | a spring's lean, its history and all | 1.7 µs | 2.1 µs |
yarn bench measures them; yarn bench:compare <ref> against another commit.
MIT
