Skip to content

Scenes

A scene is a collection of entities, components, and parent-child relationships. You create scenes visually in the Estella editor, and they are saved as JSON files. At runtime, the engine loads the scene and spawns all entities with their configured components.

Scene Structure

A scene file contains:

  • Entities — each with a unique ID and a name
  • Components — data attached to each entity (transforms, sprites, colliders, etc.)
  • Hierarchy — parent-child relationships between entities
{
"version": "1.0",
"name": "Level1",
"entities": [
{
"id": 1,
"name": "Player",
"parent": null,
"children": [2],
"components": [
{ "type": "Transform", "data": { "position": { "x": 0, "y": 0, "z": 0 } } },
{ "type": "Sprite", "data": { "texture": "player.png", "size": { "x": 32, "y": 32 } } }
]
}
]
}

Loading Scenes

In the editor, scenes are loaded automatically — you only write scripts that define components and systems.

After building your project, the engine automatically loads the scene, resolves all referenced assets (textures, materials, Spine), and creates a default camera if none exists. You don’t need to handle scene loading manually.

Finding Entities

By Name

Every entity loaded from a scene receives a built-in Name component with the name assigned in the editor. Query Name to find entities:

import { defineSystem, addStartupSystem, Query, Name, Transform } from 'esengine';
addStartupSystem(defineSystem(
[Query(Name, Transform)],
(query) => {
for (const [entity, name, transform] of query) {
if (name.value === 'Player') {
// found the player entity
}
}
}
));

By Component or Tag

Use a Query to find entities by their components:

import { defineSystem, addSystem, defineTag, Query, Mut, Transform } from 'esengine';
const Player = defineTag('Player');
addSystem(defineSystem(
[Query(Mut(Transform), Player)],
(query) => {
for (const [entity, transform] of query) {
// process each player
}
}
));

Traversing the Hierarchy

Use the Children component to walk the entity tree:

import { defineSystem, addSystem, Query, Children, Transform } from 'esengine';
addSystem(defineSystem(
[Query(Children, Transform)],
(query) => {
for (const [entity, children, transform] of query) {
// children.entities contains child entity IDs
}
}
));

Entity Hierarchy

Scene entities can form a tree via parent-child relationships. Set up parent-child relationships in the editor — the engine manages Parent and Children components automatically.

When a parent’s Transform changes, the engine automatically propagates the world transform to all descendants.

Dynamic Entities

You can spawn and despawn entities at runtime using Commands:

import { defineSystem, addSystem, Commands, Transform, Sprite } from 'esengine';
addSystem(defineSystem(
[Commands()],
(commands) => {
// Spawn a new entity with components
commands.spawn()
.insert(Transform, { position: { x: 100, y: 0, z: 0 }, rotation: { w: 1, x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } })
.insert(Sprite, { texture: bulletTexture, color: { r: 1, g: 1, b: 1, a: 1 }, size: { x: 8, y: 8 } });
// Despawn an entity
commands.despawn(entity);
}
));

Entity Visibility

Entities have a visible property (default true). When set to false in the editor, the entity’s components are skipped during scene loading — it exists in the hierarchy but has no runtime behaviour. Use this to keep reference entities in the scene file without spawning them.

Scene Manager

The SDK provides the SceneManager resource and sceneManagerPlugin for managing the loading, switching, and lifecycle of multiple scenes. The plugin is included by the engine automatically — no manual registration needed.

import { Res, SceneManager, defineSystem, addSystem } from 'esengine';
addSystem(defineSystem(
[Res(SceneManager)],
(mgr) => {
// access scene manager in a system
}
));

Registering Scenes

Most projects manage scenes entirely through the editor:

  1. Right-click in the Content BrowserCreateScene to create a .esscene file (default location: assets/scenes/main.esscene)
  2. Open Build Settings and add the scenes you want to include in the build
  3. Build the project — the build system reads each .esscene file, extracts its name, and embeds the scene data directly into the build output

At runtime, the engine automatically registers and loads these embedded scenes. You don’t need to write any scene loading code or specify any file paths.

Programmatic Registration

For multi-scene projects that need scene switching, custom systems, or setup/cleanup callbacks, register scenes in a setup function. You still don’t need path — the build system handles data embedding:

import { SceneManager, Schedule, type App } from 'esengine';
export function setup(app: App) {
const mgr = app.getResource(SceneManager);
mgr.register({
name: 'menu',
systems: [
{ schedule: Schedule.Update, system: menuUpdateSystem },
],
setup: (ctx) => {
ctx.registerDrawCallback('menu-particles', drawParticles);
},
cleanup: (ctx) => {
// called before scene unload
},
});
mgr.register({ name: 'game' });
mgr.setInitial('menu');
}

Inline Scene Data

You can pass scene data directly via data for procedurally generated or in-memory scenes:

mgr.register({
name: 'procedural',
data: {
version: '1.0',
name: 'procedural',
entities: [],
},
setup: (ctx) => {
// generate entities programmatically
},
});

Loading from URL (Advanced)

For advanced use cases — such as loading scenes from a CDN or hot-reloading during development — use the path field to specify a URL:

mgr.register({
name: 'menu',
path: '/scenes/menu.json',
});

Path resolution rules:

Path formatResolves toExample
Starts with /Absolute path from site root/scenes/menu.json/scenes/menu.json
Starts with http:// or https://Full URL, used as-ishttps://cdn.example.com/scenes/menu.json
Relative pathPrepended with AssetServer.baseUrlscenes/menu.json{baseUrl}/scenes/menu.json

Switching Scenes

Use SceneManager to switch between registered scenes. Switching unloads the current scene and loads the new one:

