让 AI 跑通“调跳跃手感“的完整闭环:funplay-unity-mcp 实战案例

游戏手感的调参一直很麻烦。

拿"跳跃"这一件事来说,要做出像马里奥那种"短按小跳、长按高跳、下落更快"的手感,开发者要在 jumpForce、重力倍数、下落额外加速度、上升中断阈值这一堆数值之间反复试。流程是:

改数值 → 等 Unity 编译 → 进 PlayMode → 按空格 → 用肌肉记忆判断"对不对" → 退出 PlayMode → 再改

每一轮都要切回编辑器、按按钮、观察、记忆。每个数值改 0.5,你都要把这套流程跑一遍。

我用 funplay-unity-mcp 把这个闭环交给 Claude Code 跑了一遍------AI 自己选参数、自己进 PlayMode、自己触发跳跃、自己采样 Y 轨迹、根据物理数据判断"飘不飘 / snap 不 snap"、再决定下一组参数。三轮迭代之后从"短/飘/snappy"分别拿到了可量化的数据和截图。这一篇就把这个过程完整记下来。

实验设置

最小可复现的跳跃 demo:

  • 一块 20×20 的灰色平面当地面
  • 一个红色 Cube 当 Player,挂 Rigidbody(冻转动、连续碰撞)
  • 左边一排白色高度标记,在 y=1/2/3/4 米处,方便目测高度
  • 侧视相机,看得清整个轨迹

挂在 Player 上的两个脚本:

csharp 复制代码
// JumpController.cs - 跳跃逻辑本体
public class JumpController : MonoBehaviour
{
    public float jumpForce = 5f;
    public float fallGravityMultiplier = 1f;
    public KeyCode jumpKey = KeyCode.Space;

    Rigidbody _rb;
    bool _grounded;

    void Update()
    {
        _grounded = Physics.Raycast(transform.position, Vector3.down, 0.55f);
        if (Input.GetKeyDown(jumpKey) && _grounded)
        {
            _rb.velocity = new Vector3(_rb.velocity.x, 0f, _rb.velocity.z);
            _rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
        }
    }

    void FixedUpdate()
    {
        // 下落时叠加额外重力,做出 snappy 手感
        if (_rb.velocity.y < 0f && fallGravityMultiplier > 1f)
            _rb.AddForce(Physics.gravity * (fallGravityMultiplier - 1f), ForceMode.Acceleration);
    }
}
csharp 复制代码
// JumpSampler.cs - 每 FixedUpdate 记录一次 (time, Y)
public class JumpSampler : MonoBehaviour
{
    public List<float> times = new();
    public List<float> ys = new();
    bool _recording;
    float _t0;
    Rigidbody _rb;

    public void TriggerJump(float force)
    {
        transform.position = new Vector3(0, 0.5f, 0);
        _rb.velocity = Vector3.zero;
        _rb.AddForce(Vector3.up * force, ForceMode.Impulse);
    }

    void FixedUpdate()
    {
        if (_recording) { times.Add(Time.time - _t0); ys.Add(transform.position.y); }
    }
    // 还有 Start/StopRecording、Summary() 略
}

为什么需要 JumpSampler?因为 AI 不能"凭感觉判断手感"------它需要可读的数值。直接采集 (t, y) 轨迹,然后从中算出峰值高度、到顶时间、上升/下降耗时比例,这些才是 AI 能拿来推理的客观信号。

整个 setup 让 AI 自己用 execute_code 工具就完成了------一段 C# 在 Unity 进程内即时编译执行,建场景、挂材质、保存 JumpDemo.unity。没有手动操作。

Iter 1:基线

AI 先跑一组保守值:jumpForce = 5fallGravityMultiplier = 1.0(标准重力,没额外加速度)。

AI 调用链:

复制代码
execute_code   → TriggerJump(5),开始采样
get_editor_state × 2  → 故意让出编辑器线程几秒,让物理跑完
execute_code   → 读 sampler,dump 出 741 个样本到 JSON

读出的数据:

指标
峰值高度 1.725 m(起点 0.5m,跳起 1.23m)
到顶时间 0.513 s
落地时间 1.013 s
上升:下降 0.51 : 0.50(对称)

apex 截图------cube 大概到第 2 个高度标记之间:

AI 的判断:跳得太矮 。1.23m 的跳跃高度在 3D 平台跳跃游戏里很短,对应大概一格半的台阶高度,玩家会觉得这个角色"腿短"。可以接受的下限大概在 2m 以上。决定下一轮把 jumpForce 加到 8。

Iter 2:加力,但飘了

参数:jumpForce = 8fallGravityMultiplier = 1.0

指标
峰值高度 3.682 m(跳起 3.18m)
到顶时间 0.816 s
落地时间 1.636 s
上升:下降 0.82 : 0.82(对称)

apex 截图------cube 在第 4 个高度标记附近:

跳跃高度够了(3.18m,接近两层台阶)。但是问题更明显了:

  • 整个空中时间 1.64 秒------这个数字在快节奏跳跃游戏里太长了。横向移动 4 个单位玩家还没落地。
  • 上升和下降耗时一样 (0.82s vs 0.82s)。真实的"好手感"跳跃几乎都是下降快于上升------这就是为什么《蔚蓝》《空洞骑士》这些游戏要专门写一段"下落额外重力"的逻辑。
  • 主观感受会是"角色像被绑了氢气球"。

AI 的判断:高度对了,但 。需要在下落阶段叠加额外重力。下一轮加 fallGravityMultiplier = 2.5

Iter 3:snappy

参数:jumpForce = 8fallGravityMultiplier = 2.5(下降时重力变成 2.5×g)。

