Skip to content

契约补充:asset / playtest 增加 root_motion 与逐帧 durations(Refs #62 #35) #63

Description

@johnnyzhang-eng

背景

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 三类动作各跑一遍预览台 无需额外字段即可正确播放 再评估是否需要方向/速度曲线

验收标准

  • 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),等契约确认后对齐命名。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions