前三篇的尺度都是"一个机制":调一次跳跃手感、给一个机制写回归测试。这篇往上跳一格------不再是单个机制,而是一个完整、能玩、有头有尾的循环。
需求就一句话,丢给接了 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.total在Awake用FindObjectsByType<Coin>自动统计,而不是硬编码 6------加减金币不用改代码。- 金币识别玩家用
GetComponentInParent,避免玩家身上有子碰撞体时识别失败。
诚实地讲:这是原型,不是游戏
这套东西能玩、闭环成立,但离"游戏"还很远,AI 也替代不了后面那段:
- 美术全是色块。红方块当角色、黄方块当金币------原型阶段够用,但"做成游戏"需要模型、动画、特效,那是另一回事。
- 没有音效、没有反馈。收集金币该有"叮"的一声、一个粒子、一下镜头微动------这些"juice"是手感的一半,目前一点没有。
- 没有关卡设计。六个金币的位置是我让它随便摆的,不是设计过的难度曲线。"跳起来爽不爽、关卡有没有节奏"必须人来调。
- 平衡靠手调 。
jumpForce=6、moveSpeed=5是拍脑袋的值,到底合不合适得真人玩着调(就像第二篇那样)。
所以这一篇的定位很清楚:AI 能极快地把一个可玩的骨架搭起来并验证它闭环,让你跳过"从 0 到能跑"那段最枯燥的体力活,直接进入"它好不好玩"的创作环节。骨架是它的,灵魂还是你的。
完整产物
- 三个脚本:
PlayerController.cs/Coin.cs/GameManager.cs(约 120 行) - 一段场景编排
execute_code - 一段自动通关验证
execute_code - 仓库:https://github.com/FunplayAI/funplay-unity-mcp
下一篇继续往上爬:让 AI 程序化生成一套平台关卡,再让它自己从起点跳到终点确认这关能通------生成 + 自验证的闭环。