Scenes
The scene system provides the visual foundation for your game. Powered by BabylonJS, the SceneEngine wraps common scene creation tasks while giving you full access to the underlying BabylonJS API when needed.
SceneEngine
The SceneEngine is available through the engine:
const sceneEngine = gameEngine.engines.sceneEngine;Scene Methods
| Method | Signature | Description |
|---|---|---|
createScene | () => Scene | Create a new BabylonJS scene |
enablePhysics | (scene, gravity?) => void | Enable Havok physics on a scene |
Camera Methods
| Method | Signature | Description |
|---|---|---|
createFreeCamera | (name, position, scene) => FreeCamera | First-person camera with 6DOF |
Light Methods
| Method | Signature | Description |
|---|---|---|
createHemisphericLight | (name, direction, scene, intensity?) => HemisphericLight | Ambient light from a hemisphere |
createDirectionalLight | (name, direction, scene, intensity?) => DirectionalLight | Directional light (sun) |
createPointLight | (name, position, scene, intensity?) => PointLight | Omnidirectional point light |
Mesh Methods
| Method | Signature | Description |
|---|---|---|
createSphere | (name, options, scene) => Mesh | Create a sphere mesh |
createBox | (name, options, scene) => Mesh | Create a box mesh |
createGround | (name, options, scene) => Mesh | Create a ground plane |
createCylinder | (name, options, scene) => Mesh | Create a cylinder mesh |
Physics Methods
| Method | Signature | Description |
|---|---|---|
addPhysics | (mesh, shapeType, options, scene) => PhysicsAggregate | Add a physics body to a mesh |
Model Loading
| Method | Signature | Description |
|---|---|---|
loadModel | (path, scene) => Promise<AssetContainer> | Load a GLB/glTF model |
importMeshes | (path, scene) => Promise<ImportMeshResult> | Import meshes from a file |
Creating a Scene
In most cases, scenes are created inside a Level's buildScene() method. Here is the general pattern:
const scene = sceneEngine.createScene();
sceneEngine.enablePhysics(scene);
const camera = sceneEngine.createFreeCamera(
'mainCamera',
new Vector3(0, 5, -10),
scene
);
camera.setTarget(Vector3.Zero());
sceneEngine.createHemisphericLight('ambient', new Vector3(0, 1, 0), scene, 0.4);
sceneEngine.createDirectionalLight('sun', new Vector3(0.5, -0.6, 0.5), scene);
const ground = sceneEngine.createGround('ground', { width: 50, height: 50 }, scene);
sceneEngine.addPhysics(ground, PhysicsShapeType.BOX, { mass: 0 }, scene);Cameras
BabylonJS cameras define the player's viewport. Common types:
| Camera | Use Case |
|---|---|
FreeCamera | First-person, 6 degrees of freedom |
ArcRotateCamera | Third-person, orbits around a target |
FollowCamera | Follows a specific mesh |
import { FreeCamera, ArcRotateCamera, Vector3 } from '@babylonjs/core';
// First-person
const fps = new FreeCamera('fps', new Vector3(0, 5, -10), scene);
fps.setTarget(Vector3.Zero());
fps.attachControl(canvas, true);
// Third-person orbit
const orbit = new ArcRotateCamera(
'orbit',
Math.PI / 4, // alpha
Math.PI / 3, // beta
10, // radius
Vector3.Zero(), // target
scene
);
orbit.attachControl(canvas, true);Lighting
import { HemisphericLight, DirectionalLight, PointLight, Vector3 } from '@babylonjs/core';
// Ambient hemisphere light
const ambient = new HemisphericLight('ambient', new Vector3(0, 1, 0), scene);
ambient.intensity = 0.4;
// Directional sunlight
const sun = new DirectionalLight('sun', new Vector3(0.5, -0.6, 0.5), scene);
sun.intensity = 0.7;
// Point light
const torch = new PointLight('torch', new Vector3(5, 3, 0), scene);
torch.intensity = 0.8;Meshes and Materials
Create meshes with the SceneEngine utilities or BabylonJS MeshBuilder directly:
import { MeshBuilder, StandardMaterial, PBRMaterial, Color3 } from '@babylonjs/core';
const sphere = MeshBuilder.CreateSphere('sphere', { diameter: 2, segments: 32 }, scene);
sphere.position.y = 1;
// Standard material
const mat = new StandardMaterial('mat', scene);
mat.diffuseColor = new Color3(0.2, 0.4, 0.8);
mat.specularColor = new Color3(0.3, 0.3, 0.3);
sphere.material = mat;
// PBR material
const pbr = new PBRMaterial('pbr', scene);
pbr.albedoColor = new Color3(0.8, 0.2, 0.1);
pbr.metallic = 0.7;
pbr.roughness = 0.3;Model Loading
Load external 3D models (GLB/glTF):
// Via SceneEngine
const container = await sceneEngine.loadModel('assets/models/character.glb', scene);
container.addAllToScene();
// Via BabylonJS SceneLoader directly
import { SceneLoader } from '@babylonjs/core';
const result = await SceneLoader.ImportMeshAsync('', 'assets/models/', 'character.glb', scene);AssetManager
For production games, use the AssetManager for centralized loading with caching, reference counting, and GLB fragment extraction:
const assetManager = gameEngine.managers.assetManager;Loading Assets
// Load a full GLB
const container = await assetManager.load('models/environment.glb');
container.addAllToScene();Fragment Syntax
Extract individual meshes from a multi-object GLB using path#meshName:
const chestLid = await assetManager.load('models/props.glb#chest_lid');
const torch = await assetManager.load('models/props.glb#wall_torch');The underlying container is loaded only once, regardless of how many fragments you extract.
Instancing and Cloning
// GPU instances -- share geometry and material, very cheap
const barrel1 = assetManager.instance('models/props.glb#barrel');
const barrel2 = assetManager.instance('models/props.glb#barrel');
barrel1.position.x = 5;
barrel2.position.x = -5;
// Independent clone -- for meshes that need modification
const unique = assetManager.clone('models/props.glb#barrel');
unique.scaling.y = 1.5;Use instance() for many identical copies (props, trees, crates). Use clone() when a copy needs its own geometry or material.
Preloading
Batch-load assets with event bus progress tracking:
gameEngine.eventBus.subscribe('asset:progress', (event) =>
{
const { path, loaded, total } = event.payload;
console.log(`Loaded ${ loaded }/${ total }: ${ path }`);
});
await assetManager.preload([
'models/environment.glb',
'models/props.glb#barrel',
'models/props.glb#crate',
'models/characters.glb#knight',
]);Reference Counting and Disposal
Each load() call increments a reference count. Call dispose() to decrement -- the container is freed only when the count reaches zero:
await assetManager.load('models/props.glb#barrel');
await assetManager.load('models/props.glb#barrel');
assetManager.dispose('models/props.glb#barrel'); // refCount: 2 -> 1
assetManager.dispose('models/props.glb#barrel'); // refCount: 0, freed
// Or release everything at once
assetManager.disposeAll();disposeAll() is called automatically during level teardown.
Raycasting
SAGE wraps BabylonJS picking with entity-aware raycasting that resolves picked meshes back to their owning entities:
const raycast = gameEngine.raycast;Screen Picking
const result = raycast.pickEntity(scene, pointerX, pointerY);
if(result)
{
console.log('Clicked:', result.entity.type);
console.log('Hit point:', result.point);
}
// Pick all entities under the cursor
const results = raycast.pickEntities(scene, pointerX, pointerY);Ray Picking
import { Ray, Vector3 } from '@babylonjs/core';
const ray = new Ray(
new Vector3(0, 5, 0),
new Vector3(0, -1, 0),
100
);
const result = raycast.pickEntityWithRay(scene, ray);
const results = raycast.pickEntitiesWithRay(scene, ray);Forward Picking (FPS Style)
Cast a ray from the camera's forward direction:
const result = raycast.pickEntityForward(scene, camera, 50);
if(result)
{
console.log('Looking at:', result.entity.type);
}The third parameter is maxDistance (default 1000).
EntityPickFilter
Filter results by type, tags, or custom predicates. All conditions are AND'd:
import type { EntityPickFilter } from '@skewedaspect/sage';
// By type
const enemies = raycast.pickEntities(scene, x, y, { type: 'enemy' });
// By tags (entity must have ALL listed tags)
const interactable = raycast.pickEntity(scene, x, y, {
tags: [ 'interactable', 'unlocked' ],
});
// Custom predicate
const alive = raycast.pickEntity(scene, x, y, {
predicate: (entity) => entity.state.health > 0,
});
// Combined
const target = raycast.pickEntityForward(scene, camera, 100, {
type: 'enemy',
tags: [ 'visible' ],
predicate: (entity) => entity.state.health > 0,
});EntityPickResult
interface EntityPickResult
{
entity : GameEntity;
point : Vector3;
distance : number;
normal : Vector3;
mesh : AbstractMesh;
pickingInfo : PickingInfo;
}| Field | Type | Description |
|---|---|---|
entity | GameEntity | The resolved entity that owns the picked mesh |
point | Vector3 | World-space intersection point |
distance | number | Distance from ray origin to hit point |
normal | Vector3 | Surface normal at the hit point |
mesh | AbstractMesh | The specific mesh that was picked |
pickingInfo | PickingInfo | The raw BabylonJS picking result |
Parent-Chain Resolution
When a picked mesh belongs to a child entity, the child entity is returned. If the mesh is not directly associated with an entity, the raycast helper walks up the scene graph parent chain until it finds one.
Environment Effects
Fog
scene.fogEnabled = true;
scene.fogColor = new Color3(0.8, 0.9, 0.8);
scene.fogDensity = 0.01;Particle Systems
import { ParticleSystem, Color4, Texture, Vector3 } from '@babylonjs/core';
const fire = new ParticleSystem('fire', 2000, scene);
fire.particleTexture = new Texture('assets/textures/flame.png', scene);
fire.emitter = new Vector3(5, 0, 10);
fire.color1 = new Color4(1, 0.9, 0.3, 1);
fire.color2 = new Color4(1, 0.5, 0.2, 1);
fire.minSize = 0.3;
fire.maxSize = 1.5;
fire.emitRate = 500;
fire.start();Debugging
Use the BabylonJS inspector for visual debugging:
import '@babylonjs/inspector';
window.addEventListener('keydown', (e) =>
{
if(e.key === 'F12')
{
if(scene.debugLayer.isVisible)
{
scene.debugLayer.hide();
}
else
{
scene.debugLayer.show();
}
}
});