ScriptObjectSystem
Loads, builds, ticks, and disposes ScriptObject scripts attached via ScriptObjectComponent. Unlike ScriptBehaviourSystem, it runs in the editor AND at runtime — there is no isEditorMode() gating.
Lifecycle
For each entity with an enabled ScriptObjectComponent that has a scriptUrl + className:
- Load — the module is fetched via the
moduleLoader, the class resolved (named export →default→__reluScriptObjectClassmarker), and instantiated.runtimeStatetransitionsidle → loading. - Build —
build()runs; the returnedTHREE.Object3Dis tagged (userData.__scriptObjectRoot) and attached under the entity'sObject3DRef.object.runtimeState → ready. - Update — every frame while enabled and visible,
update(deltaTime)is called. Inspector data edits are pushed tothis.databetween frames (dirty-checked, not per-frame). - Dispose — on removal, disable, script change, or
invalidate*(),dispose()is called and the built subtree is removed and its geometries/materials/textures freed (shared model-cache resources are skipped).
runtimeState is written only on transitions (never every frame). A build()/update() error sets runtimeState: "error" with a sanitized message and tombstones the record so a broken script doesn't reload every frame (cleared by a script/enabled change or invalidate*()).
Keying
Instances are keyed by entity id — a duplicated entity gets its own fresh instance, and two entities sharing one scriptUrl get independent instances.
Hot reload
invalidateAll()— disposes every live instance so the nextupdate()reloads with freshly compiled modules. The editor calls this on its asset-changed epoch.invalidate(scriptUrl)— same, scoped to one script.
The moduleLoader owns cache-busting: with the default dynamic-import loader the URL must change for fresh code to load; invalidate*() guarantees re-instantiation.
Wiring
Construct it and pass it to World.setup:
import {
ScriptObjectSystem,
World,
type ModuleLoader,
} from "@relu-interactives/spatial-ecs";
declare const world: World;
// Resolves a script URL to its ES module — the templates supply this.
declare const myModuleLoader: ModuleLoader;
world.setup({
systems: [new ScriptObjectSystem(myModuleLoader)],
isEditor: false,
});API reference
- Class:
ScriptObjectComponent - Base class:
ScriptObject - Source:
core/systems/ScriptObjectSystem.ts

