Project setup
The editor needs three things from a project: a description of how to build its worlds, a page that starts the editor, and a Vite plugin that lets the editor read and write the asset folder. examples/editor in the repository is a complete project set up this way.
To see it running before you set up your own:
pnpm installcd examples/editorpnpm devOpen http://localhost:5180/ for the editor; http://localhost:5180/play is the game itself. Your first scene walks through the editor.
1. Describe the project
Section titled “1. Describe the project”One object tells both the game and the editor how to build a world.
import { Engine2D } from '@shell/engine-2d';import { GamePlugin } from './game-plugin';
export const Project = { name: 'My Game', /** Folder the editor may read and write, relative to the project root. */ assetRoot: 'public/assets', /** URL prefix that folder is served from when the game runs. */ assetBaseUrl: '/assets', /** Asset path of the scene the game loads and the editor opens. */ startScene: 'scenes/main.scene.json', engine: (canvas: HTMLCanvasElement) => Engine2D({ canvas, resizeTo: canvas, spawnCamera: false }), plugins: [GamePlugin],} as const;| Field | Meaning |
|---|---|
name | Shown in the editor’s toolbar. |
assetRoot | Folder of scenes, prefabs, textures, and other files. |
assetBaseUrl | Where the game loads those files from. Component fields store URLs under this prefix. |
startScene | Asset path of the first scene. |
engine | The engine plugins for a canvas. Called once per world. |
plugins | The project’s own plugins: components, systems, resources. |
prefabs | Optional. Code-defined prefabs, by key. The editor lists them and registers them in its worlds; the game’s entry registers them too (see below). |
play | Optional. (world, scene) => Promise<void>: boots a play world your own way. By default the scene is loaded into the play world’s root. |
A scene refers to a code-defined prefab by its key, for example in a spawner’s StoreType.Prefab() field. The editor registers prefabs in its own worlds; the game’s entry does the same before it loads a scene:
for (const [key, prefab] of Object.entries(Project.prefabs)) { world.get('prefabs').register(key, prefab);}The type is ProjectDefinition from @shell/editor. Import it with import type if you annotate the object, so the game carries no editor code.
Two conventions make a project editable:
- Scene content comes from scene files, not from startup systems. The editor never runs startup schedules.
- Gameplay systems are not marked
.runInEditMode(), so the edit world shows the scene as authored. Systems that only draw, such as the renderer’s, are marked.
2. Add the editor entry
Section titled “2. Add the editor entry”The editor gets its own HTML page and entry module, next to the game’s. It is the only source file that imports editor packages.
<div id="editor"></div><script type="module" src="/src/editor.ts"></script>import { createEditor, RpcAssetSource } from '@shell/editor';import { EditorViewport2D } from '@shell/editor-viewport-2d';import { Project } from './project';
await createEditor({ target: document.querySelector<HTMLElement>('#editor')!, project: Project, viewport: EditorViewport2D, assets: import.meta.hot ? new RpcAssetSource(import.meta.hot) : undefined, // Optional: lets an agent (an MCP client) work on the open editor. See "Agents" below. hot: import.meta.hot,});The target element needs a size; the editor fills it. Give html, body, and the element height: 100%.
createEditor options:
| Option | Meaning |
|---|---|
target | Element the editor fills. |
project | The project definition. |
viewport | The scene view plugin. EditorViewport2D.config({ gameView: { width, height } }) sets the size cameras are drawn at. |
assets | Where files come from. Without it the editor reads by URL and cannot save. |
plugins | Further plugins for the edit world. See Extending the editor. |
It resolves to { world, destroy() }: the edit world, and a function that removes the editor.
Do not list editor.html as a build input. A production build then contains no editor code.
3. Add the Vite plugins
Section titled “3. Add the Vite plugins”import { shellEditor } from '@shell/editor/vite';import { svelte } from '@sveltejs/vite-plugin-svelte';import { defineConfig } from 'vite';
export default defineConfig({ plugins: [svelte(), shellEditor({ assetRoot: 'public/assets' })],});svelte()compiles the editor’s interface. The project needssvelteand@sveltejs/vite-plugin-svelteas dev dependencies.shellEditor()lets the editor page list, read, and write files underassetRoot, and tells it when they change. Use the same folder asProject.assetRoot.
Start the dev server and open /editor.html.
To have the editor at the dev server’s root, name its page: shellEditor({ assetRoot: 'public/assets', page: '/editor.html' }). The root then shows the editor and /play the game. Each page stays at its file’s name, and a build is not changed: index.html is still the game.
Scripts (optional)
Section titled “Scripts (optional)”A script is a system or a component in a file of its own, which no plugin lists.
import { shellScripts } from '@shell/scripts/vite';
plugins: [svelte(), shellEditor({ assetRoot: 'public/assets' }), shellScripts()],import { ProjectScripts } from 'virtual:shell/scripts';
plugins: [GamePlugin, ProjectScripts],@shell/scriptsis a dependency of the game, not a dev dependency:shellScripts()runs in the build too, which is what puts the scripts in the shipped game. Add"@shell/scripts/client"to thetypesoftsconfig.json.- A script is a file in
src/scripts(shellScripts({ scriptRoot })names another folder):spin.system.tswith one export made withDefine.system(), orspin.component.tswith one made withDefine.component(). Any other.tsfile there is a helper that scripts import. - A system that reacts to an event says so:
Define.system().observes(OnDamage). - A changed system is replaced in place, also in a game that is playing, and starts over with new locals. A changed component loads the page anew.
- A script that is wrong is left out, or stays as it last worked, and is named in the editor’s console. Vite’s error overlay is off in a project with
shellEditor(), unless the project setsserver.hmr.overlay. editor.html?scripts=offopens the editor with no script loaded, for a script that hangs the page.- In the editor, the Scripts panel, a tab beside Assets, lists the scripts: its buttons make a system, a component, or a folder, and a double click opens a script in the Code panel. Ctrl or Cmd+S there saves the script. The Code panel underlines type errors, lists what can follow a dot, and shows a declaration under the pointer; the dev server’s TypeScript answers that from the project’s own
tsconfig.json. A type error does not stop a script from running. - A script renamed or moved in the Scripts panel takes the import lines that name it along, in scripts and in the project’s other code.
- A script made in the editor imports from the project’s engine package: the first of
@shell/engine-2d,@shell/engine-3d, and@shell/engine-minimalthat itspackage.jsonlists, else@shell/ecs. The engine package has every name of@shell/ecs,@shell/transform, and the rest, so the one import line of a script reaches all of them, and the Code panel lists them inside its braces.
What the file server will and will not do
Section titled “What the file server will and will not do”shellEditor() writes to your disk on behalf of a web page, so it is deliberately narrow:
- Dev server only. It does nothing in
vite buildorvite preview. - Local only. If the dev server is reachable from other machines (
--host), the plugin registers nothing and logs why. SetallowRemote: trueto accept that risk. - One folder. Every path is checked against
assetRoot: absolute paths,.., hidden files, and symbolic links leading outside are refused. - No half-written files. A write goes to a temporary file that is then renamed over the target.
- No silent overwrite. A save states the file’s modification time as the editor last saw it. If the file changed or vanished since, the save is refused and the editor asks what to do.
- Nothing is overwritten or destroyed. Creating, importing, and moving refuse a path that is taken. Deleting moves the file into
.shell-editor/trash/in the project, under the time it was deleted; add.shell-editor/to.gitignore. The trash is never emptied for you. - Files over 32 MB are refused.
Letting an agent work on the editor
Section titled “Letting an agent work on the editor”With hot: import.meta.hot passed to createEditor, shellEditor() also serves an MCP endpoint at /__shell/mcp. An agent (Claude Code, or any MCP client) uses it to read and change the open editor:
claude mcp add --transport http shell-editor http://localhost:5173/__shell/mcp| Agent tools | What they do |
|---|---|
world_listComponents, world_listEntities, world_getEntity, world_query, world_snapshot, … | Read the open scene, or the running game during play |
world_spawn, world_setFields, world_insertComponent, world_despawn, world_reparent, … | Change the scene. Each call is one undo step, labelled Agent: … |
batch | Several changes as one undo step: all of them, or none |
run_action | Any editor action: file.save, play.start, play.stop, edit.undo, … |
play_step | Advance the game by a count of frames and leave it paused |
play_keys | Press and release keys of the game as a player would, held over calls, then advance it |
read_log, get_selection, set_selection, screenshot | The console, the selection, and a picture of the scene view or the game |
move_panel | Put a panel of the editor in another place, as dragging its tab does |
get_editor_state, open_document, create_scene | Which scene is open and whether it is saved; open another, or a new one |
wait_for_reload | After a code change: answered once the new code is running, or with what is wrong with it |
check_code | The type errors of the files it names, or of every script, as tsc finds them |
list_assets, move_asset, delete_asset | The files of the asset folder, each with the URL a component field stores; move, rename, or delete one |
instantiate_prefab, create_prefab | Add an instance of a prefab (a file, or code:<key>) to the scene; make a prefab file from an entity |
- The agent writes components and systems with its own file tools. A code change reloads the page; the agent’s next call waits up to 15 seconds for the new page. The reload does not ask about unsaved scene changes: they survive it. The undo history does not.
world_listComponentslists the components shown in the editor.keysasks for particular ones, shown or not, andallfor every registered one.screenshotanswers with the picture, its size in pixels, and the rectangle of the world it shows, so the agent can tell that an entity is outside the picture.move_assetupdates every scene and prefab that refers to the file, and the open scene as one undo step. A file the agent moves with its own file tools keeps none of its references.delete_assetleaves references as they are, and the dev server keeps the file in.shell-editor/trash.get_editor_stateincludescodeAt, the time the page last took new code: when it loaded, or when it replaced a system script in place. The agent reads it before a code change and passes it towait_for_reloadafter: that call is answered by the page running the new code, however soon it is made.startedAtis the time the page loaded.- Code that cannot be built is what the agent’s next call is told (“The editor page failed to start: …”). An error thrown while a project module loads is only in the browser’s console.
- The endpoint tells an agent how to work with these tools when it connects, so the project needs no instructions of its own.
- To check a game the same way every time, the agent starts it paused (
run_actionwithplay.startPaused) and advances it withplay_step. A frame is the fixed timestep, so 60 frames are one second of the game whatever the machine is doing. A screenshot of a paused game shows it as it is. - The editor page must be open in a browser. It does not have to be visible: the game keeps running in a hidden tab, and a screenshot is still drawn.
- The endpoint follows the same rules as the file server: dev server only, localhost only. It also refuses a request that carries another site’s
Origin, so a web page you have open cannot edit your scene. - With several editor tabs open, the one that loaded last answers.
A plugin adds its own agent tool with world.get('editorAgentTools').register({ name, description, inputSchema, handler }).
Checking that it works
Section titled “Checking that it works”/editor.html(or the root, withpage) shows the start scene’s entities in the hierarchy.- Change a value in the inspector and press
Mod+S. The scene file changes on disk and the page does not reload. - Edit the scene file in a text editor. The editor shows the change.
- Press Play. The game runs in the Game view; Stop returns to the scene as authored.
If the Save button is disabled, the editor is running on the read-only fallback: check that shellEditor() is in the Vite config and that assets is passed to createEditor.