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

World

<World map> mounts a map by name: its ground, the scatter standing on it, the navmesh baked from both and from the Entities’ fixed colliders and resting kinematic ones, and the daylight. All of it lives as long as the World is mounted. The <CharacterSpawn> and the Camera go inside it, as does every entity of the scene. A World component is a map inside the scene’s world, the one simulation the scene’s entities and systems run in: a scene has one simulation, and may mount two World components in it, as Two Worlds says.

The smallest scene a player walks in is the engine’s grid map, 8 m of plain floor, her character and a camera. The engine registers two maps, grid and meadow; a game registers its own with registerMap, as Maps says, and a name nobody registered throws as the World mounts:

packages/engine/test/outside/GridWorld.tsx
import { Camera, CharacterSpawn, World } from "@spawnite/engine";
/** The smallest scene a player walks in: the engine's plain grid, her
* character at its middle, and a camera behind her. */
export function GridWorld() {
return (
<World map="grid">
<CharacterSpawn />
<Camera />
</World>
);
}

Every world has one physics scene, which loads the physics, Rapier’s WebAssembly, at the first body the world declares: a World’s ground, an entity’s collider, or a body the room streams. A game that declares none, such as one that shows a model under Lighting or a card game, never downloads it, alone or in a room, and its room’s process never loads it either. The navmesh loads recast for the first World. A Game’s loading screen starts recast as it mounts, beside the scene’s files. While the physics loads, the World and its children wait inside a boundary of their own, so the rest of the scene mounts at once, and a game played alone runs no step until the load lands, so its first step meets the ground and every body however long the download took. A page in a room steps on at the room’s pace, and its scene holds each body from the first step after the load. A World mounted after that, a second one or the next scene’s, spawns its ground at once. Code that reads the physics before any body, such as a test, awaits loadRapier() first, and loadRecast() before it bakes a navmesh.

Its props are in WorldProps.

look picks a look: the sky the models light from, fog, exposure, grading and a post-processing stack, set together.

World lights its map with the engine’s Lighting, and you can replace any of its parts while keeping the rest:

  • sky is the sky behind the scene. Left out, the engine draws its scattering sky with clouds, which follows the hour. { file, sunU } draws an equirectangular image in its place, turned so its sun stands on the engine’s. false draws none, for a cave, a space station or a sky the game draws itself.
  • sun={false} leaves out the sun, and the moon after dark: the one directional light, which casts the shadow. The sky’s fill light stays.
  • fog takes a three.js Fog, linear between two distances, or FogExp2, which thickens with distance, in place of the look’s. false sets none. Make the fog once, with useMemo or outside the component.
  • shadowBox sizes the sun’s shadow box in metres, { halfWidth, depth }, 14 by 40 by default, and focus is the point it stands over, the player character by default. A top-down or strategy camera that sees farther than her surroundings widens the box and names the point the camera looks at.
  • lighting={false} mounts none of it: no sun, sky, fill, exposure, fog or look stack. The game lights the scene itself, with its own <Lighting> or its own lights. A look then sets only the hour the World opens on. TypeScript refuses any of the props above beside lighting={false}.

A scene under the sea keeps the sun for the light that reaches down, draws no sky, and fills the distance with blue. The engine’s tests mount this file:

packages/engine/test/outside/UnderwaterWorld.tsx
import { useMemo } from "react";
import { FogExp2 } from "three";
import { World } from "@spawnite/engine";
const water = "#1a4a6a";
// Under the sea: no sky, the sun kept for the light that reaches down,
// and a blue fog that thickens with distance into a blue backdrop.
export function UnderwaterWorld() {
const fog = useMemo(() => new FogExp2(water, 0.06), []);
return (
<World map="meadow" sky={false} fog={fog}>
<color attach="background" args={[water]} />
</World>
);
}

Unity keeps the skybox material, the sun source and the fog, linear or exponential, as separate entries of its Lighting settings. Unreal places the sky atmosphere, the sky light, the directional light and the height fog as separate actors that a level keeps or deletes. Godot’s WorldEnvironment holds the background, sky or plain colour, and the fog, beside a DirectionalLight3D node. Roblox’s Lighting service takes a Sky and an Atmosphere and its own fog values. World takes the same three parts as three props, each replaceable on its own, and keeps the engine’s for whatever a game leaves out.

