React + Godot Scene Editing
A conceptual recipe for a React application with a Godot-powered scene viewport. Use it to scope a bounded layout editor after evaluating the alternatives in Choosing a Web 3D Architecture.
The illustrative baseline is a single-threaded Godot web export without extension support, embedded in an iframe. This makes the document boundary explicit; it is not a universal default or a tested, runnable integration.
Project Overviewโ
Users open a project, select an allowed object, change its properties through HTML forms or viewport manipulation, undo changes, and save a layout. Developers author reusable scenes in Godot. The exported application implements the end-user editing tools; it does not embed the Godot editor.
Tech Stackโ
| Component | Role |
|---|---|
| React and TypeScript | Authentication UI, navigation, project screens, accessible object list, and property forms |
| Existing application API client | Authenticated project loading and saving |
| Godot with GDScript | Scene composition, camera, selection, manipulation, physics, frame loop, and undo |
| Exported web page and adapter | Translate a bounded browser command/event contract to runtime operations |
| Backend and storage | Authorization, snapshot validation, project version checks, and durable persistence |
Renderer, scripting, isolation, and browser limitations are maintained in Godot Web. Check those constraints before adopting this recipe.
Architecture Overviewโ
State Ownershipโ
| State | Owner | Boundary Rule |
|---|---|---|
| Auth UI, navigation, projects, backend requests | React | The backend authorizes every project read and write |
| Canonical in-session scene and selection | Godot | React sends commands; accepted runtime events update the inspector |
| Camera, manipulation, physics, frame loop | Godot | Do not stream every frame into React state |
| Committed scene undo/redo history | Godot | Both property forms and viewport gestures use the same command path |
| Inspector projection and temporary form drafts | React | Drafts are not a second canonical scene model |
| Saved snapshots and project versions | Backend | Runtime state is not durable until the save is acknowledged |
For example, typing 2. into a width field is a React draft. Submitting a valid width sends a command; Godot validates and applies it, then publishes the accepted value. A rejected command leaves the canonical scene unchanged and produces a field-level error. A viewport selection change refreshes the inspector projection and explicitly discards or resolves any draft for the previous selection.
Key Patternsโ
Command and Event Contractโ
Use stable domain object IDs, not node paths or transient runtime instance IDs. Start with a small command set: initialize a project, select an object, set an allowed property, undo, redo, and request a snapshot. Events report readiness, accepted changes, rejections, snapshots, and runtime errors.
An illustrative property command and acknowledgement might look like this:
{
"type": "set-property",
"sessionId": "edit-session-42",
"commandId": "command-17",
"baseRevision": 6,
"objectId": "chair-12",
"property": "positionX",
"value": 2.5
}
{
"type": "property-accepted",
"sessionId": "edit-session-42",
"commandId": "command-17",
"revision": 7,
"objectId": "chair-12",
"property": "positionX",
"value": 2.5
}
The session identifies one runtime/project lifecycle. A fresh session on reload lets the shell reject old events. Revisions order committed scene changes; command IDs correlate pending edits with acceptance or rejection. A stale base revision should produce an explicit rejection and a refreshed projection, not silently overwrite newer state. A duplicate command ID must not apply the edit twice.
For an initial implementation, serialize committed inspector commands and disable conflicting edits while a manipulation transaction is active. This keeps synchronization understandable without introducing a distributed event system. Acknowledgements mean the runtime accepted the edit, not that the backend saved it.
Undo and Gesture Transactionsโ
Godot owns the undo history for committed scene edits. Start a drag transaction with the original transform, apply live preview changes inside the runtime, and commit the final transform as one undoable operation. Cancellation restores the original transform without adding history. Do not create an undo entry for every pointer movement.
HTML property submissions use this same runtime history. React may keep ordinary text-field editing behavior while a field is focused, but application-level scene undo must not compete with a second React undo stack.
Embedding Optionsโ
| Option | Boundary | Work to Account For |
|---|---|---|
| Iframe and web adapter | React exchanges messages with the exported page; the adapter bridges to Godot | Origin checks, frame focus, readiness, and reload handling |
| Direct custom HTML/canvas | React shares a document with the export's Engine loader; a web adapter uses JavaScriptBridge | Canvas ownership, loader integration, callback lifetime, and cooperative shutdown |
For direct embedding, customize the exported HTML shell, provide the intended canvas to Engine, and handle asynchronous startGame() success and failure. React must not recreate the active canvas during unrelated renders. Godot's JavaScriptBridge exposes JavaScript interfaces and callbacks; retain callback references for their required lifetime.
Do not assume the loader provides a general-purpose instance disposal API. requestQuit() and onExit are cooperative lifecycle mechanisms, not guaranteed cleanup after a crash. Engine.unload() is not instance teardown. Verify loader behavior against the selected Godot release, particularly where older shell class-reference examples differ.
Message Boundary Securityโ
For the iframe baseline, use window.postMessage only between the expected shell and runtime page:
- The parent checks both the exact runtime
event.originandevent.source === iframe.contentWindow. - The child checks both the configured shell origin and
event.source === window.parent. - Both send with an explicit, exact
targetOrigin, never'*'. - Both validate payloads at runtime: allowed message types, field types, current session, object IDs, property allowlists, value bounds, and payload size limits. TypeScript types alone do not validate incoming data.
- The adapter dispatches only known operations. It does not evaluate script or accept arbitrary node paths, resource paths, or method names.
Origin and source checks do not replace payload validation or server authorization. The session ID is a correlation value, not a credential. Keep runtime assets and adapter code trusted; embedding an iframe is not a substitute for a security review.
The runtime does not need the user's authentication tokens in this design. React loads authorized domain data and sends only the required scene data to Godot. React submits snapshots through the normal authenticated API client, and the server verifies access to the project and referenced assets on every request.
Readiness and Lifecycleโ
Model explicit loading, ready, error, and disposed states. An iframe load event does not prove that the Godot runtime initialized successfully.
- Register the shell's message listener before navigating the frame. The adapter registers its listener before announcing that it can receive initialization.
- Establish a fresh session, initialize the runtime with validated project data, and wait for an application-level ready event before enabling scene commands. A bounded retry/timeout handshake avoids depending on a single startup message.
- Show loading and initialization errors in React, with a deliberate retry action. Reject unknown sessions and late events from a previous project or runtime.
- Before project switches, reloads, or navigation, resolve unsaved edits with save, discard, or cancel. Save must complete before treating navigation as safe; a browser shutdown cannot be relied on to finish a save.
- On unmount, stop sending commands, remove listeners, clear handshake timers, invalidate pending requests, and request cooperative shutdown when possible. For the iframe baseline, remove the frame after cleanup. For direct embedding, release adapter references and handle runtime exit without assuming instant memory reclamation.
After a runtime crash, offer recovery from the last successfully saved snapshot and make potential unsaved loss visible. Do not silently replay pending edits into a fresh session.
Snapshot Persistenceโ
Persist versioned domain JSON, not arbitrary .tscn scenes, scripts, or serialized engine objects. For example:
{
"schemaVersion": 1,
"objects": [
{
"id": "chair-12",
"assetId": "catalog-chair-basic",
"position": [2.5, 0, 1],
"rotation": [0, 0, 0],
"scale": [1, 1, 1]
}
]
}
Define coordinate axes, units, and rotation conventions in the domain schema. Validate schema versions, unique IDs, numeric bounds, object counts, and allowed asset references before reconstructing known runtime scenes. Migrate supported old schemas explicitly and reject unsupported ones. The server repeats validation and authorization; client validation is only an early check.
Request a coherent snapshot from Godot after pending commands and active gestures have settled. Associate it with the current session and scene revision, then send it to the backend with the expected persisted project version. The backend should reject conflicting saves rather than overwrite another update.
Track the saved revision separately from the current runtime revision. If the user edits while a save is in flight, successful persistence of the earlier snapshot must not clear the newer dirty state. Do not persist undo history unless it is a separate product requirement.
Accessible Editingโ
A canvas does not automatically expose scene objects or controls as semantic HTML. Provide a keyboard-accessible DOM object list, labeled property inputs, and scene undo/redo controls that call the same commands as viewport interaction.
Give the iframe a descriptive title, define keyboard focus transitions between the inspector and viewport, and expose accepted changes and errors through appropriate DOM feedback. Essential edits must be possible without a precision drag or a visual-only gizmo.
Task Breakdownโ
| Task | Acceptance Evidence |
|---|---|
| Export and shell lifecycle | Loading, ready, failure, retry, unmount, and project switching behave predictably |
| Contract and validation | Wrong origin/source, malformed data, stale sessions/revisions, and duplicate commands do not mutate the scene |
| Selection and inspector | Viewport and DOM selection agree; drafts, rejected edits, and accepted values are distinguishable |
| Manipulation and undo | A drag is one transaction; cancel restores the object; inspector and viewport edits share history |
| Persistence | Save/reload reconstructs the same allowed objects; invalid snapshots and conflicting saves are rejected |
| Accessibility and browser evaluation | The shared workflow works from DOM controls and on target mobile devices |
Use the architecture evaluation rubric for startup, memory, responsiveness, authoring, and integration measurements. Threaded deployment is a separate decision governed by the Godot Web isolation requirements.
Exclusionsโ
- No production scaffold, runnable bridge implementation, completed prototype, or measured delivery estimate.
- No full Godot editor, arbitrary scene/script uploads, or unrestricted asset import.
- No multiplayer editing, offline synchronization, persisted undo history, or physics-state checkpointing.
- No assumption that a single-threaded iframe meets every project's performance, security, or deployment requirements.