跳转到内容

场景

项目由场景组织。它们共享同一个 ECS 世界;SceneManager 资源加载、切换和卸载它们,并门控 每个场景的系统,使其只在该场景活动时运行。

switchTo 替换活动场景(可带过渡)。它是异步的——await 它:

import { defineSystem, Res, SceneManager } from 'esengine';
const goToLevel2 = defineSystem([Res(SceneManager)], async (scenes) => {
await scenes.switchTo('level2');
});

switchTounload 接受 TransitionOptions。用 transition: 'fade' 时,切换会经一个 纯色遮罩分三阶段淡入淡出:遮罩在 duration 的前一半内渐变到不透明,在旧场景卸载、新场景 加载期间保持全黑(加载慢只会延长停留,不会闪屏),再在后一半内渐变回来。被 await 的 promise 只在淡入完成后 resolve——加载失败则 reject。过渡进行中再调 switchTo 会被忽略并 打警告;isTransitioning() 可以查询。

如果一次加载在提交前被抢占——unload,或对同一场景的另一次加载抢先完成——待定的 promise 会以 SceneLoadCancelled 错误 reject,而不会提交一个只加载了一半的场景。它是竞态信号,不是 真正的失败,所以请在 catch 里对它单独分支处理(例如直接忽略),而不要当成加载错误上报。

选项 类型 默认 说明
transition 'none' | 'fade' 'none' 硬切,或经 color 淡入淡出。
duration number 0.3 总淡出淡入时间(秒)——前一半出、后一半入(RuntimeConfig.sceneTransitionDuration)。
color Color 不透明黑 遮罩颜色(RuntimeConfig.sceneTransitionColor;构建配置里用十六进制字符串)。
onStart / onComplete 回调 淡出开始时 / 淡入结束后触发。
keepPersistent boolean true 让经 SceneContext.setPersistent 标记的实体在卸载中存活。

transitionTo 是同一套机制上的一步到位便捷函数,用 TransitionConfig 配置 (type: 'fade'duration、可选 color)。第一个参数接受 App(引导代码、菜单胶水) 或 SceneManager 资源状态——在系统里直接传 Res(SceneManager) 给你的值。菜单 → 关卡的流程:

import { transitionTo, type TransitionConfig } from 'esengine';
const fadeToBlack: TransitionConfig = {
type: 'fade',
duration: 0.5,
color: { r: 0, g: 0, b: 0, a: 1 },
};
async function onPlayClicked() {
await transitionTo(app, 'levels/level1', fadeToBlack); // menu → level
}

把一个场景加载到当前场景之上——HUD、暂停菜单、流式区域——而不卸载下面的内容。 loadAdditive 接受一个可选的进度回调,并返回场景上下文:

await scenes.loadAdditive('pause-menu', (loaded, total) => { /* progress */ });
// …later
await scenes.unload('pause-menu');

世界大到装不下时,用 SceneStreaming 资源围绕一个焦点加载与卸载单元 (cell)——摆在世界空间里的叠加场景:

import { defineSystem, Res, SceneStreaming } from 'esengine';
const setupStreaming = defineSystem([Res(SceneStreaming)], (streaming) => {
streaming.configure({ loadRadius: 1200, unloadRadius: 1600 });
streaming.register({ scene: 'cells/forest', x: 0, y: 0, radius: 800 });
streaming.register({ scene: 'cells/village', x: 1500, y: 0, radius: 800 });
streaming.setFocusEntity(player); // 或每帧 setFocus(x, y)
});

每帧,距焦点小于 loadRadius 的单元叠加加载,超出 unloadRadius 的卸载——两个 半径之间是滞回带,玩家站在边界不会来回抖动。policy: 'sleep'休眠代替 卸载:实体留在内存里、被禁用并隐藏,重进即恢复,代价是内存。单元场景就是普通的 项目场景,按名字引用(见下)。

资源之下是 SceneStreamingController——对 SceneManager 原语 (loadAdditive / unload / sleep / wake)的纯编排;它自己不拥有任何加载。距离量到 单元边缘(中心距离减去 radius),所以大单元在中心还很远时就开始加载。加载与卸载是 异步的,控制器保证其安全:某单元有加载/卸载在途时绝不重复下发;如果加载完成时焦点已经离开, 该单元会被再次丢弃而不是泄漏驻留。没有注册任何单元时,update() 是空操作。

SceneStreamingController 说明
configure({ loadRadius, unloadRadius, policy? }) 设置半径(unloadRadius 向上钳制到不小于 loadRadius)与出界策略——'unload'(默认)或 'sleep'
register(cell) / unregister(scene) / clear() 管理单元集合({ scene, x, y, radius })。
setFocus(x, y) 直接驱动焦点(每帧调用)。
setFocusEntity(entity) / getFocusEntity() 改为跟随一个实体——流送系统每 tick 读它的 Transform(仅 play 模式)。
getActive() 控制器当前认为在范围内的场景名。
update() 按当前焦点调和驻留单元(sceneManagerPlugin 每帧调用)。

阈值数学也单独导出为纯函数 computeStreaming(cells, focusX, focusY, loadRadius, unloadRadius, active)—— 适合测试或自定义调度器。

项目场景目录下的每个场景都会随导出发布,并注册为 switchTo 的目标。场景的名字是它 相对场景目录的路径去掉 .esscene 扩展名——assets/scenes/levels/boss.esscene 就是 switchTo('levels/boss')。启动场景立即加载;其余场景在首次切换时才拉取,所以多余的场景 不会拖慢首屏。

