上周我干了一件挺有意思的事:打开 Claude(顶级模型之一)的控制台,只敲了一句话,等了几十秒,浏览器里真的跑起来一个可以转向、可以漂移、可以超车的卡丁车竞速页面——把视角拉远一点,这基本就是一个“类QQ飞车”的小游戏。我不是说画面能精细到商业产品的程度,而是“真能玩”:有赛道、有AI对手、有氮气、有圈速,跑完一局还想再来一局。这篇文章把整个过程的思路、提示词设计、代码拆解和调试记录都写出来,适合两类人看:想用 AI 快速做小游戏原型的产品经理或前端开发者,以及单纯想看看顶级模型现在到底能做到什么程度的游戏爱好者。
1. 项目整体设计与思路拆解
1.1 拆游戏:QQ飞车类玩法为什么让人上头
很多人觉得赛车游戏的核心就是“快”,其实不对。QQ 飞车这类竞速游戏真正让人反复开下一局的,是几个机制叠加出来的节奏感:
- 速度感:不是一味地快,而是有“提速过程”和“速度上限”的曲线。玩家踩油门时能看到速度表快速跳动,压力就来了。
- 漂移系统:这是手感的分水岭。漂移时车头方向和实际运动方向不一致,产生侧滑;漂移过程中积攒氮气,松开漂移键后还有一个小小的“喷气”加速。这一套“操作—反馈—奖励”循环,是游戏乐趣的核心。
- 对抗与随机性:AI 车手、道具干扰、赛道弯道分布,都会让每一局结果不完全一样,给玩家“再来一局”的动机。
- 规则闭环:赛道是闭合的,有圈数、名次、终点判定。没有这套规则,玩家跑一会儿就会失去目标。
拆完就知道,这些机制没有一个必须依赖 3D 引擎才能实现。位置、角度、速度、碰撞,本质上都是数学运算和状态更新,正好落在生成式 AI 最擅长的代码类型里。
1.2 为什么我只用一句话,而不是多轮对话逐步写
很多人用 AI 写游戏的习惯是“先做个 Canvas 赛车,再帮我加漂移,再加道具”,这种多轮方式也不是不行,但实测有个毛病:模型每一轮都会重新解释上下文,改到第五轮时经常出现两套物理逻辑并存、旧功能被新功能覆盖的情况。
而“一句话提示词”其实是反过来用的:让模型一次性输出一个完整闭环,而不是拆成碎片让它拼。
顶级模型有很强的意图补全能力。当你写下“能漂移的卡丁车”,它会把训练数据里见过的一整套竞速游戏的默认逻辑调出来:物理引擎怎么写、漂移怎么判定、氮气怎么积累、AI 车手怎么跑路点、UI 显示哪些信息。它不是在等你把需求一条条喂完,而是看到锚点之后自动补全整座冰山。
类比一下:你给一位资深外包打电话说“做一版带漂移的赛车小游戏”,他脑子里已经有一份完整方案了。你说的是需求,但他执行的是蓝图。顶级模型对自然语言的理解已经接近这个状态,所以一句话往往比十句话更高效,因为上下文内的一致性更强。
1.3 技术选型:为什么是 Canvas + JavaScript 而不是 Unity
这算是我反复试出来的经验。当前主流生成模型对 JavaScript + HTML5 Canvas 的代码生成质量,远高于对 Unity/Cocos 工程结构的生成质量,原因有三:
- 训练语料里浏览器小游戏的代码量极大,模型见得越多,生成越稳。
- Canvas 游戏的运行环境极简,双击 HTML 就能跑,不需要装编辑器、不需要管资源导入。
- 调试链路短,浏览器控制台会直接告诉你哪一行报错,修改刷新就能验证。
Unity 不是不行,但工程结构、场景序列化、预制体引用这些环节很容易出错,生成完离“能玩”还有很大距离,不适合用来做快速原型。
另外,我明确选了2D 俯视角赛道,没有去挑战 3D。3D 需要大量坐标、纹理、光照参数,模型上下文稍微一长就容易变形;而 2D 赛道用一组路点数组就能描述清楚,生成成功率最高。想要有 3D 感,可以用像素风、伪 3D 的视觉技巧来补,完全不影响“玩”这件事。
2. 提示词设计:一句话怎么写成模型看得懂的规格书
2.1 我实际使用的提示词与逐句拆解
我的提示词原文是这样的:
用纯HTML+CSS+JavaScript在Canvas里做一个类QQ飞车的俯视角卡丁车竞速游戏: 玩家用方向键控制车辆,空格键漂移并积攒氮气,Shift释放氮气加速; 赛道是闭环的公路,自带路边护栏、发卡弯和直道; 有3个AI车手围绕路点行驶,车辆之间不能互相穿透; 画面右上角显示圈数、名次和当前速度; 打开页面先显示标题界面,点击开始进入比赛; 整体像素卡通风,颜色鲜明,帧率稳定在60fps。每一句都是有目的的,不是废话,可以拆成一张表:
| 提示词部分 | 作用 | 模型会据此设计什么 |
|---|---|---|
| 纯HTML+CSS+JavaScript | 限定技术栈,避免生成 Python/其他方案 | 单文件或少量文件结构 |
| 在Canvas里 | 指定渲染方式 | 初始化 canvas 上下文、draw 方法 |
| 类QQ飞车的俯视角卡丁车 | 明确游戏类型与视角 | 车辆俯视图形、赛道俯视画面 |
| 空格键漂移并积攒氮气 | 定义核心操作 | 漂移判定、氮气槽变量 |
| Shift释放氮气加速 | 定义第二个操作 | 氮气消耗和加速逻辑 |
| 闭环公路 + 护栏 + 发卡弯 | 定义赛道形态 | 路点数组、边界生成、碰撞检测 |
| 3个AI车手围绕路点行驶 | 定义对手行为 | 每个AI的目标路点索引、转向逻辑 |
| 车辆之间不能互相穿透 | 定义碰撞规则 | 两车矩形/圆形的碰撞检测 |
| 右上角显示圈数、名次、速度 | 定义 HUD 需求 | 圈数计数、名次比较、速度显示 |
| 标题界面 + 点击开始 | 定义游戏状态机 | menu/playing/over 状态切换 |
| 像素卡通风、颜色鲜明 | 定义美术风格 | 字体、配色、车辆绘制方式 |
| 稳定在60fps | 定义性能约束 | requestAnimationFrame 时间差计算 |
2.2 顶级模型如何把一句话扩展成完整代码
这是一个关键认知:你给出的不是命令,而是锚点。模型就像一个很有经验的新同事,听到这些锚点之后,会自动填充你没写出来的部分。
举例,提示词里出现“漂移”和“氮气”,模型会自己做出如下设计:
- 定义
driftThreshold速度阈值,只有超过这个速度才能进入漂移状态 - 判断条件:空格按下 + 方向键转向 + 当前速度大于阈值
- 漂移效果:车的朝向角与速度矢量方向分离,产生侧滑
- 氮气积累公式:漂移持续时间内,按转向幅度和侧滑程度增加氮气数值
- 松开空格后,给予一个短暂的小喷:在一个时间段内提高加速度
再比如“AI车手围绕路点行驶”,模型会实现一套非常经典的寻路逻辑:
- 每个 AI 车有一个
targetPointIndex,记录当前要去第几个路点 - 每帧计算车到目标路点的方向角,然后朝这个角度转向
- 距离目标点小于某一个阈值时,
targetPointIndex + 1 - 如果索引超过路点总数,回到 0,循环跑圈
这套逻辑在很多开源赛车项目里反复出现,所以模型生成的成功率非常高。我第一版跑起来,AI 车手就已经能独立跑完整圈,只有个别弯道会蹭护栏,整体非常可用。
2.3 云端模型与本地模型:什么场景选什么方案
我这次原型用的是云端 API,因为速度和生成质量最稳。但如果你有离线需求、隐私边界或者长期调用成本的考量,本地部署的开源模型,比如 Qwen2.5-72B、DeepSeek 系列,也能通过 LM Studio、Ollama、vLLM 这类推理工具暴露一个兼容接口。像 Claude Code 这种 AI 编程代理工具,可以直接配置成调用本地模型地址,照样能完成“一句话生成游戏”的任务。
实测下来的区别是:72B 级别的开源模型对“一句话生成完整可运行游戏”的指令跟随能力,比顶尖云模型还是弱一点,容易出现某个功能缺失的情况。解决办法很简单——把一句话拆成两句话:第一句定框架,第二句补细节。先让本地模型生成“带有赛道、车辆、漂移、AI的基本竞速游戏”,跑通后再单独要求“把漂移改成空格键触发,并在漂移时累积氮气”。框架对了再补细节,成功率会高很多。
3. 实操过程:从提示词到能玩的竞速小游戏
3.1 第一版生成的代码结构长什么样
模型给我的第一版是一个单文件index.html,大约 600 行,内联了 CSS 和全部 JavaScript。单文件的好处是分享方便,复制出去就能打开,缺点就是代码结构比较平。拆解之后,里面包含这些模块:
- 初始化部分:设置 canvas 尺寸、绑定键盘监听、初始化状态机
- 赛道数据:一组
roadPoints路点坐标数组,由路点插值生成内外边界 - 车辆实体:玩家对象 + 3 个 AI 对手对象,每个对象有
x、y、angle、speed、nitro、steer等属性 - 物理更新函数:油门、刹车、转弯、速度衰减
- 漂移与氮气逻辑:单独的
updateDrift函数 - 碰撞检测:车辆之间、车辆与赛道护栏之间
- 渲染部分:
drawTrack、drawCar、drawHUD - 游戏主循环:使用
requestAnimationFrame按帧率驱动
我第一件事是把代码格式化拆模块,不是因为它不好,而是后续调试手感时我需要在逻辑块之间快速跳转。建议所有拿到 AI 生成代码的人,先格式化、再分块,不要直接在 600 行泥潭里改参数。
3.2 车辆物理模型:速度和转向是怎么算出来的
第一版生成的速度和转向逻辑大致是这样,也是所有 2D 竞速游戏最基本的地基:
function updateCar(car, dt) { // 油门和刹车 if (car.throttle) { car.speed = Math.min(car.maxSpeed, car.speed + car.accel * dt); } else if (car.brake) { car.speed = Math.max(0, car.speed - car.brakeForce * dt); } else { car.speed *= Math.max(0, 1 - friction * dt); } // 转向:速度越高,转向响应越迟钝 if (car.steer !== 0 && car.speed > 0.5) { const turnFactor = car.steer * (0.6 + 0.4 * car.speed / car.maxSpeed); car.angle += turnFactor * turnRate * dt; } // 根据角度和速度更新位置 car.x += Math.cos(car.angle) * car.speed * dt; car.y += Math.sin(car.angle) * car.speed * dt; }这几行代码看着简单,但参数极其关键,我逐个说一下:
turnFactor这行的作用是速度相关转向曲线。低速时turnFactor接近0.6,转向快,方便玩家在发卡弯修正车头;高速时接近1.0,转向变钝,避免玩家在直道上左右乱抖。没有这个因子,高速时会觉得车像陀螺一样转个没完。friction是摩擦系数,建议控制在 3 到 5 之间。太小车会像冰面滑行,松了油门还乱飘;太大就变成碰碰车,松油门立刻刹停,毫无速度惯性。第一版如果手感怪,先调这个数。- 速度单位是“像素/秒”。我这张 canvas 是 1280×720,
maxSpeed我调到 580 左右。低于 420 会感觉像乌龟爬,高于 750 很容易一跳几帧直接穿透护栏。这个数值要跟 canvas 尺寸挂钩,不是绝对的。
3.3 漂移与氮气:这个玩法的心跳
漂移是 QQ 飞车类游戏最核心的机制,AI 第一版会给你一个“能漂移”的版本,但不一定顺手。典型的第一版逻辑长这样:
function updateDrift(car, key, dt) { // 进入漂移:空格 + 足够速度 + 有转向输入 if (key.space && car.speed > driftThreshold && Math.abs(car.steer) > 0) { car.driftAngle += car.steer * 2.2 * dt; // 漂移角度累积 car.nitro += Math.abs(car.sideSpeed) * 0.5 * dt; // 氮气累积 } else { car.driftAngle *= Math.max(0, 1 - 8 * dt); // 快速回正 } // 释放氮气 if (key.shift && car.nitro > 10) { car.speed += nitroBoost * dt; car.nitro -= nitroDrain * dt; } }这里最容易出错的地方是侧滑速度的计算。很多第一版代码直接用car.x和car.y的变化量来算侧滑,但因为位置更新本身已经包含了速度方向,算出来往往方向不对。比较稳的做法是先单独维护一个速度矢量,再把它拆成沿车头方向的纵向速度和垂直于车头方向的侧向速度,漂移累积量只跟侧向速度有关。
如果你发现漂移时氮气槽涨得特别慢或者完全不涨,优先检查这一块。
另外还有一个手感细节:漂移结束后要有一个“小喷”效果,持续时间通常是 0.5 秒左右,让车辆的加速度在短时间内提升。很多第一版代码只做了漂移累积,没做小喷,结果就是漂移变成纯减速惩罚,玩家完全不想用。需要额外给模型补一句:“漂移结束后 0.5 秒内,车辆获得双倍加速度。”
3.4 赛道绘制与 AI 对手的实现思路
赛道这块,AI 生成的第一版用的是路点数组 + 边界偏移:
const trackPoints = [ { x: 200, y: 500 }, { x: 300, y: 350 }, { x: 600, y: 200 }, // 更多路点,最终回到起点形成闭环 ];渲染时把路点连成平滑曲线,然后沿法线方向向外偏移trackWidth / 2得到左右边界。这种方式的优点是想改赛道形状,只需要改路点,不需要改碰撞逻辑。
AI 对手的实现则很简单,每个 AI 维护一个targetPointIndex:
- 每帧计算 AI 车指向当前目标路点的角度
- 按一个比较温和的速率把车头转向目标角度
- 当 AI 车与目标路点距离小于阈值时,切换到下一个路点
- 跑完全部路点后回到 0,实现循环
但是这个朴素版本有个明显问题:AI 到弯道不会减速,经常直接护栏中出。解决办法是给 AI 加一个“弯道预判减速”:计算当前方向与到目标点方向的夹角,夹角越大说明弯越急,就把目标速度压低。我实际让模型改成了这样:
const angleToTarget = Math.atan2(target.y - car.y, target.x - car.x); const angleDiff = normalizeAngle(car.angle - angleToTarget); const speedFactor = 1 - Math.min(0.6, Math.abs(angleDiff) * 0.5); car.targetSpeed = aiBaseSpeed * speedFactor;加了这一行之后,AI 在发卡弯前会明显减速,入弯也稳了很多。
3.5 从生成到跑起来的完整步骤
实操流程非常简单:
- 把模型返回的完整代码保存成
index.html - 直接用浏览器打开,或者用 VS Code 的 Live Server 打开
- 点击标题界面的开始按钮,进入比赛
- 用方向键控制油门和转向,空格漂移,Shift 放氮气
- 跑完一圈看 HUD 上的圈数和名次
第一版能直接跑起来,不代表没有暗坑。我遇到的第一个问题就是白屏,后来发现是 canvas 高度设成了百分比,但父容器没有高度;第二个问题是漂移时氮气一点不涨;第三个问题是 AI 车手在弯道全部撞墙。这些问题的定位和修复,放到下一节详细说。
4. 常见问题与实际调试记录
4.1 启动问题速查表
| 现象 | 最常见原因 | 处理方式 |
|---|---|---|
| 打开就是白屏 | canvas 宽高为 0,或 JS 报错中断 | 打开控制台看报错,优先检查 canvas 尺寸 |
| 页面静止,车不动 | 缺少requestAnimationFrame循环 | 确认主循环里面有 update 和 render 调用 |
| 键盘没反应 | 监听挂在错误元素上,焦点丢失 | 改用window.addEventListener('keydown') |
| 车辆直接穿墙 | 速度太高,单帧位移超过边界检测范围 | 改成线段相交检测,或限制最高速度 |
| 帧率忽高忽低 | 没有用时间差 dt,帧率影响逻辑更新 | 把所有物理更新乘以dt,用requestAnimationFrame的时间参数 |
4.2 手感调校:一场和参数较劲的实录
第一版跑通后,我的体验是“能开,但不想开”。原因是漂移侧滑太生硬,氮气不涨,AI 像一群无头苍蝇。我陆续做了这些调整:
转向太灵敏的问题。turnRate从 3.5 降到 2.2,同时把转向曲线的下限从 0.6 降到 0.3。改完之后高速状态下不再像刀片一样晃,直线稳定多了。
漂移侧滑方向算反的问题。第一次改漂移时我把侧滑量写成了Math.cos(car.angle - car.moveAngle),结果漂移时车屁股往外甩得很假。后来改成Math.sin(car.angle - car.moveAngle),侧滑才有内味儿。这个坑特别容易踩,因为cos和sin在视觉上很接近,但对侧向速度的分解方向完全不同。
氮气积累太慢。增量系数从 0.5 提到 0.8,同时限制每秒最多积攒 30 点,避免一个弯攒满一管氮气导致游戏失衡。氮气攒满之后按 Shift 加速,提速效果要足够夸张才爽,我是把nitroBoost设成正常加速度的 1.8 倍。
视觉效果卡顿。开 shadow 和 filter 确实好看,但中等配置的电脑直接掉到 40fps。把这些效果全去掉,换成预绘制一张赛道底图,性能立刻稳了。移动端还要把canvas.width按window.devicePixelRatio限制一下,不然手机端白屏或者巨卡。
4.3 让 AI 自己修 Bug:量化反馈比形容词好用
我试过两种反馈方式,效果差别很大:
- 差评式:“手感不好,漂移太假,AI 太蠢。”模型通常会改一些观感参数,但很难对准你心里的点。
- 量化式:“转向过度灵敏。将 turnRate 从 3.2 调到 2.3,并让转向随速度增长更平缓。漂移侧速分解中,把 cos 换成 sin,同时将漂移氮气增量提高 40%。AI 在弯道前需要提前 0.4 秒减速。”
第二种反馈模型能准确执行,因为它可以精确锁定你要改的变量,而不是去猜你的主观感受。把主观感受翻译成具体变量,这是 AI 编程时代最基本也最重要的能力。
如果一轮修复失败,不要硬在同一个对话里纠结,新开一个对话,把完整代码和报错信息贴进去,然后描述问题。新对话的上下文更干净,模型重新组织思路的成功率更高。
5. 从能玩到好玩:进阶改造方向
5.1 加道具赛与本地双人
基础竞速玩腻了,可以让模型继续加内容。我的做法是继续用一句话描述模块:“新增 8 个道具点分布在赛道中,车辆经过时随机获得加速、减速、护盾三种效果之一;新增本地双人模式,P1 用 WASD,P2 用方向键,空格和右 Shift 分别负责两个人的道具。”
这种增量需求对顶级模型来说很轻松,因为基础代码的物理和碰撞逻辑已经够健壮,加一个powerUp数组和触发器就行。道具实现要注意碰撞检测的范围,不能太小,否则高速下直接穿过没吃到。
5.2 打包成 PWA 或桌面端分享
生成的东西默认只能在浏览器里玩,想发到手机给朋友试,最简单的方式是把它做成 PWA:
- 加一个
manifest.json,定义应用名称和图标 - 写一个简单的 Service Worker,缓存页面资源
- 手机访问后“添加到主屏幕”就能全屏玩
如果想做成本地桌面程序,用 Electron 包一层是常见方案,项目结构也简单:主进程加载 index.html 文件即可。这一步不需要 AI 写太多,教程遍地都是,核心代码仍然是那个游戏本体。
5.3 把“一句话生成游戏”沉淀成自己的模板库
这次经验最大的收获,是我意识到“提示词模板”是可以复利的。我现在维护了一个 Markdown 文件,里面存了不同游戏类型的标准提示词模板:
- 竞速类:玩法要点 + AI 对手 + 圈数系统
- 解谜类:关卡生成 + 碰撞逻辑 + 步数计数
- 塔防类:敌人波次 + 升级路径 + 血条 UI
每次生成成功,就把这段提示词和跑通的代码一起存下来。下次换皮肤、换规则,改几个关键词就能开工。
最后再说一个小技巧。我试过几次之后最大的体会是:提示词里的动词比形容词值钱。“能漂移”“能放氮气”“能超车”比“很酷炫”“很流畅”有用得多,因为模型会优先执行可验证的功能词,形容词最后常常变成一堆视觉效果参数,不解决核心玩法。另外,哪怕顶级模型已经能一句话写出能玩的游戏,真正决定“好玩”的还是你的手感调校和边界测试。原型能跑只是第一步,花半个小时调参数、修边界,才是让它变成正经 demo 的关键。