AlicizaX/Client/Assets/Books/Framework/Runtime/Scene.md
2026-04-01 13:20:06 +08:00

11 KiB
Raw Blame History

Scene

模块概述

Scene 模块负责:

  • 主场景加载
  • Additive 子场景加载
  • 延迟激活与解除挂起
  • 子场景卸载
  • 主场景状态追踪
  • 场景作用域Scene Scope建立与重置

它不是 Unity 默认 SceneManager 的简单封装,而是把场景切换与框架服务作用域绑定在一起。

这意味着:

  • 加载主场景时,会重置 Scene Scope
  • 加载 Additive 子场景时,不会重置主场景状态
  • 主场景和子场景在框架内是两套不同语义

快速开始

最少步骤

  1. 在场景中挂载 SceneComponent
  2. 确保 ResourceComponent 已可用
  3. 通过 GameApp.Scene.LoadSceneAsync(...) 加载场景
  4. 如需卸载 Additive 场景,调用 UnloadAsync(...)

最小示例

using AlicizaX;
using UnityEngine.SceneManagement;

public sealed class SceneQuickStart
{
    public async void Load()
    {
        await GameApp.Scene.LoadSceneAsync("Scene/Battle", LoadSceneMode.Single);
    }
}

架构说明

SceneComponent
  └─ SceneService
      └─ SceneDomainStateService
          ├─ CurrentMainSceneName
          ├─ CurrentMainSceneHandle
          ├─ SubScene Map
          └─ Handling Scene Set

关键协作关系

  • SceneComponent:注册 SceneService 并确保 Scene Scope 存在
  • SceneService:处理加载、激活、卸载与状态切换
  • SceneDomainStateService:记录当前主场景、子场景和处理中场景
  • IResourceService:主场景切换完成后触发资源回收

主场景与子场景的区别

主场景Main Scene

  • 通过 LoadSceneMode.Single 加载
  • 加载前会重置 Scene Scope
  • 加载完成后更新 CurrentMainSceneName
  • 切换完成后会触发一次资源回收

子场景Sub Scene / Additive

  • 通过 LoadSceneMode.Additive 加载
  • 记录在 _subScenes 字典中
  • 可通过 UnloadAsync(location) 卸载

核心类与接口

ISceneService

公开能力:

  • CurrentMainSceneName
  • LoadSceneAsync(...)
  • LoadScene(...)
  • ActivateScene(string location)
  • UnSuspend(string location)
  • IsMainScene(string location)
  • UnloadAsync(string location, Action<float> progressCallBack = null)
  • Unload(string location, Action callBack = null, Action<float> progressCallBack = null)
  • IsContainScene(string location)

ISceneStateService

偏状态查询接口,主要用于:

  • 查询当前主场景
  • 查询某个场景是否已记录在当前 Scene Scope 中

SceneDomainStateService

当前实现中负责维护:

  • CurrentMainSceneName
  • CurrentMainSceneHandle
  • _subScenes
  • _handlingScenes

用途:

  • 避免同一场景重复并发加载/卸载
  • IsContainScene / IsMainScene 提供判断基础

API 参考

一、场景加载

UniTask<Scene> LoadSceneAsync(string location, LoadSceneMode sceneMode = LoadSceneMode.Single, bool suspendLoad = false, uint priority = 100, bool gcCollect = true, Action<float> progressCallBack = null)

  • 必填参数:location
  • 可选参数:sceneMode
  • 可选参数:suspendLoad
  • 可选参数:priority
  • 可选参数:gcCollect
  • 可选参数:progressCallBack
  • 返回值:UniTask<UnityEngine.SceneManagement.Scene>

参数说明:

  • location:场景资源定位地址
  • sceneModeSingleAdditive
  • suspendLoad:是否挂起加载后的激活
  • priority:加载优先级
  • gcCollect:主场景切换后是否执行资源回收
  • progressCallBack:加载进度回调

行为说明:

  • Single:会重置当前 Scene Scope
  • Additive:会作为子场景注册
  • 如果同一场景正在处理,当前实现会记录错误并返回默认值

void LoadScene(string location, LoadSceneMode sceneMode = LoadSceneMode.Single, bool suspendLoad = false, uint priority = 100, Action<Scene> callBack = null, bool gcCollect = true, Action<float> progressCallBack = null)

  • 注意:这个方法不是同步加载
  • 本质上仍是异步加载,只是使用回调而不是 await

推荐:

  • 新代码优先使用 LoadSceneAsync

二、场景激活与挂起

bool ActivateScene(string location)

  • 必填参数:location
  • 返回值:bool

说明:

  • 对应场景已被挂起时,尝试激活它
  • 可用于 suspendLoad = true 的场景

bool UnSuspend(string location)

  • 必填参数:location
  • 返回值:bool

说明:

  • 解除场景挂起
  • 语义接近 ActivateScene

三、场景查询

bool IsMainScene(string location)

  • 必填参数:location
  • 返回值:bool

说明:

  • 判断给定场景是否为当前主场景
  • 内部结合 SceneDomainStateServiceSceneManager.GetActiveScene() 做判断

bool IsContainScene(string location)

  • 必填参数:location
  • 返回值:bool

说明:

  • 判断当前主场景或子场景列表中是否包含该场景

string CurrentMainSceneName

  • 返回值:主场景名

四、场景卸载

UniTask<bool> UnloadAsync(string location, Action<float> progressCallBack = null)

  • 必填参数:location
  • 可选参数:progressCallBack
  • 返回值:UniTask<bool>

说明:

  • 用于卸载 Additive 子场景
  • 当前实现不用于直接卸载主场景

void Unload(string location, Action callBack = null, Action<float> progressCallBack = null)

  • 必填参数:location
  • 可选参数:callBack
  • 可选参数:progressCallBack
  • 返回值:无