在打包对话框的构建场景列表里取消勾选的场景不会进入导出(在编辑器里仍可正常编辑和 运行),Playable 广告构建只带启动场景。见构建与导出

方法 说明
switchTo(name, options?) 替换活动场景(异步;可选过渡)。
loadAdditive(name, onProgress?) 在顶部加载一个场景;返回其上下文(异步)。
unload(name, options?) 卸载一个场景(异步;可选过渡)。
getActive() 活动场景名,或 null
getActiveScenes() 所有活动(叠加)场景名。
pause(name) / resume(name) 挂起 / 恢复一个已加载场景的更新。
sleep(name) / wake(name) 挂起并隐藏场景的实体(留在内存;唤醒即刻恢复)。
isLoaded(name) 场景是否已加载。
isActive(name) 场景是否活动。
isPaused(name) / isSleeping(name) 上述两种挂起状态。
isTransitioning() switchTo 过渡期间为 true

场景文件格式是公开表面:SceneData 是纯 JSON,编辑器和运行时读写它所用的原语都导出给 游戏代码。你会在这些时刻用到它们:保存活世界的快照(比存档 API 更完整的检查点)、加载玩家生成或下载的场景,或发布状态放不进普通字段的组件。

serializeScene(world, name?) 遍历活世界,产出能经加载器往返的 SceneData——层级折叠进 每个实体记录的 parent/children 字段,仅运行时的投影实体(瓦片碰撞体之类,由其系统重新 派生)被排除。loadSceneWithAssets(world, data, options?) 先预加载所有被引用的资源再生成 实体;同步的 loadSceneData(world, data) 不做资源解析直接生成。两者都返回序列化实体 id 到 活实体的映射,之后可用 findEntityByName(world, name) 找实体:

import { defineSystem, GetWorld, Res, Assets, serializeScene,
loadSceneWithAssets, findEntityByName, MissingAssetsError } from 'esengine';
const checkpoint = defineSystem([GetWorld(), Res(Assets)], async (world, assets) => {
// Save: snapshot the live world as ordinary scene JSON.
const data = serializeScene(world, 'checkpoint');
localStorage.setItem('checkpoint', JSON.stringify(data));
// Load: preload assets, then spawn. Old saves are migrated automatically.
try {
await loadSceneWithAssets(world, JSON.parse(localStorage.getItem('checkpoint')!), {
assets,
abortOnMissingAssets: true, // throw instead of spawning with holes
onMissingAssets: (missing) => { // called once per load (list may be empty)
for (const m of missing) console.warn(`${m.ref}: ${m.reason}`);
},
});
} catch (e) {
if (e instanceof MissingAssetsError) { /* "content missing" UI — e.missing */ }
}
const player = findEntityByName(world, 'Player');
});

SceneLoadOptions:

选项 说明
assets Assets 资源;启用生成前的资源预加载(以及预制体实例展开)。
assetBaseUrl 解析资源路径的基准 URL。
onProgress (loaded, total) 预加载进度回调。
onMissingAssets 用所有解析失败或加载失败的引用调用一次(MissingAsset[]——ref、可选 typereason: 'unresolved' | 'load-failed')。列表为空也会触发。
abortOnMissingAssets 默认 false——缺失资源拿到句柄 0,场景照常加载。true 时在预加载之后、任何实体生成之前抛出 MissingAssetsError
collectAssets 记账集合,加载把获取到的资源键加入其中,调用方(通常是场景管理器)卸载时据此释放。

版本化。 场景文件带 version(当前 SCENE_FORMAT_VERSION = '1.0')。 migrateSceneData(raw) 把任何旧形态升级到当前格式:它从不改动输入(返回迁移后的深拷贝)、 幂等、丢弃被引擎升级淘汰的组件类型,并拒绝比引擎更新的数据。每个加载器内部都会跑它—— 只有想检查 migrated / fromVersion 时才自己调用(比如提示重新保存升级后的文件):

import { migrateSceneData } from 'esengine';
const { data, migrated, fromVersion } = migrateSceneData(raw);
if (migrated) console.log(`upgraded scene from format ${fromVersion}`);

带外组件状态。 完整状态放不进普通字段记录的组件——tilemap 图层的瓦片 chunk 存在 C++ blob 里——注册一个 SceneComponentCodec,让通用(反)序列化器无需组件专有知识也能携带它:

import { registerSceneComponentCodec } from 'esengine';
registerSceneComponentCodec('MyVoxelChunk', {
exportData(entity, data) { data.chunks = myStore.serialize(entity); },
outOfBandFields: ['chunks'], // stripped before the component insert
importData(entity, outOfBand) { myStore.restore(entity, outOfBand.chunks); },
});

保存时 exportData 把额外状态写进记录;加载时列出的 outOfBandFields 在组件插入前从记录 中剥离,插入后交给 importData。注册是幂等的——由拥有它的插件在启动时完成(见 tilemapPlugin)。

  • await 场景调用——它们是异步的;未 await 的切换会与本帧其余逻辑竞态。
  • 浮层(HUD、暂停、对话框)用叠加,让玩法留在下面。
  • 用系统而非标志门控逻辑——场景里声明的系统只在它活动时运行,所以叠加 HUD 与玩法场景 独立更新。
  • 有意地给长生命实体打标签:因为场景共享一个世界,未打标签的实体可能在切换后存活。
  • 跨场景持久化用存档,而不是泄漏实体。
  • 预制体 —— 在场景内生成可复用的实体树。
  • 存档与读档 —— 跨场景切换持久化状态。
  • 资源 —— 场景是 cook 入口点。