# Scene ## 模块概述 Scene 模块负责: - 主场景加载 - Additive 子场景加载 - 延迟激活与解除挂起 - 子场景卸载 - 主场景状态追踪 - 场景作用域(Scene Scope)建立与重置 它不是 Unity 默认 `SceneManager` 的简单封装,而是把场景切换与框架服务作用域绑定在一起。 这意味着: - **加载主场景时,会重置 Scene Scope** - **加载 Additive 子场景时,不会重置主场景状态** - **主场景和子场景在框架内是两套不同语义** ## 快速开始 ### 最少步骤 1. 在场景中挂载 `SceneComponent` 2. 确保 `ResourceComponent` 已可用 3. 通过 `GameApp.Scene.LoadSceneAsync(...)` 加载场景 4. 如需卸载 Additive 场景,调用 `UnloadAsync(...)` ### 最小示例 ```csharp using AlicizaX; using UnityEngine.SceneManagement; public sealed class SceneQuickStart { public async void Load() { await GameApp.Scene.LoadSceneAsync("Scene/Battle", LoadSceneMode.Single); } } ``` ## 架构说明 ```text 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 progressCallBack = null)` - `Unload(string location, Action callBack = null, Action progressCallBack = null)` - `IsContainScene(string location)` ### `ISceneStateService` 偏状态查询接口,主要用于: - 查询当前主场景 - 查询某个场景是否已记录在当前 Scene Scope 中 ### `SceneDomainStateService` 当前实现中负责维护: - `CurrentMainSceneName` - `CurrentMainSceneHandle` - `_subScenes` - `_handlingScenes` 用途: - 避免同一场景重复并发加载/卸载 - 为 `IsContainScene` / `IsMainScene` 提供判断基础 ## API 参考 ### 一、场景加载 #### `UniTask LoadSceneAsync(string location, LoadSceneMode sceneMode = LoadSceneMode.Single, bool suspendLoad = false, uint priority = 100, bool gcCollect = true, Action progressCallBack = null)` - 必填参数:`location` - 可选参数:`sceneMode` - 可选参数:`suspendLoad` - 可选参数:`priority` - 可选参数:`gcCollect` - 可选参数:`progressCallBack` - 返回值:`UniTask` 参数说明: - `location`:场景资源定位地址 - `sceneMode`:`Single` 或 `Additive` - `suspendLoad`:是否挂起加载后的激活 - `priority`:加载优先级 - `gcCollect`:主场景切换后是否执行资源回收 - `progressCallBack`:加载进度回调 行为说明: - `Single`:会重置当前 Scene Scope - `Additive`:会作为子场景注册 - 如果同一场景正在处理,当前实现会记录错误并返回默认值 #### `void LoadScene(string location, LoadSceneMode sceneMode = LoadSceneMode.Single, bool suspendLoad = false, uint priority = 100, Action callBack = null, bool gcCollect = true, Action 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` 说明: - 判断给定场景是否为当前主场景 - 内部结合 `SceneDomainStateService` 与 `SceneManager.GetActiveScene()` 做判断 #### `bool IsContainScene(string location)` - 必填参数:`location` - 返回值:`bool` 说明: - 判断当前主场景或子场景列表中是否包含该场景 #### `string CurrentMainSceneName` - 返回值:主场景名 ### 四、场景卸载 #### `UniTask UnloadAsync(string location, Action progressCallBack = null)` - 必填参数:`location` - 可选参数:`progressCallBack` - 返回值:`UniTask` 说明: - 用于卸载 Additive 子场景 - 当前实现**不用于直接卸载主场景** #### `void Unload(string location, Action callBack = null, Action progressCallBack = null)` - 必填参数:`location` - 可选参数:`callBack` - 可选参数:`progressCallBack` - 返回值:无 说明: - 回调式异步卸载 ## 常见用法 ### 1. 加载主场景 ```csharp using AlicizaX; using UnityEngine.SceneManagement; public sealed class LoadMainSceneExample { public async void GoBattle() { await GameApp.Scene.LoadSceneAsync("Scene/Battle", LoadSceneMode.Single); } } ``` ### 2. Additive 加载子场景 ```csharp using AlicizaX; using UnityEngine.SceneManagement; public sealed class AdditiveSceneExample { public async void OpenSubScene() { await GameApp.Scene.LoadSceneAsync("Scene/PhotoRoom", LoadSceneMode.Additive); } } ``` ### 3. 卸载 Additive 子场景 ```csharp 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. 带进度的场景加载 ```csharp 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. 挂起加载后手动激活 ```csharp 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. 使用回调式加载 ```csharp 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. 查询当前主场景 ```csharp using AlicizaX; using UnityEngine; public sealed class SceneStateExample : MonoBehaviour { private void Update() { Debug.Log($"Main Scene: {GameApp.Scene.CurrentMainSceneName}"); } } ``` ## 运行行为细节 ### 1. 主场景加载会重置 Scene Scope 这是本模块最重要的设计点之一。 当调用: ```csharp 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. 主场景加载完成后会触发资源回收 当主场景切换完成后,会调用: ```csharp Context.Require().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 波动