const mgr = app.getResource(SceneManager);
// Simple switch (unloads old, loads new)
await mgr.switchTo('game');
// Switch with fade transition
await mgr.switchTo('game', { transition: 'fade', duration: 0.5 });
// Full transition options
await mgr.switchTo('game', {
transition: 'fade',
duration: 1.0,
color: { r: 0, g: 0, b: 0, a: 1 },
keepPersistent: true,
onStart: () => console.log('transition started'),
onComplete: () => console.log('transition finished'),
});

Full switchTo options:

OptionTypeDefaultDescription
transition'none' | 'fade''none'Transition animation type
durationnumberengine defaultTotal transition duration (seconds); fade-out and fade-in each take half
colorColorengine defaultMask colour for the fade
keepPersistentbooleantrueWhether to keep persistent entities
onStart() => void-Callback when the transition starts
onComplete() => void-Callback when the transition finishes

Multi-Scene (Additive Loading)

Load additional scenes on top of the current primary scene. This is useful for HUDs, pause menus, or overlay layers:

const mgr = app.getResource(SceneManager);
// Load a scene on top of the current one
await mgr.loadAdditive('hud');
await mgr.loadAdditive('pause-menu');
// Unload additive scene
await mgr.unload('pause-menu');

Scene Lifecycle

Control whether a loaded scene’s systems and entities are active:

const mgr = app.getResource(SceneManager);
// Pause — systems stop, entities remain visible
mgr.pause('game');
mgr.resume('game');
// Sleep — systems stop, entities hidden
mgr.sleep('game');
mgr.wake('game');
MethodSystemsEntities Visible
resume / wakeRunningYes
pauseStoppedYes
sleepStoppedNo

Scene Context

When a scene loads, its setup callback receives a SceneContext that provides scene-scoped operations:

app.registerScene({
name: 'game',
path: '/scenes/game.json',
setup: (ctx) => {
// Spawn entities owned by this scene
const e = ctx.spawn();
// Register draw callbacks scoped to this scene
ctx.registerDrawCallback('game-debug', drawDebug);
// Bind post-process effects scoped to this scene
const fx = PostProcess.createStack();
fx.addPass('bloom', PostProcess.createBloom());
ctx.bindPostProcess(cameraEntity, fx);
// Mark entity as persistent (survives scene unload)
ctx.setPersistent(e, true);
},
});

Entities spawned through SceneContext are automatically tagged with a SceneOwner component and cleaned up when the scene unloads — unless marked as persistent.

SceneOwner Component

Entities loaded from a scene file automatically get a SceneOwner component. Use it to check which scene owns an entity:

import { SceneOwner } from 'esengine';
const owner = world.get(entity, SceneOwner);
console.log(owner.scene); // 'game'
console.log(owner.persistent); // false

Scene-scoped Systems

Systems registered in a scene config only run when that scene’s status is 'running'. They are automatically wrapped so they skip execution when the scene is paused or sleeping:

import { defineSystem, Query, Mut, Transform, Schedule } from 'esengine';
const mySystem = defineSystem(
[Query(Mut(Transform))],
(query) => {
for (const [entity, transform] of query) {
// runs only while 'game' scene is running
}
}
);
app.registerScene({
name: 'game',
path: '/scenes/game.json',
systems: [{ schedule: Schedule.Update, system: mySystem }],
});

Scene Transitions

Use the transitionTo helper for animated scene switches:

import { transitionTo } from 'esengine';
// Fade to black, switch scene, fade in
await transitionTo(app, 'game', { type: 'fade', duration: 1.0 });
// Custom mask colour
await transitionTo(app, 'game', {
type: 'fade',
duration: 0.5,
color: { r: 1, g: 1, b: 1, a: 1 },
});

transitionTo is a convenience wrapper around mgr.switchTo(). It obtains the SceneManager resource internally and calls switchTo. The process is asynchronous — the old scene fades out, the scene switch executes, then the new scene fades in.

Querying Scene State

SceneManager provides a set of methods for inspecting scene status:

const mgr = app.getResource(SceneManager);
mgr.getActive(); // current primary scene name, or null
mgr.isActive('game'); // whether a given scene is the primary scene
mgr.getActiveScenes(); // all scene names with 'running' status
mgr.getLoaded(); // all loaded scene names
mgr.isLoaded('game'); // whether the scene is loaded
mgr.isPaused('game'); // whether the scene is paused
mgr.isSleeping('game'); // whether the scene is sleeping
mgr.isTransitioning(); // whether a scene transition is in progress
mgr.getSceneStatus('game'); // detailed scene status
mgr.getScene('game'); // get the scene's SceneContext, or null if not loaded
mgr.getLoadOrder(); // scene names in load order

getSceneStatus return values:

StatusDescription
'loading'Scene is currently loading
'running'Scene is running
'paused'Scene is paused (systems stopped, entities visible)
'sleeping'Scene is sleeping (systems stopped, entities hidden)
'unloading'Scene is currently unloading
nullScene is not loaded

Scene Layer Ordering

When multiple scenes are loaded simultaneously, rendering and system execution follow load order. Use bringToTop() to move a scene to the top:

const mgr = app.getResource(SceneManager);
await mgr.loadAdditive('hud');
await mgr.loadAdditive('dialog');
mgr.bringToTop('hud');
mgr.getLoadOrder(); // ['game', 'dialog', 'hud']

Scene Data Types

TypeFields
SceneDataversion: string, name: string, entities: SceneEntityData[], textureMetadata?: Record<string, TextureMetadata>
SceneEntityDataid: number, name: string, parent: number | null, children: number[], components: SceneComponentData[], visible?: boolean
SceneComponentDatatype: string, data: Record<string, unknown>