05 — Scripting: Entity, Behavior, @exposed, @inputMap
API tables: 14 — API Guide · Feature inventory: 13 — Feature List
Subsystem diagram: scripting.html · Code traces: lifecycle · components · @exposed · Blender @exposed · Blender components
Entity (core/Entity.ts)
The runtime wrapper around one Blender object: id (GUID), name, node (the glb TransformNode), tag, attachments (live registry — one row per successfully applied component; types in core/attachments.ts), behaviors, body? (Havok), animations (its AnimationGroups), sounds (its StaticSounds).
Component vs behavior: a *component* is authored data (TAG, COLLIDER, SCRIPT, …); a *behavior* is a runtime script class from a SCRIPT component. attachments is the single source of truth for "what components does this entity have?" — each row pairs manifest data with its runtime object when one exists (behavior, body, sound, texture, system, constraint, control). Query via GetAttachment(type) (first row), GetAttachmentsOfType(type), or HasAttachment(type) — e.g. GetAttachment("SCRIPT")?.behavior. Convenience lookups: GetBehavior(Ctor) (by class), GetAnimation(name), GetSound(name) (exact match then contains), and SendMessage(message, source) → every behavior's OnMessage. There is no entity.manifest or GetComponent. Full apply chain: trace-components · Blender authoring: Blender trace-components.
Three ways to reach another entity: an @exposed({type:"entity"}) field (cleanest), node.metadata.bjsEntity from a node, or level.ById/ByTag.
Behavior lifecycle (scripting/Behavior.ts)
Nine hooks — override only what you need. Names are PascalCase; a lowercase onStart silently never runs (it stops overriding the base hook). Interactive trace with Babylon integration: trace-lifecycle.
| Hook | When | Notes |
|---|---|---|
OnStart(): void | Once, after the whole level loads and every @exposed entity ref resolves | Cross-entity order unspecified — guard null refs. Subscribe observers here; clean up in OnDestroy. |
OnUpdate(deltaSeconds: number): void | Every render frame | deltaSeconds is seconds (Engine.getDeltaTime() / 1000). Scale continuous motion by it. Babylon velocities are already per-second — do not multiply those by deltaSeconds. |
OnDestroy(): void | Level.Dispose | Remove observers, dispose constraints you created. Errors are swallowed during teardown. |
OnMessage(message, source): void | Event Messages, 3D GUI clicks, or SendMessage | source is the sending entity. See trace-trigger for the physics path. |
OnCollisionEnter(other, contact) | Solid collider first contacts another body | Both bodies receive the callback (Unity semantics). Opt-in — only overridden hooks enable Havok collision callbacks. |
OnCollisionStay(other, contact) | While solid contact continues | Direct relay of Havok COLLISION_CONTINUED; stops when bodies sleep (like Unity). |
OnCollisionExit(other) | Solid contact ends | Requires collision-ended callbacks on the body. |
OnTriggerEnter(other) | Trigger volume first overlapped | No per-body enable flag — plugin observable fires for trigger shapes. |
OnTriggerExit(other) | Trigger overlap ends | No OnTriggerStay — Havok has no TRIGGER_CONTINUED; track overlap state in code if needed. |
Injected before OnStart: entity, scene; node is a getter for entity.node; input? when the script has no @inputMap fields (scene default map).
How it plugs into Babylon
Hooks are not called during InstantiateScripts — only after Level.Begin at end of load. Each scene.render() (from the app's runRenderLoop) fires RunFrame on onBeforeRenderObservable: input process → all OnUpdate → camera updaters → input end frame. Full sequence diagram, delta-time source, and physics step order: 02 — Runtime Basics · trace-runtime-loop. Load pass detail: 04 — Load Pipeline. Input edges: 12 — Input (frame lifecycle).
The registry contract (scripting/BehaviorRegistry.ts)
BehaviorRegistry maps a SCRIPT component's name → Behavior class (it registers *behaviors*; TAG/COLLIDER/etc. are applied directly by the loader). main.ts auto-registers every file in behaviors/ by filename stem via import.meta.glob + AutoRegisterBehaviors — exactly the key Blender's "Open Script…" picker stores. Hence the contract: one class per file, file named after the class, export default.
@exposed (scripting/exposed.ts)
Marks a field editable per-object in Blender. The decorator only records name + UI hints in a WeakMap; ApplyExposedVars writes the manifest's stored values onto the instance before OnStart (entity refs deferred to the loader's second pass; lists handled per element type). Types: float int bool string vector2 vector3 color entity enum list (+ of for list elements). Blender can't run TS — core/script_parse.py regex-parses the source — so: single-line literal defaults only, explicit type:"entity" hints, entity lists start [], and the decorator name stays lowercase (the cross-language contract; the one PascalCase exception, see STYLE_GUIDE). Press Sync in Blender after changing exposed fields.
Input
Input Actions, bindings, @inputMap, and per-frame polling are covered in 12 — Input.