本文梳理 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 BakingSubScene 关闭时、Entity Scene 文件缺失或过期时后台进程异步执行,全量重新烘焙
Incremental BakingSubScene 打开时的增量修改内存中执行,只重新烘焙被修改的部分,实时生效

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.sharedMeshUnity 自动管理,按需加载和卸载
弱引用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 --> G

Build + 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 和代码热更新各自负责的边界,避免同一资源被多个系统重复管理。

参考资料