Skip to main content

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.

note

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โ€‹

ComponentRole
React and TypeScriptAuthentication UI, navigation, project screens, accessible object list, and property forms
Existing application API clientAuthenticated project loading and saving
Godot with GDScriptScene composition, camera, selection, manipulation, physics, frame loop, and undo
Exported web page and adapterTranslate a bounded browser command/event contract to runtime operations
Backend and storageAuthorization, 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โ€‹

StateOwnerBoundary Rule
Auth UI, navigation, projects, backend requestsReactThe backend authorizes every project read and write
Canonical in-session scene and selectionGodotReact sends commands; accepted runtime events update the inspector
Camera, manipulation, physics, frame loopGodotDo not stream every frame into React state
Committed scene undo/redo historyGodotBoth property forms and viewport gestures use the same command path
Inspector projection and temporary form draftsReactDrafts are not a second canonical scene model
Saved snapshots and project versionsBackendRuntime 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:

command.json
{
"type": "set-property",
"sessionId": "edit-session-42",
"commandId": "command-17",
"baseRevision": 6,
"objectId": "chair-12",
"property": "positionX",
"value": 2.5
}
event.json
{
"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โ€‹

OptionBoundaryWork to Account For
Iframe and web adapterReact exchanges messages with the exported page; the adapter bridges to GodotOrigin checks, frame focus, readiness, and reload handling
Direct custom HTML/canvasReact shares a document with the export's Engine loader; a web adapter uses JavaScriptBridgeCanvas 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.origin and event.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.

  1. Register the shell's message listener before navigating the frame. The adapter registers its listener before announcing that it can receive initialization.
  2. 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.
  3. Show loading and initialization errors in React, with a deliberate retry action. Reject unknown sessions and late events from a previous project or runtime.
  4. 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.
  5. 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:

layout-snapshot.json
{
"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โ€‹

TaskAcceptance Evidence
Export and shell lifecycleLoading, ready, failure, retry, unmount, and project switching behave predictably
Contract and validationWrong origin/source, malformed data, stale sessions/revisions, and duplicate commands do not mutate the scene
Selection and inspectorViewport and DOM selection agree; drafts, rejected edits, and accepted values are distinguishable
Manipulation and undoA drag is one transaction; cancel restores the object; inspector and viewport edits share history
PersistenceSave/reload reconstructs the same allowed objects; invalid snapshots and conflicting saves are rejected
Accessibility and browser evaluationThe 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.