Work / Game Feel Tool

Reference Controller Fitter 手感拟合器:从游戏录像拟合 2D 平台跳跃控制器

给几段平台跳跃游戏的录像,找出能复现全部片段的最简单控制器,可以直接试玩,并导出成 Unity / Godot 脚本。

Role
个人项目
Year
2026
Team
个人项目
Tech
Python · OpenCV · SciPy · PySide6 · Unity C# · Godot 4
Status
已完成

项目演示,4 分 45 秒,带中文字幕和配音:从导入录像到导出,接着在 Unity 6 和 Godot 4 里实际使用导出的脚本。工具画面是真实程序;跟踪和拟合做了快进,框选和按键由脚本执行。引擎里的运行画面按固定帧率逐帧录制,两个引擎用的是同一串按键。配音为 AI 语音合成(高松灯社区语音模型,CC BY-NC-SA 4.0)。

概览

平台跳跃游戏的手感,由最高速度、加速度、起跳速度、上升和下落重力、松键截断、空中控制这十来个参数决定。想做出“像某款游戏”的手感,通常只能对着录像一遍遍手调。这个工具把这一步自动化:给它几段录像,它找出能同时复现所有片段的最简单控制器,可以马上用键盘试玩,再导出成 Unity 或 Godot 脚本。

它不还原原游戏的代码。它找的是一个行为相似、结构简单、参数可读、能直接运行的控制器,并且会说明匹配得多好,以及哪些参数录像确定不了。

8 / 8个参数在合成测试中恢复到真实值的 5% 以内
1.4–3.3%真实录像每段片段的轨迹误差(SuperTux、Secret Maryo Chronicles)
≤ 0.005导出脚本在 Unity 里实际运行时,与模型的高度偏差(单位)
45个测试,包括从渲染视频开始的端到端基准
状态与声明。个人项目,代码在 GitHub 上以 MIT 协议开源。范围是 2D 横版的跑、停、转身、可变高度跳跃、下落和空中控制。参考录像全部来自 Wikimedia Commons(GPL 或 CC BY-SA 3.0),作者与出处列在仓库的 references/real/SOURCES.md。SuperTux 关卡录像由 Dabmasterars 和 Dexxor 录制,SuperTux © SuperTux Development Team(GPL)。

一条管线

游戏录像 → 跟踪角色 → 扣除镜头滚动 → 推断按键
→ 所有片段联合拟合 → 模型选择 → 试玩 / 导出 Unity · Godot

工具主界面:左侧是片段列表,中间是参考录像和拟合出的原型,下面是按键时间轴,右侧是参数和模型报告

所有长度都以角色身高为单位(每段视频用跟踪框高度的中位数作为一个比例尺),导出时再填入角色在引擎里的身高,把长度参数换算过去。

控制器模型:从简单到复杂

工具准备了五级模型,每一级在上一级的基础上加机制:

模型 增加的机制 参数数 SuperTux 上的轨迹误差
A 恒定重力,瞬间达到最高速度 3 9.1%
B 地面加速 / 减速 5 8.3%
C 上升和下落用不同的重力 6 9.6%
D 松开跳跃键时截断上升速度,空中控制 8 2.0%
D2 改为松键后加大重力(马里奥式可变跳跃),空中控制 8 2.0%
E D 再加顶点附近的重力倍率、最大下落速度和转身加速度 11 2.7%

选择规则是“够用的最简单模型”。 每个模型按有效样本量计算 BIC(用残差的自相关修正样本数,误差下限设在跟踪噪声的水平),再加一条实用规则:误差不超过最好模型的 1.1 倍再加 0.2 个百分点,就算“一样好”,从中选最简单的。

只靠 BIC 不够:一段片段有几百帧,BIC 会奖励那些只是在吸收跟踪偏差的参数。第一次端到端测试时,工具就因为 0.05% 的改进选了 E;加上实用规则后,才稳定地选回真实的 D。

诚实的报告

模型报告:各项误差、每个模型为什么被选中或被拒绝,以及哪些参数录像约束不足

