Levels
The level system organizes your game into discrete, self-contained sections. Each level encapsulates its own scene, assets, physics, and entity spawning. The LevelManager handles creation, loading, activation, transitions, and disposal.
LevelManager
The LevelManager is a factory and lifecycle manager for levels. It is available through the engine:
const levelManager = gameEngine.managers.levelManager;Properties
| Property | Type | Description |
|---|---|---|
currentLevel | Level | null | The currently active level |
propertyHandlers | Map<string, PropertyHandler> | Registered scene node property handlers |
Configuration API
| Method | Signature | Description |
|---|---|---|
registerLevelConfig | (config : LevelConfig) => void | Register a level configuration for later instantiation |
registerLevelClass | (name : string, levelClass : LevelConstructor) => void | Register a custom Level class referenced by configs |
getLevelConfig | (name : string) => LevelConfig | null | Look up a registered config |
Loading API
| Method | Signature | Description |
|---|---|---|
loadLevel(name) | (string) => Promise<Level> | Load a registered level by name |
loadLevel(config) | (LevelConfig) => Promise<Level> | Load from a config (auto-registers it) |
activateLevel(name) | (string) => Promise<Level> | Load (if needed) and set as current level |
activateLevel(config) | (LevelConfig) => Promise<Level> | Load from config and set as current |
getLevel | (name : string) => Level | null | Get a loaded level by name |
unloadLevel | (name : string) => Promise<void> | Dispose and remove a loaded level |
unloadCurrentLevel | () => Promise<void> | Unload the active level |
Transition API
| Method | Signature | Description |
|---|---|---|
transition | (levelName : string, options? : TransitionOptions) => Promise<void> | Full level-to-level transition |
Property Handler API
| Method | Signature | Description |
|---|---|---|
registerPropertyHandler | (property : string, handler : PropertyHandler) => void | Register a handler for scene node metadata |
unregisterPropertyHandler | (property : string) => void | Remove a property handler |
clearAllPropertyHandlers | () => void | Remove all property handlers |
Level Base Class
The abstract Level class is the foundation for all levels. You typically use GameLevel (the YAML-driven default) or subclass Level directly for fully custom levels.
Key Methods
| Method | Description |
|---|---|
load() | Triggers buildScene() and emits progress/completion events |
$dispose() | Disposes the scene and all resources |
$emitProgress(progress, message) | Emit a level:progress event (0--100) |
Lifecycle Hooks
| Hook | When Called | Description |
|---|---|---|
onActivate() | Level becomes active during a transition | Start music, show UI, unpause physics |
onDeactivate() | Level is being replaced during a transition | Save state, stop sounds, fade out |
Both hooks are optional and async.
Creating a Custom Level
Extend Level and implement buildScene:
import { Level } from '@skewedaspect/sage';
import { Scene, Vector3, PhysicsShapeType, Color4 } from '@babylonjs/core';
class SpaceStation extends Level
{
protected async buildScene() : Promise<Scene>
{
const { sceneEngine } = this.gameEngine.engines;
const scene = sceneEngine.createScene();
this.$emitProgress(10, 'Creating scene...');
scene.clearColor = new Color4(0, 0, 0.05, 1);
sceneEngine.enablePhysics(scene, new Vector3(0, -1.62, 0));
this.$emitProgress(20, 'Physics configured...');
sceneEngine.createFreeCamera('camera', new Vector3(0, 2, -5), scene);
sceneEngine.createHemisphericLight('ambient', new Vector3(0, 1, 0), scene, 0.3);
this.$emitProgress(40, 'Camera and lighting...');
await sceneEngine.loadModel('assets/models/station.glb', scene);
this.$emitProgress(90, 'Station loaded...');
return scene;
}
async onActivate() : Promise<void>
{
// Start ambient sound, show HUD, etc.
}
async onDeactivate() : Promise<void>
{
// Save player progress, stop sounds
}
}LevelConfig (YAML)
Levels can be defined declaratively in YAML. The GameLevel class processes these configs automatically.
interface LevelConfig
{
name : string;
scene ?: string;
class ?: string;
config ?: Record<string, unknown>;
spawns ?: Record<string, SpawnDefinition>;
entities ?: Record<string, EntityDefinition>;
physics ?: boolean | { gravity ?: { x : number; y : number; z : number } };
preload ?: string[];
environment ?: EnvironmentConfig;
cameras ?: Record<string, CameraDefinition>;
lights ?: Record<string, LightDefinition>;
sounds ?: Record<string, LevelSoundConfig>;
postProcessing ?: PostProcessingConfig;
}LevelConfig Properties
| Property | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique level identifier. Used as the key for transition() and loadLevel() calls. |
scene | string | No | Path to the scene file (.glb, .gltf, .babylon). The file is imported into the scene and its nodes are processed for metadata. |
class | string | No | Name of a registered custom Level class. If omitted, GameLevel is used. |
config | Record<string, unknown> | No | Arbitrary configuration data passed to the Level instance. See Custom Config Data. |
spawns | Record<string, SpawnDefinition> | No | Spawn point definitions. Keys match the spawn metadata values on scene nodes. |
entities | Record<string, EntityDefinition> | No | Entity config overrides. Keys match the entity metadata values on scene nodes. |
physics | boolean | { gravity } | No | Enable physics. true uses default gravity (0, -9.81, 0). An object allows custom gravity. |
preload | string[] | No | Asset paths to cache before building the scene. |
environment | EnvironmentConfig | No | Skybox and IBL (image-based lighting) configuration. See Environment Configuration. |
cameras | Record<string, CameraDefinition> | No | Camera configuration. Keys match Blender camera names (override) or define new cameras (create). See Camera Configuration. |
lights | Record<string, LightDefinition> | No | Light configuration. Keys match Blender light names (override) or define new lights (create). See Light Configuration. |
sounds | Record<string, LevelSoundConfig> | No | Level-scoped sounds tied to the level lifecycle. See Level Sounds. |
postProcessing | PostProcessingConfig | No | Declarative post-processing effects applied after the scene is built. |
Custom Config Data
The config field lets you pass arbitrary data to custom GameLevel subclasses. When a level uses a custom class (via the class field), the subclass can access this data through this.config.config (or more commonly, this._config.config):
name: Arena
class: arenaLevel
scene: /assets/arena.glb
physics: true
config:
difficulty: hard
enemyCount: 12
timeLimit: 120
theme: fireimport type { Scene } from '@babylonjs/core';
import { GameLevel } from '@skewedaspect/sage';
export class ArenaLevel extends GameLevel
{
protected override async buildScene() : Promise<Scene>
{
const scene = await super.buildScene();
// Access the custom config data
const difficulty = this._config.config?.difficulty as string ?? 'normal';
const enemyCount = this._config.config?.enemyCount as number ?? 5;
const timeLimit = this._config.config?.timeLimit as number ?? 60;
// Use the config to drive level-specific logic
if(difficulty === 'hard')
{
this._spawnExtraTraps();
}
for(let i = 0; i < enemyCount; i++)
{
this._spawnEnemy();
}
return scene;
}
}The config field is ignored by GameLevel itself -- it only stores it. This makes it a clean extension point for your own level classes without modifying the engine.
SpawnDefinition
interface SpawnDefinition
{
entity : string;
config ?: Record<string, unknown>;
}| Property | Type | Required | Description |
|---|---|---|---|
entity | string | Yes | The entity type name to spawn. Must match a registered entity definition. |
config | Record<string, unknown> | No | State overrides merged into the entity's defaultState. |
When a spawn point is processed, SAGE creates a new entity of the given type at the spawn node's position, rotation, and scale. The config values are merged on top of the entity definition's defaultState:
spawns:
player_start:
entity: player
config:
health: 100
maxHealth: 100
speed: 8The spawn node (typically a Blender Empty) is disposed after the entity is created. If the entity definition includes a mesh descriptor, SAGE auto-creates the mesh at the spawn position.
EntityDefinition
interface EntityDefinition
{
config ?: Record<string, unknown>;
}| Property | Type | Required | Description |
|---|---|---|---|
config | Record<string, unknown> | No | State overrides merged into the entity's defaultState. |
Entity definitions configure entities that already exist as meshes in the scene file. Unlike spawns (which create new entities at empty nodes), the entities section wraps existing scene meshes as SAGE entities:
entities:
door:
config:
locked: true
health: 200
crate:
config:
weight: 50
breakable: trueIn Blender, you set a custom property entity = "door" on the mesh. When the level loads, SAGE finds that mesh, creates a door entity, and attaches the existing mesh as the entity's node. The config values merge into the entity's initial state.
Physics Configuration
Enable physics with default Earth gravity:
physics: trueOr specify custom gravity as a vector:
# Low gravity (moon-like)
physics:
gravity:
x: 0
y: -1.62
z: 0# Zero gravity (space)
physics:
gravity:
x: 0
y: 0
z: 0# Side-scrolling gravity (pushes objects to the right)
physics:
gravity:
x: 5
y: -9.81
z: 0When physics: true, the default gravity vector is (0, -9.81, 0). Physics must be enabled before any collider property handlers can create physics aggregates, so GameLevel initializes physics early in the buildScene() pipeline.
Complete YAML Reference
This annotated example shows every possible field in a level config:
# -----------------------------------------------
# Level name (required)
# Used as the key for transition() and loadLevel()
# -----------------------------------------------
name: My Level
# -----------------------------------------------
# Custom Level class (optional)
# References a class registered with registerLevelClass()
# If omitted, the default GameLevel class is used
# -----------------------------------------------
class: myCustomLevel
# -----------------------------------------------
# Scene file (optional)
# Path to a .glb, .gltf, or .babylon file
# All meshes and nodes are imported into the scene
# Node metadata is processed by property handlers
# -----------------------------------------------
scene: /assets/levels/my-level.glb
# -----------------------------------------------
# Physics (optional)
# true = Havok physics with default gravity (0, -9.81, 0)
# object = custom gravity vector
# omit = no physics
# -----------------------------------------------
physics:
gravity:
x: 0
y: -9.81
z: 0
# -----------------------------------------------
# Asset preloading (optional)
# Paths are loaded sequentially and cached before
# buildScene() runs. Supports GLB fragment syntax
# for preloading specific nodes from a file.
# -----------------------------------------------
preload:
- /assets/models/environment.glb
- /assets/models/props.glb#barrel
- /assets/models/props.glb#crate
- /assets/audio/ambient.mp3
# -----------------------------------------------
# Custom config data (optional)
# Arbitrary key/value pairs passed to the Level
# instance. Accessed via this._config.config in
# GameLevel subclasses. Ignored by GameLevel itself.
# -----------------------------------------------
config:
difficulty: hard
enemyCount: 12
timeLimit: 120
# -----------------------------------------------
# Environment (optional)
# Configures the skybox (visible background) and
# IBL (image-based lighting for PBR reflections).
# See "Environment Configuration" for details on
# file format behavior and the three modes.
# -----------------------------------------------
environment:
ibl: /assets/textures/studio.hdr
iblResolution: 256
skybox: /assets/textures/sky_8k.jpg
skyboxSize: 5000
rotation: 1.57
# -----------------------------------------------
# Spawn points (optional)
# Keys match the 'spawn' metadata values on scene
# nodes (Blender Empties). Each spawn creates a new
# entity at that node's position/rotation/scale.
# -----------------------------------------------
spawns:
# A Blender Empty with custom property spawn="player_start"
player_start:
entity: player
config:
health: 100
maxHealth: 100
# A Blender Empty with custom property spawn="enemy_patrol"
enemy_patrol:
entity: enemy
config:
patrolRadius: 10
aggroRange: 15
# Spawn with no config override (uses entity defaultState)
item_chest:
entity: chest
# -----------------------------------------------
# Entity markers (optional)
# Keys match the 'entity' metadata values on scene
# meshes. Unlike spawns, these wrap EXISTING scene
# meshes as SAGE entities (no new mesh is created).
# -----------------------------------------------
entities:
door:
config:
locked: true
crate:
config:
weight: 50
breakable: true
# -----------------------------------------------
# Cameras (optional)
# Keys that match a Blender camera name apply
# overrides. Keys that don't match create a new
# camera (type is required for new cameras).
# -----------------------------------------------
cameras:
# Override an imported camera's clipping planes
Camera:
minZ: 0.1
maxZ: 500
# Create a new arcRotate camera
MainCamera:
type: arcRotate
alpha: 1.5708
beta: 1.0472
radius: 25
target: { x: 0, y: 2, z: 0 }
active: true
attachControl: true
wheelPrecision: 50
lowerRadiusLimit: 10
upperRadiusLimit: 50
lowerBetaLimit: 0.1
upperBetaLimit: 1.4708
fov: 0.8
minZ: 0.1
# -----------------------------------------------
# Lights (optional)
# Keys that match a Blender light name apply
# overrides. Keys that don't match create a new
# light (type is required for new lights).
# Note: HemisphericLight has no Blender equivalent
# so it is always created via create mode.
# -----------------------------------------------
lights:
# Override a Blender sun's intensity
Sun:
intensity: 0.8
diffuse: { r: 1, g: 0.95, b: 0.8 }
# Create an ambient hemispheric light
ambient:
type: hemispheric
direction: { x: 0, y: 1, z: 0 }
intensity: 0.5
diffuse: { r: 0.9, g: 0.9, b: 1.0 }
groundColor: { r: 0.2, g: 0.15, b: 0.1 }
specular: { r: 0, g: 0, b: 0 }
# Create a spot light
spotlight1:
type: spot
position: { x: 5, y: 8, z: 0 }
direction: { x: 0, y: -1, z: 0 }
angle: 0.8
exponent: 3
intensity: 1.5
# -----------------------------------------------
# Level sounds (optional)
# Non-spatial audio tied to the level lifecycle.
# Paused on deactivate, resumed on activate,
# disposed on unload. Requires AudioManager
# (audioChannels in SageOptions).
# -----------------------------------------------
sounds:
background-music:
url: /assets/audio/exploration.ogg
channel: music
loop: true
autoplay: true
volume: 0.7
ambient-wind:
url: /assets/audio/wind-loop.ogg
channel: ambient
loop: true
autoplay: true
volume: 0.3
# -----------------------------------------------
# Post-processing effects (optional)
# Applied after the scene is built. All values are
# optional within each effect block.
# -----------------------------------------------
postProcessing:
bloom:
weight: 0.3 # Bloom intensity (default: 0.15)
threshold: 0.8 # Brightness cutoff (default: 0.9)
scale: 0.5 # Resolution scale (default: 0.5)
kernel: 64 # Blur kernel size (default: 64)
ssao:
radius: 2.0 # Sampling radius (default: 2.0)
samples: 16 # Sample count (default: 8)
totalStrength: 1.0 # Effect intensity (default: 1.0)
tonemap:
operator: aces # Options: hable, reinhard, hejidawson, photographic, aces
chromaticAberration:
amount: 0.5 # Color fringe intensity (default: 30)
grain:
intensity: 15 # Grain density (default: 30)
animated: true # Randomize per frame (default: false)
vignette:
weight: 3.0 # Darkening intensity (default: 1.5)
stretch: 0.5 # Oval stretch factor (default: 0)
color: # Vignette tint (default: black)
r: 0.1
g: 0
b: 0
sharpen:
edge: 0.3 # Edge sharpening (default: 0.3)
color: 1.0 # Color sharpening (default: 1.0)Asset Preloading
Add a preload array to cache assets before buildScene() runs. GLB fragment syntax (model.glb#NodeName) lets you preload specific nodes:
preload:
- assets/models/environment.glb
- assets/models/props.glb#barrel
- assets/models/props.glb#crate
- assets/audio/ambient.mp3Assets are loaded sequentially and cached by the AssetManager. Later calls to assetManager.load() with the same path return instantly from the cache.
Environment Configuration
The environment section configures your level's skybox and image-based lighting (IBL). IBL provides realistic PBR reflections by sampling an environment texture; the skybox provides the visible background. You can use one or both.
Before this feature, setting up a skybox and IBL required a custom GameLevel subclass with manual BabylonJS calls. Now it's fully declarative:
environment:
ibl: /assets/textures/studio.hdr
skybox: /assets/textures/milkyway_8k.jpg
skyboxSize: 5000EnvironmentConfig
interface EnvironmentConfig
{
ibl ?: string;
iblResolution ?: number;
skybox ?: string;
skyboxSize ?: number;
rotation ?: number;
}Properties
| Property | Type | Default | Description |
|---|---|---|---|
ibl | string | -- | Path to an HDR or .env file used as the PBR reflection source. This texture is not visible in the scene -- it only affects how PBR materials reflect light. |
iblResolution | number | 256 | Resolution of the IBL cubemap in pixels. Higher values produce sharper reflections but use more memory. 256 is sufficient for most scenes. |
skybox | string | -- | Path to the skybox texture. File format determines behavior (see below). |
skyboxSize | number | 1000 | Diameter of the skybox in world units. For HDR/env files this is passed to createDefaultSkybox(). For JPG/PNG files this is the sphere diameter. |
rotation | number | -- | Y-axis rotation in radians applied to the environment texture. Use this to align the skybox with your scene. |
File Format Behavior
The file extension of the skybox and ibl paths determines how SAGE creates the texture:
| Extension | Texture Type | Notes |
|---|---|---|
.hdr | HDRCubeTexture | Uncompressed HDR data. Highest quality for reflections and skyboxes. Larger file sizes. |
.env | CubeTexture (pre-filtered) | BabylonJS pre-filtered format. Smaller files, faster loading. Use createEnvTexture in the BabylonJS sandbox to convert HDR files. |
.jpg, .png | Texture on a sphere | Creates a StandardMaterial sphere with the image as emissive texture. No IBL data -- this only provides a visible background. Ideal for high-resolution panoramic images that would lose quality as cubemaps. |
Three Modes
The environment system supports three configurations depending on which fields you provide:
IBL only -- Sets the PBR reflection source with no visible skybox. Useful when you have your own sky geometry or when the scene is indoors.
environment:
ibl: /assets/textures/studio_small.hdr
iblResolution: 128Skybox only with HDR/env -- The skybox file is used for both the visible background and IBL reflections. This is the simplest setup when your skybox texture is in HDR or env format.
environment:
skybox: /assets/textures/sky.hdrSkybox only with JPG/PNG -- Creates a visible sphere background with no IBL. PBR materials will not reflect the environment. Use this when you have a high-resolution panoramic image and don't need reflections.
environment:
skybox: /assets/textures/milkyway_8k.jpg
skyboxSize: 5000Separate IBL and skybox (recommended for quality) -- Uses a low-resolution HDR for fast, accurate reflections and a high-resolution image for the visible background. This avoids the quality loss of converting a detailed panorama to a cubemap.
# Low-res HDR for reflections, high-res JPG for the visible sky
environment:
ibl: /assets/textures/milkyway.hdr
iblResolution: 256
skybox: /assets/textures/milkyway_8k.jpg
skyboxSize: 5000TIP
PBR materials require an environment texture to produce realistic reflections. If your scene uses PBR materials (the default for Blender exports), make sure to set either ibl or a skybox with an HDR/env file. Without an environment texture, PBR surfaces will appear flat and unreflective.
Camera Configuration
The cameras section lets you override cameras imported from Blender or create new ones entirely from YAML. Cameras are processed after the scene file is loaded but before post-processing (which requires an active camera).
CameraDefinition
interface CameraDefinition
{
type ?: 'free' | 'arcRotate' | 'universal';
active ?: boolean;
attachControl ?: boolean;
position ?: Vec3Config;
target ?: Vec3Config;
rotation ?: Vec3Config;
fov ?: number;
minZ ?: number;
maxZ ?: number;
speed ?: number;
alpha ?: number;
beta ?: number;
radius ?: number;
lowerRadiusLimit ?: number;
upperRadiusLimit ?: number;
lowerBetaLimit ?: number;
upperBetaLimit ?: number;
wheelPrecision ?: number;
}Override vs. Create
Camera keys work in two modes depending on whether the name matches an existing camera in the scene:
- Override mode -- The key matches a camera name from Blender (e.g.,
Camera,MainCamera). The camera already exists in the scene; SAGE applies the YAML properties as overrides. Thetypefield is omitted. - Create mode -- The key is a new name that doesn't exist in the scene. The
typefield is required and SAGE creates a fresh camera of that type.
If a key doesn't match a scene camera and has no type, SAGE logs a warning and skips it.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
type | 'free' | 'arcRotate' | 'universal' | -- | Camera type. Required for create mode, omit for override mode. |
active | boolean | false | Mark this camera as the active camera for the level. |
attachControl | boolean | true | Attach canvas input controls to the active camera. Set to false to prevent user interaction (e.g., for cutscenes). Only applies to the camera that becomes active. |
position | Vec3Config | (0, 0, 0) | Camera position in world space. Used in create mode for free and universal cameras. |
target | Vec3Config | (0, 0, 0) | Look-at target. Used by arcRotate cameras (both create and override). |
rotation | Vec3Config | -- | Camera rotation in radians. Used by free and universal cameras (both create and override). |
fov | number | -- | Field of view in radians. Applies to all camera types. |
minZ | number | -- | Near clipping plane distance. |
maxZ | number | -- | Far clipping plane distance. |
speed | number | -- | Movement speed. Applies to free and universal cameras only. |
alpha | number | Math.PI / 2 | Horizontal rotation angle in radians. ArcRotate only. |
beta | number | Math.PI / 3 | Vertical rotation angle in radians. ArcRotate only. |
radius | number | 10 | Distance from the target. ArcRotate only. |
lowerRadiusLimit | number | -- | Minimum zoom distance. ArcRotate only. |
upperRadiusLimit | number | -- | Maximum zoom distance. ArcRotate only. |
lowerBetaLimit | number | -- | Minimum vertical angle (prevents looking from below). ArcRotate only. |
upperBetaLimit | number | -- | Maximum vertical angle (prevents looking from above). ArcRotate only. |
wheelPrecision | number | -- | Mouse wheel zoom sensitivity. Higher values = slower zoom. ArcRotate only. |
Active Camera Selection
When cameras is present in the config, the active camera is selected as follows:
- The first camera with
active: truebecomes active. - If no camera has
active: true, the first camera in the record becomes active.
When cameras is not present, the first camera imported from the scene file (if any) is activated automatically.
The active camera's canvas controls are attached unless attachControl: false is explicitly set on that camera's definition.
Examples
Override a Blender-imported camera's clipping plane:
# The scene file contains a camera named "Camera"
cameras:
Camera:
minZ: 0.1
maxZ: 500Create an arcRotate camera for a top-down view:
cameras:
MainCamera:
type: arcRotate
alpha: 1.5708 # PI/2 — looking from the side
beta: 1.0472 # PI/3 — angled down
radius: 25
target: { x: 0, y: 2, z: 0 }
attachControl: true
wheelPrecision: 50
minZ: 0.1
lowerRadiusLimit: 10
upperRadiusLimit: 50
lowerBetaLimit: 0.1
upperBetaLimit: 1.4708Create a free camera for first-person movement:
cameras:
PlayerCam:
type: free
position: { x: 0, y: 1.8, z: -5 }
rotation: { x: 0, y: 0, z: 0 }
speed: 0.5
fov: 0.8
minZ: 0.1
active: trueLight Configuration
The lights section lets you override lights imported from Blender or create new ones from YAML. Lights are processed after cameras and before post-processing.
LightDefinition
interface LightDefinition
{
type ?: 'hemispheric' | 'directional' | 'point' | 'spot';
intensity ?: number;
diffuse ?: ColorConfig;
specular ?: ColorConfig;
position ?: Vec3Config;
direction ?: Vec3Config;
groundColor ?: ColorConfig;
angle ?: number;
exponent ?: number;
}Override vs. Create
Lights follow the same pattern as cameras:
- Override mode -- The key matches a light name from Blender. SAGE applies the YAML properties (intensity, color, etc.) to the existing light. The
typefield is omitted. - Create mode -- The key is a new name. The
typefield is required and SAGE creates a new light.
TIP
HemisphericLight does not exist in Blender or glTF. If you want ambient hemisphere lighting, you must always use create mode with type: hemispheric. This is the most common use case for the lights section.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
type | 'hemispheric' | 'directional' | 'point' | 'spot' | -- | Light type. Required for create mode, omit for override mode. |
intensity | number | -- | Light brightness. Applies to all light types. |
diffuse | ColorConfig | -- | Diffuse color as { r, g, b } with values 0.0--1.0. |
specular | ColorConfig | -- | Specular highlight color as { r, g, b } with values 0.0--1.0. |
position | Vec3Config | (0, 0, 0) | World position. Used by point and spot lights. |
direction | Vec3Config | (0, -1, 0) | Light direction vector. Used by hemispheric, directional, and spot lights. |
groundColor | ColorConfig | -- | Ground reflection color. Hemispheric lights only. |
angle | number | Math.PI / 3 | Cone angle in radians. Spot lights only. |
exponent | number | 2 | Falloff exponent controlling how light fades from center to edge of the cone. Spot lights only. |
Vec3Config and ColorConfig
Both are simple objects used throughout camera and light configuration:
interface Vec3Config
{
x : number;
y : number;
z : number;
}
interface ColorConfig
{
r : number;
g : number;
b : number;
}In YAML, these can be written in block or inline form:
# Block form
direction:
x: 0
y: -1
z: 0
# Inline form
direction: { x: 0, y: -1, z: 0 }Examples
Override a Blender sun light's intensity:
# The scene file contains a light named "Sun"
lights:
Sun:
intensity: 2.0
diffuse: { r: 1, g: 0.95, b: 0.8 }Create a hemispheric ambient light (always create mode since glTF has no hemispheric lights):
lights:
ambient:
type: hemispheric
direction: { x: 0, y: 1, z: 0 }
intensity: 0.7
diffuse: { r: 0.9, g: 0.9, b: 1.0 }
groundColor: { r: 0.2, g: 0.15, b: 0.1 }
specular: { r: 0, g: 0, b: 0 }Create a spot light:
lights:
spotlight1:
type: spot
position: { x: 5, y: 8, z: 0 }
direction: { x: 0, y: -1, z: 0 }
angle: 0.8
exponent: 3
intensity: 1.5
diffuse: { r: 1, g: 0.9, b: 0.7 }Combine overrides and new lights in a single config:
# Dim the imported sun and add ambient fill
lights:
Sun:
intensity: 0.5
ambientFill:
type: hemispheric
direction: { x: 0, y: 1, z: 0 }
intensity: 0.4
diffuse: { r: 0.6, g: 0.6, b: 0.8 }
groundColor: { r: 0.1, g: 0.1, b: 0.1 }Level Sounds
The sounds section defines audio that is tied to the level's lifecycle. Sounds are created when the level loads, paused when the level is deactivated (during a transition), resumed when the level is reactivated, and disposed when the level is unloaded. This is the right tool for background music, ambient soundscapes, and any non-spatial audio that belongs to a level.
Before this feature, level sounds required a custom GameLevel subclass with manual AudioManager calls and hand-written onActivate/onDeactivate hooks. Now it's declarative:
sounds:
background-music:
url: /assets/audio/exploration.ogg
channel: music
loop: true
autoplay: true
volume: 0.7Prerequisites
Level sounds require the AudioManager to be initialized. You must pass audioChannels in your SageOptions when creating the engine:
const engine = await createGameEngine(canvas, {
audioChannels: [ 'music', 'sfx', 'ambient' ],
});If no AudioManager is configured, SAGE logs a warning and skips sound creation.
LevelSoundConfig
interface LevelSoundConfig
{
url : string;
channel ?: string;
loop ?: boolean;
autoplay ?: boolean;
volume ?: number;
}Properties
| Property | Type | Default | Description |
|---|---|---|---|
url | string | (required) | Path to the audio file (.ogg, .mp3, .wav, etc.). |
channel | string | -- | Audio channel name (e.g., 'music', 'sfx', 'ambient'). Must match a channel name passed in audioChannels. Controls volume grouping. |
loop | boolean | false | Whether the sound loops continuously. |
autoplay | boolean | false | Whether the sound starts playing immediately when the level finishes loading. |
volume | number | 1 | Playback volume from 0.0 (silent) to 1.0 (full). This is the per-sound volume, independent of the channel volume. |
Lifecycle Behavior
Level sounds are managed automatically through the level lifecycle:
| Lifecycle Event | Sound Behavior |
|---|---|
| Level loads | Sounds are created via AudioManager.createSound(). Sounds with autoplay: true begin playing. |
| Level deactivates | All currently playing sounds are paused. SAGE tracks which sounds were playing so only those are resumed later. |
| Level activates | Sounds that were playing before deactivation are resumed. Sounds that were already paused (e.g., not yet triggered) remain paused. |
| Level unloads | All sounds are disposed. |
TIP
If you subclass GameLevel and override onActivate() or onDeactivate(), make sure to call super.onActivate() or super.onDeactivate() so that level sound management continues to work.
Examples
Background music that loops for the duration of the level:
sounds:
theme:
url: /assets/audio/dungeon-theme.ogg
channel: music
loop: true
autoplay: true
volume: 0.6Ambient soundscape layered with music:
sounds:
music:
url: /assets/audio/forest-theme.ogg
channel: music
loop: true
autoplay: true
volume: 0.5
wind:
url: /assets/audio/wind-loop.ogg
channel: ambient
loop: true
autoplay: true
volume: 0.3
birds:
url: /assets/audio/bird-calls.ogg
channel: ambient
loop: true
autoplay: true
volume: 0.2A sound that is loaded but not auto-played (triggered later by game logic):
sounds:
victory-fanfare:
url: /assets/audio/victory.ogg
channel: sfx
loop: false
autoplay: false
volume: 1.0Level sounds vs. the sound property handler
The sounds YAML section creates non-spatial audio tied to the level lifecycle -- think background music and ambient loops. The sound property handler (set on Blender meshes) creates spatial audio positioned in 3D space -- think a torch crackling or a waterfall. They serve different purposes and can be used together in the same level.
Post-Processing
Declarative post-processing effects are applied to the level's scene after it is built and disposed automatically with the scene. A scene must have an active camera for post-processing to take effect.
PostProcessingConfig
interface PostProcessingConfig
{
bloom ?: { weight ?: number; threshold ?: number; scale ?: number; kernel ?: number };
ssao ?: { radius ?: number; samples ?: number; totalStrength ?: number };
tonemap ?: { operator ?: 'hable' | 'reinhard' | 'hejidawson' | 'photographic' | 'aces' };
chromaticAberration ?: { amount ?: number };
grain ?: { intensity ?: number; animated ?: boolean };
vignette ?: { weight ?: number; stretch ?: number; color ?: { r : number; g : number; b : number } };
sharpen ?: { edge ?: number; color ?: number };
}Bloom
Bloom makes bright areas of the scene glow and bleed light into surrounding pixels. It simulates the way real cameras handle very bright light sources.
| Parameter | Type | Default | Description |
|---|---|---|---|
weight | number | 0.15 | Intensity of the bloom glow. Higher values produce a stronger, more visible bloom. Range: 0.0 (off) to 1.0 (very strong). Values around 0.2--0.5 are typical. |
threshold | number | 0.9 | Brightness cutoff. Only pixels brighter than this value produce bloom. Lower values bloom more of the scene. Range: 0.0 to 1.0. |
scale | number | 0.5 | Resolution scale of the bloom texture. Lower values are cheaper but blurrier. Range: 0.0 to 1.0. |
kernel | number | 64 | Size of the blur kernel. Larger kernels produce smoother, wider bloom halos but cost more. Common values: 32, 64, 128. |
# Subtle bloom for a clean look
postProcessing:
bloom:
weight: 0.2
threshold: 0.85
# Heavy bloom for a dreamy or sci-fi feel
postProcessing:
bloom:
weight: 0.8
threshold: 0.6
kernel: 128SSAO (Screen-Space Ambient Occlusion)
SSAO darkens creases, corners, and contact points where ambient light would naturally be occluded. It adds depth and realism to scenes, making geometry feel grounded. SSAO uses its own dedicated pipeline (SSAO2RenderingPipeline), separate from the main DefaultRenderingPipeline.
| Parameter | Type | Default | Description |
|---|---|---|---|
radius | number | 2.0 | Sampling radius around each pixel. Larger values darken wider areas but can look unrealistic. Range: 0.5 to 8.0 for most scenes. |
samples | number | 8 | Number of samples per pixel. More samples reduce noise but cost more. Common values: 8, 16, 32. |
totalStrength | number | 1.0 | Overall strength of the darkening effect. Higher values make occlusion more pronounced. Range: 0.0 to 2.0. |
SSAO is ideal for architectural scenes, dungeons, or any environment where you want creases and corners to feel darker and more defined. It is one of the more expensive effects, so consider using fewer samples on lower-end hardware.
# Subtle ambient occlusion for an indoor scene
postProcessing:
ssao:
radius: 1.5
samples: 16
totalStrength: 0.8
# Strong SSAO for a dungeon with deep crevices
postProcessing:
ssao:
radius: 3.0
samples: 32
totalStrength: 1.5INFO
SAGE creates the SSAO pipeline with ssaoRatio: 0.5 and blurRatio: 1.0. These are not currently configurable through the YAML config.
Tone Mapping
Tone mapping controls how HDR (high dynamic range) colors are compressed into the displayable range. It affects the overall color feel and contrast of the scene.
| Parameter | Type | Default | Description |
|---|---|---|---|
operator | string | 'hable' | The tone mapping algorithm. See below for options. |
| Operator | BabylonJS Mapping | Description |
|---|---|---|
hable | TONEMAPPING_STANDARD | A filmic curve with gentle highlight rolloff. Good general-purpose choice. |
reinhard | TONEMAPPING_STANDARD | Classic Reinhard mapping. Similar to Hable in BabylonJS's implementation. |
hejidawson | TONEMAPPING_STANDARD | Hejl-Dawson mapping. Also maps to standard in BabylonJS. |
photographic | TONEMAPPING_STANDARD | Photographic-style mapping. Also maps to standard in BabylonJS. |
aces | TONEMAPPING_ACES | Academy Color Encoding System. Richer colors with a more cinematic feel. The only option that uses a different BabylonJS algorithm. |
TIP
In practice, the only meaningful choice is between aces and everything else. The operators hable, reinhard, hejidawson, and photographic all map to TONEMAPPING_STANDARD in BabylonJS. If you want a cinematic look with richer, more saturated colors, use aces.
postProcessing:
tonemap:
operator: acesChromatic Aberration
Simulates the color fringing seen in real camera lenses, where red, green, and blue channels are slightly offset. Adds a subtle cinematic or retro quality.
| Parameter | Type | Default | Description |
|---|---|---|---|
amount | number | 30 | Intensity of the color fringing. The BabylonJS default is 30, but for subtle use values of 0.3--2.0 work well. Higher values produce an extreme, obviously distorted effect. |
WARNING
The BabylonJS default of 30 is very strong. For most games, use values between 0.3 and 2.0 for a subtle cinematic effect. The SAGE examples use 0.4.
# Subtle lens fringe
postProcessing:
chromaticAberration:
amount: 0.4
# Heavy distortion (horror, glitch effect)
postProcessing:
chromaticAberration:
amount: 5.0Film Grain
Adds a noise texture over the image, simulating analog film grain. Useful for gritty, horror, or retro aesthetics.
| Parameter | Type | Default | Description |
|---|---|---|---|
intensity | number | 30 | Density of the grain noise. The BabylonJS default is 30, which is quite visible. Values of 5--20 produce a subtle texture. |
animated | boolean | false | When true, the grain pattern changes every frame, creating a shimmering film-like quality. When false, the grain is static. |
# Subtle animated grain
postProcessing:
grain:
intensity: 10
animated: true
# Heavy static grain (VHS / found footage look)
postProcessing:
grain:
intensity: 40
animated: falseVignette
Darkens the edges and corners of the screen, drawing focus toward the center. Often combined with a color tint for mood.
| Parameter | Type | Default | Description |
|---|---|---|---|
weight | number | 1.5 | Intensity of the edge darkening. Higher values produce a more pronounced effect. Range: 0.0 to 10.0. Values around 1.0--4.0 are typical. |
stretch | number | 0 | How much to stretch the vignette into an oval. 0 is circular; higher values elongate it horizontally. Range: 0 to 1.0. |
color | { r, g, b } | Black (0, 0, 0) | The tint color of the vignette. Each channel is 0.0--1.0. |
# Standard dark vignette
postProcessing:
vignette:
weight: 3.0
stretch: 0.5
# Red-tinted vignette (damage indicator, horror)
postProcessing:
vignette:
weight: 4.0
color:
r: 0.5
g: 0
b: 0
# Warm sepia vignette (vintage look)
postProcessing:
vignette:
weight: 2.5
stretch: 0.3
color:
r: 0.3
g: 0.15
b: 0.05Sharpen
Enhances edge contrast to make the image appear crisper. The edge parameter controls how much edges are sharpened, while color controls how much the color channels are boosted.
| Parameter | Type | Default | Description |
|---|---|---|---|
edge | number | 0.3 | Edge sharpening intensity. Higher values make edges more pronounced. Range: 0.0 to 1.0. |
color | number | 1.0 | Color channel sharpening. Higher values make colors more vivid at edges. Range: 0.0 to 1.0. Lower values desaturate sharpened edges. |
# Subtle sharpening
postProcessing:
sharpen:
edge: 0.2
color: 0.8
# Strong sharpening with desaturated edges
postProcessing:
sharpen:
edge: 0.5
color: 0.3Pipeline Architecture
Most effects use the BabylonJS DefaultRenderingPipeline, created with the name 'sage-default'. SSAO uses a separate SSAO2RenderingPipeline named 'sage-ssao'. Both pipelines are attached to all cameras in the scene.
Runtime Tweaks
Access BabylonJS pipelines directly for runtime adjustments:
import { DefaultRenderingPipeline } from '@babylonjs/core';
const pipeline = scene.postProcessRenderPipelineManager
.supportedPipelines
.find((p) => p.name === 'sage-default') as DefaultRenderingPipeline;
if(pipeline)
{
pipeline.bloomWeight = 1.0;
}Scene Node Metadata and Property Handlers
When a level's scene file is loaded, GameLevel walks every transform node and mesh in the scene and reads its metadata. For GLB/glTF files, BabylonJS stores Blender custom properties in node.metadata.gltf.extras. GameLevel normalizes this automatically so property handlers receive flat metadata objects.
Two metadata keys are handled directly by GameLevel (not via property handlers):
spawn-- Marks a node as a spawn point. The value must match a key in the config'sspawnssection.entity-- Marks a node as an entity marker. The value must match a key in the config'sentitiessection.
All other metadata keys are dispatched to registered property handlers.
Built-in Property Handlers
SAGE ships with handlers for six metadata keys. Register them all at once:
import { registerAllPropertyHandlers } from '@skewedaspect/sage';
registerAllPropertyHandlers(levelManager);Or register them individually:
import {
registerColliderHandler,
registerLodHandler,
registerOccluderHandler,
registerSoundHandler,
registerTriggerHandler,
registerVisibleHandler,
} from '@skewedaspect/sage';
registerColliderHandler(levelManager);
registerTriggerHandler(levelManager);
// ... etcProperty Handler Reference
This table lists every metadata key the built-in handlers look for on scene nodes. Set these as custom properties in Blender (or directly in node metadata) to trigger the corresponding handler behavior.
| Metadata Key | Value Type | Handler | Description |
|---|---|---|---|
spawn | string | Built-in (GameLevel) | Marks a node as a spawn point. Value must match a key in the YAML spawns section. The node is disposed after the entity is created. |
entity | string | Built-in (GameLevel) | Marks a node as an entity. Value must match a key in the YAML entities section. The existing mesh is attached to the created entity. |
collider | string | registerColliderHandler | Physics collider shape. Values: "box", "sphere", "mesh", "none". Creates a static physics aggregate (mass = 0). |
collider_mesh | boolean | (Used by collider) | Set on a child mesh to designate it as the collision geometry for a parent with collider = "mesh". The child is made invisible. |
trigger | string | registerTriggerHandler | Trigger zone name. The mesh is made invisible. Emits trigger:enter and trigger:exit events when other meshes intersect it. |
lod_distances | string | registerLodHandler | Comma-separated distances for LOD levels (e.g., "10,25,50"). Child meshes are assigned as LOD levels in order. Beyond the last distance, the mesh is hidden. |
occluder | boolean | registerOccluderHandler | If true, marks the mesh as an occlusion culling occluder. The mesh is made invisible but used by the engine to hide objects behind it. |
visible | boolean | string | number | registerVisibleHandler | Controls mesh visibility. Accepts true/false, "true"/"false", or 1/0. |
sound | string | registerSoundHandler | Path to a sound file. Creates a spatial audio source at the node's position. See Sound Properties for additional prefixed properties. |
Collider Details
The collider handler creates static physics aggregates (mass = 0) for scene geometry. The four collider types:
| Value | Physics Shape | Use Case |
|---|---|---|
"box" | PhysicsShapeType.BOX | Rectangular objects like walls, crates, platforms |
"sphere" | PhysicsShapeType.SPHERE | Round objects like boulders, balls |
"mesh" | PhysicsShapeType.MESH | Complex shapes that need precise collision. If a child node has collider_mesh = true, that child's geometry is used instead (and the child is hidden). |
"none" | None | Explicitly disables collision on a node |
Trigger Details
Triggers create invisible intersection zones. When any non-trigger mesh enters or exits the zone, events are published to the global event bus:
| Event | Payload |
|---|---|
trigger:enter | { trigger : string, other : AbstractMesh } |
trigger:exit | { trigger : string, other : AbstractMesh } |
Triggers automatically detect meshes added after the handler runs (e.g., dynamically spawned entities).
LOD Details
The lod_distances property takes a comma-separated string of distances. The mesh's child meshes are used as LOD levels in order:
lod_distances = "10,25,50"- 0--10 units: First child mesh (highest detail)
- 10--25 units: Second child mesh (medium detail)
- 25--50 units: Third child mesh (low detail)
- 50+ units: Not visible (null LOD)
Sound Properties
The sound handler uses a prefixed property convention. The primary sound property contains the file path, and additional properties use the sound_ prefix:
| Metadata Key | Type | Default | Description |
|---|---|---|---|
sound | string | (required) | Path to the audio file |
sound_volume | number | 1 | Playback volume, 0.0 to 1.0 |
sound_loop | boolean | true | Whether to loop playback |
sound_spatial | boolean | true | Enable 3D spatial audio (positional sound) |
sound_distance | number | 100 | Maximum audible distance for spatial audio |
sound_autoplay | boolean | true | Start playback immediately |
sound_channel | string | 'ambient' | Audio channel name for volume grouping |
Writing a Custom Property Handler
Register custom handlers to process your own Blender custom properties:
import type { TransformNode } from '@babylonjs/core';
import type { LevelInstance, PropertyHandler } from '@skewedaspect/sage';
// Register a handler for nodes with a 'waypoint' metadata property.
// In Blender, set a custom property: waypoint = "patrol_route_a"
levelManager.registerPropertyHandler('waypoint', (
node : TransformNode,
value : unknown,
level : LevelInstance,
gameEngine
) =>
{
const routeName = value as string;
const position = node.position.clone();
// Store the waypoint for your AI system
const aiManager = gameEngine.managers.entityManager;
gameEngine.eventBus.publish({
type: 'waypoint:registered',
payload: {
route: routeName,
position: { x: position.x, y: position.y, z: position.z },
nodeName: node.name,
},
});
// Optionally hide the waypoint marker
node.setEnabled(false);
});The handler function receives four arguments:
| Argument | Type | Description |
|---|---|---|
node | TransformNode | The scene node that has this metadata property |
value | unknown | The value of the metadata property |
level | LevelInstance | The level being loaded (provides access to name and scene) |
gameEngine | GameEngine | The game engine instance (provides access to all managers and engines) |
Handlers can be synchronous or async. Errors thrown inside a handler are caught and logged without aborting the level load.
Transitions
The transition() method orchestrates a full level-to-level transition with lifecycle hooks and events.
async transition(levelName : string, options ?: TransitionOptions) : Promise<void>The manager tracks the current level internally — you only pass the target level name.
TransitionOptions
interface TransitionOptions
{
keepAlive ?: boolean;
preloadOnly ?: boolean;
}| Option | Type | Default | Description |
|---|---|---|---|
keepAlive | boolean | false | Keep the old level loaded in memory after transitioning |
preloadOnly | boolean | false | Load the new level but do not activate it |
Transition Flow
- Emit
level:transition:startwith{ from, to } - Call
onDeactivate()on the old level (if defined) - Emit
level:transition:progresswith{ stage: 'loading', levelName } - Load the new level (
buildScene()runs) - Dispose old level (unless
keepAlive: true) - Set new level as current
- Call
onActivate()on the new level (if defined) - Emit
level:transition:complete
If preloadOnly: true, the flow stops after step 4.
If any step throws, level:transition:error is emitted with { from, to, error }, and the exception re-throws.
Usage
// Basic transition
await levelManager.transition('desert');
// Keep old level in memory
await levelManager.transition('desert', { keepAlive: true });
// Preload without switching
await levelManager.transition('desert', { preloadOnly: true });Transition Events
gameEngine.eventBus.subscribe('level:transition:start', (event) =>
{
showLoadingScreen();
});
gameEngine.eventBus.subscribe('level:transition:complete', (event) =>
{
hideLoadingScreen();
});
gameEngine.eventBus.subscribe('level:transition:error', (event) =>
{
const { from, to, error } = event.payload;
showErrorScreen(`Failed to transition from ${ from } to ${ to }`);
});INFO
SAGE does not provide built-in visual transitions (fades, wipes, etc.). Use the transition events to drive your own UI overlays or canvas effects.
Progress Events
Levels emit progress events during loading. Use these to build loading screens.
| Event | Payload | Description |
|---|---|---|
level:progress | { levelName, progress, message } | Loading progress (0--100) |
level:complete | { levelName } | Level loaded successfully |
level:error | { levelName, message } | Level loading failed |
Emitting Progress
Inside buildScene(), call $emitProgress():
protected async buildScene() : Promise<Scene>
{
const { sceneEngine } = this.gameEngine.engines;
const scene = sceneEngine.createScene();
this.$emitProgress(0, 'Starting...');
sceneEngine.enablePhysics(scene);
this.$emitProgress(25, 'Physics enabled...');
await sceneEngine.loadModel('assets/level.glb', scene);
this.$emitProgress(75, 'Models loaded...');
this.$emitProgress(95, 'Finalizing...');
return scene;
}Subscribing to Progress
gameEngine.eventBus.subscribe('level:progress', (event) =>
{
const { levelName, progress, message } = event.payload;
updateLoadingBar(progress);
});
gameEngine.eventBus.subscribe('level:complete', () =>
{
hideLoadingScreen();
});GameLevel buildScene Pipeline
When GameLevel.buildScene() is called, it executes the following steps in order:
- Preload assets -- If
preloadis configured, all listed assets are loaded and cached. - Create scene -- An empty BabylonJS scene is created.
- Enable physics -- If
physicsis configured, Havok physics is initialized with the given gravity. - Load scene file -- If
sceneis configured, the GLB/glTF/Babylon file is imported into the scene. - Set up environment -- If
environmentis configured, the IBL texture and/or skybox are created. This runs before cameras so the background is ready when the scene renders. - Process cameras -- Cameras from the
camerasconfig are applied (overrides) or created. The active camera is set and canvas controls are attached. - Process lights -- Lights from the
lightsconfig are applied (overrides) or created. - Apply post-processing -- If
postProcessingis configured, rendering pipelines are set up. Requires an active camera from step 6. - Process node properties -- All scene nodes are walked. Metadata is normalized from glTF extras. Each node is checked for
spawnandentitymarkers, and all registered property handlers are invoked for matching metadata keys. - Process spawn points -- Spawn points collected in step 9 are matched against the
spawnsconfig. Entities are created at each spawn position. - Process entity nodes -- Entity markers collected in step 9 are matched against the
entitiesconfig. Existing meshes are wrapped as SAGE entities. - Load level sounds -- If
soundsis configured andAudioManageris available, level-scoped sounds are created. Sounds withautoplay: truebegin playing. This is the last step so all other scene elements are ready before audio starts.
Subclasses that override buildScene() should call super.buildScene() to retain this pipeline. Adding custom logic before or after super.buildScene() is the standard pattern:
protected override async buildScene() : Promise<Scene>
{
this.$emitProgress(5, 'Custom setup...');
// Runs the full GameLevel pipeline (steps 1-12)
const scene = await super.buildScene();
// Add your custom level logic here
this._setupPuzzleMechanics();
this._subscribeToEvents();
return scene;
}Level Lifecycle
- Registration -- Config registered with
registerLevelConfig() - Loading --
loadLevel()creates the instance and callsbuildScene() - Activation -- Level set as
currentLevel,onActivate()fires - Active -- Scene is rendered, entities are updated
- Deactivation --
onDeactivate()fires during transition - Disposal --
$dispose()cleans up the scene and resources
Custom Disposal
Override $dispose() for custom cleanup. Always call super.$dispose():
class MyLevel extends Level
{
private ambientSound : Sound | null = null;
async $dispose() : Promise<void>
{
if(this.ambientSound)
{
this.ambientSound.stop();
this.ambientSound.dispose();
this.ambientSound = null;
}
await super.$dispose();
}
}GameLevel.$dispose() automatically destroys all spawned entities, disposes all level sounds, and clears internal collections before calling Level.$dispose(), which disposes the BabylonJS scene.
LevelContext
The runtime context injected into levels by the LevelManager:
interface LevelContext
{
gameEngine : GameEngine;
propertyHandlers : Map<string, PropertyHandler>;
logger ?: LoggingUtility;
}PropertyHandler
Handlers process custom metadata properties on scene nodes during level loading:
type PropertyHandler = (
node : TransformNode,
value : unknown,
level : LevelInstance,
gameEngine : GameEngine
) => void | Promise<void>;Register handlers on the level manager:
levelManager.registerPropertyHandler('sound', (node, value, level, gameEngine) =>
{
// Process nodes with a 'sound' metadata property
});Teardown
The LevelManager implements Disposable. Calling gameEngine.$teardown() automatically disposes all loaded levels, clears the registry, and resets the current level.