说明:

  • 回调式异步卸载

常见用法

1. 加载主场景

using AlicizaX;
using UnityEngine.SceneManagement;

public sealed class LoadMainSceneExample
{
    public async void GoBattle()
    {
        await GameApp.Scene.LoadSceneAsync("Scene/Battle", LoadSceneMode.Single);
    }
}

2. Additive 加载子场景

using AlicizaX;
using UnityEngine.SceneManagement;

public sealed class AdditiveSceneExample
{
    public async void OpenSubScene()
    {
        await GameApp.Scene.LoadSceneAsync("Scene/PhotoRoom", LoadSceneMode.Additive);
    }
}

3. 卸载 Additive 子场景

using AlicizaX;

public sealed class UnloadSubSceneExample
{
    public async void CloseSubScene()
    {
        if (GameApp.Scene.IsContainScene("Scene/PhotoRoom"))
        {
            bool ok = await GameApp.Scene.UnloadAsync("Scene/PhotoRoom");
            UnityEngine.Debug.Log($"Unload result: {ok}");
        }
    }
}

4. 带进度的场景加载

using AlicizaX;
using UnityEngine;
using UnityEngine.SceneManagement;

public sealed class SceneProgressExample
{
    public async void LoadWithProgress()
    {
        await GameApp.Scene.LoadSceneAsync(
            "Scene/Battle",
            LoadSceneMode.Single,
            suspendLoad: false,
            priority: 100,
            gcCollect: true,
            progressCallBack: progress =>
            {
                Debug.Log($"Scene progress: {progress:P0}");
            });
    }
}

5. 挂起加载后手动激活

using AlicizaX;
using UnityEngine.SceneManagement;

public sealed class SuspendLoadExample
{
    public async void LoadThenActivate()
    {
        await GameApp.Scene.LoadSceneAsync(
            "Scene/Battle",
            LoadSceneMode.Single,
            suspendLoad: true);

        GameApp.Scene.ActivateScene("Scene/Battle");
    }
}

6. 使用回调式加载

using AlicizaX;
using UnityEngine;
using UnityEngine.SceneManagement;

public sealed class SceneCallbackExample
{
    public void LoadLobby()
    {
        GameApp.Scene.LoadScene(
            "Scene/Lobby",
            LoadSceneMode.Single,
            suspendLoad: false,
            priority: 100,
            callBack: scene =>
            {
                Debug.Log($"Loaded scene: {scene.name}");
            },
            gcCollect: true,
            progressCallBack: progress =>
            {
                Debug.Log($"Loading: {progress:P0}");
            });
    }
}

7. 查询当前主场景

using AlicizaX;
using UnityEngine;

public sealed class SceneStateExample : MonoBehaviour
{
    private void Update()
    {
        Debug.Log($"Main Scene: {GameApp.Scene.CurrentMainSceneName}");
    }
}

运行行为细节

1. 主场景加载会重置 Scene Scope

这是本模块最重要的设计点之一。

当调用:

GameApp.Scene.LoadSceneAsync("Scene/Battle", LoadSceneMode.Single)

内部会:

  1. Context.ResetScene()
  2. 重新注册 SceneDomainStateService
  3. 标记新主场景进入加载中

这意味着:

  • 旧的 Scene Scope 服务会被重建
  • 与旧主场景强绑定的场景级服务也应重新初始化

2. Additive 场景不会重置主场景作用域

使用 LoadSceneMode.Additive 时:

  • 场景会被加入 _subScenes
  • 主场景状态保留
  • 适合加载摄影间、剧情副场景、临时房间等

3. UnloadAsync 只对 Additive 子场景有效

当前实现中:

  • 只有 _subScenes 中登记的场景才会走卸载逻辑
  • 主场景切换依赖新的 LoadScene(Single),而不是单独 Unload 主场景

4. 同一场景并发处理会被拦截

SceneDomainStateService 会使用 _handlingScenes 记录“正在加载/卸载”的场景。

效果:

  • 避免同一路径重复加载或重复卸载
  • 减少状态错乱

5. 主场景加载完成后会触发资源回收

当主场景切换完成后,会调用:

Context.Require<IResourceService>().ForceUnloadUnusedAssets(gcCollect);

因此:

  • 场景切换是资源回收的重要时间点
  • gcCollect 参数会影响切场景后的回收强度

6. LoadScene 方法名容易误导

虽然名字像“同步加载”,但当前实现中:

  • LoadScene(...) 仍然是异步加载
  • 区别只是它通过回调返回结果,而不是 await

最佳实践

  • 主场景切换统一交给流程层管理
  • Additive 场景只用于临时叠加内容,不要滥用为主流程状态切换
  • 若需要加载转场动画,可用 suspendLoad = true + 手动激活
  • 场景切换后如有场景级服务初始化,放在新的 Scene Scope 生命周期里完成

常见错误

1. 试图用 UnloadAsync 卸载主场景

现象:

  • 返回 false 或警告

正确方式:

  • 通过加载新的 Single 主场景来替换

2. 把 LoadScene(...) 当同步函数使用

现象:

  • 加载还没完成就执行后续依赖逻辑

规避:

  • 优先使用 LoadSceneAsync(...)
  • 或把后续逻辑写入回调中

3. 重复 Additive 加载同一场景

现象:

  • 异步版可能直接抛异常
  • 回调版会记录警告

规避:

  • 在加载前先用 IsContainScene(location) 做检查

性能注意事项

  • 场景切换本身是重量级操作,不要把短生命周期面板式内容做成 Additive 场景
  • 进度回调每帧执行UI 刷新时应尽量轻量
  • 主场景切换后伴随资源回收,切场景阶段要预估回收开销和 GC 波动