拟合完的报告会写出:

  • 每项误差:轨迹、速度曲线、顶点时刻、落地时刻、最大高度、水平位移;
  • 模型比较:更简单的模型为什么被拒绝,更复杂的模型为什么不值得;
  • 置信度和理由:比如 SuperTux 只给到 MEDIUM,因为 D 和 D2 拟合得一样好(录像分不清原游戏用的是哪种可变跳跃),跳跃截断系数和未知的松键时刻互相牵制,减速度约束很弱。

录像里没有出现的动作,对应的参数会被固定并写明原因:没有一短一长两次跳跃,就定不了跳跃截断;空中没有改变过方向,就定不了空中控制。拟合后不确定度超过 100% 的修饰参数会被设回中性值再重拟合,避免导出一个手感奇怪的控制器。

解释不了的片段也会被标出来。SuperTux 速通录像里有一段看起来像小跳,工具报告它无法解释(落地时刻误差 140%)。检查后发现是 Tux 跑过了一个平滑的斜坡,而斜坡不在模型范围内,这段就被移除了,没有硬拟合。

从画面到运动轨迹

跟踪结果:橙色线是角色脚底在游戏世界里的轨迹,镜头滚动已经扣除

跟踪器和镜头估计的选择,都是在合成基准上实测后决定的。合成基准用一个已知参数的控制器渲染出 7 段视频,带镜头跟随、视差背景、HUD、挤压拉伸的角色和 H.264 压缩:

跟踪器 脚底位置平均误差
模板匹配(最终选用) 0.41 px
光流 0.54 px
CSRT 1.07 px
MIL 1.99 px(一次跳到 24 px)
KCF 2.06 px

真实录像暴露的问题比合成数据多,每一个都记录在 DEVLOG 里:

  • 挤压拉伸:固定大小的跟踪框让脚底位置偏了最多 3.5 px,拟合用重力偏差去补偿,结果选错了模型。加入纵向缩放搜索后,误差从 0.41 降到 0.15 px,模型也选回了正确的 D。
  • 镜头估计:合成录像开头镜头每帧平移 30 px,关键帧法因此出现 60–150 px 的错误。改成只在预期偏移小于 16 px 时使用关键帧,并和逐帧估计交叉检查,最大误差 0.21 px。
  • 视差层:Secret Maryo Chronicles 里缓慢移动的背景山丘占据了大部分特征点,估出的镜头位移只有真实值的四成。改为优先参考角色脚下那一层。
  • 15 fps 录像:一开始 89% 的帧跟丢。按帧率放大搜索范围,加上外观库和颜色直方图兜底,降到 5.9%。

推断按键

按键时间轴:绿色是按住右,蓝色是左,橙色是跳跃;橙点是录像轨迹,青线是控制器轨迹

录像里看不到按键,只能从运动推断。用阈值判断时,SuperTux 一段 9 秒的录像被判出 70 次左右切换,因为掉帧和动画会让速度抖动。改成带切换惩罚的三状态 Viterbi 解码后,整段变成“一直按右”,合成数据上的按键时刻误差在 1–3 帧以内。

推断出的按键只是一个可信的解释,不是真实的按键,所以:

  • 时间轴上可以右键增删、拖动修改,也可以锁定某段的按键,不让拟合器调整;
  • 拟合时,每段片段的按键时刻也是优化变量(方向键 ±0.12 s,跳跃 ±0.05 s,加上松键时刻)。

拟合

所有片段共用一套控制器参数,每段片段有自己的按键时刻,一起用 scipy.optimize.least_squares 求解:带边界的信赖域方法,soft-L1 鲁棒损失,稀疏雅可比矩阵(一段片段的按键只影响它自己)。

  • “够得着平台”残差:落地是离散事件,一条弧线如果顶点差一点没到平台,就得不到有用的梯度。对观测到落在高处的跳跃,额外加一项“顶点比平台低了多少”。
  • 多起点:精确复现上一级模型的解(所以 D 不会比 C 差)、按跳跃高度排序的松键时刻初值,加几次随机扰动;最后用最好模型的参数把每个模型再拟合一遍。在这之前,SuperTux 上 D 的误差会因为数据的微小变化在 1.9%、5.7%、2.5% 之间跳动。

