跳转到内容

场景

场景是实体、组件和父子关系的集合。你可以在 Estella 编辑器中可视化地创建场景,场景以 JSON 文件保存。运行时引擎会加载场景并生成所有实体及其配置的组件。

场景结构

场景文件包含:

  • 实体 — 每个实体都有唯一 ID 和名字
  • 组件 — 附加到实体上的数据(变换、精灵、碰撞体等)
  • 层级 — 实体之间的父子关系
{
"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 } } }
]
}
]
}

加载场景

在编辑器中,场景会自动加载 — 你只需编写定义组件和系统的脚本。

构建项目后,引擎会自动加载场景、解析所有引用的资源(纹理、材质、Spine),并在没有相机时创建默认相机。你不需要手动处理场景加载。

查找实体

按名字查找

从场景加载的每个实体都会自动获得内置的 Name 组件,包含在编辑器中指定的名字。通过查询 Name 来查找实体:

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') {
// 找到了玩家实体
}
}
}
));

按组件或标签查找

使用 Query 按组件查找实体:

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) {
// 处理每个玩家
}
}
));

遍历层级

使用 Children 组件遍历实体树:

import { defineSystem, addSystem, Query, Children, Transform } from 'esengine';
addSystem(defineSystem(
[Query(Children, Transform)],
(query) => {
for (const [entity, children, transform] of query) {
// children.entities 包含子实体 ID
}
}
));

实体层级

场景实体可以通过父子关系形成树状结构。在编辑器中设置父子关系 — 引擎会自动管理 ParentChildren 组件。

当父实体的 Transform 变化时,引擎会自动将世界变换传播到所有后代。

动态实体

你可以在运行时使用 Commands 生成和销毁实体:

import { defineSystem, addSystem, Commands, Transform, Sprite } from 'esengine';
addSystem(defineSystem(
[Commands()],
(commands) => {
// 生成一个带组件的新实体
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 } });
// 销毁实体
commands.despawn(entity);
}
));

实体可见性

实体拥有 visible 属性(默认 true)。在编辑器中将其设为 false 时,场景加载会跳过该实体的组件 — 它在层级中存在但没有运行时行为。适合用于在场景文件中保留参考实体而不在运行时生成。

场景管理器

SDK 提供了 SceneManager 资源和 sceneManagerPlugin 插件,用于管理多个场景的加载、切换和生命周期。该插件由引擎自动包含,无需手动注册。

import { Res, SceneManager, defineSystem, addSystem } from 'esengine';
addSystem(defineSystem(
[Res(SceneManager)],
(mgr) => {
// 在系统中访问场景管理器
}
));

注册场景

编辑器工作流(推荐)

大多数项目完全通过编辑器管理场景:

  1. Content Browser 中右键 → 创建场景,创建 .esscene 文件(默认位置:assets/scenes/main.esscene
  2. 打开构建设置,添加需要包含在构建中的场景
  3. 构建项目 — 构建系统读取每个 .esscene 文件,提取场景名称,并将场景数据直接嵌入构建产物

运行时引擎会自动注册和加载这些嵌入的场景。你不需要编写任何场景加载代码,也不需要指定任何文件路径。

编程注册

对于需要场景切换、自定义系统、或 setup/cleanup 回调的多场景项目,在 setup 函数中注册场景。同样不需要 path — 构建系统会自动处理数据嵌入:

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) => {
// 场景卸载前调用
},
});
mgr.register({ name: 'game' });
mgr.setInitial('menu');
}

内联场景数据

也可以通过 data 直接传入场景数据对象,适用于动态生成或内存中的场景:

mgr.register({
name: 'procedural',
data: {
version: '1.0',
name: 'procedural',
entities: [],
},
setup: (ctx) => {
// 通过代码生成实体
},
});

从 URL 加载(高级)

如需从 CDN 加载或开发时热加载等高级场景,可使用 path 字段指定 URL:

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

路径解析规则:

路径格式解析方式示例
/ 开头从站点根目录的绝对路径,原样使用/scenes/menu.json/scenes/menu.json
http://https:// 开头完整 URL,原样使用https://cdn.example.com/scenes/menu.json
相对路径拼接 AssetServer.baseUrl 前缀scenes/menu.json{baseUrl}/scenes/menu.json

切换场景

通过 SceneManagerswitchTo 方法切换场景。切换会先卸载当前主场景,再加载目标场景:

const mgr = app.getResource(SceneManager);
// 简单切换(卸载旧场景,加载新场景)
await mgr.switchTo('game');
// 带淡入淡出过渡的切换
await mgr.switchTo('game', { transition: 'fade', duration: 0.5 });
// 完整的切换选项
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'),
});

