场景
场景是实体、组件和父子关系的集合。你可以在 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 } }));实体层级
场景实体可以通过父子关系形成树状结构。在编辑器中设置父子关系 — 引擎会自动管理 Parent 和 Children 组件。
当父实体的 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) => { // 在系统中访问场景管理器 }));注册场景
编辑器工作流(推荐)
大多数项目完全通过编辑器管理场景:
- 在 Content Browser 中右键 → 创建 → 场景,创建
.esscene文件(默认位置:assets/scenes/main.esscene) - 打开构建设置,添加需要包含在构建中的场景
- 构建项目 — 构建系统读取每个
.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 |
切换场景
通过 SceneManager 的 switchTo 方法切换场景。切换会先卸载当前主场景,再加载目标场景:
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' | 过渡动画类型 |
duration | number | 引擎默认值 | 过渡总时长(秒),淡出和淡入各占一半 |
color | Color | 引擎默认值 | 淡入淡出的遮罩颜色 |
keepPersistent | boolean | true | 是否保留持久化实体 |
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(); // 当前主场景名,无主场景时返回 nullmgr.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,未加载时返回 nullmgr.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']场景数据类型
| 类型 | 字段 |
|---|---|
SceneData | version: string, name: string, entities: SceneEntityData[], textureMetadata?: Record<string, TextureMetadata> |
SceneEntityData | id: number, name: string, parent: number | null, children: number[], components: SceneComponentData[], visible?: boolean |
SceneComponentData | type: string, data: Record<string, unknown> |