完成 Game Core P0 验收收口
This commit is contained in:
102
docs/fixes/audio-cancellation-semantics-fix.md
Normal file
102
docs/fixes/audio-cancellation-semantics-fix.md
Normal file
@@ -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<IResourceHandle<AudioClip>> LoadClipAsync(
|
||||
string key,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var handle = await _resources.LoadAsync<AudioClip>(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<AudioSource>().Length;
|
||||
|
||||
yield return ThrowsAsync<OperationCanceledException>(
|
||||
service.PlayBgmAsync("bgm-a", cancellationToken: cts.Token));
|
||||
|
||||
Assert.That(Object.FindObjectsOfType<AudioSource>().Length, Is.EqualTo(sourceCountBeforePlay));
|
||||
}
|
||||
```
|
||||
|
||||
如果现有测试工具方法只支持 `Func<Task>`,也可以按当前测试文件风格新增一个接收 `Task` 的 `ThrowsAsync<TException>` helper。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- `PlayBgmAsync` 在取消时抛出 `OperationCanceledException`。
|
||||
- `PlaySfxAsync` 在取消时抛出 `OperationCanceledException`。
|
||||
- 取消时不创建 `AudioSource`。
|
||||
- 取消时不把异常包装成 `InvalidOperationException`。
|
||||
- 现有 PlayMode 音频测试继续通过。
|
||||
|
||||
## 后续备注
|
||||
|
||||
该问题不是架构阻塞项,但修复成本很低。建议本轮直接修掉,避免后续异步服务取消语义不一致。
|
||||
Reference in New Issue
Block a user