AlicizaX/Client/Assets/Books/Framework/Runtime/Scene.md

478 lines
11 KiB
Markdown
Raw Normal View History

2026-04-01 13:20:06 +08:00
# 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<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`:场景资源定位地址
- `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<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`
说明:
- 判断给定场景是否为当前主场景
- 内部结合 `SceneDomainStateService``SceneManager.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. 加载主场景
```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<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 波动