The scatter is what stands on the ground: trees, rocks, bushes and grass, placed from the map’s seed. A kind that blocks a walker, a tree or a rock, always stands in full: on every device, and in a room on the server and every client, so all of them stop her at the same trunk. scatterShare thins only what decorates, a bush or a tuft of grass, which she walks through. When a scene passes no scatterShare, the quality level in force sets the share, and a game changes each level’s share with Game’s quality prop; a scene that passes its own keeps it at every level.

Bushes and tufts shrink away into their feet past a distance from the eye that the quality level sets, 60 m at High, and a pass draws none past it; trees and rocks draw at every distance, since they are the map’s shape. So the map camera’s far zoom draws the ground, the trees and the rocks.

Each kind draws as one instanced draw per pass, of only the copies that pass’s camera holds: the screen draws what the view sees, and the shadow map what the sun’s shadow box holds. The copies stand still, so a tree over them, built once, finds each pass’s set without testing every copy. Each pass keeps its set, and finds it again only on a frame when its camera moved, turned or changed shape, or the scatter changed, so a still frame sends the GPU no new list.

Three props change what the World draws its map with, each on its own:

  • ground is the ground’s material.
  • sea is the water’s.
  • scatter is the scatter’s, each kind’s in turn. A bush or a tuft hands over the engine’s copy that shrinks it with distance, and a tree or a rock a copy of its file’s material, never the loaded file’s own.

Each takes a function handed the engine’s material, which returns either that material patched, such as with an onBeforeCompile of the game’s, or a material of the game’s in its place. A patched material keeps what the engine’s does: the ground’s paint of each cell, its detail and the quality level’s changes to it, and a bush’s shrink with distance. A material of the game’s replaces all of it: the engine goes on updating its own material out of sight, and the mesh goes on drawing the game’s. Either way, the ground still holds the walkers up and answers a ray, such as a click to walk. The engine’s program key carries the hook’s text, so a patched ground and a plain one never share a program.

The ground’s material is a three.js MeshStandardMaterial, so a patch splices the standard material’s chunks, such as #include <emissivemap_fragment>. Its mesh lies turned a quarter turn about x, so a shader reads world positions, modelMatrix * vec4(transformed, 1.0), rather than the mesh’s own position. The sea’s material is a ShaderMaterial of the engine’s own.

The function runs once each time a World mounts, and never again while it stands: a quality change patches the engine’s material in place. Define it once, outside the component. A material the engine handed over and the function returned patched is still the engine’s, which it disposes of with the World. A material of the game’s is the game’s to dispose: one made at the module’s top lives for the page, and one the game makes for each World is disposed in the cleanup of the component that mounts that World.

false draws none of it, for a game that draws its own. Each of these turns off the drawing only: what the simulation reads stays.

  • ground={false} draws no ground. The engine’s ground still holds the walkers up, answers a ray such as a click to walk, and bakes the navmesh, unseen, so the game’s own ground draws over the same shape.
  • sea={false} draws no water. The sea’s height still rules the ground’s materials, the scatter and the navmesh.
  • scatter={false} draws none of the map’s scatter, nor its props of a scatter kind. A tree or a rock still blocks a walker where it stands, and the ground’s ScatterTrait still lists every placement, for a game that draws them itself.
  • grass={false} grows no grass, whatever the materials’ covers say. Otherwise grass takes the blades’ shape, as Grass says.

The meadow under snow keeps the engine’s shaders and lays white over their colour before the light, its lake is frozen, and the snow buries its grass. The engine’s tests mount this file:

packages/engine/test/outside/Snowfield.tsx
import { World, type ModelMaterial } from "@spawnite/engine";
// The meadow under snow: the ground and the scatter keep the engine's own
// shaders, with white laid over their colour before the light, and the
// lake is frozen, so the World draws no water. The snow buries the
// grass, so none grows.
/** Patches the material the engine hands over and returns it, so the
* ground keeps its paint and the quality level's changes, and a bush
* keeps shrinking with distance. Made once, outside the component. */
const snow: ModelMaterial = (engine) => {
const own = engine.onBeforeCompile.bind(engine);
engine.onBeforeCompile = (shader, renderer) => {
own(shader, renderer);
shader.fragmentShader = shader.fragmentShader.replace(
"#include <emissivemap_fragment>",
"diffuseColor.rgb = mix(diffuseColor.rgb, vec3(0.92, 0.95, 1.0), 0.7);\n#include <emissivemap_fragment>",
);
};
return engine;
};
export function Snowfield() {
return (
<World
map="meadow"
ground={snow}
scatter={snow}
sea={false}
grass={false}
/>
);
}

