Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions frontend/src/pages/playtest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Playtest

Playtest 是角色动画的只读调试和核验界面。它既能直接操控角色验证 idle、walk、jump
之间的响应,也能逐帧查看动作、时长、位移与质量标记,并在不改变角色素材的前提下记录
整体核验结论。

## 入口

- **Demo:`/playtest/demo`**。直接使用明确标注的少年开发 fixture,方便独立查看 idle 和
walk 动作。
- **正式:`/playtest/:characterId/:outfitId`**。需要由应用提供 `PlaytestPageAPIs`;页面读取
指定角色和最新核验结论。

正式入口未配置 APIs 时,页面会明确显示“Playtest 后端接口尚未配置”。它不会加载或回退到
Demo 少年数据。

## 数据与只读边界

Demo 数据来自 `public/playtest-fixtures/boy/` 的已提取少年帧,以及
`testing/demo-character.ts` 中的开发 fixture。素材来源和用途说明见该目录的 `SOURCE.md`。

Playtest 不修改角色、造型、动作、帧、工作流或版本。正式入口唯一可保存的内容是独立的
Playtest 核验结论;Demo 中的核验状态仅保留在当前页面,不写入本地存储或后端。

## 统一工作台

页面只有一套 `PlaybackController`。动作列表、方向选择、统一舞台、播放控制、时间线,
以及右侧“帧检查 / 问题记录 / 资产导出”三个页签共同读取当前动作、方向和帧。键盘、按钮或
时间线改变帧后,舞台与右侧工具会同步更新,不存在隐藏的第二套状态。

- 左侧动作栏展开宽度为 190px,也可收起为窄条;三栏在宽屏共用相同高度和底边,右侧长内容
在栏内滚动,不再向下撑出额外空白。
- 按住 A/D 时临时播放 `walk` 的逐帧动画,并按各帧 `rootMotion` 移动;松开后,若按键前处于
暂停状态则恢复暂停,原本正在播放则继续播放。A 面向左,D 面向右。
- 没有可播放 `walk` 时,A/D 不改变当前动作、镜像或播放状态。
- ← / → 才是当前序列的上一帧 / 下一帧控制,长按时可连续切帧。
- W 选择首个包含有效帧的 jump 动作并从首帧播放。
- S 选择首个包含有效帧的 crouch 动作并从首帧播放。
- jump/crouch 缺失时,W/S 不改变当前动作,也不会用其他动作图片冒充。

当前 Demo 少年包含 idle 和 walk,不包含 jump 或 crouch,因此界面会明确显示“未提供跳跃
动作”和“未提供下蹲动作”。以后正式 Character 提供对应动作后,同一套控制器会自动启用
W/S。

## 播放与核验

- 初始为暂停;切换动作、方向或手动选择帧时暂停。
- 每帧的 `durationMs` 优先;未提供有效时长时才按动作 FPS 计算显示与播放间隔。
- 关闭循环时,播放停留在末帧;开启循环时,末帧会回到首帧。
- 页面会以无障碍实时播报当前动作、帧位置和播放状态。
- 舞台始终只渲染当前帧角色,不叠加上一帧、下一帧或其他动作的角色虚影。
- 有横向根位移的角色到达舞台左右边界后会自动掉头并继续播放。边界按舞台和角色的实际显示
宽度计算;窗口尺寸改变后会重新钳制。自动掉头不显示文字,也不触发额外播报。

## 逐帧自动审核依据

逐帧审核会在浏览器中读取每张图片的 Alpha 像素,提供只读的几何检查依据。Alpha 大于
24 的像素视为候选前景;算法按八邻域过滤小于 `max(4 像素, 最大连通区域的 0.2%)` 的
孤立噪点,同时始终保留最大主体。

右侧分别展示三类位移,不能混为一个数:

- **画面内额外漂移**:过滤噪点后,相邻帧透明像素质心的变化;自动连续性判断只使用它。
- **预期根位移增量**:当前帧的 `rootMotion`;它是本次自动播放帧推进应累加的增量,y 正值
表示向上。
- **合成预览位移**:播放器按每次自动播放推进累计根位移后的最终位置;它用于解释播放效果,
不重复参与自动异常判断。

自动审核会输出带问题代码、严重程度和帧位置的结构化结果,覆盖图片不可用、空白主体、画布
尺寸、边缘裁切、覆盖率、相邻重复帧、位移突变、脚底/高度/面积变化,以及画面位移与根位移
方向矛盾。重复帧依据主体边界内归一化的 8×8 Alpha/亮度指纹判断,避免小型精灵被整张透明
画布稀释;位移异常同时使用序列中位数、MAD 和按动作设置的绝对上限,避免整段异常素材用
自身均值掩盖问题。

