Editing while playing
The devtools hold an editor for the running game:
- The ground. The Map section’s Terrain tools sculpt, smooth and flatten the ground, paint and fill its materials, set the sea’s height, and paint the scatter, its trees, bushes, rocks and grass, thicker, thinner or away.
- Props. In the Assets section, press a model’s or a shape’s card, then click the ground to stand it there as a prop of the map. Handles on a placed model or shape move, turn and scale it, and Save writes the change to the map’s file.
- Undo and Save. Each change is one step that Undo and Redo walk, and Save writes the map’s changes to its file,
src/maps/<map>.json. - Play from here. A right-click on the ground opens its menu, whose Play from here stands the player’s character at that spot and plays.
What lasts depends on where the change goes. A map edit lasts once Save writes it to the file, and an unsaved one is gone on reload. Everything else the devtools change in the running world, a behaviour’s props, a hidden or deleted entity, an added behaviour, is tried and gone on reload, as Play-mode edits says, until Send to agent or Copy as prop turns it into code.
Each of the map tools is also a command and an MCP tool, so an agent makes the same edit without the page:
| In the editor | Command | MCP tool |
|---|---|---|
| Sculpt, Smooth, Flatten, Paint and Scatter | spawnite map terrain <tool> |
edit_terrain |
| Fill | spawnite map terrain fill |
edit_terrain with tool fill |
| The sea’s height | spawnite map terrain sea |
edit_terrain with tool sea |
| A material’s look, and Duplicate | spawnite map material, with --from for a copy |
edit_material |
| Placing, moving and removing a prop | spawnite map place |
place_props |
| Reading a point or the whole map | spawnite map query and spawnite map describe |
map_query and map_describe |
The devtools edit the running world, as Unity’s play mode does. Each call on this page acts on useDevtools, the store that the Devtools API describes, or on useTerrainEditor, the Terrain section’s store.
The recording
Section titled “The recording”The overlay’s Spawnite toggle sets editing, and the overlay turns it off when it unmounts. While editing is on, the loop records every frame as a dump of the world, keeping the last ten seconds. recordedFrames counts them, and seek(frame) reads one, where 0 is the oldest kept. setEditing(false) drops the recording. Seeking reads a frame; it does not move the world. Rewind, resuming from a scrubbed frame, is a later issue.
setFreeCamera(true) puts FreeCamera over whatever camera the scene mounts, the engine’s or the game’s own, and the character stands still while the movement keys fly it, as Unreal’s eject does. Game mounts it inside the canvas, so a scene with no camera gets one too. The scene’s camera stays mounted with its controls turned off, and the free camera’s view is what draws, whatever the scene’s camera writes to the canvas camera each frame. The free camera takes every other camera’s entity out of the world until it goes, one a scene mounts meanwhile included, so the world reads the free camera alone. setFreeCamera(false) gives the scene’s camera back, and so does leaving edit mode.
setMapCamera(true) puts MapCamera over the scene’s camera the same way, the city builder’s camera over the map, and turns the free camera off, as setFreeCamera(true) turns the map camera off; either way the character stands still. It sets mapFlight too, which the map camera reads once its orbit stands: one eased camera-controls move from where the eye is to the pose readMapFlight(eye, aspect) gives, the whole map from 80° above on the side of the map the eye stands, framed by frameShot from the active map or, with none, from the bounds the view draws, at mapCameraSettings.flightZoom so it lands closer than the frame that fits every corner; setMapCamera(true, zoom) lands at a zoom of its own, as spawnite play profile --map-camera <zoom> asks for one. The camera calls endMapFlight() when the move settles, or at once where nothing is mounted to frame or the eye already stands there. setMapCamera(false) and leaving edit mode clear the flight too. The map camera pans by mapCameraSettings.panShare of the eye’s distance a second, turns at turnRate, and tilts tiltPerDoubling for each doubling of the distance the wheel or a pinch makes. The free camera flies at freeCameraSettings.metresPerSecond, and isBoostHeld(), either Shift, multiplies that by freeCameraSettings.boostFactor.
readPalette() is the Assets section’s content: models, every registered model with its url, its height from the manifest’s capsule and how many entities carry a ModelTrait trait naming it; avatars, every registered avatar and every name an AvatarTrait trait carries, with how many characters wear each; scatter, the six kinds with the file each draws and how many placements every ground stands of it; and shapes, the four prop shapes. selectPlaceable(placeable, adding) selects the rows of every entity drawing the model or wearing the avatar, or the scatter kind’s counted row under every ground that stands it; adding adds them to the selection without removing any; a shape or a placeable nothing draws changes nothing. A press on a model’s, a scatter kind’s or a shape’s card also calls the terrain editor’s holdCard(placeable), which the next paragraph covers. readModelThumbnail(url, options), exported from the engine’s root as well, renders the model or avatar file once on a shared offscreen renderer with a transparent background, two renders a frame at most, and keeps the PNG data URL for the page’s life; its options are size in pixels, framing, the whole model or its face, yaw to turn a file whose front is not +z, and clip, a VRMA to pose an avatar in, where a model poses in its file’s idle clip; readModelSilhouette(name) is the manifest hull’s outline from the front, for the card to show before the picture lands.
useTerrainEditor is the store of the Map section’s Terrain section, apart from useDevtools and exported beside it. readMapChoices() lists every registered map, each marked mounted where a World shows it, and openMap(name) shows that map alone as ?map= does, writes it in the address and starts the undo list again. holdTool("sculpt" | "smooth" | "flatten" | "paint" | "fill" | "scatter" | null) picks a tool up, which turns the map camera on unless the map or the free camera is on already, and turning the last of them off, or leaving edit mode, puts it down. mode is the cursor’s mode, "select", "terrain" or "look", which no control picks yet, which decides whether a click in the game selects: a tool in hand is "terrain", putting it down turns "terrain" to "select", and setMode(mode) sets it, "select" and "look" putting the tool down. setBrush({ radius, strength, shape }) keeps the brush within terrainBrushLimits, and the browser keeps it for the next session, as it keeps lower, which setLower(true) sets so Sculpt lowers the ground and a stroke begun with Ctrl held flips. readSea(map) is the map’s sea as the page shows it: the map’s sea, else 1 m below the lowest point of its base. previewSea(map, height) shows a drag of the Sea field’s label in seaPreview, which the sea’s plane follows at once, and setSea(map, height) keeps it as one undo step that save() writes as one sea post to /__map. With no tool in hand, Alt and a click on the ground sets the sea to the height there. readPaintMaterials(map) lists the materials Paint can lay, each with its family, the row the editor shows it in, its colour texture in the game’s set, null in the flat set, its swatch colour and its tint, setMaterial(name) picks one and holds Paint unless Fill is in hand, and pickMaterial(surface, [x, z]) picks the one under a point, as Alt and a click does. beginStroke, extendStroke and endStroke() are what the brush in the canvas calls: each move applies only the dabs it adds, with the engine’s beginTerrainStroke or beginPaintStroke, which leaves the preview equal to the whole stroke painted at once, and a point that repeats the last, as a held brush sends, dabs again; the end registers the page’s terrain with the stroke in it and keeps its change as one step of undoSteps; every write is made against the map’s revision the page stands on, which readMapRevision(map) answers. undo() and redo() show the step’s exact values again. Every edit sets unsaved; save() writes the points, cells and materials the edits since the last save touched, as one restore to /__map and one restoreMaterial for each material, revert() shows the file again as one undo step, and resetToBase(map) writes the map’s base at once as one undo step. A spawnite:terrain window event from the dev server for a revision the page did not write goes to followOutsideWrite(map): with nothing unsaved it reloads the file, clears both lists and sets notice, which the section shows, and over unsaved edits it sets changedOnDisk, as a Save refused as older does; save({ anyway: true }) lays the edits over the file as it now stands and writes them. A Save refused over a moved base sets baseMoved, and settleBase("keep-heights" | "move-with-ground") settles it and saves again. With Fill, beginStroke and extendStroke take the rectangle’s two corners and endStroke() keeps it as a fill; fillRegion(map, region) fills one of readMapRegions(map). setScatter({ families, action }) picks the families Scatter paints and whether it grows them toward double, which grows back what was erased, thins them toward half or erases each instance under the brush by its spot, "more", "thin" or "erase", holds Scatter, and the browser keeps it. makeEditable(instance) takes a scattered instance’s placement from describe, writes a prop of its kind where it stands, named such as tree1, and erases the seeded one, as one undo step, and answers the name; removeScattered(instance) erases one instance, as the ground menu’s Remove this does, as one step; moveProp(map, name, [x, z]) and removeProp(map, name) move and delete a prop, each one step. findScatterPropRow(map, name) is the row of the instance a prop stands as, once the ground stands it. setClear(true) makes Paint and Fill hand cells back to the rules, setLevel(height) sets Flatten’s level until setLevel(null) returns to auto, leaving it as it is for a number that is not finite, as a field’s bad text parses to, pickLevel(surface, [x, z]) sets it to the ground’s height under a point, as Alt and a click with Flatten does, and a height stroke’s preview names in level the height it levels to, and resetHere(map, surface, [x, z]) hands the points and cells under the brush back to the base as one undo step. previewMaterial(map, name, look) shows a change to a material’s look while a slider drags, refineMaterial keeps it as one step of undoSteps back to the look before the first preview, duplicateMaterial(map, name) makes a new material from it and holds it as one step, whose undo takes it out and holds name again, and readMaterialLook(map, name) reads its tint, tintAmount, tile, bumps and wet. preloadCard(placeable) starts loading a model card’s model, as a mouse resting on the card or focus reaching it does, and holdCard(placeable) picks an Assets card up, as a press on it does, and loads its model too: it puts the tool down, sets mode to "select" and turns the map camera on as a tool does, and card holds it until holdCard(null), Escape, leaving Assets or the last camera going off puts it down; a tool picked up puts it down too. placeCard(map, [x, z]) stands the card in hand there as a prop of the map, { model } for a model, { kind } for a scatter kind or a shape, at the spot rounded to the centimetre, named after the card, villager, then villager-2 where that name stands, and keeps it as one step of undoSteps, shown at once through registerMapProps and unsaved; it returns the name, or null with no card in hand or an avatar’s. save() writes every prop placed, made editable, moved or taken out since the last save as one props post to /__map, which writes through writeMapProps, the writer spawnite map place uses, and revert() takes them back with the rest. In the canvas, the card in hand draws its footprint in the brush cursor’s look under the pointer, a metre’s square for a shape and a model’s measured radius otherwise, and a still click places it, then selects its row once the World draws it; a drag still moves the map camera, and the click selects nothing else. pickGroundSpot(x, y) reads the ground under a canvas point as pickRow takes one, with the map, its surface, cellMaterial, the cell’s own material that the menu’s Duplicate and Wet act on, and scattered, the seeded instance in front of the ground there, which the menu’s Remove this erases, and readGroundSpot(surface, [x, z]) reads a point’s height, slope in degrees and material as spawnite map query answers it: the ground’s right-click menu reads both.
Play-mode edits
Section titled “Play-mode edits”Everything below that changes the world, an edited prop, a hidden or deleted entity, an added behaviour, acts on the running world and is gone on reload, as an edit in Unity’s play mode is. The scene’s code stays the source of truth, and two paths turn an edit into code. Send to agent puts the edit in the bundle. Copy as prop, on a card whose row names the prop its component takes, copies the changed fields as that prop, movement={{ speed: 7 }}, for you to paste. An edited card reads “Tried, not saved” until then. Unity’s play mode discards its edits on exit, and Unreal keeps them only through Keep Simulation Changes; the copy is the same step for a component written in code. Duplicate takes that path too: it opens Send to agent with the intent “Add another beside this one”, because a copy made in the world would have no component to draw it and no row.
Editing a behaviour’s props
Section titled “Editing a behaviour’s props”setProp({ id, name, value }) writes one prop of a behaviour row onto its trait through set, so every subscriber hears it. A row that adds two traits, as Chase does, writes the one whose schema has the field. The row records the write under edits, so the inspector marks the behaviour changed and shows the edited value, and resetProps(id) drops the edits and puts the component’s own props back on the trait. A prop that holds a function cannot be set, and a prop is set only to a value of its own type, so a typed-over array or number never reaches a trait as text. A number is clamped to the row’s declared range.
On a room’s replica, an entity the room streams is the room’s. Where the room’s welcome says it takes edits, setProp also sends the room an edit message: the entity’s dump key, the trait’s dump name, the prop and the value. The room writes it, clamped to the range the trait’s behaviour declares, and the write streams back to every client. The devtools’ hour and day length fields, and Jump to, go to the room the same way, as edits of its streamed clock’s dayClock trait: the room wraps the hour into the day, as setTimeOfDay does, and refuses a length the clock cannot run on. Where the room refuses, every row of a streamed entity over a trait with a dump name is read-only, greyed with “The room keeps its settings”, and so are the World card’s hour and day length and Jump to. A row over only traits with no dump name stays editable: the room never streams them, so nothing overwrites the edit. A room takes edits only when started with allowEdits or ROOM_EDITS=1, a development room: Roblox lets a client write its own WalkSpeed, the classic speed hack, and Unreal keeps movement on the server.
When the component renders again with the same props, the edit stays: the behaviour hook sets the trait only when the props’ fields differ from the ones it last wrote. A changed prop wins and the edit goes, which is React DevTools’ rule for an overridden prop. Headless, no component has mounted, so there are no rows to edit; a test sets the trait itself. Cost per frame: none; a shallow compare per render of the behaviour.
Presets
Section titled “Presets”A preset is a named set of the props one entity’s behaviours hold, saved under the entity’s component name, so a preset saved on one Coin applies to every Coin. presets lists the names for the selected row’s component. savePreset(name) takes the selected entity’s props as they stand, and applyPreset(name) writes them through setProp. Each behaviour’s props are kept under its name, as a list in the entity’s order where it has two of one kind, so each applies to its own row; a read-only row is left as it is, and so is a field a stat sets, so a preset saved before the stat existed still applies the rest. The Stats component’s row is neither saved nor applied, since its stats are not props. preset names the last applied on the selected row. Presets persist in the browser’s storage under the game’s name, as the save does, and a test builds the store over a map. Unity’s Preset assets are the shape. Cost per frame: none.
The context menu’s actions
Section titled “The context menu’s actions”The overlay’s menu offers actions the store carries out. readDisabledActions(id) says why an action is off for a row, by action, and the menu shows the reason; an action called while off throws it. The actions are the following:
focusCamera(id)flies the mounted orbit to the entity, at the distance that fits its model’s bounds, with a transition, and sets the camera’sfollowto null so the frame’s aim stops pulling the eye back to the player. Leaving edit mode puts the follow back. Off with “No camera is mounted” or “Nothing draws it”. In Unity, Unreal, Godot and Roblox Studio, F moves an editor camera; the game has one camera, so it stops following instead.hide(id)andshow(id)flip the entity’s object’svisible, andhiddenIdslists the hidden rows. The frame loop applies it again after the transforms sync, so a model that loads later stays hidden, andpickEntityskips a hidden object. Physics and behaviours run on, as under Unity’s scene visibility and Unreal’s H key: the view is hidden, not the entity. Off with “Nothing draws it”.deleteEntity(id)destroys the entity.EntityandPlayersee their entity go and take its children and its row down. Off with “The world” and “The camera’s rig”, whose components do not watch for that.addBehaviour(id, name)adds a registered behaviour’s trait with its defaults and registers a row for it under the entity, which goes when the entity does.behaviourOptions(id)lists what the entity can take: each behaviourdefineBehaviourmade that it lacks, with thedescriptionit declares as its summary. The engine defines Spin, Bob, Pickup and Health. A behaviour whose component does more than add the trait, as Interact draws its prompt and listens for E, Trigger fires its callbacks and Chase adds a body, is not offered, because its trait alone would run half of it; it puts its trait in the dump withdefineTrait. Off with “Has every registered behaviour”.playFromHere([x, y, z])is the ground’s menu’s Play from here: it stands the player’s character at the point, at rest, facing the way the camera the page draws with looks, turns the scene’s orbit to the same yaw, so a camera that follows the character starts behind them, moves the character out of any prop the point stands inside, then leaves edit mode and resumes. In a map preview, which has no character, it setspreviewPlayerto the point and the yaw instead, and the preview mounts<PlayFromHere position facing>there: the game’s own component that rendered its last mounted<CharacterSpawn>, such as aSpiritthat wraps it, with its props and what it draws beside the Player, or, where the scene wrote the<CharacterSpawn>in its World directly, a<CharacterSpawn>with the props it took, its children among them; under the last<Camera>,<SideCamera>or<ChaseCamera>that followed one, with its props; or the engine’s defaults where the Game has mounted none. Its<CharacterSpawn>spawns at the point and the yaw whatever the game’s component hands it, which takes them too aspositionandfacingunless its ownpositionis another shape than[x, y, z], such as aVector3, and with the default respawn comes back there when it falls; once the character’s body is built, it is moved out of any prop it spawned in. A component that mounts a camera of its own beside the<CharacterSpawn>keeps it, and no other is added.readPlayFromHereReason()says why it is off, which is also what it throws: “No player in this scene” in a game scene with no character, such as a lobby, or “The room places the player” on a room’s replica. An agent makes the same move withspawnite play from-here --at 4,-2, which reads the ground’s height under the point and calls it, and--map <name>opens another map in its preview first, as the terrain editor’sopenMapdoes; the session stays held. A room places its players, and therespawnite play set '[controlled]' transform.position '[x,y,z]'has the room move her. Unreal’s Play From Here and Roblox Studio’s Play Here start play with the character at the clicked point.- Duplicate goes through Send to agent, as above. Copy id, Inspect, Select parent and Send to agent are the overlay’s own.
Every action is a call on the store, so an agent’s what-if is the same call: delete a coin, add Spin, stepUntil, then screenshot. Headless, focus and hide report their reason. Cost per frame: none for focus and delete; one visibility write per hidden entity.
Previewing a map
Section titled “Previewing a map”In development, ?map=<name> in the URL shows that registered map alone: the overlay registers a map-preview scene holding a <World map={name}> and a camera the page drags, and goes to it. No player spawns until playFromHere asks for one, and every <Hud> draws nothing while mapPreviewScene is the active scene, so a readout the game mounts outside its scenes stays off the map. The game’s start scene mounts first, because the overlay loads after it, and then gives way. The terrain editor’s openMap(name) shows another map the same way, with no reload. The overlay exposes both through useInspect as the page’s map preview view, maps from readMapChoices() and open as openMap, which is how spawnite play from-here --map reads the game’s maps and opens one: spawnite play eval "views.page['map preview'].maps" lists them. frameView("map") then frames the whole preview from above.
Recording a clip
Section titled “Recording a clip”The top bar’s Record button, or F9, records what the canvas draws as a clip, a short video for marketing a mechanic, a move or a fly-over, and a second press stops it. A clip stops by itself after maxSeconds, and the dev server keeps up to keep clips, refusing a new one past that rather than deleting one; the game’s vite.config.ts changes either through the engine’s plugin:
spawnite({ clips: { maxSeconds: 10, keep: 20 } });The defaults are 30 seconds and 50 clips. The page hands the canvas to Mediabunny, which encodes it as H.264 through the browser’s WebCodecs and keeps the MP4 in memory until the clip stops, at 1920 by 1080, or 1080 by 1920 for the Clips section’s 9:16, about 15 Mbps and 60 frames a second, with a keyframe every half second and no audio. The canvas letterboxes to the chosen shape while it records. The video holds the canvas alone, so the devtools and the HUD stay out, and so does what the editor draws into the canvas, such as the brush’s cursor, the ability rings, the hit zones and the navmesh: each draws inside EditorDrawing, a group from @spawnite/engine/devtools that hides while a clip records, and a game’s own debug drawing wraps itself in it to stay out too. At the stop the page decodes up to 30 stills from the finished video, draws the contact sheet, and posts it all to the dev server once. The dev tools close while a clip records and Record waits beside the Spawnite toggle. A clip also saves the replay’s window as .replays/saved/clip-<name>, on a page whose replay records a room’s play, so a later tool can play the moment back. The dev server page says what a clip’s folder holds, and spawnite clip show <name> and the MCP’s read_clip read it. An agent records the same clip from a held session with spawnite play record, or the MCP’s record_clip, on the world’s steps, as the director page says.