02 — Runtime Basics: loop, objects, and Babylon hooks
Subsystem diagram: runtime-loop.html · Code traces: runtime loop · lifecycle · load
Behavior authors: this is usually your first engine chapter — it explains what runs when and how the kit hooks into Babylon.js. Skim 01 — Architecture first only if you need the two-file export model (glb + manifest).
App vs kit
Your game app (e.g. apps/playground/src/main.ts) owns the Babylon Engine, the Scene, and the browser render loop. The kit (@bjs/engine) owns loading a level into that scene and driving gameplay scripts.
| Layer | Typical owner | Role |
|---|---|---|
Engine | App | WebGL/WebGPU context, runRenderLoop, getDeltaTime() |
Scene | App | Babylon scene graph — meshes, lights, cameras, physics plugin |
LevelLoader / Level | Kit | Load manifest + glb; container for entities, subsystems, update loop |
Entity / Behavior | Kit | One Blender object + its SCRIPT instances and component attachments |
TransformNode / PhysicsBody | Babylon | Actual nodes and Havok bodies the kit creates from manifest data |
The kit never calls engine.runRenderLoop — that is always the app. Without it, OnUpdate never runs.
App bootstrap (once, at startup)
Typical order in main.ts:
- Fetch manifest (optional but required for large-world rendering) —
FetchAndValidateManifestthenCreateLevelEngine(canvas, antialias, manifest)fromcore/bootstrap.tswhen the Blender scene enables Large World Rendering. const scene = new Scene(engine)await EnableHavokPhysics(scene, …)— must run beforeLoad; registers Havok on the Babylon scene.- Register behaviors (
BehaviorRegistry+AutoRegisterBehaviors). const level = await loader.Load(manifestUrl, manifest)— async; builds everything and callslevel.Begin()insideFinalizeLevel(see 04 — Load Pipeline).engine.runRenderLoop(() => scene.render())— starts the per-frame loop.
Debug keys (C/I), fallback cameras, and resize handlers are app concerns — see 09 — Workflow.
Load time vs run time
| Phase | When | What happens |
|---|---|---|
| Load | LevelLoader.Load() until it returns | Append glb, build entities, ApplyEntityComponents (the component registry), InstantiateScripts (no lifecycle hooks yet), second pass for entity refs, FinalizeLevel, then Begin() → every OnStart once. |
| Run | Every frame after runRenderLoop | scene.render() → Babylon observables → Level.RunFrame → every OnUpdate. |
OnStart is not called from the render loop. It runs once at the end of load, after physics bodies, triggers, and constraints exist.
The frame loop (how OnUpdate hooks into Babylon)
Each browser frame, the app calls scene.render(). Babylon fires onBeforeRenderObservable callbacks, then steps physics, then draws. The kit registers Level.RunFrame on that observable in Level.Begin().
sequenceDiagram participant App as main.ts participant Engine as Engine.runRenderLoop participant Scene as Scene.render participant Particles as onBeforeRender_first participant Level as Level.RunFrame participant Physics as Havok_step participant Draw as GPU_draw participant Msdf as onAfterRender App->>Engine: requestAnimationFrame tick Engine->>Scene: render() Scene->>Particles: empty emitter positions Scene->>Level: onBeforeRenderObservable Note over Level: InputManager.Process Note over Level: all behavior.OnUpdate Note over Level: Level.AddUpdater callbacks Note over Level: InputManager.EndFrame Scene->>Physics: physics engine step Scene->>Draw: meshes shadows post Scene->>Msdf: MSDF text pass
Key facts:
deltaSecondspassed toOnUpdateisscene.getEngine().getDeltaTime() / 1000(seconds, not milliseconds) — read inside the observer registered inLevel.Begin(core/Level.ts).OnUpdateruns once perscene.render(), ononBeforeRenderObservable, typically before Havok's step and before the GPU draw for that frame.- Particle empty-emitter tracking also uses
onBeforeRenderObservablewithinsertFirst=true, so it runs beforeRunFrame(subsystems/particles.ts). - MSDF text draws on
onAfterRenderObservable— after the main pass (ui/msdfText.ts).
Inside RunFrame, order is fixed:
InputManager.Process()— poll gamepad, evaluate actions, fire callbacks- Every behavior's
OnUpdate(deltaSeconds)(entity iteration order unspecified) - Registered
Level.AddUpdatercallbacks (offset/follow cameras) InputManager.EndFrame()— soWasPressedThisFrameedges last exactly one frame
Interactive trace: trace-runtime-loop · trace-lifecycle.
Behavior lifecycle (summary)
Full scripting detail: 05 — Scripting. Input polling: 12 — Input.
| Hook | When |
|---|---|
OnStart() | Once, end of load (Level.Begin), after all entity refs resolve |
OnUpdate(deltaSeconds) | Every frame, inside RunFrame (see above) |
OnDestroy() | Level.Dispose — remove observers you added in OnStart |
OnMessage(message, source) | Triggers, 3D GUI clicks, or Entity.SendMessage — not tied to the frame loop |
Hooks must be PascalCase (OnStart, not onStart). Scale continuous motion by deltaSeconds; Babylon velocities are already per-second — do not multiply those by deltaSeconds.
Other kit systems on the timeline
| System | Babylon hook | When | Kit entry |
|---|---|---|---|
| Behaviors | onBeforeRenderObservable | Before physics/draw each frame | Level.RunFrame |
| Input actions | Same observer + keyboard observable | Process/EndFrame inside RunFrame | InputManager |
| Particles (empty emitters) | onBeforeRender (insert first) | Before RunFrame | WireParticleEmitterTracking |
| Physics (Havok) | Internal to scene.render() | After beforeRender callbacks | EnableHavokPhysics |
| Animation clips | AnimationGroup | Babylon animation system each frame | ApplyAutoPlayAnimations |
| Post / atmosphere | Camera / effects | Setup in FinalizeLevel; post after Begin | ApplyPostProcessing |
| MSDF text | onAfterRenderObservable | After main draw | WireMsdfTextRendering |
Physics vs OnUpdate
Havok steps after your OnUpdate callbacks for that frame. Practical rules:
- STATIC bodies — never move; colliders only.
- DYNAMIC — use forces, impulses, or set velocity; writing
node.positionevery frame fights the solver unless the body is ANIMATED. - ANIMATED — drive
nodetransforms inOnUpdate(moving platforms, kinematic characters). See 06 — Physics.
Practical rules for behavior authors
- Subscribe to Babylon observables in
OnStart; remove them inOnDestroy. - Use
@exposedentity refs — they resolve beforeOnStart; guardnullanyway. - Do not expect a
Levelhandle on behaviors — uselevel.ByIdfrom app code or@exposedfields. - Component data vs runtime code: TAG/COLLIDER/SCRIPT are manifest components; only SCRIPT rows become
Behaviorclasses. See trace-components.
Where to go next
- 04 — Load Pipeline — what happens during
Load() - 05 — Scripting —
@exposed, registry, entity API - 12 — Input — action maps and frame edges
- 06 — Physics — bodies, triggers, constraints
- 10 — Feature Traces — Blender → runtime file chains