从一句话到可玩原型:用 funplay-unity-mcp 让 AI 搭起完整游戏循环

前三篇的尺度都是"一个机制":调一次跳跃手感、给一个机制写回归测试。这篇往上跳一格------不再是单个机制,而是一个完整、能玩、有头有尾的循环

需求就一句话,丢给接了 funplay-unity-mcp 的 Claude Code:

做一个金币收集的平台跳跃小游戏:角色能移动和跳跃,跳上几个平台收集所有金币就算赢,掉下去自动重生,按 R 重开。

下面是它实际做出来的东西,以及做的过程里值得说的点。

先看结果

开局是 Coins: 0 / 6,玩家(红块)在地面,金币散在地面和三层平台上。跳上三层平台、把六个金币全部收集之后,胜利横幅弹出、金币全部消失:

中间的过程------三个脚本、一段场景搭建、一次自动通关验证------全部由 AI 通过 MCP 工具完成。

AI 怎么拆这个需求

一个关键的分工,是它自己分清了两类工作

  • 持久的游戏逻辑 → 写成 .cs 脚本文件,需要 Unity 编译。角色控制、金币、计分这种"游戏运行时一直存在"的行为属于这类。
  • 一次性的场景编排 → 用 execute_code 即时执行,不落文件、不触发编译。"在这些坐标摆地面/平台/金币、配好材质、放好相机"属于这类。

这个区分很重要:如果把摆场景也写成脚本,会污染项目、还要等编译;如果把游戏逻辑塞进 execute_code,退出运行就没了。AI 分得很清楚。

三个脚本(持久逻辑)

PlayerController.cs ------ 移动 + 跳跃,复用了前几篇调好的"下落额外重力"手感:

csharp 复制代码
[RequireComponent(typeof(Rigidbody))]
public class PlayerController : MonoBehaviour
{
    public float moveSpeed = 5f;
    public float jumpForce = 6f;
    public float fallGravityMultiplier = 2.5f;
    public float groundCheckDistance = 0.55f;
    Rigidbody _rb;
    Vector3 _spawn;

    void Awake() { _rb = GetComponent<Rigidbody>(); _rb.constraints = RigidbodyConstraints.FreezeRotation; _spawn = transform.position; }
    bool IsGrounded() => Physics.Raycast(transform.position, Vector3.down, groundCheckDistance);

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space) && IsGrounded())
        {
            _rb.velocity = new Vector3(_rb.velocity.x, 0f, _rb.velocity.z);
            _rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
        }
        if (transform.position.y < -5f) Respawn();   // 掉出地图
    }

    void FixedUpdate()
    {
        float h = Input.GetAxisRaw("Horizontal"), v = Input.GetAxisRaw("Vertical");
        var dir = new Vector3(h, 0f, v);
        if (dir.sqrMagnitude > 1f) dir.Normalize();
        _rb.velocity = new Vector3(dir.x * moveSpeed, _rb.velocity.y, dir.z * moveSpeed);
        if (_rb.velocity.y < 0f)
            _rb.AddForce(Physics.gravity * (fallGravityMultiplier - 1f), ForceMode.Acceleration);
    }

    public void Respawn() { _rb.velocity = Vector3.zero; _rb.position = _spawn; transform.position = _spawn; }
}

Coin.cs ------ 旋转 + 被玩家触碰即收集:

csharp 复制代码
public class Coin : MonoBehaviour
{
    public float spinSpeed = 120f;
    void Update() => transform.Rotate(Vector3.up, spinSpeed * Time.deltaTime, Space.World);
    void OnTriggerEnter(Collider other)
    {
        if (other.GetComponentInParent<PlayerController>() != null)
        {
            if (GameManager.Instance != null) GameManager.Instance.Collect();
            Destroy(gameObject);
        }
    }
}

GameManager.cs ------ 计分、胜负、重开,HUD 直接用代码生成(不用手摆 Canvas):

csharp 复制代码
public class GameManager : MonoBehaviour
{
    public static GameManager Instance;
    public int total;
    int _collected;
    Text _label, _banner;

    void Awake()
    {
        Instance = this;
        total = FindObjectsByType<Coin>(FindObjectsSortMode.None).Length;  // 自动数场景里的金币
    }
    void Start() { BuildHUD(); Refresh(); }
    void Update() { if (Input.GetKeyDown(KeyCode.R)) SceneManager.LoadScene(SceneManager.GetActiveScene().name); }

    public void Collect()
    {
        _collected++;
        Refresh();
        if (_collected >= total && _banner != null) _banner.text = "YOU WIN!\n按 R 重开";
    }
    void Refresh() { if (_label != null) _label.text = $"Coins: {_collected} / {total}"; }
    // BuildHUD() 用代码建 Canvas + 两个 Text,字体取 Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf")
}

场景编排(一次性)

写完脚本、request_recompile 等编译完之后,AI 用一段 execute_code 把整个场景搭出来:新建空场景、放地面、三层平台、玩家(加 PlayerController,靠 [RequireComponent] 自动带上 Rigidbody)、六个金币(缩小的方块 + isTrigger 碰撞体 + Coin 脚本)、一个空的 GameManager、摆好相机和光照,最后存成 PlayablePrototype.unity

