本文梳理 Unity ECS 中 SubScene 从编辑、烘焙(Baking)到运行时加载,再到远程内容更新的完整流程,并说明它与 Addressables、代码热更新方案之间的边界。
本文以
[email protected]的文档和 API 为基准。这里的“热更”指 SubScene 场景数据及其资源内容的更新,不包含 C# 代码热更新。
一、SubScene 基础概念
ECS 中场景分为三种类型,理解三者关系是理解热更的前提。
| 类型 | 说明 |
|---|---|
| Authoring Scene(创作场景) | 普通 Unity 场景,包含 GameObject 和 MonoBehaviour,供编辑器操作 |
| Entity Scene(实体场景) | Baking 产出的 .entities 文件,包含 ECS 运行时数据,性能最优 |
| SubScene(子场景) | 一个 GameObject 组件,作为 Authoring Scene 的挂载点,触发 Baking 和流式加载 |
SubScene 本质上是一个入口,真正用于运行的是 Baking 后的 Entity Scene 文件。
SubScene 开/关状态的区别
| 状态 | 行为 |
|---|---|
| 打开 | 编辑器实时显示 Authoring GameObject;增量 Baking 实时生效;进入 Play Mode 后实体立即可用 |
| 关闭 | 显示已 Baked 的 Entity 数据;异步流式加载;进入 Play Mode 后实体需要等待加载完成 |
构建后,SubScene 的行为与编辑器中的关闭状态一致:异步流式加载,不能假设数据立即可用。
二、Baking 流程
Baking 是将 Authoring Data(GameObject)转换为 Runtime Data(Entity)的过程,仅在编辑器中执行,不在运行时发生。
流程图
flowchart TD
A["Authoring Scene\nGameObject + MonoBehaviour"] --> B["触发 Baking"]
B --> C["阶段1:创建 Entity\n为每个 GameObject 创建对应 Entity"]
C --> D["阶段2:Baker 阶段\n各 Baker 读取 Authoring 组件\n写入 ECS Component"]
D --> E["阶段3:Baking System\n运行带 BakingSystem 属性的 ECS System"]
E --> F["产出:Entity Scene 文件\n存储于 Library/EntityScenes 或 StreamingAssets"]两种 Baking 模式
| 模式 | 触发时机 | 特点 |
|---|---|---|
| Full Baking | SubScene 关闭时、Entity Scene 文件缺失或过期时 | 后台进程异步执行,全量重新烘焙 |
| Incremental Baking | SubScene 打开时的增量修改 | 内存中执行,只重新烘焙被修改的部分,实时生效 |
Baker 代码示例
// Authoring 组件(挂在 GameObject 上)
public class RotationSpeedAuthoring : MonoBehaviour
{
public float degreesPerSecond;
}
// ECS 运行时组件
public struct RotationSpeed : IComponentData
{
public float radiansPerSecond;
}
// Baker:将 Authoring 数据转为 ECS 数据
public class RotationSpeedBaker : Baker<RotationSpeedAuthoring>
{
public override void Bake(RotationSpeedAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, new RotationSpeed
{
radiansPerSecond = math.radians(authoring.degreesPerSecond)
});
}
}
三、Content Archive 内容管理系统
核心概念
Entities 有自己的内容管理系统。Unity 会将 SubScene 引用的场景和对象存入 Content Archive,并提供适用于 ECS 的弱引用和内容分发 API。
构建 Player 时,Unity 会自动为 SubScene 引用的资源生成 Content Archive。多个 SubScene 引用同一对象时,Unity 会将该对象移入共享 Archive。
两种资源引用方式
| 方式 | 说明 | 生命周期 |
|---|---|---|
| 强引用 | 直接赋值给 MonoBehaviour 属性,如 MeshFilter.sharedMesh | Unity 自动管理,按需加载和卸载 |
| 弱引用 | Baking 时将 WeakObjectReference<T> 或 UntypedWeakReferenceId 写入 ECS Component | 手动发起加载并管理释放时机 |
弱引用示例(Baking 阶段)
public class MeshRefAuthoring : MonoBehaviour
{
public WeakObjectReference<Mesh> mesh;
class MeshRefBaker : Baker<MeshRefAuthoring>
{
public override void Bake(MeshRefAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, new MeshComponent { mesh = authoring.mesh });
}
}
}
public struct MeshComponent : IComponentData
{
public WeakObjectReference<Mesh> mesh;
}
运行时加载弱引用资源
public partial struct RenderMeshSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
foreach (var (transform, data) in
SystemAPI.Query<RefRW<LocalToWorld>, RefRW<MeshComponent>>())
{
if (!data.ValueRW.mesh.IsLoaded)
{
data.ValueRW.mesh.LoadAsync();
}
else if (data.ValueRO.mesh.LoadingStatus == ObjectLoadingStatus.Completed)
{
// 资源就绪后再交给渲染逻辑使用。
var mesh = data.ValueRO.mesh.Result;
}
}
}
}
与 Addressables 划分资源边界
同时使用 ECS Content Management 和 Addressables 时,关键不是按资源类型做硬性划分,而是避免同一资源被两个系统重复收集和打包。直接引用和 Addressable 标记并存时,需要通过构建报告和内存分析确认是否产生重复依赖。
一种容易维护的分工方式是:
| 内容 | 建议管理方式 |
|---|---|
| SubScene 及其直接依赖 | ECS Content Archive |
| 明确通过地址独立加载的 UI、音频等内容 | Addressables |
| C# 逻辑 | 代码热更新方案或重新构建 Player |
四、SubScene 运行时加载
推荐方式:EntitySceneReference
public struct SceneLoader : IComponentData
{
public EntitySceneReference sceneReference;
}
#if UNITY_EDITOR
public class SceneLoaderAuthoring : MonoBehaviour
{
public UnityEditor.SceneAsset scene;
class Baker : Baker<SceneLoaderAuthoring>
{
public override void Bake(SceneLoaderAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.None);
AddComponent(entity, new SceneLoader
{
sceneReference = new EntitySceneReference(authoring.scene)
});
}
}
}
#endif
[RequireMatchingQueriesForUpdate]
public partial class SceneLoaderSystem : SystemBase
{
private EntityQuery m_NewRequests;
protected override void OnCreate()
{
m_NewRequests = GetEntityQuery(typeof(SceneLoader));
}
protected override void OnUpdate()
{
var requests = m_NewRequests
.ToComponentDataArray<SceneLoader>(Allocator.Temp);
for (int i = 0; i < requests.Length; i++)
{
SceneSystem.LoadSceneAsync(
World.Unmanaged,
requests[i].sceneReference);
}
requests.Dispose();
EntityManager.DestroyEntity(m_NewRequests);
}
}
构建时只有通过 EntitySceneReference 或 SubScene 组件引用的场景才会被检测并打包。直接使用 GUID 引用的场景不会被构建系统检测到,Entity Scene 文件可能缺失。
五、热更新流程解析
核心问题
SubScene 经 Baking 后生成 Entity Scene 和相关 Content Archive。热更新的核心是发布新的内容文件和 Catalog,并让运行时下载、安装和使用新版 Catalog,不需要重新构建整个 Player。
完整热更流程
flowchart TD
subgraph editor [编辑器侧]
A["修改 SubScene 内容"] --> B["重新 Baking\n生成新 Entity Scene 文件"]
B --> C["Build 阶段\nRemoteContentCatalogBuildUtility.BuildContent()\n提取 SubScene 引用的对象到临时目录"]
C --> D["Publish 阶段\nRemoteContentCatalogBuildUtility.PublishContent()\n按内容哈希重命名文件\n生成 Catalog 映射"]
D --> E["上传到 CDN / 文件服务器"]
end
subgraph runtime [运行时侧]
F["启用 ENABLE_CONTENT_DELIVERY\n脚本宏"] --> G["RuntimeContentSystem.LoadContentCatalog()\n下载并缓存远端 Catalog"]
G --> H["ContentDeliveryGlobalState\n等待 ContentReady 状态"]
H --> I["通过弱引用 ID\n加载新版 Entity Scene / 资源"]
end
E --> GBuild + Publish 代码示例
// Editor 脚本:构建并发布内容更新
static void CreateContentUpdate()
{
var buildTarget = EditorUserBuildSettings.activeBuildTarget;
var tmpFolder = "Library/ContentUpdateBuildDir/MyGame";
// 收集所有 SubScene 的 GUID
var subSceneGuids = new HashSet<Unity.Entities.Hash128>();
foreach (var scene in EditorBuildSettings.scenes)
{
foreach (var ssGuid in EditorEntityScenes.GetSubScenes(scene.guid))
{
subSceneGuids.Add(ssGuid);
}
}
// 此处以客户端 Player GUID 为例。
var instance = DotsGlobalSettings.Instance;
var playerGuid = instance.GetClientGUID();
// 1. Build:提取内容到临时目录
RemoteContentCatalogBuildUtility.BuildContent(
subSceneGuids, playerGuid, buildTarget, tmpFolder);
// 2. Publish:按内容哈希重命名并生成 Catalog
var publishFolder = "Builds/MyGame-RemoteContent";
RemoteContentCatalogBuildUtility.PublishContent(
tmpFolder, publishFolder, _ => new[] { "all" });
}
运行时接收内容更新
// 需要在 Project Settings > Player > Scripting Define Symbols 中添加:
// ENABLE_CONTENT_DELIVERY
public partial struct LoadRemoteCatalogSystem : ISystem
{
private const int MaxLoadAttempts = 3;
private bool m_Initialized;
private int m_LoadAttempts;
public void OnCreate(ref SystemState state)
{
ContentDeliveryGlobalState.RegisterForContentUpdateCompletion(
OnContentReady);
}
private void OnContentReady(
ContentDeliveryGlobalState.ContentUpdateState updateState)
{
if (updateState >=
ContentDeliveryGlobalState.ContentUpdateState.ContentReady)
{
m_Initialized = true;
// 内容已就绪,可以开始加载场景或资源。
}
}
public void OnUpdate(ref SystemState state)
{
if (m_Initialized || m_LoadAttempts >= MaxLoadAttempts)
{
return;
}
m_LoadAttempts++;
RuntimeContentSystem.LoadContentCatalog(
"https://your-cdn.example/content/",
Application.persistentDataPath + "/content-cache/",
"all",
true);
}
}
示例中的 URL 是占位地址,实际使用时应替换为自己的 CDN 或内容服务器,并为失败、离线缓存和版本回退设计完整策略。
使用菜单更新内容
如果不需要自定义内容集合,也可以使用:
Assets > Publish > Content Update
执行后,新的 Content Archive 和 Catalog 会写入 StreamingAssets,可以用于更新应用中的内容,而无需重新构建 Player。
六、方案汇总对比
| 方案 | 适用场景 | 优点 | 局限 |
|---|---|---|---|
| 官方 Content Update(菜单或代码) | SubScene 及其资源内容更新 | 官方支持,无需重新构建 Player | 需要内容服务器;不能更新 C# 逻辑 |
| 自行替换 Entity Scene 文件 | 旧的自定义方案 | 可以完全控制流程 | 需要自行处理目录、Catalog、兼容性和回退,维护成本高 |
| HybridCLR 等代码热更新方案 | C# 逻辑更新 | 可以更新业务代码 | 不属于 Entities Content Delivery,需要单独规划资源边界 |
| 重新构建完整 Player | 引擎、代码或内容变化较大 | 链路最直接 | 用户需要重新下载安装包 |
推荐组合策略
SubScene 场景和资源更新 → 官方 Content Archive + CDN
C# 逻辑更新 → 独立代码热更新方案
UI、音频等独立内容 → Addressables(避免与 ECS 重复收集)
七、常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
| 构建后 SubScene 数据缺失 | 使用 GUID 引用,构建系统没有检测到场景 | 改用 EntitySceneReference 或 SubScene 组件 |
| 内容被重复打包或内存异常 | 同一依赖同时进入 ECS CMS 和 Addressables | 检查构建报告,明确每类资源的唯一管理入口 |
| 热更后内容未生效 | Catalog 未更新,或运行时没有重新加载 | 确认 ENABLE_CONTENT_DELIVERY 和 Catalog 加载流程 |
| Incremental Baking 与 Full Baking 结果不一致 | 两种 Baking 的实体顺序和 Chunk 布局可能不同 | 不依赖实体顺序,发布前使用 Full Baking 验证 |
| Play Mode 进入后实体未立即可用 | 关闭状态的 SubScene 使用异步流式加载 | 等待 SceneSystem.IsSceneLoaded() 返回 true |
总结
SubScene 热更不是在运行时重新执行 Baking,而是将编辑器生成的 Entity Scene 和 Content Archive 作为可分发内容发布。运行时通过 Catalog 找到新版文件,完成下载与缓存后,再按弱引用或场景引用加载内容。
实际项目中最重要的两点是:一是固定 Player 与内容构建所使用的 Entities 版本;二是明确 ECS Content Archive、Addressables 和代码热更新各自负责的边界,避免同一资源被多个系统重复管理。