Unity’s terrain takes a material of the game’s, and its trees and details their prefabs’ materials. Unreal’s landscape takes a landscape material, and its foliage types their meshes’ materials. Godot’s terrain add-ons and its MultiMeshInstance3D each take a material override, and Roblox’s terrain takes a MaterialVariant for each of its materials. Each of them hands the game the whole material. World hands over the engine’s own as well, so a game can patch it and keep the engine’s paint, or replace it.

The ground grows grass blades on its own wherever its material grows them, as Unreal’s Landscape Grass Type grows from a layer and Roblox’s Grass terrain grows its decoration. The built-in grass grows green blades and dry-grass straw-coloured ones; any material grows them with a cover in its entry, in the game’s src/maps/materials.json or the map’s own materials, and a density of 0 grows none:

{
"materials": {
"grass": { "cover": { "density": 1.5, "height": 0.45 } },
"moss": {
"from": "dirt",
"cover": { "height": 0.12, "colour": "#3f5a2a" }
}
}
}
  • density, from 0 to 2, is how thick it grows; the scatter brush’s grass multiplier scales it per cell, up to 2, so the brush that thins tufts thins the blades too.
  • height is the tallest blade in metres, 0.32 when left out.
  • colour is the blades’ colour; the material’s swatch, tinted as the material is, when left out.
  • sway: false holds the blades still; they sway when left out.

The blades shrink where a cell’s grass gives way to another material, and grow from the cells a stroke paints, popping in as the scatter does. They draw in square patches of 6 m, only the patches within reach of the eye, each thinning with its distance, as the quality level sets: at Low a sparse field out to 12 m, where the ground’s texture carries the grass beyond it, and none at Minimum. The blades within 0.45 m of a character bend aside and stand shorter, and stand again once a jump lifts the character off them. They bend in the wind at Medium and High, and stand still at Low, under Reduced motion and on a material whose cover says sway: false; the player’s Grass sway setting wins over the level and Reduced motion. Still, the shader skips the wind’s work. The wind blows on the effects’ clock: the devtools’ pause and a pausing modal hold it, and the devtools’ speed leaves it at the wall clock’s pace. A pointer’s ray passes through them to the ground.

A game that paints its own ground over the engine’s shapes the blades with grass, GLSL the blade’s vertex shader runs after the engine’s own, as three’s onBeforeCompile splices a chunk. Holdfast gives a cover to every material its map resolves to, dirt and stone included, so the engine grows blades everywhere, and its shape trims them by the layout its paint draws: they give way to its roads, its cobbled ring and its worn earth, and take its paint’s colour. shape reads bladeRoot, bladeSeed, groundUp and coverShare, and changes bladeHeight and bladeColour; functions holds what shape calls, and uniforms its numbers, colours and textures, each declared for it.

import { World } from "@spawnite/engine";
export function Fort() {
return (
<World
map="holdfast"
grass={{
shape: "bladeHeight *= 1.0 - smoothstep(30.0, 38.0, length(bladeRoot.xz));",
}}
/>
);
}

A map is a value the map schema in @spawnite/schema accepts: a seed, a size and a cell, its hills, regions, paths and rim. registerMap(name, values) parses it and keeps it under the name; it throws on a value the schema refuses, at registration rather than at mount. readMap(name) throws on a name nobody registered, which a <World map> with that name does as well, into the game’s boundary. The engine registers two maps, each a square centred on the origin:

  • grid, the map a <World> with no map mounts: a flat floor 8 m across, from −4 to 4 m on x and z, of the grid material, one grey with a darker line every metre. It has no hills, no scatter, no grass and no water, and it loads no texture and no model, so a scene that shows a character or an effect opens fast. Roblox’s default place is the same flat Baseplate.
  • meadow, the nice map: 100 m across, from −50 to 50 m, with hills, a level clearing in the middle, a textured ground, the scatter and grass. A scene names it with <World map="meadow">.

