背景
docs/architecture.md(#62)把 playtest 域的播放数据定为「可播放动作 / 帧序列 / 帧率 / 循环方式 / 方向 / 预览资源地址」,asset 域保存「精灵图集 / 序列帧 / 来源生成任务 / object_key / 版本」。
在 ai_engine 侧把视频路线的动作生成跑通后(walk / jump / attack / idle,实测见 #35),发现这套字段不足以让引擎正确播放:缺两项引擎必需的逐帧数据。本 Issue 提出把它们补进 asset / playtest 契约。
不改任何既有字段语义,只做加法。
缺口一:root_motion —— 逐帧位移轨道
现象
跳跃如果把腾空位移画进序列帧,引擎就只能按动画烘死的轨迹走,玩家无法在空中调整、也无法让悬空时长由物理决定。反之如果序列帧原地不动而不给位移数据,引擎不知道该把角色抬多高,跳跃就变成"原地抽搐"。
业界做法(调研 2026-07-28)
- 连续位移动作几乎一律 in-place animation + 引擎代码驱动移动,因为玩家要即时操控:跑动中转向应立刻响应,而不是等一段烘死的位移播完。
- 2D 平台游戏的跳跃是姿势定格 + 引擎物理驱动上下,不是把抛物线画进像素。
来源:Root Motion vs In-Place Animation(MoCap Online)、Unity 2D Character Animations
实测
同一段跳跃视频,两种处理:
| 处理 |
序列帧里的腾空幅度 |
引擎能否控制悬空时长 |
| 位移烘进像素 |
90 px |
否(轨迹写死) |
| 位移抽成轨道 + 帧原地 |
0 px(帧原地) |
是(引擎按 root_motion 施加) |
抽出的轨道形如(兽人跳跃,单位 px,y 向上为正):
dy = 0 → -50 → -166 → -245 → -263(顶点) → -217 → -88 → 0
提议字段
asset.root_motion: list[tuple[int, int]] # 逐帧 (dx, dy),相对首帧,y 向上为正,单位 px
- 长度 == 帧数;
walk/run 的 dx 即前进量,jump 的 dy 即腾空高度。
- 无位移的动作(idle / attack)为全零,不必特殊处理。
缺口二:durations —— 逐帧时长
现象
只给一个 fps 表示"全程等时长"。等时长会让一次性动作发飘、没有重量感:攻击的触点一闪而过,读不出打击感。
业界做法(同上调研)
"Frame timing beats frame count. Four well-timed frames will always look better than twelve frames at uniform speed."
常用区间:idle 400–500 ms/帧、walk 100–150 ms、run 80–100 ms、attack 起手 80–100 ms 且触点定格 150–200 ms。
来源:Sprite animation frames(Sprite-AI)、How Many Frames Does a Sprite Animation Need(NovaSprite)
提议字段
asset.durations: list[int] # 逐帧时长(ms),长度 == 帧数
asset.key_frame: int | None # 关键帧下标(攻击触点 / 跳跃顶点),供引擎挂判定与特效
fps 保留,作为 durations 缺省时的回退(等时长),不破坏既有消费方。
key_frame 由几何自动定位(攻击取位移极值、跳跃取脚线最高点),也是引擎挂攻击判定帧的天然锚点。
备选方案与选择理由
| 方案 |
说明 |
结论 |
| A. 位移烘进序列帧,不加字段 |
契约不动 |
否决:与业界做法相反,引擎失去控制权,玩家无法在空中调整 |
B. 放进 asset.qa 之类的自由 dict |
不改结构 |
否决:引擎必需数据不应放在无约束的自由字段里,前端也无法生成类型 |
C. 只给 fps,时长交前端硬编码 |
后端省事 |
否决:时长是动作的属性(触点定格属于这个攻击),不是播放器偏好,硬编码会在每个消费方重复且不一致 |
D. 加入 asset/playtest 契约(本提案) |
三个只读字段,纯加法 |
采纳:与业界一致;引擎、预览台、导出三个消费方共用同一份数据 |
影响面
- asset:表增三列(
root_motion jsonb、durations jsonb、key_frame int null)。
- playtest:播放数据一并返回,预览台按
durations 播放、按 root_motion 施加位移。
- export:导出到引擎格式时可写入对应的帧时长/位移(如 Cocos plist 的 delay);GIF 导出可直接用
durations。
- 前端:
shared/api/generated 随 OpenAPI 重新生成;字段可选,不影响现有页面。
- 不影响:generation / review / 积分 / 工作流。
假设与验证
| 命题 |
验证方式 |
通过标准 |
失败退路 |
| 位移交引擎比烘进像素更可控 |
预览台用同一套帧,分别按"烘进像素"与"root_motion 驱动"播放跳跃 |
后者能在空中改变悬空时长且角色不变形 |
退回 A 方案,并在文档写明引擎不可控 |
| 逐帧时长能显著改善打击感 |
同一攻击序列,等时长 vs 触点定格 180ms,盲评 |
多数评审者认为定格版更有重量感 |
保留 fps 等时长,durations 降为可选 |
| 三字段够用,不需要更复杂的运动数据 |
用 walk / jump / attack 三类动作各跑一遍预览台 |
无需额外字段即可正确播放 |
再评估是否需要方向/速度曲线 |
验收标准
关联
Refs #62(架构总纲 · asset/playtest 域定义)、#35(视频路线实测)、#21(逐帧对齐与循环闭合)、#22(导出成品包)。
ai_engine 侧已按此形状产出(AssetPackageRef.root_motion / durations),等契约确认后对齐命名。
背景
docs/architecture.md(#62)把playtest域的播放数据定为「可播放动作 / 帧序列 / 帧率 / 循环方式 / 方向 / 预览资源地址」,asset域保存「精灵图集 / 序列帧 / 来源生成任务 / object_key / 版本」。在 ai_engine 侧把视频路线的动作生成跑通后(walk / jump / attack / idle,实测见 #35),发现这套字段不足以让引擎正确播放:缺两项引擎必需的逐帧数据。本 Issue 提出把它们补进
asset/playtest契约。不改任何既有字段语义,只做加法。
缺口一:
root_motion—— 逐帧位移轨道现象
跳跃如果把腾空位移画进序列帧,引擎就只能按动画烘死的轨迹走,玩家无法在空中调整、也无法让悬空时长由物理决定。反之如果序列帧原地不动而不给位移数据,引擎不知道该把角色抬多高,跳跃就变成"原地抽搐"。
业界做法(调研 2026-07-28)
来源:Root Motion vs In-Place Animation(MoCap Online)、Unity 2D Character Animations
实测
同一段跳跃视频,两种处理:
抽出的轨道形如(兽人跳跃,单位 px,y 向上为正):
dy = 0 → -50 → -166 → -245 → -263(顶点) → -217 → -88 → 0提议字段
walk/run的 dx 即前进量,jump的 dy 即腾空高度。缺口二:
durations—— 逐帧时长现象
只给一个
fps表示"全程等时长"。等时长会让一次性动作发飘、没有重量感:攻击的触点一闪而过,读不出打击感。业界做法(同上调研)
常用区间:idle 400–500 ms/帧、walk 100–150 ms、run 80–100 ms、attack 起手 80–100 ms 且触点定格 150–200 ms。
来源:Sprite animation frames(Sprite-AI)、How Many Frames Does a Sprite Animation Need(NovaSprite)
提议字段
fps保留,作为durations缺省时的回退(等时长),不破坏既有消费方。key_frame由几何自动定位(攻击取位移极值、跳跃取脚线最高点),也是引擎挂攻击判定帧的天然锚点。备选方案与选择理由
asset.qa之类的自由 dictfps,时长交前端硬编码asset/playtest契约(本提案)影响面
root_motionjsonb、durationsjsonb、key_frameint null)。durations播放、按root_motion施加位移。durations。shared/api/generated随 OpenAPI 重新生成;字段可选,不影响现有页面。假设与验证
fps等时长,durations降为可选验收标准
asset契约新增root_motion/durations/key_frame,并在 OpenAPI 中体现;playtest播放数据返回上述字段;docs/contracts/asset-lifecycle.md记录三字段语义(单位、长度约束、缺省行为);durations播放、按root_motion施加位移;durations为空时回退等时长、root_motion为空时不施加位移。关联
Refs #62(架构总纲 · asset/playtest 域定义)、#35(视频路线实测)、#21(逐帧对齐与循环闭合)、#22(导出成品包)。
ai_engine 侧已按此形状产出(
AssetPackageRef.root_motion/durations),等契约确认后对齐命名。