React Three Game is a game creation toolbox for the web. A prefab is a JSON scene document: nodes describe objects, and components give them settings and behavior. Agents can work through the running editor, edit those documents directly, or add components in application source. Choose the path that fits the task and available files.
Choose how to work
| Task or context | Approach |
|---|---|
| Adjust a visible scene and inspect placement | Use the editor API |
| Change a saved scene, generate content, or review a file diff | Edit prefab JSON directly |
| Add reusable behavior, hooks, or a new rendering feature | Add custom components in source |
| Store a small script with the scene | Use a Runtime component |
| Render JSON alongside the app's own canvas content | Embed PrefabRoot |
| Connect gameplay, physics, lighting, or loading | State and resources and Focused references |
These approaches can be combined: add a component in source, configure instances in JSON, then inspect them in the editor or viewer. Use the existing schemas and nearby application code to discover conventions before inventing new ones.
Edit a scene
Connect and find the object
Run JavaScript in the frame containing PrefabEditor:
const scene = window.scene;
scene.info(); // Check mode and how this scene can be saved.
const { node } = scene.find({ query: 'north wall' }); // Requires one match.
await scene.setMode({ mode: 'edit' });
scene.look({ id: node.id });
Read only what the edit needs:
scene.search({ query: 'wall', limit: 20 }); // Choose an ID when find is ambiguous.
scene.get({ id: node.id, resolved: true }); // Full properties and defaults.
scene.components({ names: ['Material'] }); // Supported settings for a type.
Component instance keys belong to the node and can differ from type names.
If window.scene is missing, check the frame and wait for the editor to mount. agentTools={false} disables exposure; showUI={false} only hides panels. A viewer alone has no window API. Only one agent-enabled editor is supported per page; reacquire it after navigation.
Make one change and inspect it
Writes require Edit mode. Start with a suitable existing object and preserve the scene's hierarchy and visual conventions.
await scene.setMode({ mode: 'edit' });
scene.update({ id: node.id, transform: { position: [2, 1, 0] } });
scene.look({ id: node.id });
const image = await scene.capture({ helpers: false });
Wait for assets, then display image.dataUrl and inspect the result. Each update is one undo step.
scene.undo(); // Reverse an authored edit when needed.
await scene.setMode({ mode: 'play' }); // Check animation or physics.
// Observe the behavior before continuing.
await scene.setMode({ mode: 'edit' });
await scene.reset(); // Restore live objects from current JSON; keep edits/history.
Changing mode alone does not reset live state.
Save the result
if (scene.info().saveMethod === 'save') {
await scene.save(); // Host's onSaveScene callback.
} else {
const json = scene.exportJSON(); // Write this string to the scene's source file.
}
Exporting alone does not save. Report what changed, what you verified, and where it was saved.
Find a more specific operation
scene.help() lists methods. Read the relevant section of Editor scene for agents for capture options, component edits, placement, downloads, packing, or atomic batches. In this checkout, use docs/public/editor-scene-for-agents.md; deployed docs may lag.
Individual edits use the current revision automatically. Use expectedRevision when coordinating against a previous read; validate() and batch() require it. If a guarded write conflicts, reread and replan. Prefer batches when dependent changes must land together, rather than for every edit.
Edit prefab JSON directly
Find the file the app actually loads; in this repository, scenes live under docs/public/prefabs.
// In the consuming app: follow this import (or the PrefabInstance URL).
import level from './level.json';
// Edit level.json directly; no running editor is needed.
// Preserve node IDs, component keys, and unrelated fields.
// Check component schemas in source before adding properties.
// Check JSON syntax, unique IDs, and component registration in the app.
// Reload the editor/viewer, inspect the result, and review the file diff.
// File edits do not update an already-open editor's in-memory document.
// Reload before continuing API work so a stale save cannot overwrite the file.
Render JSON in the app's canvas
Render JSON beside the user's own JSX and gameplay systems. Import custom registrations before mounting; keep data stable between renders.
With GameCanvas
import './components'; // Application component registrations.
import { GameCanvas, PrefabRoot } from 'react-three-game/viewer';
import type { Prefab } from 'react-three-game/core';
import level from './level.json';
<GameCanvas>
<ambientLight intensity={1} />
<PrefabRoot data={level as Prefab} />
<mesh position={[3, 0, 0]}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
</GameCanvas>
With an existing R3F Canvas
Keep the app's existing WebGPU setup. This shows a minimal renderer callback if needed:
import { Canvas } from '@react-three/fiber';
import { WebGPURenderer } from 'three/webgpu';
import { PrefabRoot } from 'react-three-game/viewer';
import type { Prefab } from 'react-three-game/core';
import './components';
import level from './level.json';
<Canvas gl={async ({ canvas }) => {
const renderer = new WebGPURenderer({ canvas });
await renderer.init();
return renderer;
}}>
<PrefabRoot data={level as Prefab} />
{/* Existing JSX, lights, controls, and gameplay systems stay here. */}
</Canvas>
Share resources or access the prefab
Inside the canvas, using the app's Gameplay component:
import { SceneRuntime, PrefabRoot } from 'react-three-game/viewer';
<SceneRuntime>
<PrefabRoot id="level" data={level}>
<Gameplay /> {/* Can use usePrefab() for this document. */}
</PrefabRoot>
<PrefabRoot id="props" data={propsPrefab} />
{/* Sibling userland content shares runtime resources, not a prefab context. */}
</SceneRuntime>
GameCanvas already supplies SceneRuntime; a standalone PrefabRoot supplies
its own runtime when needed. These providers do not supply a game loop.
No editor or window API is required. For visual authoring, mount PrefabEditor
from react-three-game/editor; see the editor guide.
Prefab conventions
Annotated JSON below uses comments for teaching; omit comments in .json files.
{
"root": {
"id": "world", // IDs are unique within this prefab.
"children": [{
"id": "box", // Keep IDs stable across edits.
"components": {
// Instance keys (left) can differ from registered type names (right).
"transform": {
"type": "Transform",
"properties": {
"position": [0, 1, 0], // Local to the parent; Y is up.
"rotation": [0, 0, 0], // XYZ Euler radians.
"scale": [1, 1, 1] // Scaling also affects children and colliders.
}
},
"geometry": {
"type": "Geometry",
"properties": { "geometryType": "box", "args": [2, 1, 1] }
},
"material": { "type": "Material", "properties": { "name": "stone", "color": "#999999" } }
}
}]
}
}
Common asset entries under components:
// Model assets retain their embedded materials; a sibling Material won't override them.
"model": { "type": "Model", "properties": { "filename": "/models/tree.glb" } }
// For primitive meshes: texture is the color map, normalMapTexture the normal map.
"surface": { "type": "Material", "properties": { "texture": "/textures/stone.jpg" } }
// Compose another scene document. Relative asset URLs resolve against basePath.
"building": { "type": "PrefabRef", "properties": { "url": "/prefabs/building.json" } }
Use known asset URLs; remote assets need CORS. Validate loading in the browser.
Keep one active Camera and Fog per scene. CameraFollow targets a local node;
its offsets are world-space. Edit mode uses editor camera controls.
Scripted behavior
Add this entry under a node's components, through JSON or the editor API:
"spin": {
"type": "Runtime",
"properties": {
"data": { "speed": 1 },
"setup": "const y = object.rotation.y; return () => { object.rotation.y = y; };",
"update": "object.rotation.y += data.speed * delta;"
}
}
setup returns optional cleanup; update receives seconds as delta. Both run
while enabled in Play after preparation. Code changes restart setup; data changes
do not. Use context.data for current inputs inside setup-created callbacks.
Scripts share state and receive nodeId, node, object, data, prefab,
events, and context. They execute as trusted page code without React hooks.
Read the Runtime schema for field details; use a source component when hooks are needed.
Custom components
Use application source for reusable behavior, React hooks, and new visual components.
Settings only
name and properties are required. View is optional; settings alone do not run behavior.
import { registerComponent, type Component } from 'react-three-game/viewer';
const Health: Component<{ max: number }> = {
name: 'Health',
properties: { max: { default: 100 } },
};
registerComponent(Health);
Settings with behavior
Add a View to use React hooks and the live node object. Return children to preserve composition.
import { useFrame } from '@react-three/fiber';
import { registerComponent, useNode, useGameObject,
type Component, type ComponentViewProps } from 'react-three-game/viewer';
type SpinProps = { speed: number };
function SpinView({ properties, enabled, children }: ComponentViewProps<SpinProps>) {
const object = useGameObject();
const { editMode, preparing } = useNode();
// Animate the live object; document writes are for authored edits.
useFrame((_, delta) => {
if (enabled && !editMode && !preparing && object.transform) {
object.transform.rotation.y += properties.speed * delta;
}
});
return <>{children}</>;
}
const Spin: Component<SpinProps> = {
name: 'Spin', View: SpinView,
// Defaults feed both the view and the generated inspector.
properties: { speed: { default: 1, step: 0.1 } },
};
registerComponent(Spin);
Attach to a node
Import the registration module before mounting the editor or viewer, then add an
entry under the node's components. The instance key is local to the node;
type matches the registered name.
"spin": { "type": "Spin", "properties": { "speed": 2 } }
Property and rendering contracts
Fields inside a component definition:
properties: {
speed: { default: 1 }, // Numbers infer their type.
label: { type: 'string', default: 'Box' }, // Other values specify a type.
mode: {
type: 'select', default: 'walk',
options: [{ value: 'walk', label: 'Walk' }, { value: 'run', label: 'Run' }],
},
},
// View receives resolved defaults; don't duplicate them in the view.
// Ordinary fields get an inspector automatically.
// Omit slot for ordinary behavior. When supplying a render-graph part:
slot: 'geometry', // Or 'object' / 'material'; each slot is exclusive on its node.
// Geometry gets an implicit mesh; object views supply their own render object.
// The View must implement attachment and preserve children where appropriate.
For a custom inspector, in an editor-only module:
import { registerComponentEditor } from 'react-three-game/editor';
registerComponentEditor(Spin, SpinInspector); // App-defined component and inspector.
// Keep this module out of runtime imports.
State and resources
| Need | Use |
|---|---|
| Current node object | useGameObject() |
| Node/edit state | useNode() |
| Document and local objects | usePrefab() |
| Shared scene/mode | useScene() |
| Serializable edit | Prefab mutations |
| View animation | Refs/live Three objects in R3F useFrame |
| Gameplay simulation | Host-owned state and tick loop |
| URL-backed chunk | PrefabInstance |
Load and activate chunks
Inside the canvas:
import { PrefabInstance } from 'react-three-game/viewer';
// Mount to load, compile, and activate automatically.
<PrefabInstance id="courtyard" url="/prefabs/courtyard.json" />
// onStatus: preparation status/errors. onActivate: gameplay is active.
// Unmount to release. Use static only when the chunk will remain immutable.
Modify geometry
const Stretch: Component<{ amount: number }> = {
name: 'Stretch',
properties: { amount: { default: 1 } },
modifyGeometry: (source, { amount }) => source.clone().scale(1, amount, 1),
};
registerComponent(Stretch);
Return a new owned geometry; leave the source untouched. Modifiers run in component
order. The optional third argument supplies node scale as context.scale.
Declare assets and share materials
In an asset-backed component definition, declare each resource used by its view:
dependencies: ({ filename }) => [{ kind: 'model', path: filename }],
Kinds are model, texture, sound, and prefab; paths respect basePath.
Custom shared materials use useSharedMaterialResource and remain immutable.
Focused references
Read only the reference needed for the task: