ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DeepSeek实战:用Three.js从零开发3D越野卡丁车游戏

DeepSeek实战:用Three.js从零开发3D越野卡丁车游戏 开头我先说个结论这期项目玩下来最大的收获不是“AI 能写代码”这件事本身而是发现 DeepSeek 在“把一个模糊的玩法想法拆成具体模块”这件事上比我预想的要靠谱得多。我这个系列已经做到第 20 期了前十几期做过网页工具、做过数据可视化、也做过小体量的 2D 游戏但这是第一次尝试让它帮我造一辆 3D 越野卡丁车——带起伏地形、能翻车、能飞跃、能计时破纪录还能把上一轮最佳成绩的“幽灵车”拉出来陪你一起跑。这篇就把整个实战过程完整拆开从需求定义、提示词设计、3D 场景搭建到物理手感、记录系统、幽灵车回放最后再到调试翻车现场全部记录下来。如果你也对“让 AI 参与游戏开发”这件事感兴趣或者想在浏览器里做出一个有点手感的 3D 驾驶小游戏这篇应该能给你一份能直接抄作业的参考答案。1. 先搞清楚一辆“会翻车还会破纪录”的卡丁车到底要什么1.1 从标题反推核心玩法先把“游戏体验”翻译成“功能需求”很多人用 AI 做游戏失败卡在第一步自己都没想清楚要什么。标题里那几个关键词——“会翻车”“会飞跃”“破纪录”“幽灵车”——听起来很有画面感但要让它成为可执行的项目需求就得逐个拆成具体的规则和状态。先说“翻车”。翻车不是 bug而是玩法。那问题来了翻车之后怎么办是直接重置回赛道还是给玩家一个“车辆翻转回正”的动画我最后选择的是检测到车体翻转超过一定角度后触发翻车状态给玩家 2.5 秒时间自动回正如果回正失败则从最近检查点重来。这样既保留了碰撞翻车的乐趣又不会让玩家因为一次失误就从头跑而烦躁。再说“飞跃”。越野赛道里要制造起跳点这需要地形的起伏。单纯把地面做得坑坑洼洼不够需要有明显的坡道和落差让车在高速通过时腾空。腾空状态下车辆应该失去转向控制但保留部分水平速度落地时候还有冲击反馈。这样玩家的“飞跃感”才真实。“破纪录”和“幽灵车”是一对组合。破纪录需要一个完整的计时系统从起点出发开始计时经过终点线停止幽灵车则要在玩家开始新一轮挑战时把上一轮最快的那条线路重新播放出来作为陪跑参照。这两者合在一起才形成“反复刷记录”的核心循环。把四个点都拆完我才有底气去跟 AI 说清楚需求。一个最基本的判断是如果连你自己都说不清规则AI 生成出来的代码一定是“能跑但不好玩”的通用框架。1.2 技术选型为什么仍然选择浏览器里的 Three.js这个项目做 3D 越野卡丁车技术栈其实有好几条路。可以用 Unity 或 Godot 导成 WebGL也可以在浏览器里直接用原生 WebGL 一点点手写渲染管线。但我在这个系列里一直用的方案是 Three.js 加原生 JavaScript这一次也延续了同样的选择。选择 Three.js 的核心原因是项目体量。这个游戏的场景只有一个车辆只有一台地面有起伏但没有超大地图不需要非常复杂的资源管理和光照烘焙。Three.js 对这类中小体量 3D 场景的支持非常成熟API 封装在易用性和可控性之间平衡得刚刚好。卡丁车的车身用几个 BoxGeometry 和 CylinderGeometry 组合出来地形通过修改 PlaneGeometry 的顶点高度也能实现不需要外部加载任何模型文件。另一个重要原因是“可传播性”。浏览器直接打开就能玩不用安装任何客户端这对一个偏实验性质的项目来说特别关键。我经常在手机或平板上做快速测试Web 方案天然跨端。而且用 Three.js 做出来的项目生成的都是纯文本代码DeepSeek 在生成这类代码时的表现最稳定——不需要资源管线、不需要 Shader 编译步骤代码就是那个代码跑起来整个项目就在眼前。从这期项目的结果来看这个选型没有让我后悔。整个游戏的代码规模控制在一个 HTML 文件加少量 JS 模块内调试成本低几乎处处可以实时看到改动的效果。对于想让 AI 帮你做 3D 小游戏的朋友我强烈建议不要嫌 Three.js“不够专业”先把“能在浏览器里跑起来”这件事做踏实比什么都重要。2. 让 DeepSeek 把需求拆成架构提示词设计的实战记录2.1 第一版提示词给足场景约束但不拘死实现跟 AI 协作这么多年我摸索出的一个重要经验是提示词不要太长但关键约束一定要给足。第一版的提示词我控制在 300 字以内核心是描述清楚“什么场景、什么操作、什么反馈”而不是告诉它“用哪个函数、怎么写循环”。我当时的提示词大概是这样的结构首先说明这是一个基于 Three.js 的浏览器 3D 游戏需要一辆越野卡丁车在起伏地形上行驶然后列出核心操作——WASD 控制方向空格键一键启动R 键重置车辆接着描述物理感受——希望车辆能因为地形起伏而跳跃、能在高速碰撞时翻车、能正常回正最后提到需求里要有计时系统和幽灵车回放功能并且支持本地保存最佳纪录。这份提示词里没有出现任何具体的 Three.js API 名称也没有给代码结构定框架。原因很简单AI 在代码结构上的规划能力比我们想象中强如果我们在提示词里过早指定实现方案反而限制了它的发挥空间。同时我明确了“纯前端、单页应用、所有代码都是可读可改的文本”这个约束DeepSeek 自然就会往“一个主 HTML 文件 内嵌或引用的 JS 模块”这个方向去想。第一版输出出来的结构让我比较意外——它没有直接甩给我一大坨代码而是先列了一个模块划分建议场景管理、地形生成、车辆控制、物理模拟、计时系统、幽灵车回放、UI 交互。这七个模块的划分虽然还有冗余但整体方向是对的后续的迭代基本都建立在这个骨架上。2.2 架构确认后的第二版提示词按模块逐个击破拿到了模块划分之后我没有让它一口气把全部代码写完而是选择了“分模块推进”的方式。这不是因为我怀疑 AI 的能力而是因为一次性生成几百行代码之后一旦某个模块的逻辑不对排查成本会很高。第二个版本的提示词按模块拆成了多个对话回合。第一个回合只聚焦场景和地形创建一个起伏的曲面赛道赛道上有明显的坡道、低洼和边界标识。第二个回合聚焦车辆构建和基本移动。第三个回合才进入物理调教要求 AI 给出重力和碰撞响应的参数。到了第四个回合才告诉它加入计时系统第五个回合加幽灵车回放。这里有一个特别重要的细节每轮对话之间我都会把上一轮生成的代码先运行一遍确认没有报错、手感基本可用才进入下一轮。这样做的好处是问题始终是“增量”的不会出现好几轮代码叠加之后一堆错误缠在一起的局面。而且我还发现DeepSeek 在后续回合中能记清楚之前生成的变量名和函数结构极少出现代码前后不连贯的问题。2.3 别让 AI 做超出范围的事需要人拍板的设计决策AI 能把“需求”转成“代码”但有些产品的决定还是需要人来拍板的。比如“翻车后怎么救援”这条规则我第一版的提示词没有写清楚DeepSeek 就默认采用了“直接重置位置”的方式玩家翻车后瞬间回到起点体验非常撕裂。后来我在提示词里补充了“车辆翻转超过 90 度进入翻车状态2.5 秒无操作自动回正若处于腾空状态则忽略翻转检测”这样的描述AI 给出的方案就合理了很多。这说明一个边界清晰的规则描述比“让 AI 自己悟”要可靠得多。在这个项目里AI 是执行者但产品规则的定义者必须是人这一点在做 AI 辅助开发时一定要时刻记住。3. 搭建 3D 场景地形、赛道、车体一个都不能少3.1 地形生成起伏要有章法不能纯粹随机越野卡丁车最核心的赛道元素就是地形。Three.js 里生成起伏地形标准做法是通过修改 PlaneGeometry 的顶点坐标来实现。我让 AI 生成了一版地形生成逻辑先用一个 200 乘 200 的平面网格然后对每个顶点的 y 值施加一组不同频率和振幅的正弦叠加形成连绵起伏的丘陵感同时在特定区域手工“压出”几条沟壑和坡道作为比赛路线的大致引导。这里有个踩坑后换来的经验纯随机噪声生成地形看起来很炫但跑起来根本不知道哪里有路玩家体验非常糟糕。我最后采用的是“程序化随机 人工规则叠加”的方式。简单说就是先生成一版随机起伏然后再用几条贝塞尔曲线在场景里勾勒出主路径沿路径把地面高度压低让玩家能自然地沿着赛道跑同时赛道两侧保留不平整的侧坡故意制造“偏航就翻车”的空间。地面材质方面我用 MeshStandardMaterial 加了一个很淡的网格纹理让地形在高速移动中有明确的速度参照。没有额外贴图主要靠光照和网格线来体现地面细节。这个做法还有一个隐藏好处调试物理的时候能非常清楚地看到车辆接触地面的位置而不需要专门的 Debug 线框。3.2 卡丁车模型不追求精细但每个部件都要有反馈感卡丁车的 3D 模型完全用 Three.js 基础几何体拼装。车体主体是一个略微压扁的 BoxGeometry四个轮子用圆柱体构成车手位置用一个球体和半球体的组合模拟头盔与身体。整个模型加起来不到 20 个几何体没有外部资源依赖。真正让我下功夫的是“反馈感”。这辆卡丁车在转弯时前轮要有相对转向角度的旋转车辆在跳跃腾空时车体会自然地轻微仰起。这些细节在纯逻辑层面根本不影响物理计算结果但它们对玩家的“手感”影响巨大。我用一个独立的车辆模型节点来分层模型节点保持位移和旋转这样 AI 生成的代码里只需要对某个子节点做偏转视觉反馈就出来了。还有一个容易忽略的细节是底盘。我给车体下方加了一个接近地面的 BoxGeometry 作为底盘表示同时把碰撞检测的包围盒设计为一根胶囊体而不是整个车身。这样做的好处很直接车辆在侧倾时真正先擦到地面的部分是底盘包围体而不是整个车身模型翻车判定的触发时机就会更准确不会出现“看着还没翻系统已经判定翻了”的割裂感。3.3 相机与灯光机位到位了速度感才出得来越野卡丁车需要很强的速度感但这个游戏里没有真正的“道路”所以传统赛车游戏的“摄像机跟随道路”方案不适用。我选择了第三人称跟随相机相机固定在车辆后方偏上位置随着车体转向做平滑插值移动。这个方案最符合直觉玩家视角中的所有起伏和飞跃都有足够清晰的视觉反馈。灯光上用了一组半球光和一组方向光。半球光用来保证场景整体不过暗方向光用来形成车辆和地面的明暗对比强化立体感。第一版我贪心加了阴影结果帧率在手机上掉了不少最后果断关闭了阴影用假阴影一个半透明圆形平面跟着车体走替代。对于这种轻量级 3D 游戏阴影就是性能黑洞能省就省。4. 物理手感翻车、飞跃全靠一套自写物理循环4.1 为什么不用物理引擎自己写其实更可控一开始我确实想过直接引入 Ammo.js 或者 Cannon.js 这类物理引擎但在和 DeepSeek 讨论后我很快意识到对这个场景来说有点“杀鸡用牛刀”。越野卡丁车的物理核心无非是三件事地面碰撞、重力加速度、速度矢量。用物理引擎虽然能一步到位但引擎内部的摩擦、阻尼、睡眠参数都需要额外调教调试周期反而更长。自己的物理循环实现起来并不难而且可控性极强。每一帧要做的事情非常清晰读取输入产生加速度把加速度累积到速度上用速度更新车辆位置然后检测车辆与地形的碰撞并修正位置和姿态。整个过程大概 80 行代码但每一项都是显式可调的——比如重力加速度调大车辆在坡顶的悬空时间变短阻尼加大车辆滑行的距离缩短。这些参数我能直接感知到变化比在一个物理引擎的黑盒里调摩擦系数要直观得多。4.2 车辆的驱动与转向控制车辆控制的第一版代码很简单概括起来就是前后键控制油门和刹车左右键控制转向速度越快转向的幅度略微降低。这里有个重要参数叫“转向速度衰减”如果不加这个限制车辆在高速时依然能以低速时的转向灵敏度转弯结果就是轻轻一碰方向键车辆横着飞出去非常不真实。实际调教中我把最大速度设为 60 单位/秒加速度为 20 单位/秒方留给细节题……转向时的角速度会随速度线性衰减。这样在低速时能做出比较灵活的调头动作在高速时则明显感觉到“车辆变重了”需要提前打方向才能过弯。下面这段是当时让 DeepSeek 生成并整理的车辆更新逻辑示意注释是我后续加的// 车辆物理更新dt 为帧间隔 function updateVehicle(dt) { // 1. 读取输入计算目标加速度 const accel input.forward ? enginePower : 0; const brake input.backward ? brakePower : 0; // 2. 根据当前速度状态切换 加速/减速 if (accel 0) { vehicle.speed accel * dt; } else if (brake 0) { vehicle.speed - brake * dt; } else { // 没有输入时阻尼让车速缓慢下降 vehicle.speed * Math.max(0, 1 - drag * dt); } // 3. 限制最大速度和倒车速度 vehicle.speed clamp(vehicle.speed, -maxReverseSpeed, maxSpeed); // 4. 转向速度越快同样输入对应的角速度越小 const steerAngle input.left ? 1 : input.right ? -1 : 0; const steerFactor steerStrength / (1 vehicle.speed / 15); vehicle.yaw steerAngle * steerFactor * dt; // 5. 把朝向和速度换算成位置增量 vehicle.velocity.set(0, 0, vehicle.speed); vehicle.velocity.applyAxisAngle(up, vehicle.yaw); vehicle.position.addScaledVector(vehicle.velocity, dt); }这段代码是目前项目里最稳定、最核心的部分。相比引入物理引擎的方案这种显式控制方式特别适合 AI 生成因为它的每一步逻辑都和自然语言的描述一一对应调试时也能非常快速定位到具体参数。4.3 地面碰撞与姿态修正让车“贴”着地形走车辆在地形上行驶最棘手的部分是如何处理地面起伏。如果不能正确地让车体贴合地面车辆要么悬空漂移要么直接穿模。我的方案是采用射线检测从车体中心向下发射一条射线与地形网格求交得到实际地面高度和法线方向。拿到法线之后车辆的上向量会在一帧内逐渐转向地面法线方向。这一过程同样使用了插值而不是瞬间硬切。因为如果瞬间把车体贴合到坡面上车辆模型会出现明显的“瞬移”感。经过反复调试我把插值速度控制在每帧 5 度到 10 度之间视觉上车辆就像“吸”在地面上一样过陡的侧坡时依然会打滑或侧翻。为了处理跳跃和落地的边界情况我还加了一个“是否贴地”的状态判断。当地面射线距离超过 0.15 个车身高度时认为车辆处于腾空状态此时不对车辆姿态做向地面的插值保留空中自由旋转的自由度。这个判断足够简单但在实际游戏中表现出的效果已经接近物理引擎的体验了。4.4 翻车检测与回正机制翻车检测的核心是“车辆上方向 与 地面法线方向 的点积”。当点积小于阈值比如小于 0.2即车体倾角超过 78 度左右且车辆处于贴地状态时就判定为翻车。判定翻车后系统会启动救援倒计时。玩家在 2.5 秒内可以通过按 R 键手动重置车辆位置如果什么也不做倒计时结束也会自动把车辆摆正到最近的检查点。这里有一个我特意保留的设计翻车状态下车辆不会自动恢复而是需要玩家自己操作回正或重置。因为如果自动回正太容易翻车就变得毫无代价玩家反而会对地形危害视若无睹失去越野的挑战感。第一版我犯了个错误把翻车检测直接对撞车时的冲击力做了阈值判断导致高速撞上小坡时车辆会弹跳但不会翻转缺乏爆发感。后来改成同时考虑“倾角”和“贴地状态”两个维度才真正实现了标题里说的“会翻车”——尤其是在飞跃后落地姿态不正时车辆会顺势滚一到两圈视觉效果很过瘾同时也不会让玩家觉得判定不公平。5. 计时、破纪录与幽灵车回放5.1 计时系统从起点到终点要有明确的闭环计时系统第一眼看起来很简单——开始计时、停止计时、记录用时三步搞定。但如果只有这一层逻辑就会出现一个很尴尬的情况玩家可以故意绕远路把计时系统绕过跑出一个看似很极限实际犯规的成绩。所以我加了一个“必须通过检查点”的验证逻辑。赛道上有三个检查点起点、中点和终点。玩家从起点出发时计时开始必须依次经过中点和终点成绩才算有效。检查点的判定方式是距离检测当车辆位置与检查点中心距离小于阈值时标记该检查点已通过。跑完全部的检查点回到终点后停止计时并提交成绩。计时的具体实现我用的是 requestAnimationFrame 的帧回调里累积 dt。在车辆通过终点线的瞬间记录累计时间并停止更新。同时在 UI 上显示当前圈速和上一圈成绩之间的差值用正负颜色区分快慢给玩家即时的反馈。5.2 把最佳记录存进 localStorage最佳记录不能只存在内存里不然刷新页面就丢。localStorage 是这里最合适的存储方案原因有两个一是项目本身就是纯前端localStorage 无需任何后端逻辑二是保存的数据量极小就是一段 JSON 字符串加几毫秒的时间值。我把最佳成绩和对应的幽灵车采样数据一起存储刷新后仍然能复现幽灵车。这里要提一个容易踩的坑localStorage 的存储上限一般在 5MB 左右幽灵车采样数据如果是每帧记录一次位置和旋转四元数跑一圈 60 秒就可能有 3600 个采样点每个点包含 3 个位置分量和 4 个旋转分量加起来不到 100KB完全没问题。但如果你把采样频率提高到每帧多次或者保存几十圈数据很快会逼近上限。实际项目中我控制采样率为每秒 30 帧既保证了回放平滑也控制了数据量。5.3 幽灵车轨迹记录与回放的精髓幽灵车的实现方式很多人会误解以为要重新跑一遍 AI 控制的赛车。实际上我们只需要记录上一轮最佳成绩的车辆的“位置和朝向”然后在新一轮游戏中把这份记录按时间回放出来。它是一个纯粹的“数据回放”不参与任何物理计算也不会和玩家车辆发生碰撞。记录逻辑很简单在计时开始后每隔固定帧数把车辆的位置向量和旋转四元数存进数组。回放时根据当前游戏时间找到对应时间段的两条采样记录用线性插值把位置和旋转平滑地插出来赋值给一个半透明的幽灵车模型。线性插值之所以放在这里很有必要是因为记录频率是每秒 30 帧但渲染频率是每秒 60 帧如果直接把记录值赋过去幽灵车会出现肉眼可见的抖动。半透明的效果是通过 MeshBasicMaterial 的 transparent 和 opacity 属性实现的。我特意把幽灵车材质改成与玩家车辆不同的颜色比如玩家车辆是红色幽灵车是蓝色这样即使两辆车重叠玩家也能第一时间区分出自己车和“影子车”。注意一点幽灵车是从起点线同时出发的它的位置代表的是“上一轮同一时刻的最佳车手位置”。玩家看到幽灵车比自己快就知道这个弯自己慢了多少这种直观的对比比任何速度表都来得震撼。6. 调试现场翻车、穿模、幽灵车灵异位移6.1 翻车判定误触发坡顶也有灵异事件第一版翻车检测上线之后我发现在过一些陡坡的时候车辆并没有真正翻过去系统却提示“翻车正在重置”。排查发现问题出在车辆腾空时地面的射线检测还保留着上一次贴地时的法线信息导致在坡顶悬空的一瞬间车辆倾角被错误计算成巨大角度触发了翻车。修复方法是增加腾空状态判断。车辆在腾空时不进行翻车检测只有重新触地且倾角超过阈值时才判定翻车。这个修复很符合直觉从代码角度看只是加了一个布尔判断但从体验角度体验实现了“飞跃不强制翻车”玩家的跳跃体验顺畅了很多。6.2 幽灵车抖动严重插值采样频率问题幽灵车第一次跑起来画面相当惊悚——车辆的位移像跳帧一样一卡一卡尤其是在高速过弯时幽灵车仿佛在“闪现”。一开始我怀疑是回放时数据赋值逻辑有问题排查后发现真凶是采样率和渲染率的差异。我当时的采样频率是每帧一条记录而帧率在 60 到 120 之间波动导致回放时的记录分布不均匀。如果游戏主循环的帧率偶尔掉到 40记录间隔就变大回放时用线性插值虽然理论上能补齐但因为测量不均匀插值结果也会产生明显的不平滑。最终我把采样方式改成“按时间间隔采样”每 33 毫秒记录一次然后在回放时以时间为基准查找前后两条记录再做线性插值问题就彻底消失了。6.3 性能优化手机上别开阴影几何体要合并这个游戏我在手机浏览器上测试时帧率一度只有 25 帧左右。排查后发现两个重大性能瓶颈场景里的每棵装饰树都是独立 Mesh数量一多 Drawcall 激增并且树和车都开了实时阴影每次渲染都要多跑一遍阴影 Pass。修复方案有两条。一是把地形上的装饰物合并成一个 InstancedMesh相同类型的树和石头共享同一份几何体渲染效率和加载速度都提升了很多。二是关闭所有阴影用假的半透明圆形阴影贴在地面虽然光线细节少了但帧率稳定在 55 帧以上对游玩体验的影响完全在可接受范围内。如果你也在做类似场景建议从一开始就把“合并实例”和“阴影开关”作为两个默认变量提前规划别等跑不动了再回来改。6.4 记录被无效成绩覆盖检查点校验救了一命测试中有一个恶性 bug玩家故意不走检查点直接冲到终点系统竟然把这个“作弊成绩”存进了 localStorage还覆盖了之前的最佳记录。原因在于第一版计时系统只判断了“车辆是否到达终点”没有强制要求“经过所有检查点”。加了一套检查点状态数组之后每次通过终点线都会先校验检查点数组的状态。只有全部检查点都是“已通过”成绩才被接纳。同时我在 UI 上加了明显的检查点提示文字避免玩家因为没看见检查点而白白跑一圈。这种边界问题代码上只是几行但它对成绩系统的公平性起到了决定性作用。7. 这次实战让我对 AI 写游戏代码有了新认识第 20 期这个项目做完我自己的心态有了明显变化。以前我觉得 AI 生成游戏代码最大的价值是“快速出原型”细节手感还得靠人肉调。但这个越野卡丁车的项目让我看到AI 在“按照清晰规则实现功能”上的完成度已经相当高了。只要你提前把玩法规则定义清楚AI 产出的代码在正确率、稳定性和可读性上都具备直接进入测试迭代的资格。当然仍有明显短板。比如物理参数的调优AI 给出的初版数值往往偏向保守需要人工根据实际手感改参数又比如产品层面的“好玩感”AI 很难自己主动提出“加一个起跳坡道”或者“在终点附近放一个道具”这种设计建议。这些创意层面的决策还是得由人来拍板AI 更像是执行力极强的初级工程师而不是产品经理。如果你也想尝试类似项目我的建议是先别急着追求复杂的玩法把一辆车、一条不平的路、一个简单的计时系统和一个幽灵车回放做出来就已经能跑出很完整的体验闭环了。在此基础上再慢慢加入新的地形元素、道具、多人模式幽灵车就是一个天然的多人替身项目会越来越有生命力。这种“小步快跑、不断叠加”的节奏也是 AI 辅助开发模式下最高效的推进方式。
返回列表