Either is a map like a game’s own: gridMap and terrainMap in @spawnite/schema hold their values, and a game’s map can copy one.

A game keeps its maps as JSON files under src/maps/, one per map, and registers them all from its app file with registerMaps(import.meta.glob("../maps/*.json", { eager: true })), which names each after its file: src/maps/blockout.json is blockout. A file named meadow.json takes the place of the engine’s meadow. add_map writes a new file, and a tool rewrites one safely, where it could not rewrite a TypeScript module. spawnite map check, spawnite map describe and spawnite map query read src/maps/<map>.json when the game has it, and the game’s only map file when no --map is given; a game with several map files names one.

A map’s edits from the map editor live in its own file, under a terrain key: a grid of height offsets over the map’s base, a dot where no brush touched, written by the devtools’ Terrain section, spawnite map terrain and the MCP’s edit_terrain through the one writer every map file goes through, which changes only its own key. registerMaps loads the block with its map, so a game’s code does not change; the first edit of the engine’s meadow writes the map into the game as src/maps/meadow.json. The ground bakes the base, then adds each edited point’s offset, and each scatter family’s multiplier per cell, from 0 to 2, scales how thick it grows there; an instance the editor erased, named by its spot, stands nowhere. New terrain, from a stroke’s end or an agent’s command, stands the ground on a new surface without reloading the page, and the physics, the navmesh, the scatter and the props follow it. A block written for another size or cell is refused at load, naming spawnite map terrain resample.

The rim is a bank an actor climbs, and the map’s edge behind it is a wall: nothing walks or jumps off the map, so a game needs no check for a fall. Each wall stands the map’s size above its highest ground, 100 m on meadow and 8 m on grid; a jumpHeight above that would clear it.

A map’s sea is one height in metres below which the ground is under water, the way Roblox’s Sea Level fills water up to a height and Unreal’s ocean is a plane at one. Every map has one: left out, it stands 1 m below the lowest point of the map’s base, the heights before any terrain offset, so the map is dry until its ground is dug below that, and the World draws no water while all the ground stands above it. The World draws the water across the whole map at that height, with the water material’s waves, lighter in the shallows and broken into foam where the ground rises out of it. The waves and the foam move on the effects’ clock, as the grass’s wind does: the devtools’ pause and a pausing modal hold them, and the devtools’ speed leaves them at the wall clock’s pace. The scatter thins toward the shore over its last 4 m and stands nothing under water, and the ground’s water rules, deeperThan and shallowerThan under the sea and shoreWithin beside it, match by the depth and the distance to the water. The navmesh leaves out every triangle of the ground that dips more than wadeDepth, half a metre, under the sea, so a walker that follows it, a chaser or a character sent walking, wades into the shallows and no further; on a coarse grid it stops up to a cell short of that depth. The water has no collider and no swimming: a character the player steers walks on its floor. The ground’s surface answers sea, the height it stands at, isUnderWaterAt and getShoreDistanceAt, metres to the nearest water within a cell of the coast. The map editor’s Sea field sets it, spawnite map terrain sea --level 1 and the MCP’s edit_terrain write it, spawnite map check gives its height and names a view, a path or a prop under water, and spawnite map describe gives its height, the share of the map under water and the shoreline’s length.

A map’s views are named cameras: an eye, a target, and a vertical field of view, or an ortho span in metres for an orthographic view. A view is a bookmark an agent writes once, at the angle its concept art was drawn from, and shoots again after every edit so the shots compare. frameView(map, name) returns the camera for a name: a views entry as written, a region, a path or a prop framed from 45° above at two and a half radii, or map, which every map answers with the whole ground from straight above. It throws on a name the map has not got, listing the ones it has.

An agent matching the map to concept art works in a loop: it writes a view at the art’s angle, edits named entries, checks the map’s facts with spawnite map check, shoots the top-down map view and the concept view, and asks a second agent to score both against the art until they pass or ten rounds are spent.