指标
峰值高度 3.682 m(和 Iter 2 同高)
到顶时间 0.804 s
落地时间 1.324 s(比 Iter 2 少了 0.31s)
上升:下降 0.80 : 0.52

关键指标变了------下降耗时只是上升的 0.65 倍。这就是 snappy 手感的物理标志。

三轮放在一张图里,对比一目了然:

灰色(Iter 1):矮的对称弧。蓝色(Iter 2):高的对称弧,但下降段拖得很长。橙色(Iter 3):上升段和蓝色重合(因为 jumpForce 一样),但峰值之后突然加速下落,0.52 秒就落地。

这条橙色曲线就是"想要的手感"------给玩家"我能跳得高,但落地很快、节奏紧凑"的感觉。

这个闭环用到了什么

整套迭代下来,funplay-unity-mcp 提供的工具其实就这几个:

工具 用途
execute_code 建场景、写 sampler 数据、触发 jump、读结果------绝大多数动作都靠它
request_recompile 写完 JumpController.cs / JumpSampler.cs 后让 Unity 重新编译
wait_for_compilation 等编译完,避免后续动作跑在旧代码上
enter_play_mode / exit_play_mode 进出运行态
get_editor_state 既是状态查询,也是"让一让线程,让物理跑几百毫秒"的工具
capture_game_view 截 Game 视图作可视证据

注意一件事:这一轮没有用 simulate_key_press 。我们绕过了 Input 系统,直接在 execute_codeRigidbody.AddForce 触发跳跃。这是有意的------simulate_key_press 要发到 Input.GetKeyDown 那条路径,受帧率、按键时序影响;而调参实验需要的是"在完全可控的初始条件下重复触发同一个动作",直接给 impulse 更纯粹。

但如果你要验证的不是物理参数,而是玩家操作链 ("按住空格 0.2 秒 vs 0.5 秒,跳跃曲线有什么差别"),那 simulate_key_press 就是对的工具------它支持 duration 参数,能模拟"按住---释放"。

这种闭环对游戏开发意味着什么

调一个参数 → 编译 → 进 PlayMode → 主观感受 → 退出 → 再调,这个流程的痛点不在任何一步本身------单看任何一步都不慢。痛点在于这一切都在你这一个开发者的注意力上。你要保持上下文:上一轮跳了多高?这次比上次高了还是矮了?上升 / 下降的节奏感是变好了还是变差了?

AI 能把这个上下文外化成数字。peakY 从 1.725 涨到 3.682,下降耗时从 0.82 降到 0.52------这些数字不会因为你被电话打断、被同事问问题而忘记。AI 可以一轮一轮跑下去,把每一组参数和结果记在结构化的 JSON 里。你回来看的时候,看到的是一张轨迹对比图,而不是"我记得我之前调到 6 的时候好像不错"。

这不是替代调参------主观手感判断还是人来做("snappy 到什么程度玩家觉得舒服","是不是太硬了"这一类)。这是把"测量 + 记录 + 复现"这一段交出去,让人只做"判断"那一部分。

局限

这个闭环现在还做不好的事:

主观手感的最终评判。AI 能告诉你"下降快了 36%",但不能告诉你"这个 36% 对 Metroidvania 游戏是不是合适、对休闲跳跳乐是不是过头"。这需要游戏设计师拍板。

复杂操作链的模拟 。"二段跳 + 滑墙 + 蹬墙跳"这种组合,纯靠 simulate_key_press 模拟很容易出现时序错位。这种场景建议人来录一段然后回放,AI 负责分析。

视觉手感。轨迹是数值上的,但玩家感受里还有挤压形变、粒子特效、屏幕震动、镜头跟随阻尼------这些只能靠看截图/视频判断。funplay 能截图,但"美不美"还得人来说。

完整数据

复制代码
Iter 1: f=5,   g=1.0  → peakY=1.725  peakT=0.513  landT=1.013  ratio=1.00
Iter 2: f=8,   g=1.0  → peakY=3.682  peakT=0.816  landT=1.636  ratio=1.00
Iter 3: f=8,   g=2.5  → peakY=3.682  peakT=0.804  landT=1.324  ratio=0.65

代码(JumpController + JumpSampler)和样本数据 JSON 都在我本地的实验目录里,看 funplay-unity-mcp 仓库的话能复现:

下一篇会写"让 AI 帮你跑 PlayMode 回归测试"------同样是闭环思路,但场景换成已有项目的 BUG 复现 + 修复验证。

相关推荐
EdenMa1 小时前
从飞书 PRD 到代码实现:我的AI编程workflow
openai·ai编程
wuhen_n1 小时前
LangChain 核心:Chain 链式调用实现复杂 AI 任务
前端·langchain·ai编程
黑暗森林观察者1 小时前
OpenAI 重启机器人团队,靠的是这个让机器人"脑子里先演练一遍"的技术
人工智能
七牛开发者1 小时前
让生产级 Agent 实现自进化:MOSS 的源码级实验
人工智能·机器学习·程序员
腾视科技AI1 小时前
什么是AI算力模组?
人工智能·科技·perl·ai算力模组·ai模组·ai边缘算力模组
AQin10121 小时前
【超易懂】Self-Attention 到底是做什么的?
人工智能
X54先生(人文科技)1 小时前
《元创力》纪实录·卷宗2.1刻舟求剑:一场关于“唯一解”的范式战争
人工智能·架构·开源·零知识证明
winlife_2 小时前
从一句话到可玩原型:用 funplay-unity-mcp 让 AI 搭起完整游戏循环
人工智能·游戏·unity·ai编程·mcp·游戏原型
碳基硅坊2 小时前
llama.cpp本地部署Qwen3.6-27B
人工智能·llama·推理加速·qwen3.6-27b