# 帧动画系统第一、二阶段手动测试方案 ## 1. 测试目标 本文档用于手动验收帧动画系统前两个阶段: - 第一阶段:数据模型、Graph 解析、运行时播放器、统一求值器和播放 Handle。 - 第二阶段:Aseprite JSON 解析、ImportSource、差异预览、稳定刷新、只读 Sprite 匹配和自动切图。 测试重点是验证实际工作流和资产稳定性,不验收第三阶段的完整 Graph 工作台、节点画布或正式删除流程。 ## 2. 测试前准备 ### 2.1 环境 - Unity:2022.3.7f1c1。 - 打开项目后等待脚本编译完成。 - 清空 Console,确认没有编译错误。 - 打开 `Window > General > Test Runner`,确认可以看到: - `AibisDream.FrameAnimation.Tests.EditMode` - `AibisDream.FrameAnimation.Tests.PlayMode` ### 2.2 生成测试样例 依次执行: 1. `Tools > Frame Animation > Rebuild Runtime Sample` 2. `Tools > Frame Animation > Rebuild Import Sample` 生成内容: - 运行时样例场景:`Assets/Scenes/FrameAnimationRuntimeTest.unity` - 第一阶段样例:`Assets/GameContent/Test/FrameAnimation/` - 第二阶段样例:`Assets/GameContent/Test/FrameAnimation/Import/` 第二阶段测试中可以修改 Import 文件夹内的测试 JSON。测试结束后再次执行 `Rebuild Import Sample` 即可恢复,不要修改生产动画资源。 ### 2.3 测试记录 每个用例记录: | 项目 | 内容 | |---|---| | 结果 | 通过 / 失败 / 阻塞 | | 实际表现 | 简述观察结果 | | Console | 是否出现 Error/Exception | | 证据 | 截图、录屏或相关资产路径 | | 缺陷 | 可复现步骤和预期/实际差异 | ## 3. 第一阶段:运行时播放测试 ### RT-01 样例场景基础播放(P0) 操作: 1. 打开 `FrameAnimationRuntimeTest.unity`。 2. 进入 Play Mode。 3. 同时观察左侧 SpriteRenderer 和右侧 UI Image。 4. 选中两个对象,在 `FrameAnimationPlayer` Inspector 中观察 Runtime State。 预期: - 左侧 `Flow Sample - SpriteRenderer` 立即显示 Intro 第 0 帧。 - 左侧约 1.25 秒后从 Intro 自动进入 Idle,之后持续循环 Idle。 - 左侧 Playable 始终为 `IntroToIdle`,Clip 从 `Intro` 变为 `Idle`。 - 右侧 `Direct Idle - Image` 立即显示 Idle 第 0 帧并持续循环。 - 右侧 Playable 和 Clip 均为 `Idle`,Node 为空。 - 两个目标均不依赖 Animator 或 AnimatorController。 - Console 没有 Error 或 Exception。 ### RT-02 长时间循环稳定性(P0) 操作: 1. 保持样例场景运行至少 60 秒。 2. 观察两个目标和 Inspector 中的帧索引。 预期: - Idle 每约 4 秒循环一次。 - 不出现停播、闪空、越界帧或明显累计漂移。 - 内存和 Console 不持续产生异常或日志刷屏。 ### RT-03 动态速度(P0) 操作: 1. Play Mode 中将 Player 的 Speed 改为 `0`。 2. 等待数秒,再改为 `0.5`、`2`,最后恢复 `1`。 预期: - Speed 为 `0` 时停在当前帧,但 State 仍为 Playing。 - 恢复正数后从当前进度继续,不从头重播。 - `0.5` 明显变慢,`2` 明显变快。 - 不丢失 Flow 节点或当前 playable。 ### RT-04 Pause/Resume、Stop 和 Handle 语义(P0) 操作: 1. 在 Test Runner 中运行第一阶段 EditMode 测试。 2. 重点查看 PlaybackSession、PlaybackHandle、Stop、Pause/Resume 和替换请求相关用例。 预期: - Pause 不改变当前帧,Resume 从原进度继续。 - 新的合法 Play 将旧请求以 Replaced 结算。 - Stop 将活动请求以 Stopped 结算。 - 正常播完以 Completed 结算。 - 无效请求以 Failed 结算,且不会打断正在播放的合法请求。 - Task、协程和回调观察到同一个结果,每个请求只结算一次。 说明:当前样例 Inspector 只展示状态,没有完整运行时控制面板,因此这一组 API 语义以 Test Runner 的可重复结果作为验收依据。 ### RT-05 Disable/Enable 生命周期(P0) 操作: 1. Play Mode 中等待动画进入非首帧。 2. 禁用 `FrameAnimationPlayer` 组件。 3. 观察显示目标和 Runtime State。 4. 重新启用 Player。 5. 再对整个 GameObject 执行一次禁用和启用。 预期: - 禁用时活动请求以 Stopped 结束。 - 当前 Sprite 保留,不自动 Clear 或 Hide。 - 重新启用时,因为 `playOnEnable = true`,默认 playable 从头播放。 - 左侧重新从 Intro 第 0 帧开始,右侧重新从 Idle 第 0 帧开始。 ### RT-06 目标配置校验(P0) 在临时场景或复制对象上执行,测试后不要保存改动。 | 配置 | 预期 | |---|---| | Player,无 SpriteRenderer/Image | Inspector 显示缺失目标错误,Play 返回 Failed | | Player + SpriteRenderer + Image | Inspector 显示目标冲突,Play 返回 Failed | | Player + 唯一目标,但 Graph 为空 | Inspector 显示 Graph 缺失,Play 返回 Failed | | Speed 为负数 | Inspector 显示非法速度;运行时 SetSpeed 不接受负值 | 失败请求不得清空或替换其他对象上正在播放的合法动画。 ### RT-07 Clear 与 HideTarget(P1) 操作: 1. 在 PlayMode Test Runner 中运行 Clear/HideTarget 相关用例。 2. 检查 SpriteRenderer 和 Image 两套目标的结果。 预期: - Clear:Sprite 变为 null,但显示组件仍启用。 - HideTarget:只禁用 SpriteRenderer 或 Image。 - Hide 后再次成功 Play 会重新启用目标,并立即显示第 0 帧。 - SpriteRenderer 与 Image 语义一致。 ## 4. 第二阶段:Aseprite 导入测试 ### IMP-01 样例与中文数据(P0) 操作: 1. 执行 `Rebuild Import Sample`。 2. 选中 `ImportSampleGraph.asset`。 3. 展开 Import Sources。 4. 点击 `Preview All Enabled`。 预期: - 存在 Object/只读切图和 Array/自动切图两个来源。 - internalId 是只读 GUID,并可复制。 - 中文来源名、Tag 和 frameName 没有乱码。 - 可看到 `待机`、`眨眼`、`转身`、`惊讶` 等 Imported Clip。 - 初始预览不出现 Error;成功构建后 Clip 状态应为 Unchanged。 - Imported Clip 显示为 Graph 的 sub-asset,而不是独立 `.asset`。 ### IMP-02 Object/Array 与 direction(P0) 操作: 1. 在 Test Runner 中运行第二阶段 Parser 和 TagExpansion 测试。 2. 查看 Object、Array 和四种 direction 用例。 预期: - Object 和 Array 得到相同语义的 SourceFrame。 - Object 帧顺序保持 JSON 属性原始顺序,不按名称重新排序。 - `forward`、`reverse`、`pingpong`、`pingpong_reverse` 顺序正确。 - pingpong 不重复首尾端点,单帧 Tag 只产生一帧。 ### IMP-03 普通内容更新与原地刷新(P0) 操作: 1. 在 Project 窗口展开 `ImportSampleGraph`,选中任意 Imported Clip并保持 Inspector 锁定。 2. 打开对应测试 JSON,将该 Tag 使用帧的 `duration` 改为另一个正整数。 3. 回到 Unity,等待 JSON 重新导入。 4. 点击对应来源的 `Preview Source`。 5. 确认显示 SourceChanged Warning 和 Updated Clip。 6. 点击 `Refresh Source`。 预期: - Preview 不直接修改 Clip。 - Refresh 后仍是原来的 Clip sub-asset,锁定的引用不丢失。 - Clip 的帧时长更新。 - `displayName`、`speed` 和 `defaultEndBehavior` 不被覆盖。 - 刷新后再次 Preview,状态变为 Unchanged,来源变化 Warning 消失。 ### IMP-04 用户重命名 Clip id 后刷新(P0) 操作: 1. 选择一个未作为默认 playable 的 Imported Clip。 2. 将 Clip `id`、`displayName` 和 `speed` 改为自定义值。 3. 修改其源 JSON 的 duration。 4. 对来源执行 Preview 和 Refresh。 预期: - 刷新仍通过 `importSourceId + sourceTagName` 找到原 Clip。 - frames 被更新。 - 用户设置的 id、displayName、speed 和结束行为保持不变。 - 不会因为 Clip id 已改变而创建重复 Clip。 测试完成后执行 `Rebuild Import Sample` 恢复样例,避免重命名影响默认 playable 或 Flow 引用。 ### IMP-05 Tag Missing 与恢复(P0) 操作: 1. 从 Object 测试 JSON 的 `frameTags` 中暂时删除 `待机` 条目,不删除 frames。 2. 点击 `Preview Source`。 3. 确认 `待机` 显示 Missing,再执行 Refresh。 4. 检查 Graph sub-assets。 5. 将原 Tag 完整恢复,再次 Preview 和 Refresh。 预期: - Tag 消失时原 Clip 保留,只设置 `isMissingFromSource = true`。 - 原 Clip 的 Sprite 和帧表不被自动删除。 - Missing Clip 在完整 Graph 校验中报告错误。 - Tag 恢复后更新同一个 Clip,并清除 Missing。 - Node、Flow 或其他对象对原 Clip 的引用不丢失。 ### IMP-06 Tag 改名规则(P1) 操作: 1. 将测试 JSON 中一个 Tag 改为全新的、不冲突的名称。 2. Preview 并 Refresh。 预期: - 旧 Tag 对应 Clip 变为 Missing。 - 新 Tag 创建新的 Imported Clip sub-asset。 - 系统不猜测两者关联,也不迁移旧引用。 执行 `Rebuild Import Sample` 恢复样例。 ### IMP-07 只读 Sprite 匹配(P0) 操作: 1. 选择 Object/只读切图来源,确认 `manageSpriteSlicing = false`。 2. 记录对应 PNG `.meta` 的 Git diff 状态。 3. Preview 和 Refresh 一次未变化的来源。 4. 将 JSON 中一个 frame rect 改成无法匹配已有 Sprite 的位置,再 Preview。 预期: - 正常情况下按 frameName 和 rect 唯一匹配现有 Sprite。 - 未变化刷新不会修改 TextureImporter 或 PNG `.meta`。 - rect 无法匹配时出现 SpriteMatchFailed Error。 - 系统不会自动改切图,也不会应用 Graph/Clip 修改。 ### IMP-08 自动切图与稳定 Sprite ID(P0) 操作: 1. 选择 Array/自动切图来源,确认 `manageSpriteSlicing = true`。 2. 将某个 Sprite 拖到临时场景中的 SpriteRenderer,形成真实序列化引用。 3. 修改 JSON 中该 frame 的 rect,但保持 frameName 不变且 rect 合法。 4. 点击 `Preview Source`。 5. 检查 Sprite diff 后点击 `Refresh Source`,确认 TextureImporter 修改对话框。 预期: - 差异明确显示 Updated SpriteRect。 - 必须确认后才修改 TextureImporter。 - 刷新后同名 Sprite 保持原 spriteID。 - 临时 SpriteRenderer 的 Sprite 引用不变,不出现 Missing。 - rect/pivot 更新为 JSON 和 ImportSource 设置。 - JSON 中消失的 frameName 对应旧 SpriteRect仍保留。 ### IMP-08A 合并帧共享 Sprite(P0) 操作: 1. 使用包含多个 SourceFrame 指向相同 `frame.x/y/w/h` 的 Aseprite JSON。 2. 确认来源设置为 `manageSpriteSlicing = true`,点击 `Preview Source`。 3. 检查来源摘要中的逻辑帧、SpriteSlot 和共享别名数量。 4. 确认刷新后检查对应 Imported Clip 的帧表和 Sprite引用。 5. 再次执行 Preview 和 Refresh。 佩佩素材的预期统计: - 347 个逻辑 SourceFrame。 - 205 个唯一 SpriteSlot。 - 142 个共享别名。 预期: - 相同 rect 只创建一个有效 SpriteRect。 - 不同 SourceFrame仍保留各自的 frameName、sourceIndex、duration 和 Tag顺序。 - 指向同一 rect 的 Clip Frame直接引用同一个 Unity Sprite。 - 第二次 Preview 中 SpriteSlot 和 Clip均显示 Unchanged,Sprite ID保持稳定。 - 原始 PNG 和 JSON不被修改。 ### IMP-09 多写入所有者冲突(P0) 操作: 1. 在样例 Graph 中点击 `Add ImportSource`。 2. 启用新来源,绑定自动切图来源正在使用的同一 Texture 和任意有效 JSON。 3. 设置 `manageSpriteSlicing = true`。 4. 点击 `Preview All Enabled`。 预期: - 出现 TextureOwnershipConflict Error,并列出冲突来源。 - Refresh 不修改 TextureImporter、Graph 或 Clip。 - 将新增来源禁用后冲突消失。 测试完成后执行 `Rebuild Import Sample` 清理额外来源。 ### IMP-10 阻断错误与单来源原子性(P0) 依次测试以下任一错误: - duration 改为 `0`。 - frame rect 越出 Texture。 - `trimmed = true`。 - `rotated = true`。 - direction 改为未知值。 - Tag 范围越界。 操作: 1. 先记录当前 Clip 帧表和 Texture `.meta`。 2. 制造错误并点击 `Preview Source`。 3. 再点击 `Refresh Source`。 预期: - Preview 显示具体结构化 Error。 - 不出现部分 Clip 更新。 - TextureImporter 和 `.meta` 不变化。 - `lastSourceHash` 不更新。 - 现有 Clip 和 sub-asset 引用保持原样。 ### IMP-11 全部来源原子性(P0) 操作: 1. 在一个来源中修改合法 duration,使其产生 Updated。 2. 在另一个来源中制造 duration 为 `0` 的阻断错误。 3. 点击 `Preview All Enabled`,然后点击 `Refresh All Enabled`。 预期: - 预览同时显示合法变化和阻断错误。 - 因任一启用来源失败,整批刷新不应用。 - 合法来源的 Clip 也保持刷新前状态。 - 两个来源的 hash 均不更新。 ### IMP-12 命名冲突(P1) 分别制造: - 两个启用 ImportSource 使用同名 Tag。 - 新 Tag 与现有 Clip id 同名。 - 新 Tag 与现有 Flow id 同名。 预期: - Preview 显示 TagConflict 或 PlayableIdConflict。 - 错误信息能够指出来源和冲突名称。 - 系统不自动加前缀、不自动改名、不创建部分 Clip。 ### IMP-13 Undo/Redo 与保存稳定性(P1) 操作: 1. 执行一次成功的 duration 刷新。 2. 使用 Undo,检查 Clip 帧表和来源 hash。 3. 使用 Redo,再次检查。 4. 保存项目,关闭并重新打开 Unity。 5. 重新检查 Graph、Imported Clip、ImportInfo 和中文名称。 预期: - Undo/Redo 对 Graph 和 Clip 修改成组生效。 - 新建 sub-asset 不留下孤立对象。 - 重启后中文名称、sourceTagName、internalId 和引用不变化。 - 再次 Preview 能正确判断当前来源是否已刷新。 ## 5. 自动测试回归 手动测试完成前至少执行: 1. Test Runner > EditMode > Run All。 2. Test Runner > PlayMode > Run All。 通过标准: - 第一阶段既有测试全部通过。 - 第二阶段 Parser、direction、只读匹配、稳定 sub-asset、Missing 和所有权测试全部通过。 - 没有新增 Console Error 或未处理 Exception。 ## 6. 总体验收标准 以下项目全部满足后,第一、二阶段可视为通过: - 不依赖 AnimatorController 即可播放直接 Clip 和线性 Flow。 - SpriteRenderer 与 Image 的首帧、循环、速度、生命周期和显示语义正确。 - 播放请求完成原因和 Failed 隔离符合定义。 - Object/Array Aseprite JSON 均能正确解析,中文数据稳定。 - Imported Clip 能新增、原地更新、Missing 和恢复。 - 用户拥有字段不会被刷新覆盖。 - 只读来源绝不修改 TextureImporter。 - 自动切图为同名 frameName 保留稳定 Sprite ID。 - 合并帧只创建唯一 SpriteSlot,同时保留全部逻辑帧时长与播放顺序。 - 多写入所有者、素材错误和命名冲突能够阻断刷新。 - 单来源和全部来源刷新都不产生部分 Graph/Clip 更新。 - 保存、Undo/Redo 和重新打开 Unity 后资产引用保持稳定。 若任一 P0 用例失败,先停止进入第三阶段并记录缺陷;P1 问题可以评估后决定是否阻断,但不得破坏资产引用、原子刷新或运行时播放语义。