精确积分器

运动模型的积分是精确的:每一步里找出加速度下一次改变的时刻(达到目标速度、竖直速度过零、进入顶点区、达到最大下落速度、落地),分段用解析式积分。

这样结果不依赖步长:拟合器按视频帧采样,试玩原型按 60 Hz 运行,Unity 按 fixedDeltaTime 运行,三者都一致。轨迹对参数和按键时刻也是连续的,适合用梯度法求解。同样的约 60 行代码移植到了 C# 和 GDScript。

导出到 Unity 和 Godot

Unity 6:添加 FittedPlatformerController 组件后,Inspector 里就是拟合出的参数

Unity C# 是一个完整的组件:

  • Kinematic Rigidbody2D,用 Physics2D.BoxCast 自己处理碰撞。每一步先自由移动,再处理墙(X),再从新的 X 位置探测地面或天花板;碰到地面就用那个高度重算这一步,让落地发生在精确的时刻;
  • 同时支持旧的 Input Manager 和 Input System;也可以由外部脚本输入,用于 AI、回放和测试;
  • 可选的土狼时间和跳跃缓冲(默认关闭,以保证行为和拟合一致);
  • 参数都是 Inspector 字段,也可以用导出的 JSON 在启动时覆盖。

验证方式:对 Unity 2022.3.51、2022.3.62、6000.3.8 的程序集编译,0 错误 0 警告;在 2022.3.62 和 6000.3.8 的批处理模式里,分别用两种输入后端跑“跑动 + 长跳 + 短跳”,x 和模型完全一致,y 误差 ≤ 0.005 单位,顶点相同。

同一个控制器、同一串按键,在 Unity 和 Godot 里的运动并排对比

Godot 4 GDScript:在 Godot 4.7.2 里无界面运行,没有脚本错误,跑动速度一致,顶点误差在 0.002 单位以内。不同的是落地发生在物理帧边界(move_and_slide),所以最多晚一帧,实测正好一帧。

结果

合成测试(真实参数已知)。 真实控制器是:最高速度 8,加速度 40,减速度 55,起跳速度 12,上升重力 38,下落重力 52,空中控制 0.55,跳跃截断 0.45。7 段渲染视频经过跟踪和拟合后:

  • 选中模型 D;
  • 8 个参数都在真实值的 5% 以内,大部分在 2% 以内;
  • 在真实按键下,与真实控制器的行为误差为 0.05–0.44%;在拟合时没见过的按键序列上为 0.35–0.61%(目标 ≤ 5%)。

真实录像。

素材 模型与置信度 每段轨迹误差(目标 ≤ 8%)
SuperTux:两位玩家的 2 段录像,4 个片段 D,MEDIUM 1.4%、1.6%、1.9%、3.1%
Secret Maryo Chronicles:15 fps 录像,2 个片段 B,HIGH 2.6%、3.3%

范围与局限

  • 只支持 2D 横版。蹬墙跳、攀边、冲刺、二段跳、游泳、斜坡和移动平台都不在模型里,含有这些动作的片段会被标为 LOW(无法解释)。
  • 没有关卡几何模型:工具知道角色落在了哪个高度,但不知道天花板和墙,所以选片段时要避开顶头和撞墙。
  • 只有一个最高速度。有“走 / 跑”两档的游戏,要把走和跑的片段分开拟合。
  • 可变跳跃实现了“松键截断速度”(D)和“松键加大重力”(D2)两种,“松键限制速度”和“按住维持计时”两种还没有,用 D / D2 近似。
  • 自动识别角色不可靠,只是辅助按钮。手动框选是主要方式。
  • 镜头补偿假设镜头只平移,不处理缩放、旋转和大幅震屏。