switchTo 的完整选项:

选项类型默认值说明
transition'none' | 'fade''none'过渡动画类型
durationnumber引擎默认值过渡总时长(秒),淡出和淡入各占一半
colorColor引擎默认值淡入淡出的遮罩颜色
keepPersistentbooleantrue是否保留持久化实体
onStart() => void-过渡开始时的回调
onComplete() => void-过渡完成时的回调

多场景(叠加加载)

除了切换主场景,还可以使用叠加加载在当前场景之上加载额外场景。适合用于 HUD、暂停菜单等需要与主场景共存的 UI 层:

const mgr = app.getResource(SceneManager);
// 在当前场景上叠加加载
await mgr.loadAdditive('hud');
await mgr.loadAdditive('pause-menu');
// 卸载叠加场景
await mgr.unload('pause-menu');

场景生命周期

每个已加载的场景都有自己的状态。你可以暂停、恢复、休眠和唤醒场景:

const mgr = app.getResource(SceneManager);
mgr.pause('game'); // 系统停止执行,实体保持可见
mgr.resume('game'); // 恢复系统执行
mgr.sleep('game'); // 系统停止执行,实体隐藏
mgr.wake('game'); // 恢复系统执行并显示实体
方法系统执行实体可见
resume / wake
pause
sleep

场景上下文

场景加载时,setup 回调接收一个 SceneContext 对象,用于执行作用域为当前场景的操作:

app.registerScene({
name: 'game',
path: '/scenes/game.json',
setup: (ctx) => {
// 生成属于此场景的实体
const e = ctx.spawn();
// 注册作用域为此场景的绘制回调
ctx.registerDrawCallback('game-debug', drawDebug);
// 绑定作用域为此场景的后处理效果
const fx = PostProcess.createStack();
fx.addPass('bloom', PostProcess.createBloom());
ctx.bindPostProcess(cameraEntity, fx);
// 标记实体为持久(场景卸载时不销毁)
ctx.setPersistent(e, true);
},
});

通过 ctx.spawn() 生成的实体会自动获得 SceneOwner 组件,在场景卸载时会被一并销毁(除非标记为持久)。

SceneOwner 组件

从场景文件加载的实体和通过 ctx.spawn() 生成的实体都会自动获得 SceneOwner 组件,用于标识实体所属的场景:

import { SceneOwner } from 'esengine';
// 检查实体属于哪个场景
const owner = world.get(entity, SceneOwner);
console.log(owner.scene); // 'game'
console.log(owner.persistent); // false

场景作用域系统

registerScene 中通过 systems 注册的系统会自动包装,仅在场景状态为 running 时执行:

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

场景过渡动画

使用 transitionTo 函数实现带动画效果的场景切换:

import { transitionTo } from 'esengine';
// 淡出到黑色,切换场景,淡入
await transitionTo(app, 'game', { type: 'fade', duration: 1.0 });
// 自定义遮罩颜色
await transitionTo(app, 'game', {
type: 'fade',
duration: 0.5,
color: { r: 1, g: 1, b: 1, a: 1 },
});

transitionTo 是对 mgr.switchTo() 的便捷封装,内部会获取 SceneManager 资源并调用 switchTo。整个过程是异步的——先淡出旧场景,执行场景切换,再淡入新场景。

查询场景状态

SceneManager 提供了一组方法用于查询当前场景的状态:

const mgr = app.getResource(SceneManager);
mgr.getActive(); // 当前主场景名,无主场景时返回 null
mgr.isActive('game'); // 判断指定场景是否为主场景
mgr.getActiveScenes(); // 所有状态为 'running' 的场景名数组
mgr.getLoaded(); // 所有已加载的场景名数组
mgr.isLoaded('game'); // 场景是否已加载
mgr.isPaused('game'); // 场景是否处于暂停状态
mgr.isSleeping('game'); // 场景是否处于休眠状态
mgr.isTransitioning(); // 是否正在执行场景过渡
mgr.getSceneStatus('game'); // 返回场景详细状态
mgr.getScene('game'); // 获取场景的 SceneContext,未加载时返回 null
mgr.getLoadOrder(); // 按加载顺序返回场景名数组

getSceneStatus 的返回值类型为:

状态说明
'loading'场景正在加载中
'running'场景正在运行
'paused'场景已暂停(系统停止,实体可见)
'sleeping'场景已休眠(系统停止,实体隐藏)
'unloading'场景正在卸载中
null场景未加载

场景层序

当多个场景同时加载时,渲染和系统执行按加载顺序进行。使用 bringToTop() 将指定场景移到最顶层:

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

场景数据类型

类型字段
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>