Skip to content

ScriptBehaviourSystem

Loads user-authored Behaviour scripts via dynamic import(), instantiates their classes, and drives the full Behaviour lifecycle (init, onUpdate, onDestroy) each frame.

Overview

ScriptBehaviourSystem runs every frame and is responsible for:

  • Discovering entities with a ScriptComponent and iterating their entries array.
  • Dynamically importing each script's compiled URL the first time it is seen (or when the URL changes).
  • Resolving the exported Behaviour subclass by name (or falling back to the default export).
  • Instantiating the class and calling init() once.
  • Calling onUpdate(deltaTime) on every active script instance each frame.
  • Detecting stale instances (removed entries, changed URLs, destroyed entities) and calling onDestroy() before discarding them.

Queried components

ComponentAccess
ScriptComponentRead (entries, defaultData, runtimeData)

Behaviour resolution

The system resolves the Behaviour constructor from the imported module using:

  1. Named export matching ScriptEntry.className.
  2. default export as a fallback.
  3. A __reluBehaviourClass static marker check to handle cases where esbuild bundles a second copy of @relu-interactives/spatial-ecs into the script blob (breaking the instanceof identity check).

If no valid Behaviour subclass is found the entry is skipped and an error is written to the console.

Behavior notes

  • Instance keying — each script instance is keyed by entityId + scriptUrl + className. A new instance is created only when any of these three values changes.
  • Error isolation — errors thrown inside init() or onUpdate() are caught per-instance and logged to the console. A failing script does not crash the frame loop or affect other scripts.
  • Destroyed entity cleanup — when an entity is removed from the world, all of its script instances are destroyed and removed from the registry on the next update.
  • Editor vs preview — the system is used in both the editor and preview. By default, scripts are not executed in the editor. Set executeInEditor: true on a ScriptEntry to opt a script into full lifecycle execution (including init()) while the editor is open. Scripts without this flag stay in idle state in editor mode and never call init() or onUpdate().

Editor mode execution

When ScriptEntry.executeInEditor is true:

  1. The script module is loaded and the class is instantiated as normal.
  2. init() is called once (same as preview).
  3. onUpdate() is called every frame while the script is enabled.
  4. Direct Three.js scene access is available via this.world.getScene(), getCamera(), and getRenderer().

This allows scripts to create geometry, shaders, materials, and any other Three.js construct that the editor's object palette does not expose.

WARNING

Writing to ECS components (e.g. TransformComponent) every frame in editor mode will generate an undo history entry on every tick. Keep per-frame mutations in local Three.js state to avoid flooding the undo stack.

See Running scripts in the editor for usage examples.

API reference