From e1197b5c0f9b6c87512d40baf1e051e16e2b8bc3 Mon Sep 17 00:00:00 2001 From: "JSD\\13999" <1399945104@qq.com> Date: Wed, 20 May 2026 11:57:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=20Game=20Core=20P0=20?= =?UTF-8?q?=E9=AA=8C=E6=94=B6=E6=94=B6=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SceneTemplateSettings.json | 121 +++++++++ .../fixes/audio-cancellation-semantics-fix.md | 102 +++++++ docs/reviews/flowscope-framework-review.md | 248 ++++++++++++++++++ 3 files changed, 471 insertions(+) create mode 100644 My project/ProjectSettings/SceneTemplateSettings.json create mode 100644 docs/fixes/audio-cancellation-semantics-fix.md create mode 100644 docs/reviews/flowscope-framework-review.md diff --git a/My project/ProjectSettings/SceneTemplateSettings.json b/My project/ProjectSettings/SceneTemplateSettings.json new file mode 100644 index 0000000..5e97f83 --- /dev/null +++ b/My project/ProjectSettings/SceneTemplateSettings.json @@ -0,0 +1,121 @@ +{ + "templatePinStates": [], + "dependencyTypeInfos": [ + { + "userAdded": false, + "type": "UnityEngine.AnimationClip", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.Animations.AnimatorController", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.AnimatorOverrideController", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.Audio.AudioMixerController", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.ComputeShader", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.Cubemap", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.GameObject", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.LightingDataAsset", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.LightingSettings", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Material", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.MonoScript", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.PhysicMaterial", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.PhysicsMaterial2D", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Rendering.PostProcessing.PostProcessProfile", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Rendering.PostProcessing.PostProcessResources", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Rendering.VolumeProfile", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.SceneAsset", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.Shader", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.ShaderVariantCollection", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.Texture", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Texture2D", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Timeline.TimelineAsset", + "defaultInstantiationMode": 0 + } + ], + "defaultDependencyTypeInfo": { + "userAdded": false, + "type": "", + "defaultInstantiationMode": 1 + }, + "newSceneOverride": 0 +} \ No newline at end of file diff --git a/docs/fixes/audio-cancellation-semantics-fix.md b/docs/fixes/audio-cancellation-semantics-fix.md new file mode 100644 index 0000000..3cff312 --- /dev/null +++ b/docs/fixes/audio-cancellation-semantics-fix.md @@ -0,0 +1,102 @@ +# AudioService 取消语义修复说明 + +日期:2026-05-20 +范围:`My project/Assets/FlowScope/Runtime/Audio/AudioService.cs` +优先级:P0 小修复 +结论:建议立即顺手修复 + +## 问题概述 + +`AudioService.PlayBgmAsync` 和 `AudioService.PlaySfxAsync` 支持传入 `CancellationToken`,但当前内部 `LoadClipAsync` 会捕获所有异常,并统一包装为 `InvalidOperationException`。 + +这会导致调用方主动取消音频加载时,拿到的不是 `OperationCanceledException`,而是普通失败异常。 + +## 影响 + +取消语义被破坏后,调用方无法可靠区分以下两类情况: + +- 生命周期或用户操作导致的正常取消。 +- 资源缺失、Addressables 失败、类型不匹配等真实加载错误。 + +可能造成的问题: + +- 正常取消被记录成错误日志。 +- UI 或业务层误判为资源加载失败。 +- 后续重试、错误兜底、统计埋点出现噪音。 +- 框架内不同服务的取消语义不一致,增加异步生命周期排查成本。 + +## 推荐修改 + +在 `AudioService.LoadClipAsync` 中单独保留 `OperationCanceledException`,不要包装。 + +目标代码形态: + +```csharp +private async Task> LoadClipAsync( + string key, + CancellationToken cancellationToken) +{ + try + { + var handle = await _resources.LoadAsync(key, cancellationToken); + return ValidateLoadedClip(key, handle); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception exception) + { + throw new InvalidOperationException($"Failed to load audio clip '{key}'.", exception); + } +} +``` + +## 测试建议 + +在 `AudioServiceTests` 中补充异步取消测试。 + +建议覆盖: + +1. `PlayBgmAsync_WhenCanceled_ThrowsOperationCanceledException` +2. `PlaySfxAsync_WhenCanceled_ThrowsOperationCanceledException` + +测试要点: + +- 创建 `CancellationTokenSource`。 +- 调用 `Cancel()`。 +- 调用 `PlayBgmAsync` 或 `PlaySfxAsync`。 +- 断言抛出 `OperationCanceledException`。 +- 断言没有创建新的 `AudioSource`。 + +示例结构: + +```csharp +[UnityTest] +public IEnumerator PlayBgmAsync_WhenCanceled_ThrowsOperationCanceledException() +{ + var service = CreateService(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + var sourceCountBeforePlay = Object.FindObjectsOfType().Length; + + yield return ThrowsAsync( + service.PlayBgmAsync("bgm-a", cancellationToken: cts.Token)); + + Assert.That(Object.FindObjectsOfType().Length, Is.EqualTo(sourceCountBeforePlay)); +} +``` + +如果现有测试工具方法只支持 `Func`,也可以按当前测试文件风格新增一个接收 `Task` 的 `ThrowsAsync` helper。 + +## 验收标准 + +- `PlayBgmAsync` 在取消时抛出 `OperationCanceledException`。 +- `PlaySfxAsync` 在取消时抛出 `OperationCanceledException`。 +- 取消时不创建 `AudioSource`。 +- 取消时不把异常包装成 `InvalidOperationException`。 +- 现有 PlayMode 音频测试继续通过。 + +## 后续备注 + +该问题不是架构阻塞项,但修复成本很低。建议本轮直接修掉,避免后续异步服务取消语义不一致。 diff --git a/docs/reviews/flowscope-framework-review.md b/docs/reviews/flowscope-framework-review.md new file mode 100644 index 0000000..3c4ae1e --- /dev/null +++ b/docs/reviews/flowscope-framework-review.md @@ -0,0 +1,248 @@ +# FlowScope 框架评审文档 + +评审日期:2026-05-19 +评审对象:`My project/Assets/FlowScope` +评审范围:Runtime、Tests、Samples、asmdef、核心服务实现与测试覆盖 +评审结论:建议有条件通过,定位为 P0/P1 阶段轻量 Unity 游戏基础框架 + +## 1. 总体结论 + +FlowScope 当前已经具备一个轻量游戏框架的基本形态,适合用于小型项目、原型项目或作为后续框架演进的 P0 基座。它的优势在于分层清晰、抽象克制、测试意识较好,并且围绕 Unity 常见横切能力建立了可替换接口。 + +当前不建议直接定义为稳定生产框架。主要原因是资源异步、运行时对象销毁、Addressables 依赖方式和自研 JSON 转换器仍存在生产风险。这些问题不影响它作为框架雏形继续推进,但应作为进入生产复用前的前置整改项。 + +## 2. 技术基线 + +- Unity 版本:`2022.3.62f2c1` +- 主要依赖: + - `com.cysharp.r3` + - `org.nuget.r3` + - `com.unity.ugui` + - `com.unity.textmeshpro` + - `com.unity.timeline` +- 目录结构: + - `Runtime`:框架运行时代码 + - `Tests/EditMode`:编辑器测试 + - `Tests/PlayMode`:运行时测试 + - `Samples/MainMenuP0`:P0 示例场景与示例业务 +- asmdef: + - `FlowScope.Runtime` + - `FlowScope.Tests.EditMode` + - `FlowScope.Tests.PlayMode` + - `FlowScope.Samples.MainMenuP0` + +## 3. 现有模块评价 + +### 3.1 Flow + +代表文件:`Runtime/Flow/GameFlow.cs` + +`GameFlow` 负责 Feature 生命周期编排,包括启动、切换、关闭、失败清理和状态保护。整体职责清晰,和 `IFeature`、`FeatureContext` 的边界较自然。 + +优点: + +- 状态机明确,避免非法状态调用。 +- Feature 使用独立 scope,便于做生命周期隔离。 +- 失败路径中有资源、容器、Feature 的清理逻辑。 +- 测试覆盖了启动、切换、关闭、失败和取消场景。 + +风险: + +- `StartupAsync` 固定依赖 `IConfigProvider.LoadAllAsync`,会让无配置项目、分包配置、热更新配置的启动流程不够灵活。 +- `GameFlow` 当前更像“启动编排器 + Feature 生命周期管理器”的混合体,后续业务复杂时可能需要把配置加载职责移回 Bootstrap。 + +评价:结构良好,建议保留为框架核心,但需要降低对配置加载的硬绑定。 + +### 3.2 Container + +代表文件:`Runtime/Container/Container.cs` + +自研容器实现轻量,支持实例注册、工厂注册、作用域、父子容器、循环依赖检测和拥有对象释放。 + +优点: + +- API 简单,学习成本低。 +- scope 模型适合 Feature 生命周期。 +- dispose 顺序和循环依赖都有测试。 +- 没有引入大型 DI 框架,符合轻量框架定位。 + +风险: + +- `GeneratedFactories` 需要外部注册,长期需要配套生成器或明确手写规范。 +- 反射工厂适合开发期,但移动端/AOT 环境需要持续验证。 +- 当前没有生命周期枚举,例如 transient、scoped、singleton,实际行为需要文档明确。 + +评价:P0 阶段可接受。后续重点不是扩大能力,而是写清注册规范和 AOT 策略。 + +### 3.3 Resources + +代表文件:`Runtime/Resources/AddressablesResourceService.cs` + +资源层抽象了 `IResourceService`、`IResourceHandle` 和 `IResourceGroup`,并实现了 Addressables 加载、引用计数和分组释放。 + +优点: + +- 使用句柄释放资源,方向正确。 +- 支持同 key 同类型加载去重。 +- `ResourceGroup` 适合跟随 Feature 生命周期批量释放。 +- PlayMode 测试覆盖了复用、引用计数和取消等路径。 + +风险: + +- Addressables 通过字符串反射调用,编译期无法发现 API 变化。 +- Runtime asmdef 没有显式引用 Addressables,当前设计牺牲了类型安全。 +- 取消等待后底层 Addressables 任务可能仍继续执行,后续需要确认资源完成后的释放策略。 + +评价:抽象方向正确,但 Addressables 适配层应从 Runtime 核心拆出,改为显式依赖的独立 asmdef。 + +### 3.4 UI + +代表文件:`Runtime/UI/UIManager.cs` + +UI 模块基于 `UIPanelAttribute` 标记层级和路径,通过 `UIManager` 加载 prefab、挂载到 Canvas、绑定 ViewModel,并支持 Destroy/Cache 策略。 + +优点: + +- 使用 Attribute 降低打开面板时的字符串散落。 +- 支持按层关闭、关闭全部、缓存复用。 +- 打开失败时有句柄释放和实例销毁逻辑。 +- PlayMode 测试覆盖了路径、层级、缓存、关闭顺序和 Bind 失败。 + +风险: + +- 运行时代码使用 `DestroyImmediate`,不适合常规 PlayMode/Runtime 生命周期。 +- 面板路径约定较隐式,需要文档约束或配置化。 +- 当前只允许关闭栈顶面板,适合简单 UI,但复杂弹窗管理可能需要更细的策略。 + +评价:可作为 P0 UI 管理器,但 `DestroyImmediate` 应优先修正。 + +### 3.5 Audio + +代表文件:`Runtime/Audio/AudioService.cs` + +音频模块提供 BGM、SFX、音量、静音、淡入淡出和 SFX 池。 + +优点: + +- 接口简单,适合快速接入。 +- SFX 池和复用策略清楚。 +- PlayMode 测试覆盖了播放、停止、池耗尽、音量和淡入淡出。 + +风险: + +- `LoadClip` 内部使用 `.GetAwaiter().GetResult()` 同步等待异步资源加载,在 Unity 主线程和 Addressables 场景下存在卡顿或死锁风险。 +- 音频服务是 `MonoBehaviour`,但依赖通过 `Initialize` 注入,需要文档明确创建流程。 +- 当前播放接口是同步方法,与异步资源系统存在模型冲突。 + +评价:功能够用,但异步资源加载方式必须调整后才适合生产复用。 + +### 3.6 Save / Config / Data + +代表文件: + +- `Runtime/Save/SaveService.cs` +- `Runtime/Config/JsonConfigProvider.cs` +- `Runtime/Data/ReactivePropertyJsonConverter.cs` + +数据相关模块围绕 R3 的 `ReactiveProperty` 做了 JSON 序列化、存档读写和配置加载。 + +优点: + +- Save 的 storage 和 serializer 可替换。 +- Config 支持多表加载和重复 id 检查。 +- Data 层能处理 `ReactiveProperty`,契合 R3 数据模型。 +- 相关 EditMode 测试较完整。 + +风险: + +- JSON parser/serializer 是自研实现,长期边界成本较高。 +- 当前更适合简单 DTO,不适合复杂对象图、版本迁移、字段重命名、特殊字符完整兼容等场景。 +- `GetAll` 返回新 List,顺序来自 Dictionary values,不应作为稳定排序依赖。 + +评价:P0 可以使用,但应明确“简单数据模型”边界。生产化建议接入成熟 JSON 库,保留 R3 适配层。 + +## 4. 测试覆盖评价 + +现有测试覆盖优于一般框架雏形,尤其是: + +- `GameFlowTests` 覆盖生命周期和异常清理。 +- `ContainerTests` 覆盖注册、scope、dispose、循环依赖。 +- `UIManagerTests` 覆盖 UI 打开、关闭、缓存和异常。 +- `AudioServiceTests` 覆盖播放、停止、池和音量。 +- `SaveServiceTests`、`JsonConfigProviderTests`、`DataSerializationTests` 覆盖数据链路。 + +仍建议补充: + +- Addressables 真实包接入测试或集成验证。 +- AOT/linker 场景验证。 +- UI 使用 `Destroy` 后的帧级测试调整。 +- 音频异步加载接口改造后的取消、失败和重复播放测试。 +- Config/Save 数据版本迁移测试。 + +## 5. 主要风险清单 + +| 优先级 | 风险 | 影响 | 建议 | +| --- | --- | --- | --- | +| P0 | `AudioService` 同步等待异步资源 | 可能导致主线程卡顿或死锁 | 改为异步播放或预加载机制 | +| P0 | Runtime 使用 `DestroyImmediate` | 运行时生命周期不稳 | 改为 `Destroy`,测试按帧等待 | +| P1 | Addressables 反射调用 | 类型安全弱,升级风险高 | 拆独立 Addressables 适配 asmdef | +| P1 | 自研 JSON 转换器 | 边界和维护成本高 | 明确 DTO 限制或接入成熟 JSON 库 | +| P1 | `GameFlow` 固定加载配置 | 启动流程不够弹性 | 配置加载移到 Bootstrap 或可选策略 | +| P2 | 容器生命周期语义文档不足 | 使用者容易误判对象生命周期 | 补充注册和释放规范 | + +## 6. 审批意见 + +结论:有条件通过。 + +通过范围: + +- 可继续作为 FlowScope P0/P1 基础框架推进。 +- 可用于示例项目、小型原型项目和内部验证项目。 +- 可作为后续 Runtime 包化、模块拆分和 API 稳定化的基础。 + +限制条件: + +- 暂不建议标记为生产稳定版本。 +- 暂不建议在多人长期项目中无约束扩散使用。 +- 在进入生产复用前,必须完成 P0 风险整改。 + +前置整改项: + +1. 将 `UIManager` 运行时销毁逻辑从 `DestroyImmediate` 调整为 `Destroy`。 +2. 将 `AudioService` 的资源加载从同步阻塞改为异步或预加载模型。 +3. 为 Addressables 适配明确依赖策略,优先拆出独立 asmdef。 +4. 补充框架使用约定文档,包括 Bootstrap、Feature、资源释放、UI 路径、配置 DTO 限制。 + +## 7. 后续建议 + +短期建议: + +- 先修生命周期和异步资源问题,不急于扩展新模块。 +- 保持框架轻量,不引入事件总线、复杂状态机编辑器、大型 DI 等重能力。 +- 将 Samples 继续作为验收样板,所有框架规则都应能在 Sample 中体现。 + +中期建议: + +- 将 Runtime 拆为核心层和适配层,例如 `FlowScope.Runtime`、`FlowScope.Addressables`。 +- 为容器工厂生成建立明确流程,降低手写注册成本。 +- 给 Save/Config 增加数据版本和迁移策略。 + +长期建议: + +- 明确 FlowScope 的定位:小型游戏基础设施,而不是全能游戏引擎框架。 +- API 稳定后再考虑 package 化和跨项目复用。 +- 保持测试先行,所有生命周期规则和失败路径都应有测试保护。 + +## 8. 最终评级 + +| 维度 | 评级 | 说明 | +| --- | --- | --- | +| 架构清晰度 | 良好 | 模块边界清楚,抽象克制 | +| 可测试性 | 良好 | 已有较完整 EditMode/PlayMode 测试 | +| Unity 生命周期安全 | 一般 | `DestroyImmediate` 和同步等待需修正 | +| 生产稳定性 | 一般 | 仍处于 P0/P1 框架雏形 | +| 可演进性 | 良好 | 适合继续拆模块、补规范、稳定 API | + +综合评级:B+。 + +结论摘要:FlowScope 是一个有工程意识的轻量 Unity 框架骨架,值得继续推进;但在正式生产复用前,需要优先修正异步资源、运行时销毁和 Addressables 依赖方式三个关键问题。