这一段不写进任何脚本文件------它是"布置这一个场景"的动作,跑完即弃。

让 AI 自己玩通关来验证

搭完不等于做对了。前一篇的回归测试思路在这里复用:让 AI 自己把它玩一遍,用客观信号确认闭环成立

真人玩是 WASD + 空格。但要在编辑器失焦、无人操作的情况下做确定性验证,AI 走了另一条更干净的路:把物理切到脚本步进模式,把玩家依次"瞬移"到每个金币位置,每次手动 Physics.Simulate(0.02f) 步进几帧触发 OnTriggerEnter

csharp 复制代码
Physics.simulationMode = SimulationMode.Script;
foreach (var coin in Object.FindObjectsByType<Coin>(FindObjectsSortMode.None))
{
    rb.position = coin.transform.position;
    rb.velocity = Vector3.zero;
    for (int i = 0; i < 3; i++) Physics.Simulate(0.02f);   // 同步步进,触发 trigger
}
Physics.simulationMode = SimulationMode.FixedUpdate;

跑完读 HUD:

复制代码
开始自动通关,金币数=6
通关后:HUD='Coins: 6 / 6'  banner='YOU WIN! 按 R 重开'

收集 → 计分 → 胜利整条链路成立。这比"看起来能玩"强------它是被一段确定性脚本验证过的。

这一篇难在哪

前几篇都是单点:一个 Rigidbody 参数、一个 raycast 距离。这篇第一次要让五个子系统同时对上

  • 输入Input.GetAxisRaw / GetKeyDown 读移动和跳跃
  • 物理:Rigidbody 速度控制 + 地面检测 + 下落额外重力
  • 触发 :金币 isTrigger + OnTriggerEnter + 正确识别"碰到的是玩家"(GetComponentInParent<PlayerController>
  • UI:代码生成 Canvas / Text,并在收集时刷新
  • 场景管理 :胜利判定、SceneManager.LoadScene 重开、掉落重生

任何一个环节错了,循环就不闭合------金币不消失、分数不动、赢了不提示、重开重置不干净。AI 一次把这五个对上,靠的是它能在同一个上下文里既写脚本、又搭场景、又跑验证,中间不用人来回翻译。

几个它处理得对、但新手容易栽的细节:

  • HUD 用 Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf") 取内置字体------新版 Unity 把老的 Arial.ttf 改名了,直接 new Text 不给字体会渲染不出来。
  • GameManager.totalAwakeFindObjectsByType<Coin> 自动统计,而不是硬编码 6------加减金币不用改代码。
  • 金币识别玩家用 GetComponentInParent,避免玩家身上有子碰撞体时识别失败。

诚实地讲:这是原型,不是游戏

这套东西能玩、闭环成立,但离"游戏"还很远,AI 也替代不了后面那段:

  • 美术全是色块。红方块当角色、黄方块当金币------原型阶段够用,但"做成游戏"需要模型、动画、特效,那是另一回事。
  • 没有音效、没有反馈。收集金币该有"叮"的一声、一个粒子、一下镜头微动------这些"juice"是手感的一半,目前一点没有。
  • 没有关卡设计。六个金币的位置是我让它随便摆的,不是设计过的难度曲线。"跳起来爽不爽、关卡有没有节奏"必须人来调。
  • 平衡靠手调jumpForce=6moveSpeed=5 是拍脑袋的值,到底合不合适得真人玩着调(就像第二篇那样)。

所以这一篇的定位很清楚:AI 能极快地把一个可玩的骨架搭起来并验证它闭环,让你跳过"从 0 到能跑"那段最枯燥的体力活,直接进入"它好不好玩"的创作环节。骨架是它的,灵魂还是你的。

完整产物

下一篇继续往上爬:让 AI 程序化生成一套平台关卡,再让它自己从起点跳到终点确认这关能通------生成 + 自验证的闭环。

相关推荐
狠活科技5 小时前
GPT Image 2.5 发布:AI 生图又进化了
人工智能·gpt·ai作画·aigc·image2.5
程xu袁6 小时前
AI写歌词和AI作曲有什么区别?想做完整歌曲,AI音乐工具怎么选?
人工智能
ACME20446 小时前
破产财产处置新规落地,管理人如何选聘拍卖机构?
人工智能
AI让世界更懂你6 小时前
计算机专业研究生核心能力培养(6)——论文写作的表达与打磨
人工智能
风之清扬6 小时前
Agent学习之四-初识Agent CLI
人工智能·科技·学习
零依赖极客6 小时前
Day 9·1 KV 也量化——q8 KV 把缓存与 decode 带宽压到一半
c语言·开发语言·arm开发·人工智能·缓存·矩阵
陈童学哦6 小时前
AI术语大全
人工智能
tuanxiang6 小时前
用去AI味工具调整大模型生成的技术文档,我踩了检测阈值的坑
人工智能
天辛大师6 小时前
天辛大师谈AI时代的沉思录,All in AI 持续做事与放大效应
大数据·人工智能·随机森林·重构·启发式算法
不要生病了6 小时前
Brain-JEPA:用功能梯度定位与时空遮蔽预训练 fMRI 基础模型
人工智能·深度学习