vibe coding实战:AI辅助编程从零构建策略游戏原型 📅 发布时间:2026/8/31 2:41:07 👁 浏览次数: 刚接触“vibe coding”这个概念时我的第一反应是这不就是让人工智能帮我写代码吗真正动手做了一个暑假的独立策略游戏原型后我对它的理解发生了很大变化。vibe coding 的本质并不是“偷懒”而是把开发重心从“逐行敲代码”转移到“产品设计、系统拆解、代码审查与调试验证”上让 AI 承担可预测的编码工作把人的精力留给更需要判断力的环节。这篇文章记录了我用 vibe coding 方式从零开发一个独立策略游戏的全过程包括工具选择、工作流搭建、需求拆解、核心系统实现、AI 生成代码的审查与排错以及暑假开发期间踩过的坑。文章面向两类读者一类是刚开始接触 AI 辅助编程想用它做点真实项目的大学生另一类是有一定开发基础、想尝试把 AI 引入游戏开发的独立开发者。读完你能掌握一套相对完整的“需求设计 AI 生成 人工审查 反复迭代”的开发闭环。1. vibe coding 是什么为什么适合独立游戏1.1 从“写代码”到“描述系统”的转变vibe coding 最早由 Andrej Karpathy 提出大意是开发者用自然语言描述需求AI 生成代码开发者看完运行结果后继续提出修改要求整体处于一种“跟着感觉走”的开发节奏中。很多人把它理解为“随口说需求代码自动完成”但在实际项目中必须加一个前提你需要知道自己想要什么并且能看懂 AI 给出的代码是否合理。以策略游戏为例如果你对 AI 说“做一个文明游戏”AI 大概率会生成一个非常空泛的页面因为你没有拆解清楚“文明游戏”包含哪些系统。但如果你说“做一款 10x10 网格地图的回合制策略游戏玩家和 AI 各有 3 个单位每个单位可以移动 2 格、攻击范围为 1 格输出到 Canvas 上”AI 生成结果的可用性会大幅提升。1.2 独立策略游戏为什么适合 vibe coding策略游戏的核心是数据结构、规则逻辑和状态流转而不是复杂的物理引擎或大量美术资源。游戏循环可以抽象成“玩家行动 → 校验规则 → 更新状态 → 渲染画面 → AI 行动 → 校验胜负”边界清晰非常适合让 AI 理解并生成。同时策略游戏的规模可控。暑假一个多月时间要做 3A 大作不现实但做一个回合制战棋原型、一个简单的资源运营游戏、或者一个 roguelike 地牢探索都是完全可行的。这些项目不需要团队协作不需要美术外包代码量通常在几百到几千行之间正好是 vibe coding 发挥效率的最佳区间。1.3 开发者仍然需要掌握的核心能力vibe coding 不是“不需要会编程”而是对编程能力提出了另一种要求系统设计能力能把完整游戏拆成地图、单位、行动、AI、渲染等模块。代码审查能力AI 生成的代码可能有逻辑漏洞你需要能发现并修复。调试能力报错信息、状态异常、AI 行为不合预期这些都需要自己定位。需求描述能力把模糊想法变成 AI 能执行的具体任务。如果你完全看不懂代码错误出现时只会不断把报错信息复制给 AI很容易陷入“AI 改一版坏一版”的循环。我在项目后期深有体会当代码量上到一定程度后没有人工审查AI 会不断引入新的回归问题。2. 项目规划打磨游戏设计文档2.1 确定游戏类型和核心玩法暑假开始时我给自己定的目标是“用 vibe coding 做一款能玩 10 分钟的回合制策略游戏”。我选择的是网格战棋 单位控制的玩法因为这一类游戏结构清晰AI 也容易理解。项目名称暂定为《星域争锋》核心玩法如下玩家和 AI 各自控制 3 个作战单位分布在 10x10 的网格地图上。每个单位有生命值、攻击力、移动范围、攻击范围。玩家回合可以选中单位查看可移动范围和可攻击目标。执行移动或攻击后结束回合AI 自动行动。任意一方全部单位被消灭游戏结束。游戏流程从“构思”开始我用 Markdown 整理了一份简版设计文档先写清楚系统边界再开始写代码。2.2 技术选型为什么选用 HTML5 Canvas我选择 HTML5 Canvas 原生 JavaScript 作为技术栈原因非常实际浏览器直接运行不需要安装额外的游戏引擎。Canvas 绘制的网格、单位、选中状态便于 AI 理解。原生 JS 减少框架依赖AI 生成的代码更可控。游戏状态可以直接存储在 JS 对象中方便调试和扩展。如果你后续想做更复杂的策略游戏可以迁移到 Phaser 等 2D 游戏框架但作为第一款 AI 辅助开发的原型原生 Canvas 足够用。2.3 建立 MVP 功能清单在开始写代码前我明确了 MVP最小可行产品需要包含哪些功能防止功能蔓延必须功能 1. 渲染 10x10 网格地图 2. 渲染双方单位区分颜色 3. 单位数据模型id、阵营、位置、生命值、攻击力、移动范围、攻击范围 4. 玩家点击选中单位显示可移动格子 5. 玩家点击可移动格子单位移动 6. 玩家点击可攻击敌方单位发动攻击 7. 结束回合按钮AI 自动行动 8. 胜负判断 可选功能后期再做 1. 地形遮挡与掩体 2. 不同单位类型近战、远程 3. 攻击动画 4. 音效和背景音乐把功能清单给到 AI 后生成的代码方向就非常明确了。这也是 vibe coding 最重要的一步不是让 AI 替你决定做什么而是你决定做什么AI 负责怎么做。3. 开发环境准备与 AI 工作流搭建3.1 本地开发环境我用的开发环境非常轻量操作系统Windows 11编辑器VS Code语言JavaScriptES6运行环境Chrome 浏览器本地服务器VS Code Live Server 插件对于纯 Canvas 项目其实不装 Live Server 也能直接打开 HTML 文件运行但建议加上 Live Server因为后续调试时修改代码自动刷新会方便很多。项目结构如下star-conflict/ ├── index.html ├── game.js └── style.css3.2 AI 工具选择与工作流这个暑假我主要使用了两类 AI 工具对话式 AI 编程助手直接通过自然语言对话生成代码片段、排查报错。AI 构建平台可以输入一个完整的项目描述AI 自动生成整个项目结构但它生成的代码骨架更通用通常还需要手动调整。实际使用下来我的工作流是第一步在 Markdown 中撰写需求描述尽量具体 第二步让 AI 根据需求生成完整代码文件 第三步在本地运行观察错误和效果 第四步发现问题带着错误信息和上下文让 AI 修改 第五步人工审查关键逻辑确认没有隐藏问题 第六步重复第三步到第五步直到功能稳定这个循环看起来很朴素但效率提升非常明显。以前我写一个带 AI 敌人的战棋原型可能要两周现在两天就能跑通基础版本。3.3 写好提示词的核心原则我用了一个暑假总结出给 AI 下达编码任务时效果最好的提示词结构角色你是一名精通 JavaScript 和 HTML5 Canvas 的游戏开发者。 任务实现一个 10x10 网格的回合制战棋游戏。 功能要求 1. 网格大小为 10x10每个格子渲染为 50x50 像素。 2. 玩家单位用蓝色圆表示AI 单位用红色圆表示。 3. 玩家点击蓝色单位时显示可移动范围黄色高亮。 4. 玩家点击高亮格子完成移动。 5. 玩家单位附近有敌方单位时显示可攻击目标。 6. 点击“结束回合”按钮后AI 自动移动并攻击。 输出要求生成 index.html、style.css、game.js 三个文件的完整代码。关键点在于“功能要求”部分要写成可验证的行为描述而不是抽象概念。例如“显示可移动范围”比“做一个移动系统”更容易让 AI 生成正确结果。4. 核心开发实战从空项目到可玩游戏4.1 创建项目基础结构首先创建index.html它负责承载游戏画布和操作按钮。!-- 文件路径star-conflict/index.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title星域争锋 - 回合制策略游戏/title link relstylesheet hrefstyle.css /head body div idgame-container h1星域争锋/h1 div idgame-status当前回合玩家/div canvas idgameCanvas width500 height500/canvas div idcontrols button idendTurnBtn结束回合/button button idrestartBtn重新开始/button /div div idbattle-log/div /div script srcgame.js/script /body /html接着是style.css主要做居中布局和按钮样式。/* 文件路径star-conflict/style.css */ body { margin: 0; padding: 20px; background: #1a1a2e; display: flex; justify-content: center; align-items: center; min-height: 100vh; font-family: Microsoft YaHei, sans-serif; color: #eee; } #game-container { background: #16213e; padding: 24px; border-radius: 12px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); text-align: center; } h1 { margin: 0 0 10px 0; font-size: 24px; letter-spacing: 4px; } #game-status { margin-bottom: 12px; font-size: 16px; color: #e94560; } canvas { border: 2px solid #0f3460; background: #0f3460; cursor: pointer; display: block; margin: 0 auto 12px auto; border-radius: 4px; } #controls { margin-bottom: 12px; } button { background: #e94560; color: #fff; border: none; padding: 10px 20px; margin: 0 6px; border-radius: 6px; font-size: 14px; cursor: pointer; transition: background 0.2s; } button:hover { background: #c73652; } #battle-log { min-height: 40px; max-height: 80px; overflow-y: auto; background: #0f3460; padding: 8px; border-radius: 6px; text-align: left; font-size: 13px; }4.2 编写游戏核心数据模型游戏的核心数据模型是整个项目的地基。我要求 AI 把所有状态集中到GameState对象中方便调试。// 文件路径star-conflict/game.js // 游戏配置常量 const CONFIG { GRID_SIZE: 10, CELL_SIZE: 50, CANVAS_WIDTH: 500, CANVAS_HEIGHT: 500, PLAYER_COLOR: #3498db, AI_COLOR: #e74c3c, MOVE_COLOR: rgba(241, 196, 15, 0.5), ATTACK_COLOR: rgba(231, 76, 60, 0.5), SELECTED_COLOR: #2ecc71 }; // 单位工厂函数 function createUnit(id, team, row, col) { return { id: id, team: team, // player 或 ai row: row, col: col, hp: 100, maxHp: 100, attack: 30, moveRange: 2, attackRange: 1, hasMoved: false, hasAttacked: false }; } // 游戏全局状态 let gameState { currentTurn: player, units: [], selectedUnitId: null, moveTargets: [], attackTargets: [], gameOver: false, turnNumber: 1 }; // 初始化游戏 function initGame() { gameState.units [ createUnit(1, player, 8, 1), createUnit(2, player, 8, 4), createUnit(3, player, 8, 8), createUnit(4, ai, 1, 1), createUnit(5, ai, 1, 4), createUnit(6, ai, 1, 8) ]; gameState.currentTurn player; gameState.selectedUnitId null; gameState.moveTargets []; gameState.attackTargets []; gameState.gameOver false; gameState.turnNumber 1; updateStatus(); clearLog(); addLog(游戏开始你有 3 个作战单位。); renderGame(); }这里重点解释单位数据模型中的几个字段moveRange单位每回合可以移动的格数我暂时统一设为 2。attackRange攻击距离这里设为 1也就是只能攻击相邻格子。hasMoved和hasAttacked用来记录单位本回合是否已经移动或攻击防止重复行动。这个模型可以很方便地扩展如果要加入“远程单位”只需要把attackRange改成 3 或 4AI 在寻路时也会自动适配。4.3 实现网格寻路与行动范围计算策略游戏最核心的算法之一就是可选范围计算。当一个单位被选中时我们需要找出它在moveRange内能到达的所有格子。由于当前版本没有障碍物移动范围的计算等同于计算曼哈顿距离。// 判断坐标是否在地图内 function isInBounds(row, col) { return row 0 row CONFIG.GRID_SIZE col 0 col CONFIG.GRID_SIZE; } // 计算两个格子的曼哈顿距离 function manhattanDistance(r1, c1, r2, c2) { return Math.abs(r1 - r2) Math.abs(c1 - c2); } // 获取某个单位可以移动到的格子列表 function getMoveTargets(unit) { const targets []; for (let row 0; row CONFIG.GRID_SIZE; row) { for (let col 0; col CONFIG.GRID_SIZE; col) { // 排除自己所在的格子 if (row unit.row col unit.col) continue; // 排除其他单位所在的格子 const occupied gameState.units.some(u u.row row u.col col); if (occupied) continue; // 判断是否在移动范围内 if (manhattanDistance(unit.row, unit.col, row, col) unit.moveRange) { targets.push({ row, col }); } } } return targets; } // 获取单位可以攻击的目标列表 function getAttackTargets(unit) { let enemyTeam unit.team player ? ai : player; return gameState.units.filter(u { if (u.team ! enemyTeam) return false; if (u.hp 0) return false; const distance manhattanDistance(unit.row, unit.col, u.row, u.col); return distance unit.attackRange; }); }这一段逻辑虽然简单但它决定了游戏的基础规则。在没有障碍物的版本里曼哈顿距离足够但后续如果要添加“堵路”的障碍物就需要替换成广度优先搜索BFS算法。这也是我在迭代中要求 AI 完成的第一个升级。4.4 实现 Canvas 渲染接下来是渲染部分。Canvas 渲染的逻辑很直观先画网格再画高亮区域最后画单位。// 获取 canvas 上下文 const canvas document.getElementById(gameCanvas); const ctx canvas.getContext(2d); // 绘制游戏画面 function renderGame() { ctx.clearRect(0, 0, CONFIG.CANVAS_WIDTH, CONFIG.CANVAS_HEIGHT); drawGrid(); drawMoveTargets(); drawAttackTargets(); drawUnits(); } // 绘制网格 function drawGrid() { ctx.strokeStyle #1a4968; ctx.lineWidth 1; for (let row 0; row CONFIG.GRID_SIZE; row) { ctx.beginPath(); ctx.moveTo(0, row * CONFIG.CELL_SIZE); ctx.lineTo(CONFIG.CANVAS_WIDTH, row * CONFIG.CELL_SIZE); ctx.stroke(); } for (let col 0; col CONFIG.GRID_SIZE; col) { ctx.beginPath(); ctx.moveTo(col * CONFIG.CELL_SIZE, 0); ctx.lineTo(col * CONFIG.CELL_SIZE, CONFIG.CANVAS_HEIGHT); ctx.stroke(); } } // 绘制可移动范围高亮 function drawMoveTargets() { gameState.moveTargets.forEach(target { ctx.fillStyle CONFIG.MOVE_COLOR; ctx.fillRect( target.col * CONFIG.CELL_SIZE, target.row * CONFIG.CELL_SIZE, CONFIG.CELL_SIZE, CONFIG.CELL_SIZE ); }); } // 绘制可攻击目标高亮 function drawAttackTargets() { gameState.attackTargets.forEach(unit { ctx.fillStyle CONFIG.ATTACK_COLOR; ctx.fillRect( unit.col * CONFIG.CELL_SIZE, unit.row * CONFIG.CELL_SIZE, CONFIG.CELL_SIZE, CONFIG.CELL_SIZE ); }); } // 绘制所有单位 function drawUnits() { gameState.units.forEach(unit { // 跳过已阵亡单位 if (unit.hp 0) return; const centerX unit.col * CONFIG.CELL_SIZE CONFIG.CELL_SIZE / 2; const centerY unit.row * CONFIG.CELL_SIZE CONFIG.CELL_SIZE / 2; const radius CONFIG.CELL_SIZE * 0.35; // 绘制单位外圈 if (gameState.selectedUnitId unit.id) { ctx.fillStyle CONFIG.SELECTED_COLOR; ctx.beginPath(); ctx.arc(centerX, centerY, radius 5, 0, Math.PI * 2); ctx.fill(); } // 绘制单位主体 ctx.fillStyle unit.team player ? CONFIG.PLAYER_COLOR : CONFIG.AI_COLOR; ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.fill(); // 绘制单位边框 ctx.strokeStyle #fff; ctx.lineWidth 2; ctx.stroke(); // 绘制血条背景 ctx.fillStyle #333; ctx.fillRect(unit.col * CONFIG.CELL_SIZE 8, unit.row * CONFIG.CELL_SIZE 6, 34, 6); // 绘制血条 let hpPercent unit.hp / unit.maxHp; let hpColor hpPercent 0.5 ? #2ecc71 : (hpPercent 0.25 ? #f39c12 : #e74c3c); ctx.fillStyle hpColor; ctx.fillRect(unit.col * CONFIG.CELL_SIZE 8, unit.row * CONFIG.CELL_SIZE 6, 34 * hpPercent, 6); // 绘制单位 ID ctx.fillStyle #fff; ctx.font 12px sans-serif; ctx.textAlign center; ctx.textBaseline middle; ctx.fillText(unit.id, centerX, centerY); }); }这段渲染代码的可维护性不错但还不支持动画效果。如果要加攻击动画需要在renderGame之外维护一个动画队列这部分我放到了后续迭代中。4.5 实现鼠标交互与回合管理交互逻辑是整个游戏最重要也最容易出错的部分。我需要处理三种点击场景点击我方单位选中、点击移动格子移动、点击敌方单位攻击。// 将 canvas 坐标转换为网格坐标 function canvasToGrid(e) { const rect canvas.getBoundingClientRect(); const scaleX canvas.width / rect.width; const scaleY canvas.height / rect.height; const x (e.clientX - rect.left) * scaleX; const y (e.clientY - rect.top) * scaleY; const col Math.floor(x / CONFIG.CELL_SIZE); const row Math.floor(y / CONFIG.CELL_SIZE); return { row, col }; } // 点击事件处理 canvas.addEventListener(click, (e) { if (gameState.gameOver) return; if (gameState.currentTurn ! player) return; const { row, col } canvasToGrid(e); // 情况1点击我方单位选中它 const clickedUnit gameState.units.find(u u.row row u.col col u.team player u.hp 0); if (clickedUnit) { gameState.selectedUnitId clickedUnit.id; gameState.moveTargets getMoveTargets(clickedUnit); gameState.attackTargets getAttackTargets(clickedUnit); renderGame(); return; } // 情况2没有选中单位忽略点击 if (gameState.selectedUnitId null) return; const selectedUnit gameState.units.find(u u.id gameState.selectedUnitId); // 情况3点击可攻击目标执行攻击 const attackTarget gameState.attackTargets.find(u u.row row u.col col); if (attackTarget) { performAttack(selectedUnit, attackTarget); return; } // 情况4点击可移动格子执行移动 const moveTarget gameState.moveTargets.find(target target.row row target.col col); if (moveTarget) { performMove(selectedUnit, moveTarget); return; } // 情况5点击空白区域取消选中 gameState.selectedUnitId null; gameState.moveTargets []; gameState.attackTargets []; renderGame(); });4.6 实现移动、攻击与 AI 回合移动和攻击逻辑相对直接但有几个细节值得注意单位攻击后不能再移动吗移动后还能攻击吗在传统战棋里通常移动后可以攻击但攻击后不能再移动。我的实现是单位只要本回合没有移动就可以移动移动后如果没有攻击仍然可以攻击。// 执行移动 function performMove(unit, target) { unit.row target.row; unit.col target.col; unit.hasMoved true; addLog(单位 ${unit.id} 移动到 (${target.row}, ${target.col})。); // 移动后攻击范围发生变化重新计算 gameState.moveTargets []; gameState.attackTargets getAttackTargets(unit); // 如果移动后周围没有攻击目标取消选中状态 if (gameState.attackTargets.length 0) { gameState.selectedUnitId null; } renderGame(); } // 执行攻击 function performAttack(attacker, target) { target.hp - attacker.attack; attacker.hasAttacked true; addLog(单位 ${attacker.id} 攻击单位 ${target.id}造成 ${attacker.attack} 点伤害。); if (target.hp 0) { target.hp 0; addLog(单位 ${target.id} 被消灭了); } // 攻击后取消选中 gameState.selectedUnitId null; gameState.moveTargets []; gameState.attackTargets []; // 检查是否分出胜负 checkGameOver(); renderGame(); } // 检查游戏是否结束 function checkGameOver() { const playerUnits gameState.units.filter(u u.team player u.hp 0); const aiUnits gameState.units.filter(u u.team ai u.hp 0); if (playerUnits.length 0) { gameState.gameOver true; updateStatus(你输了…… AI 取得了胜利。); addLog(游戏结束AI 胜利。); } else if (aiUnits.length 0) { gameState.gameOver true; updateStatus(你赢了所有敌方单位已被消灭。); addLog(游戏结束玩家胜利。); } }AI 回合的实现我用了最简单的策略AI 单位逐个执行“寻找最近敌人 → 靠近敌人 → 攻击”的动作。这里的关键在于AI 不能因为移动路径上的格子被占就死循环所以我限制了最大寻路步数。// 获取距离坐标最近的敌方单位 function getNearestEnemy(unit) { const enemyTeam unit.team player ? ai : player; let nearest null; let minDistance Infinity; gameState.units.forEach(enemy { if (enemy.team ! enemyTeam || enemy.hp 0) return; const distance manhattanDistance(unit.row, unit.col, enemy.row, enemy.col); if (distance minDistance) { minDistance distance; nearest enemy; } }); return nearest; } // AI 回合执行 function aiTurn() { updateStatus(AI 回合...); const aiUnits gameState.units.filter(u u.team ai u.hp 0); let turnDelay 0; aiUnits.forEach((unit, index) { // 每个 AI 单位行动间隔 500ms方便玩家观察 setTimeout(() { if (gameState.gameOver) return; const enemy getNearestEnemy(unit); if (!enemy) return; const distance manhattanDistance(unit.row, unit.col, enemy.row, enemy.col); // 如果在攻击范围内直接攻击 if (distance unit.attackRange) { performAttack(unit, enemy); return; } // 否则向敌人移动一格 const move getAiMoveStep(unit, enemy); if (move) { performMoveSimple(unit, move.row, move.col); } }, turnDelay index * 500); }); // 所有 AI 行动结束后切换回玩家回合 setTimeout(() { if (gameState.gameOver) return; startPlayerTurn(); }, turnDelay aiUnits.length * 500 300); } // 获取 AI 移动一步的坐标向敌人靠近 function getAiMoveStep(unit, enemy) { let bestMove null; let bestDistance manhattanDistance(unit.row, unit.col, enemy.row, enemy.col); const directions [ { row: unit.row - 1, col: unit.col }, { row: unit.row 1, col: unit.col }, { row: unit.row, col: unit.col - 1 }, { row: unit.row, col: unit.col 1 } ]; directions.forEach(pos { if (!isInBounds(pos.row, pos.col)) return; const occupied gameState.units.some(u u.row pos.row u.col pos.col); if (occupied) return; const distance manhattanDistance(pos.row, pos.col, enemy.row, enemy.col); if (distance bestDistance) { bestDistance distance; bestMove pos; } }); return bestMove; } // AI 单位简单移动不触发玩家交互逻辑 function performMoveSimple(unit, row, col) { unit.row row; unit.col col; addLog(AI 单位 ${unit.id} 移动至 (${row}, ${col})。); renderGame(); } // 开始玩家回合 function startPlayerTurn() { gameState.currentTurn player; gameState.turnNumber; gameState.selectedUnitId null; gameState.moveTargets []; gameState.attackTargets []; // 重置所有单位行动状态 gameState.units.forEach(unit { unit.hasMoved false; unit.hasAttacked false; }); updateStatus(当前回合玩家第 ${gameState.turnNumber} 回合); addLog(第 ${gameState.turnNumber} 回合开始轮到玩家行动。); renderGame(); }最后是按钮事件绑定、状态更新函数和初始化入口// 初始化事件绑定 function setupEvents() { document.getElementById(endTurnBtn).addEventListener(click, () { if (gameState.currentTurn ! player) return; if (gameState.gameOver) return; // 清空选中状态 gameState.selectedUnitId null; gameState.moveTargets []; gameState.attackTargets []; // 切换到 AI 回合 gameState.currentTurn ai; updateStatus(AI 回合...); renderGame(); aiTurn(); }); document.getElementById(restartBtn).addEventListener(click, () { initGame(); }); } // 更新顶部状态文字 function updateStatus(text) { document.getElementById(game-status).textContent text; } // 添加战斗日志 function addLog(text) { const log document.getElementById(battle-log); const line document.createElement(div); line.textContent [第${gameState.turnNumber}回合] ${text}; log.appendChild(line); log.scrollTop log.scrollHeight; } // 清空战斗日志 function clearLog() { document.getElementById(battle-log).innerHTML ; } // 启动游戏 setupEvents(); initGame();到这里一个可玩的回合制战棋游戏原型就完成了。用 VS Code 打开index.html在浏览器中运行就能看到 10x10 的网格地图、双方各 3 个单位点击蓝色单位可以看到黄色移动范围移动后如果附近有红色单位红色单位会显示攻击高亮点击即可发动攻击。5. 实战常见问题与排查思路一个暑假的开发过程中AI 生成的代码并不是一蹴而就的。我整理了最常见的几类问题以及排查思路这些经验比代码本身更有价值。问题现象常见原因解决思路点击单位没有反应事件绑定重复或 canvas 坐标换算错误检查 canvas.addEventListener 是否只绑定一次检查 canvasToGrid 是否考虑 CSS 缩放单位移动后仍然能移动hasMoved 状态没有正确更新在 performMove 中检查并设置 hasMoved true并在 startPlayerTurn 中重置AI 回合没有执行setTimeout 回调中 gameOver 状态判断过早检查 AI 行动多个 setTimeout 之间的时序确保玩家不会在 AI 行动期间操作单位重叠在同一个格子移动目标列表没有排除单位占用格子在 getMoveTargets 中过滤已占用格子AI 攻击自己人getNearestEnemy 筛选条件写错检查单位筛选时 enemyTeam 是否正确排除己方重新开始后状态没有重置初始化函数没有重置所有字段确保 initGame 中重置所有全局变量包括回合数、选中单位、行动状态画面显示模糊canvas 尺寸与 CSS 尺寸不一致使用 getBoundingClientRect 做坐标换算或将 canvas 尺寸设置为固定值5.1 AI 生成代码的“幻觉”问题vibe coding 最折磨人的是 AI 会像人类一样“一本正经地犯错”。有一次我要求 AI 添加地形系统它给每个格子随机生成了地形但在计算移动范围时直接通过terrain[row][col]读取数据却没有先初始化地形数组导致点击单位时直接抛错。排查这种 AI 幻觉问题的思路是先看报错信息定位到具体行。检查该行访问的数据结构是否被正确初始化。观察 AI 的逻辑是否与真实需求一致而不是它自己理解的规则。如果错误无法快速定位直接让 AI 打印关键状态到控制台。5.2 代码越来越难以维护怎么办随着功能增加game.js 文件会不断膨胀。我第一版代码只有 300 多行后来加入了多种单位类型和地形后直接涨到了 1500 行。这时 AI 修改代码经常会破坏其他功能。我的解决方案是“模块化重构”。把代码拆成几个文件star-conflict/ ├── index.html ├── game.js # 入口和初始化 ├── units.js # 单位数据操作 ├── ai.js # AI 行动逻辑 ├── render.js # Canvas 渲染 └── style.css模块化之后每次让 AI 修改 AI 逻辑时我会明确告诉它“只修改 ai.js 文件”其他文件不要动。这样大幅减少了回归问题。6. 从原型到独立游戏工程化最佳实践6.1 使用 Git 进行版本管理不管是不是 vibe coding但凡写超过 500 行的代码都应该用 Git。AI 改代码的速度太快了如果每次修改都不留痕迹你会经常面临“这个功能以前是好的怎么现在坏了”的困境。我的习惯是每完成一个小功能就提交一次git add . git commit -m feat: 实现敌方 AI 基础行动逻辑如果 AI 修改后出现严重问题直接git revert或者git stash回到上一个可用版本避免在错误基础上继续迭代。6.2 建立提示词仓库暑假开发到中期我发现很多提示词是可以复用的。我建了一个prompts/目录专门存放常用提示词模板。prompts/ ├── 生成项目骨架.md ├── 添加地形系统.md ├── 实现单位升级.md ├── 修复点击事件.md └── AI生成单元测试.md每次遇到一个完成度较高的提示词我就保存下来。到了一周之后新功能的生成效率明显提高因为可以直接套用之前验证过的提示词结构而不是每次重新描述需求。6.3 代码审查的关键检查点AI 生成的代码不能直接信任我每次提交前会重点检查几个方面边界条件数组越界、空值判断、负值处理。状态一致性单位移动后攻击范围是否重新计算。事件安全重复绑定事件是否会造成重复操作。性能问题循环嵌套是否过深是否有不必要的重复计算。可读性变量命名是否清晰是否会误导后续 AI 修改。6.4 用 AI 辅助测试对于策略游戏这种规则复杂的项目人工测试很难覆盖所有情况。我会让 AI 生成单元测试代码用 Node.js 跑纯逻辑测试。例如为getMoveTargets写一个测试// 文件路径star-conflict/test/movement.test.js // 以下为示例思路需根据你的模块导出方式调整 const { getMoveTargets } require(../game); function testMoveTargets() { const unit createUnit(1, player, 5, 5); const targets getMoveTargets(unit); // 移动到 (5,5) 的曼哈顿距离为 2 以内且不包含自己 const containsSelf targets.some(t t.row 5 t.col 5); const allInRange targets.every(t Math.abs(t.row - 5) Math.abs(t.col - 5) 2 ); const allInBounds targets.every(t t.row 0 t.row 10 t.col 0 t.col 10 ); console.assert(!containsSelf, 移动目标不应包含自身); console.assert(allInRange, 所有移动目标应在范围内); console.assert(allInBounds, 所有移动目标应在地图内); console.log(移动范围测试通过); } testMoveTargets();AI 辅助生成测试用例的好处是它能快速想到成千上万种边界情况。但要注意测试断言的结果必须由人来判断AI 不能替你做质量把关。6.5 安全与合规提醒暑假做项目时要注意一个容易被忽略的问题如果你打算把项目开源或发布使用的 AI 工具生成代码时要留意代码许可证问题。目前主流的代码生成工具通常不会直接复制 GitHub 上的受保护代码但仍然建议发布前进行人工审查。涉及用户数据的游戏不要在后端硬编码密钥即使只是本地原型也要养成“密钥不入代码库”的习惯。7. 如何继续迭代从原型到完整游戏第一款原型跑通后我的暑假项目并没有结束。后续迭代方向和建议如下增加地形系统在地图上添加障碍物、森林、山丘让移动和攻击规则更复杂。这一步需要把移动范围计算从曼哈顿距离改成 BFS 搜索。多种单位类型近战单位高血量高攻击、远程单位低血量远程攻击、移动型单位移动范围大但攻击弱让阵容搭配产生策略深度。战斗动画用 Canvas 的 requestAnimationFrame 实现移动、攻击的动画过渡需要把状态更新和渲染拆开。存档功能使用浏览器 localStorage 保存游戏状态让玩家可以继续上次未完成的战役。关卡设计设计多个 PvE 关卡每关有不同的地图、敌人配置和胜利条件。这些迭代中建议继续遵循同一个循环写需求文档 → 让 AI 生成核心模块 → 人工审查 → 测试 → 提交 Git。一个可以复用的“新增单位类型”提示词示例在当前游戏项目中增加远程单位类型 1. 远程单位攻击范围为 3移动范围为 2生命值为 70攻击力为 20。 2. 在 createUnit 工厂函数中增加 unitType 字段取值为 melee 或 ranged。 3. 渲染时远程单位用三角形表示近战单位继续用圆形表示。 4. 远程单位在移动后可以攻击攻击后不能再移动。 5. 只在 game.js 中修改其他文件不动。 6. 修改之前先打印当前 createUnit 函数的完整代码确认理解后再改。明确告诉 AI“打印现有代码再修改”可以显著降低它“凭记忆重写”带来的回归问题。如果你也想在暑假用 vibe coding 做一个独立策略游戏我的核心建议只有一条不要把任务交由 AI 全权接管而是让自己做架构师让 AI 做施工队。你需要理解每一段核心代码的作用掌握基础的调试手段并且在关键时刻敢于对 AI 说“不对重写这块”。这个暑假我最大的收获不是游戏成品而是掌握了与 AI 协同开发、快速迭代真实项目的方法这套方法在未来的学习与工作中都会长时间适用。