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

478 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 波动