A map’s props are named objects that stand at a written spot, as a level file in Unity, Godot or Unreal keeps its placed instances. Each prop is one of three kinds:

  • A shape: kind is box, sphere, cylinder or plane, with a size in metres across, tall and deep, 1 m each by default, and a color.
  • A scatter kind: kind is one of the scatter’s kinds, such as treeRound or rockBoulder. It stands with the scatter whatever the scatterShare, and one that blocks a walker blocks it as the scatter’s own do. It stands at the middle of its kind’s heights.
  • A model: model is a registered model’s name, or a path the game serves, such as assets/statue.glb, which needs no registration. The engine reads a registered name as that name, and any other value with a / as a path. spawnite map check cannot see what a game registers, so it reads any value with a / as a path and reports a path with no file behind it in the game’s public/ folder. The path reads from the page’s own folder, a leading / included, because the games site serves each game under its own folder, such as /arena/.

Every prop takes a position: [x, z] stands it on the ground there, and [x, y, z] holds it at that height. yaw turns it, in degrees, and scale multiplies its size, as one number or one per axis; a scatter kind takes one number, because the scatter scales a kind evenly. A model prop draws its file unturned, so at a yaw of 0 its front faces +z, where an <Entity model> turns it to face -z. A shape or a model is an entity named for its prop in the devtools tree, which draws with the World’s other views and not headless.

"props": {
"tower": { "kind": "box", "position": [0, 0], "size": [4, 12, 4], "color": "#8a8f99" },
"oak": { "kind": "treeRound", "position": [8, -6], "yaw": 30 },
"statue": { "model": "assets/statue.glb", "position": [-6, 4], "scale": 1.5 }
}

Gravity belongs to the world’s physics scene, not to a World: every actor and every dynamic body falls at it, 9.8 m/s² down by default. A game sets it once with definePhysics({ gravity: [0, -20, 0] }), exported from src/game.ts as physics and passed to <Game physics>, as Plugins says, and a system changes it in play with setGravity(step, gravity), which the room streams to every page. A character falls along its y component. The devtools’ gravity card sets the same value in a game played alone.

Each World keeps a day clock, which the sun, the moon and the sky follow. Two props set it:

  • startHour is the hour the World opens on, from 0 up to 24, with the minutes as a fraction: 18.5 is half past six in the evening. An hour past either end wraps into the day, so 25 opens at 1. When left out, the look’s hour is used, or 10 with no look.
  • dayLengthSeconds is the real seconds a whole day takes, 1200 by default.

The clock runs in the step, as the first system of the rules phase, core.advanceDayClock, so a game’s rules read the hour of the step they run in. A room, a headless run and spawnite simulate all move it; the devtools’ pause holds it and their speed scales it. In a room the clock runs on the room alone, and each page draws the room’s hour, which the stream brings.

Two functions read and set the hour:

  • findTimeOfDay(world) returns the hour, from 0 up to but not 24, and a view reads it too. It returns undefined where it finds no clock: in a scene with no World, such as a menu, on a page in a room before the room’s clock has streamed in, and for a ground with no clock.
  • setTimeOfDay(authority, hour) puts the clock at an hour, wrapped into the day, and the clock keeps running from there. A jump in the hour is an outcome, so it takes the step’s authority, as dealDamage does. It throws in a world with no day clock, naming <World> as the fix, and on an hour that is not finite.

Where two Worlds each keep a clock, both take { ground }, the ground entity of the World whose clock to use, and throw without it.

The hours of the night are the game’s own. This plugin spawns a prowler at each lair as night falls and destroys them at dawn, both through the step, and leaves every other entity alone. A plugin runs in every scene of the game, so the lairs are what keep the night to one scene: each is an entity the scene places, <Entity prefab={lair} position={[10, 0, 0]} />, and a scene that places none, such as the lobby, never sees a prowler: the plugin removes every prowler once no lair stands. Each lair keeps whether the last step was night there, so its prowler comes once a night however many fall, and at once in a scene that opens at night. The engine’s tests run it against a real room and alone:

