← Index · Prev · Next →

01 — Architecture

Writing behaviors? Skim the two-artifact table below, then jump to 02 — Runtime Basics and 14 — API Guide. Come back here when you need GUID matching or the repo layout.

The core idea: two artifacts

If glTF can already express it, the glb owns it and the engine writes no code for it. If it can't, it goes in the manifest and a small subsystem applies it.

For how the loaded Level hooks into Babylon's render loop (runRenderLoop, OnUpdate, delta time), read 02 — Runtime Basics next.

ArtifactCarriesConsumed by
level.glbgeometry, transforms, hierarchy, materials (standard PBR), lights, cameras, animation clipsBabylon's glTF importer
level.scene.jsoncomponents (TAG/RENDERING_GROUP/LAYER_MASK/COLLISION_LAYER/COLLIDER/RIGIDBODY/SCRIPT/CAMERA/AUDIO/CONSTRAINT/…), optional materials[] (Node Material Editor overrides), per-light/camera/scene settings, scene.inputActions + scene.defaultInputMap, scene.collisionLayers, schema v4our LevelLoader

The manifest never duplicates the glb — it only adds. This is why there's a subsystems/physics/ but no meshes.ts: meshes ride entirely in the glb.

Entity identity (GUIDs)

Every addressable Blender object gets a GUID in a custom property (bjs_id, defined once as ID_KEY on both sides). The exporter writes it into the glTF node's extras; the manifest references the same GUID. At load it surfaces at node.metadata.gltf.extras.bjs_id (via the ExtrasAsMetadata loader extension) and the loader matches manifest entity → glb node by GUID, name as a legacy fallback.

A GUID is what makes an object an entity; pure geometry without one just lives in the glb. GUIDs are auto-assigned to anything that needs to be findable: objects with components, lights, cameras, and any object referenced by an entity field, trigger event, or constraint target.

The GUID is also the discriminator for two subtle cases: multi-material submeshes have no GUID (so they're "owned" by their wrapper — see Physics), while real parented children do (so they're excluded from a parent's collider).

Visibility and shadows (export → load)

Optional detail — skip on first read unless you're debugging invisible objects or shadow casters.

Monorepo layout

bjs-level-kit/
  package.json            # npm workspaces root: dev / typecheck / create
  packages/engine/        # "@bjs/engine" — the runtime, shared by every app
    src/
      index.ts            #   public barrel (apps import "@bjs/engine")
      core/               #   types.ts (schema) · attachments.ts · Entity.ts · Level.ts · LevelLoader.ts
                          #   loader/ (manifest · nodeResolution · entityBuilder · componentRegistry · scripts · sceneSettings)
      scripting/          #   Behavior.ts · exposed.ts · BehaviorRegistry.ts
      input/              #   InputManager · maps/actions/bindings · @inputMap
      subsystems/         #   physics lights cameras/ shadows constraints audio
                          #   triggers environment fog atmosphere postprocess animation
                          #   materials (NME overrides)
  apps/                   # each game is an app; engine reaches it via symlink
    playground/           #   the dev/test app (Vite); template for npm run create
  blender_addon/          # the editor half (Python, Blender 4.2+ extension)
  scripts/create-app.mjs  # scaffolds apps/<name>
  docs/                   # this documentation, plans, specs

Apps declare "@bjs/engine": "*"; npm install at the root satisfies it with a symlink to packages/engine (no publishing, no copying). The engine ships TS source (exportssrc/index.ts); Vite compiles it across the link and hot-reloads engine edits into running apps. Engine and Blender add-on versions move in lockstep.

End-to-end data flow

Blender scene (objects + components + GUIDs)
   │  Export Level / Live Link            [03-BLENDER-ADDON.html]
   ▼
level.glb  +  level.scene.json  (+ audio/ files, env textures)
   │  Vite dev server serves apps/<app>/public/levels/
   ▼
LevelLoader.Load(manifestUrl)             [04-LOAD-PIPELINE.html]
   │  InputManager.LoadAsset → appendSceneAsync (right-handed) → GUID index
   │  → per-entity pass → second pass (refs, camera targets) → FinalizeLevel
   ▼
Level (entities, update loop) ── behaviors run OnStart/OnUpdate
                                          [05-SCRIPTING.html]

Per-frame order (app runRenderLoopscene.renderRunFrame → Havok step): 02 — Runtime Basics.