阈值按动作类别解释:idle 严格检查脚底和高度,walk 允许周期变化,jump 允许离地,crouch
允许主体高度下降,attack/custom 使用更宽的序列离群范围。画布 256×256、主体覆盖率 65%、
脚底漂移 3 px 和轮廓面积变化 28% 仍作为绝对安全线。

这些结果只能发现位置、缩放、背景和轮廓突变,不能判断变脸、人体结构、动作语义或服装
细节。图片加载失败、完全透明、Canvas 不可用或跨域像素受限时会明确显示“无法计算”,不会
伪造为 0。检查结果不会修改 `Frame.qc`、Character 或核验记录;当前 Action 尚无明确循环
合同,因此不计算循环首尾接缝。

## 人工问题记录

“问题记录”页签把自动发现和人工标记分开显示。用户可以为当前动作、方向和帧选择问题类型、
补充说明,并修改或删除人工记录。人工问题只保存在当前 React 会话,刷新后清空,不写入
localStorage、Character、Frame 或后端。后端“核验通过 / 发现问题”的总体结论仍在独立区域
由用户提交,不会被逐帧问题自动改变。

## 游戏资产导出

“资产导出”页签只导出状态为 `confirmed` 且包含帧的动作序列。单个 ZIP 中只有:

1. 保留原 MIME/扩展名的逐帧原图;
2. 每个“动作 × 方向”一张 PNG Sprite Sheet;
3. 与每张 Sprite Sheet 对应的 `animation.json`。

Sprite Sheet 每行最多 8 帧,不缩放原图;不同尺寸使用序列最大宽高作为单元格并居中。图片
加载失败时仍生成资产包:对应单元格保持透明,JSON 标记 `available: false`,页面提示导出
不完整。已有自动或人工问题时,导出页会先提示问题数量但不阻断导出。切换右侧页签不会卸载
进行中的导出,因此不能通过切换页签并发启动第二份任务。ZIP 不包含 manifest、审核结果、
核验结论或开发说明。打包采用同一导出文件内的标准无压缩 ZIP 封装,不新增运行时依赖。

## 键盘控制

| 按键 | 操作 |
| ---------- | ------------------------------------------------ |
| A | 按住时面向左并播放 Walk;松开后恢复原播放状态 |
| D | 按住时面向右并播放 Walk;松开后恢复原播放状态 |
| W | 有 jump 动作时从首帧播放;缺少时不改变当前状态 |
| S | 有 crouch 动作时从首帧播放;缺少时不改变当前状态 |
| Space | 播放或暂停当前动作 |
| ← / → | 当前序列上一帧 / 下一帧,可长按连续切帧 |
| Home / End | 当前序列首帧 / 尾帧 |
| L | 切换循环 |

焦点位于按钮、输入框、文本区域、选择器或可编辑文本区域时,Playtest 不拦截键盘控制。
长按 Space、L、W 或 S 不会重复触发;A、D、方向键、Home 和 End 保持可连续触发。

## 视觉参考

三栏信息布局只参考 `live-demo-ui-components/animation-workbench.tsx` 的视觉层次。统一舞台保留
旧工程的网格、地面线和透明背景视觉。`rootMotion` 是逐帧位移增量,只在自动播放实际推进
时累计;它为 `null` 或零时角色原地播放。向左/向右镜像、位移累计、时间线和检查器均由同一
`PlaybackController` 驱动,不使用前端写死的行走速度、重力或跳跃高度。Playtest 不导入 Cocos
运行时、旧 iframe 或微信小程序适配;当前导出只生成通用逐帧、Sprite Sheet 与 JSON,不生成
Cocos 或小程序工程。
18 changes: 18 additions & 0 deletions frontend/src/pages/playtest/demo-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {
PLAYTEST_DEMO_CHARACTER,
PLAYTEST_DEMO_ACTION_ID,
PLAYTEST_DEMO_OUTFIT_ID,
} from "./testing/demo-character";
import { PlaytestWorkbench } from "./workbench";

/** Explicit development fixture entry point. It intentionally bypasses all production APIs. */
export function PlaytestDemoPage() {
return (
<PlaytestWorkbench
character={PLAYTEST_DEMO_CHARACTER}
outfitId={PLAYTEST_DEMO_OUTFIT_ID}
initialActionId={PLAYTEST_DEMO_ACTION_ID}
inspectionMode="demo"
/>
);
}
Loading