Vibe Coding实战:用React和Canvas开发飞机大战游戏的全流程复盘

发布时间:2026/10/10 8:43:45
Vibe Coding实战:用React和Canvas开发飞机大战游戏的全流程复盘
前阵子一个朋友问我“现在是不是随便说句话就能写一个完整项目了”我没直接回答而是把一个用Vibe Coding做的React飞机大战游戏发给他让他先玩两把。结果他说玩着挺爽问我代码是不是手写的。我说大部分不是但也不是纯靠AI吐出来就能直接跑。今天把整个过程、踩过的坑、以及几个关键代码细节写下来给那些想尝试AI配合开发的朋友一个真实参考。所谓Vibe Coding简单说就是让AI根据自然语言描述生成代码你负责“描述需求→审视代码→跑起来验证→继续提需求”。听着很轻松但真正上手做一个带实时动画、碰撞检测、音量反馈的React小游戏时就会发现AI可以帮你把80%的代码框架立刻搭好剩下20%的“游戏逻辑正确性”反而更需要你自己理解。这篇文章就用“飞机大战”这个经典小游戏作为载体从需求拆分、实操过程、核心代码到排错思路完整复盘一遍。适合两类人一类是想体验AI写代码但不知道从哪下手的开发者另一类是React入门想通过小项目理解状态管理、Canvas和组件生命周期的同学。1. 项目思路拆解先搞清楚Vibe Coding到底在“vibe”什么1.1 Vibe Coding不是“让AI全包”而是“让AI打草稿”我第一次听说“Vibe Coding”这个词是在Karpathy的一次分享里他的描述很有意思写代码的时候不再逐行敲而是“描述你想要的氛围/方向”让AI生成然后你沉浸在代码里快速迭代。听起来很浪漫但实际用起来我给它换了个更朴素的定义用自然语言作为编程接口让AI完成机械性编码人类守住逻辑边界。这不是“AI替代程序员”更像是带着一个手速极快、但缺乏项目经验的实习生。你告诉他“帮我写一个React飞机大战游戏”他可以刷刷生成几百行代码但你问他“子弹和敌机碰了为什么会穿过去”他就开始一本正经地胡说八道了。所以在这类项目里AI负责从0到1的脚手架搭设而判断“什么才是正确的游戏逻辑”“性能瓶颈在哪里”“为什么这里会重复渲染”仍然是程序员自己的活。这也决定了整套流程的操作方式先让AI把骨架搭起来再按章节逐段替换掉那些看起来“能用但不靠谱”的部分。Vibe Coding的精髓不是“无脑要代码”而是一个高频的“生成—检查—修正”循环。1.2 为什么偏偏选React写飞机大战现在做个飞机大战选择很多原生Canvas写几百行也能跑Phaser等游戏引擎更是开箱即用。但用React Canvas组合有它独特的价值。飞机大战需要几个核心模块玩家飞机移动、子弹发射、敌机生成、碰撞检测、得分、游戏结束。这些模块天然适合拆成“UI组件 游戏引擎逻辑”两层。React负责的是界面层分数面板、开始按钮、游戏结束提示、暂停菜单这些都是典型的组件化UI用useState管理状态非常方便。Canvas负责的是渲染层飞机、子弹、敌机、爆炸特效这些高频变化的画面不适合走React的state更新否则会引发每秒几十次的重新渲染直接卡成PPT。所以这种做法其实是“React做外壳、Canvas做内核”的混合架构它比纯Canvas多了一层现代前端开发体验又比游戏引擎更接地气、更容易理解底层。对我这个项目来说选React还有一个额外好处AI对这种经典组合的训练数据非常充足。模型看到过大量“React飞机大战”的开源代码所以它首屏生成的代码往往八九不离十这本身就是Vibe Coding选型时的重要考量——你选的框架越主流AI的生成质量越有下限。2. 从需求到代码Vibe Coding实操三步法2.1 第一步不要只给一句话要给“产品说明书”很多人用AI写代码失败问题出在提示词太笼统。你说“帮我写个飞机大战”AI就真给你一个最简陋的版本一个方框当飞机几个方块当敌人代码堆在一起跑起来能玩但不爽。如果你想要一个能控制、有反馈、能计分、有胜负的游戏必须把需求写清楚。我在开始之前直接把提示词写成了这样一段你是一位资深的前端工程师。请用React Canvas帮我写一个飞机大战游戏要求如下 1. 游戏区域为Canvas画布宽480高640背景为星空动态闪烁效果。 2. 玩家飞机通过键盘方向键或A/D移动按空格键发射子弹子弹从飞机头部发出间隔150ms一发。 3. 敌机从屏幕顶部随机位置出现向下移动速度随机被子弹击中后消失并加分我方飞机与敌机碰撞则游戏结束。 4. 游戏状态包括运行中、游戏结束。UI部分用React组件展示当前得分并提供开始/重新开始按钮。 5. 代码结构要清晰组件按职责拆开不要写在一个文件里。 请给出完整的可用代码。这个提示词里最关键的是“资深前端工程师”这个角色设定和“代码结构要清晰”的约束。AI在扮演资深工程师时生成的代码在分层和变量命名上会明显比“帮我写个游戏”规范得多。同时我把需求拆成了五条可验证的功能点这样AI生成之后我逐一测试每一条都有明确的验收标准。换句话说人脑先把需求拆碎AI的代码才能不散架。2.2 第二步先跑通MVP再补细节拿到AI生成的代码后我不会直接看每一行。第一件事是跑起来看游戏能不能玩。如果整体流程能走通飞机能移动、能射击、敌机能出现、碰撞能触发游戏结束那这就算MVP过了。第一版AI代码的结构通常长这样src/ App.js - 主组件包含Canvas和UI状态 hooks/ useGameLoop.js - 负责requestAnimationFrame循环 components/ ScoreBoard.js - 得分组件 GameOver.js - 结束提示组件 utils/ collision.js - 碰撞检测工具我第一次运行后发现飞机移动、发射、得分都能正常跑。但仔细玩了两分钟就看出了问题游戏速度不稳定帧率忽高忽低而且当连续按方向键时飞机移动会出现“顿挫感”。原因后面会细说但至少这一版能让我站在一个“可玩的基础版本”上继续迭代。这就够了。Vibe Coding的一阶段目标不是完美而是“能运行的最小闭环”有了这个闭环之后的修改都建立在真实反馈之上而不是凭空想象。2.3 第三步补丁时间——AI写快乐逻辑你负责“正确性”MVP跑通后我开始了逐项修正。这个阶段最考验编程基本功。比如AI第一版用的是setInterval驱动游戏循环这就导致一个经典问题setInterval中的回调函数每次执行时读到的都是闭包里的旧值飞机坐标更新不及时操作响应自然发闷。我后来把它改成了requestAnimationFrame配合useRef来存游戏数据问题立刻消失。再比如AI生成的碰撞检测代码经常只会判断“子弹是否碰到敌机”但不会同时判断“敌机是否已经越过底部边界”导致敌机飞到屏幕外之后会一直存在内存里整个游戏的数组越来越大帧率越来越低。这些都是需要人类自己去发现并修正的。所以我的第二个提示词开始变得非常具体请把游戏循环改成requestAnimationFrame驱动并确保循环只在运行中状态启动。 另外飞机/子弹/敌机的数据不要用useState管理改用useRef存储避免每帧触发React重渲染。 敌机飞出屏幕后要从数组里移除。AI收到这种“外科手术式”的指令时修改速度依然飞快。但注意它给出的修复方案不一定正确你可能需要继续验证。这个循环往复的过程就是Vibe Coding最能带来成长感的时刻AI负责动手你负责动脑双方互相补位。3. 核心技术细节React Canvas避坑指南3.1 游戏画面千万别放进React state里这是整个项目里最重要的一条经验。很多AI生成的代码会写成这样const [player, setPlayer] useState({ x: 200, y: 500 }); const [bullets, setBullets] useState([]); useEffect(() { const interval setInterval(() { setPlayer(prev ({ ...prev, x: prev.x 10 })); }, 16); }, []);表面上看飞机确实动了。但问题是每移动一次都会触发整个组件的重新渲染而游戏循环一秒要执行60次相当于每秒钟触发60次React更新。更要命的是Canvas绘制一般也在组件内部渲染期间又要读取最新状态层层嵌套导致主线程被占满。玩起来就是“卡顿—掉帧—卡顿”。正确做法是用useRef保存游戏数据只在Canvas上手动绘制。useRef的改变不会触发React重新渲染它就是一个可变的盒子非常适合保存高频变化的游戏状态。const gameRef useRef({ player: { x: 220, y: 520, width: 50, height: 60, speed: 4 }, bullets: [], enemies: [], score: 0, gameOver: false, keys: {}, });这样React组件只在分数变化时setState一次画面绘制全部由Canvas自己按帧处理两者各司其职性能直接提升一个量级。判断React该管什么、Canvas该管什么是这类项目的最关键分界线。3.2 生命周期与事件监听AI最容易在这里埋雷AI生成代码时特别喜欢在useEffect里直接绑定事件监听但经常忘记“返回清理函数”。这在单页面开发中不算致命但当你把游戏挂到路由里切换页面后再回来旧的键盘监听还挂着新页面又绑定了一个两套监听一起响应就会出现“明明松手了飞机还在动”的灵异现象。标准的写法是这样的useEffect(() { const handleKeyDown (e) { gameRef.current.keys[e.code] true; }; const handleKeyUp (e) { gameRef.current.keys[e.code] false; }; window.addEventListener(keydown, handleKeyDown); window.addEventListener(keyup, handleKeyUp); return () { window.removeEventListener(keydown, handleKeyDown); window.removeEventListener(keyup, handleKeyUp); cancelAnimationFrame(animationIdRef.current); }; }, []);这段代码里有个细节cancelAnimationFrame要放在清理函数里并且用ref保存当前动画帧ID。如果你在useEffect里自动启动游戏循环但没有在组件卸载时清除游戏会默默在后台消耗CPU。这是AI第一版几乎必犯的错也是我排查时最先检查的地方。3.3 键盘控制、碰撞检测和音效三个看似简单但总翻车的小模块键盘控制的正确姿势不是“按下就移动”而是“记录按键状态每帧根据状态决定位移”。这样同时按住上下左右时方向不会互相覆盖也不会因为系统按键重复触发而出现一顿一顿的移动效果。游戏循环里这样写const step () { const state gameRef.current; const keys state.keys; if (keys[ArrowLeft] || keys[KeyA]) state.player.x - state.player.speed; if (keys[ArrowRight] || keys[KeyD]) state.player.x state.player.speed; if (keys[ArrowUp] || keys[KeyW]) state.player.y - state.player.speed; if (keys[ArrowDown] || keys[KeyS]) state.player.y state.player.speed; };碰撞检测我推荐用矩形碰撞简单够用。因为游戏里飞机和子弹都是矩形纹理矩形碰撞的判定效率极高在几十个实体范围内完全不需要做空间分割优化。核心就是判断两个矩形的四条边是否重叠const isCollide (a, b) { return ( a.x b.x b.width a.x a.width b.x a.y b.y b.height a.y a.height b.y ); };音效这块有个典型坑现代浏览器默认阻止页面加载时的自动播放音频如果你直接在页面加载后调用audio.play()控制台会报NotAllowedError。解决方法是让音效播放逻辑绑定到用户的点击操作上或者先用AudioContext恢复一次。我在游戏里用了一个通用的做法在“开始游戏”按钮的点击回调里先把所有音效的play()执行一次并立即暂停这样用户主动交互后音频上下文就解锁了后续游戏内的射击音效才能正常触发。3.4 动态生成内容从敌机生成到分数更新AI在生成敌机生成逻辑时默认模式是每个固定间隔推一个新对象。这里要注意生成频率不能写死否则关卡感不足。我后来改成按分数动态加速分数越高敌机生成间隔越短敌机下落速度越快。这其实就是把游戏难度曲线具象化成几个简单公式比写一堆复杂关卡逻辑更容易维护。分数更新走React的useState但不要每帧都set。正确做法是在碰撞检测时判断“分数是否发生变化”发生变化再setScore(newScore)。这样React组件的渲染频率只和关键事件绑定不会和无休止的动画循环互相干扰。游戏结束后把所有实体清空、分数归零、飞机位置复位一套标准的重置逻辑AI生成时经常遗漏我在这个项目里给它补了个resetGame函数。4. 常见问题与排查实录AI生成的代码翻车现场4.1 AI代码“看起来对跑起来崩”的三种典型症状我在这一个项目里收集到三类高频问题几乎可以做成一个AI生成代码的“排查清单”症状根因排查与修复飞机移动卡顿、一顿一顿游戏数据存在state里每帧触发React重渲染改用useRef存游戏数据state只保存分数等低频信息切走页面再回来操作失灵或加速事件监听器重复绑定旧监听未清理在useEffect清理函数中移除监听检查是否有多处addEventListener运行一段时间后越来越卡子弹/敌机出界后未从数组中移除每帧过滤掉超出边界的实体限制子弹最大数量表格里这三类问题前两个我前面已经详细说过了第三个是最容易让新手摸不着头脑的。我调试的时候打开Chrome的Performance面板看到内存曲线像坐了火箭一样往上蹿马上意识到对象数组在无限膨胀。修复方法是在每帧更新逻辑里加数组过滤state.bullets state.bullets.filter( (b) b.y -20 b.y 660 ); state.enemies state.enemies.filter( (e) e.y 680 );4.2 用Prompt让AI帮你修bug要看会“复述问题”很多人在Vibe Coding过程中遇到bug只会甩一句话给AI“游戏卡了帮我修一下。”这种模糊描述即便人类工程师听了也头大。正确的做法是把问题复述出来带着可复现步骤、期望行为和报错信息。我在这个项目里写过几次“高质量bug报告”效果拔群比如目前游戏在运行3分钟后明显卡顿。我观察到的现象 1. 敌机持续生成但部分敌机飞出屏幕后仍然存在内存中。 2. 子弹发射数量未做上限导致子弹数组无限增长。 请检查实体数组中是否缺少出界清理逻辑并加上子弹数量上限比如同时最多20发。这个Prompt包含了现象、原因猜测和预期修改方案。AI看到后几乎不需要来回追问直接就能定位到问题位置。这背后其实是一个通用原则你能把问题描述得越精确AI的修复就越像外科手术而不是大炮打蚊子。4.3 帧率优化与Canvas绘制的最后一公里游戏基本跑顺之后我还做了一轮帧率优化。首屏Canvas默认尺寸是300x150如果直接用CSS拉伸到480x640画面会特别糊。这是因为Canvas实际像素分辨率没有跟着放大正确做法是用Canvas的width和height属性设置物理尺寸然后用CSS控制展示尺寸const canvas canvasRef.current; canvas.width 480; canvas.height 640; // CSS里设置 canvas { width: 480px; height: 640px; }更进阶一点的优化是适配设备像素比window.devicePixelRatio在视网膜屏上让Canvas按2倍像素渲染画面会锐利很多。但要注意启用DPR适配后后面所有鼠标/触控坐标换算也要对应乘以比例否则点击位置会偏移。我在这个项目里没有做触摸控制所以这部分先略过但如果你要接入移动端触控这一定是第一个要处理的问题。绘制时还有一个经验不要每帧清除整个画布然后重绘全部而是先用背景色填充再逐层画星空、敌机、子弹、飞机和特效。AI默认的代码往往是ctx.clearRect(0, 0, w, h)然后重绘这没问题但如果要加星空滚动背景记得把背景作为一个图层来画不然星星会一帧一帧闪得人眼花。4.4 工具与工作流Vibe Coding时代我的四条经验做完这个项目我对Vibe Coding这件事有了非常明确的感受。如果你也打算试水这几条经验应该能帮你少走弯路。第一锁需求再放开想象。AI生成能力很强但想象力也很强它会在你不注意的时候偷偷加个“试炼模式”或者“双人对战功能”。如果需求不锁定代码会越跑越偏。我每次迭代都只提2到3个明确改动改完验证完再进入下一轮。第二让AI解释自己写的代码。当我不理解AI生成的某个函数时我会直接把代码贴回去问它“解释一下这几行的作用并说明如果我把XXX改掉会发生什么。”这比自己在网上搜效率高得多。它甚至能主动指出代码里潜在的问题等于多了个代码评审工具。第三定期手动读代码。Vibe Coding的陷阱在于AI可以“自洽地写出看起来正确的错误代码”。我在一个边界检测问题上被坑了半小时AI生成的判断条件y -height和y 0写反了导致敌机一出生就被销毁。这种低级错误你指望AI自查是查不出来的必须靠肉眼读代码或者设计边界用例来测试。第四保留一份手写核心逻辑的“锚点”。飞机大战里最核心的循环、碰撞、和输入控制我是自己重新确认过的。不是说AI写不出来而是这些逻辑是整个游戏的灵魂一旦出错代价极大。把它掌握在自己手里就等于握住了项目的方向盘AI再折腾也翻不了车。最后再说一个小技巧如果你也在用React做这种小游戏建议在开发时开启React Strict Mode。它虽然会双调用useEffect看起来好像“帮倒忙”但恰恰能暴露你的事件监听清理是否规范、是否存在副作用残留。我在开发过程中就被它逮到过一次重复生成游戏循环的问题修完之后游戏运行变得异常干净。这算是意外收获也印证了工具链里每一条看似麻烦的规范背后其实都在帮你兜底。