packages/room/test/outside/night.ts
import { createQuery, Not, type World } from "koota";
import {
ChaseTrait,
definePlugin,
definePrefab,
defineTrait,
destroy,
findEntity,
HealthTrait,
findTimeOfDay,
spawn,
TargetableTrait,
TransformTrait,
updateEach,
type AuthoritativeStep,
} from "@spawnite/engine/core";
// Prowlers that hunt at night, built as a creator's plugin is, from the
// engine's public entry alone: as night falls the room spawns one at each
// lair, each chasing the nearest player's character, and as day breaks it
// removes the ones still standing and no other entity. Their traits
// stream, so every page draws them, and a game played alone runs the
// same. The lairs are entities a scene places, so a scene with none, a
// lobby or a menu, never sees a prowler, and a prowler left behind goes.
/** Marks a prowler, so the dawn removes this plugin's own; the stream
* carries it. */
export const ProwlerTrait = defineTrait("outsideProwler");
/** The hour night falls, and the hour day breaks. */
export const duskHour = 19;
export const dawnHour = 6;
/** Marks a lair, where a prowler comes from at dusk. */
export const LairTrait = defineTrait("outsideLair");
/** One lair: a scene places it with `<Entity prefab={lair} position>`. */
export const lair = definePrefab("outsideLair", { traits: [LairTrait] });
/** On each lair: whether the last step was night there, so its prowler
* comes once a night however many fall, and at once in a scene that
* opens at night. Named, so a checkpoint restores it; the room alone
* keeps it. */
const LairNightTrait = defineTrait(
"outsideLairNight",
{ isNight: false },
{ serverOnly: true },
);
const prowlers = createQuery(ProwlerTrait);
const lairs = createQuery(LairTrait);
const newLairs = createQuery(LairTrait, Not(LairNightTrait));
const watchedLairs = createQuery(TransformTrait, LairNightTrait);
/** One prowler: a model that chases the nearest player's character and
* can be targeted. `visor-bot` is the example template's own model; a
* game draws its prowlers with any model it registers. */
export const prowler = definePrefab("outsideProwler", {
traits: [
ProwlerTrait,
HealthTrait({ current: 20, maximum: 20 }),
ChaseTrait({ speed: 3, reach: 1 }),
TargetableTrait,
],
model: "visor-bot",
});
export const night = definePlugin({
name: "night",
// No runsOn: the room alone spawns, and a game played alone is its
// own room.
systems: { rules: { prowl: { system: prowlAtNight } } },
});
function prowlAtNight(world: World, step: AuthoritativeStep) {
const hour = findTimeOfDay(world);
// No World in the scene, such as a menu, keeps no night.
const isNight = hour !== undefined && (hour >= duskHour || hour < dawnHour);
for (
let placed = findEntity(world, newLairs);
placed;
placed = findEntity(world, newLairs)
)
placed.add(LairNightTrait);
updateEach(world, watchedLairs, ([{ position }, lairNight]) => {
if (lairNight.isNight === isNight) return;
lairNight.isNight = isNight;
if (isNight) spawn(step, prowler, { position });
});
// Day, or a scene with no lair, such as a lobby or a menu: no prowler
// stands.
if (isNight && findEntity(world, lairs)) return;
for (
let standing = findEntity(world, prowlers);
standing;
standing = findEntity(world, prowlers)
)
destroy(step, standing);
}

Roblox’s Lighting.ClockTime holds the same number, from 0 up to 24, and stands still until a script moves it; its TimeOfDay is the same hour as a string, such as "18:30:00". In Unity, Unreal and Godot, a game or a plugin drives the sun. Here the clock runs on its own, in the step, because the hour decides what spawns and must be the same in a room, on every page and in a headless run.

A scene can mount two Worlds at once. Each dresses, draws and bakes only its own ground, and puts its scatter’s rows under its own row in the devtools tree. Each ground’s terrain is a fixed body in the world’s one physics scene, so an actor walks on whichever terrain she stands on, and a character spawned in the second World stands on its ground. The two share the scene’s gravity: a game that needs two separate physics spaces runs two Games. The devtools frame the World the player’s character is in, or the first World mounted when she is in none: frameView and frameShot read that World’s map. The inspector of a World’s row reads and sets the day clock of the World the selected row sits in, or the first World’s with no row in a World selected, and titles its card with that World’s row label, such as World #3.

The ground is one entity with the ground, scatter, obstacles and navmesh traits on it, and its terrain is one fixed body on that entity, holding the heightfield, the wall along each edge and the scatter’s hulls, which a map edit replaces alone, so every other body keeps its pose and velocity; and the day clock is another, which carries InWorldTrait naming its ground. useWorldEntity() reads the ground from anywhere inside the World, for UI that wants the map.