紅警風(fēng)格 RTS 游戲:AI 輔助編程的硬核實(shí)踐)
最近我干了一件特別“離譜”的事用 GPT 6.1 從頭寫了一個(gè)可以玩的《紅色警戒》風(fēng)格即時(shí)戰(zhàn)略游戲而且已經(jīng)把完整源碼開源出來了。先別急著吐槽說真的這個(gè)項(xiàng)目做完之后我自己都挺意外的——不是意外 AI 能寫代碼而是意外它居然能把 RTS 這種硬骨頭啃下來從地圖尋路到戰(zhàn)斗數(shù)值再到 UI 交互最終竟然跑出了一個(gè)能對(duì)戰(zhàn)能推家的可玩版本。我先把話說清楚這不是拿現(xiàn)成引擎改個(gè)皮也不是調(diào)用某個(gè)游戲框架套個(gè)殼。整個(gè)游戲的邏輯層、渲染層、AI 行為樹、尋路系統(tǒng)甚至資源和經(jīng)濟(jì)循環(huán)全部是基于 GPT 6.1 生成代碼并手工整合而成的。項(xiàng)目本身是 Web 實(shí)現(xiàn)瀏覽器打開就能玩跨平臺(tái)、免安裝對(duì)局邏輯全部跑在本地代碼已經(jīng)打包好放到開源平臺(tái)了。這篇文章我會(huì)把整個(gè)從構(gòu)思到落地再到開源的經(jīng)過完整拆一遍包括技術(shù)選型是怎么想的、Prompt 是怎么寫的、哪些環(huán)節(jié)最容易翻車、以及最后是怎么把代碼質(zhì)量打磨到可以見人的程度的。無論你是對(duì) AI 輔助開發(fā)感興趣還是單純好奇 RTS 游戲底層是怎么運(yùn)作的這篇都能給你一點(diǎn)實(shí)打?qū)嵉臇|西。1. 項(xiàng)目整體設(shè)計(jì)與思路拆解1.1 為什么偏偏選了 RTS 這種硬骨頭很多人一聽說“用 AI 寫游戲”第一反應(yīng)是做個(gè)貪吃蛇或者彈球小游戲。說實(shí)話那種項(xiàng)目我也試過讓 GPT 寫一個(gè)貪吃蛇確實(shí)幾分鐘就能跑起來但做完之后毫無成就感本質(zhì)上就是在復(fù)制一個(gè)已經(jīng)被寫過無數(shù)次的東西。做“紅警”的念頭來自于一次很偶然的閑聊朋友說現(xiàn)在的 AI 寫代碼越來越厲害但充其量就是寫寫工具函數(shù)和頁面組件稍微帶點(diǎn)狀態(tài)機(jī)的游戲邏輯就會(huì)開始胡編。這句話我記下了但心里不服氣——RTS 游戲在編程領(lǐng)域被稱為“交互復(fù)雜度之王”背后涉及網(wǎng)格地圖、單位尋路、碰撞檢測(cè)、戰(zhàn)斗結(jié)算、資源管理、AI 策略等多個(gè)核心系統(tǒng)聯(lián)動(dòng)如果你能把這個(gè)東西用 AI 輔助從零做出來那才真正說明 AI 輔助開發(fā)已經(jīng)跨過了玩具級(jí)門檻。選定《紅色警戒》風(fēng)格還有一個(gè)現(xiàn)實(shí)原因類型足夠經(jīng)典。幾乎所有玩家對(duì)紅警的底層玩法都有直覺認(rèn)知采礦、造兵、推家、開圖。這種強(qiáng)烈的認(rèn)知模板幫我省了大量需求描述的時(shí)間——我不用跟 GPT 解釋“什么是即時(shí)戰(zhàn)略游戲”“為什么要集結(jié)點(diǎn)”它訓(xùn)練語料里本身就包含了海量 RTS 相關(guān)的代碼和設(shè)計(jì)模式相當(dāng)于站在一個(gè)非常成熟的語義基座上工作。1.2 技術(shù)方案選型用什么殼子裝這顆心技術(shù)棧的選型直接影響整個(gè)項(xiàng)目走向我大概對(duì)比過三條路線最后選了一條最省心也最適合 AI 輔助的。第一條路線是 Unity C#。優(yōu)勢(shì)是引擎成熟、網(wǎng)上資料多、紅警同人項(xiàng)目一抓一大把但問題也很明顯Unity 的工程結(jié)構(gòu)極其復(fù)雜腳本生命周期、Prefab 系統(tǒng)、資源管線這些概念即使讓 GPT 來寫也免不了大量手工拖拽和裝配工作AI 生成的代碼大概率沒法直接融合進(jìn)龐大的引擎架構(gòu)里。OpenRA紅警的開源重制版源碼倒是現(xiàn)成的但那叫“二次開發(fā)”不叫“從零寫一個(gè)”違背了我定義這個(gè)項(xiàng)目時(shí)的最初動(dòng)機(jī)。第二條路線是 Python Pygame。語法簡(jiǎn)單、GPT 生成代碼的正確率高但 Pygame 做 RTS 有三個(gè)致命傷性能捉急單位一多就卡成幻燈片、跨平臺(tái)分發(fā)困難要裝解釋器和依賴、Web 化幾乎沒有可能。做游戲的人都知道RTS 玩家最看重的就是“千軍萬馬”的流暢感性能天花板太低這個(gè)項(xiàng)目注定走不遠(yuǎn)。第三條路線就是我最終選擇的純前端 Web 實(shí)現(xiàn)核心游戲邏輯用 JavaScript 編寫Canvas 2D 負(fù)責(zé)渲染沒有任何運(yùn)行時(shí)依賴。為什么這條路最對(duì)原因有三。第一瀏覽器的 Canvas 2D 性能其實(shí)比很多人想象的好得多用簡(jiǎn)單臟矩形和緩沖優(yōu)化跑幾百個(gè)單位完全沒有壓力而 RTS 的地圖操作天然適合 Canvas 做網(wǎng)格化渲染。第二Web 項(xiàng)目不存在平臺(tái)分發(fā)問題寫完了往靜態(tài)托管平臺(tái)一扔鏈接發(fā)過去任何人打開就能玩開源之后別人 Clone 下來也不需要配置任何環(huán)境。第三也是最重要的JavaScript 生態(tài)里沒有強(qiáng)制的“工程化范式”一個(gè) HTML 里既能寫邏輯也能調(diào)渲染GPT 對(duì)單文件代碼的完整生成能力是最強(qiáng)的這大大降低了 AI 產(chǎn)出代碼與項(xiàng)目結(jié)構(gòu)之間的摩擦系數(shù)。1.3 我把項(xiàng)目切成了幾個(gè)可以喂給 AI 的模塊在實(shí)際動(dòng)工之前我先做了一件很關(guān)鍵的事把整個(gè)紅警項(xiàng)目拆成一張模塊清單。用人話說就是我不可能讓 GPT 一次性生成一個(gè) 5000 行的巨大文件也不應(yīng)該在整體架構(gòu)還沒定型之前就讓 AI 往一個(gè)方向亂寫。拆模塊這件事的作用是雙重的對(duì)我自己來說它讓我清楚了完成這個(gè)項(xiàng)目需要哪些組成部分對(duì) AI 來說每一塊都是相對(duì)獨(dú)立、職責(zé)明確的子任務(wù)Prompt 可以寫得非常聚焦。最終我拆出了下面這幾個(gè)核心子系統(tǒng)地圖系統(tǒng)負(fù)責(zé)生成和管理戰(zhàn)場(chǎng)網(wǎng)格包括地形類型平原、山脈、水域和寬度高度參數(shù)單位系統(tǒng)處理所有單位的屬性、行為、生命周期和移動(dòng)邏輯從步兵到坦克都掛在這棵樹下尋路系統(tǒng)解決“從 A 點(diǎn)到 B 點(diǎn)怎么走”的問題要求能繞開障礙物并支持大量單位同時(shí)尋路戰(zhàn)斗系統(tǒng)是傷害計(jì)算和攻擊行為的總調(diào)度包含射程判定、攻擊間隔和開火特效AI 系統(tǒng)負(fù)責(zé)電腦玩家的決策邏輯包括擴(kuò)張、造兵和作戰(zhàn)資源與經(jīng)濟(jì)系統(tǒng)管礦場(chǎng)、礦石、建造消耗這些數(shù)值關(guān)聯(lián)的東西UI 交互系統(tǒng)接住鼠標(biāo)點(diǎn)擊、框選、命令下發(fā)的最小可用界面。這個(gè)模塊劃分思路在這個(gè)項(xiàng)目里被反復(fù)驗(yàn)證了——模塊邊界越清晰Prompt 里讓 AI 生成的東西就越不會(huì)跑偏。后面你會(huì)發(fā)現(xiàn)我把這些模塊用“垂直切片”的方式一個(gè)個(gè)交給 GPT 去生成再一步步縫合起來每一步都能驗(yàn)證結(jié)果是否可用。2. 核心系統(tǒng)實(shí)現(xiàn)與代碼拆解2.1 地圖載具網(wǎng)格系統(tǒng)與資源繪制先說地圖模塊。RTS 的地圖和棋盤的底層邏輯幾乎一模一樣本質(zhì)就是一個(gè)二維網(wǎng)格數(shù)組每個(gè)格子記錄自己的地形類型和狀態(tài)。為了方便后續(xù)尋路和 AI 模塊復(fù)用地圖的數(shù)據(jù)結(jié)構(gòu)和渲染分離是必須的——數(shù)據(jù)層就是一個(gè)純數(shù)組渲染層才負(fù)責(zé)在 Canvas 上畫顏色。GPT 生成的地圖初始化代碼非常規(guī)整這里我把核心邏輯提煉出來class GameMap { constructor(width, height) { this.width width; this.height height; this.tiles []; // 核心用一個(gè)二維數(shù)組存儲(chǔ)每個(gè)格子的地形類型 // 0 平地, 1 山脈(不可通行), 2 水域(不可通行) // 3 礦石區(qū)(可通行且可采集) for (let y 0; y height; y) { this.tiles[y] []; for (let x 0; x width; x) { this.tiles[y][x] 0; } } this.generateTerrain(); } generateTerrain() { // 使用簡(jiǎn)單的噪聲函數(shù)生成連續(xù)地形區(qū)域 // 并不是隨機(jī)撒點(diǎn)而是用perlin-like模糊制造山脈與水系 const seed Math.random() * 1000; for (let y 0; y this.height; y) { for (let x 0; x this.width; x) { const noiseVal this.smoothNoise(x / 12, y / 12, seed); if (noiseVal 0.25) this.tiles[y][x] 1; else if (noiseVal 0.85) this.tiles[y][x] 2; else if (noiseVal 0.65) this.tiles[y][x] 3; } } } }這段代碼的思路很典型不是把地圖隨機(jī)撒滿障礙物而是通過平滑噪聲函數(shù)讓地形產(chǎn)生連續(xù)的自然感。山脈和水域連成片礦石散布在特定海拔區(qū)域這樣地圖更容易形成天然的攻防通道和資源爭(zhēng)奪點(diǎn)玩起來才有紅警的味道。數(shù)據(jù)層設(shè)計(jì)是純數(shù)值的渲染層只是按照 tile 數(shù)值填色我讓 GPT 在渲染時(shí)又加了一層“近似色抖動(dòng)”的邏輯——相鄰格子的顏色會(huì)在一個(gè)很窄的范圍內(nèi)浮動(dòng)遠(yuǎn)看有紋理感而不是死板的色塊。這個(gè)模塊是整個(gè)項(xiàng)目的地基后面所有的尋路和 AI 邏輯都建立在地圖數(shù)據(jù)之上所以務(wù)必保證數(shù)據(jù)訪問接口簡(jiǎn)潔統(tǒng)一。我在這里做了一次小的重構(gòu)把所有對(duì)map.tiles[y][x]的直接訪問統(tǒng)一封裝成getTile(x, y)和isWalkable(x, y)這種語義明確的方法后面三四個(gè)模塊都因此省了大事。2.2 單位對(duì)象數(shù)據(jù)模型與狀態(tài)流轉(zhuǎn)單位的對(duì)象模型是整個(gè)游戲里最容易被 AI 寫飄的部分。很多 AI 生成的游戲代碼習(xí)慣性地把單位設(shè)計(jì)成一個(gè)巨大的類里面堆了移動(dòng)速度、攻擊力、血量、冷卻時(shí)間、生產(chǎn)費(fèi)用、圖像顏色、動(dòng)畫幀等全部字段。這種寫法在小規(guī)模 demo 里沒問題但一旦單位類型多起來維護(hù)和擴(kuò)展都會(huì)失控。我給 GPT 的指令里特別加了約束單位狀態(tài)和行為邏輯必須拆開屬性走配置行為走方法。用一個(gè)配置表定義每種單位的基礎(chǔ)數(shù)值單位實(shí)例只負(fù)責(zé)持有動(dòng)態(tài)狀態(tài)。// 單位類型配置表所有數(shù)值集中管理 const UNIT_TYPES { rifleman: { hp: 50, speed: 1.2, damage: 8, range: 80, cost: 100, cooldown: 600, color: #4a9e5c }, tank: { hp: 150, speed: 0.9, damage: 25, range: 110, cost: 500, cooldown: 900, color: #5b6d47 }, harvester:{ hp: 120, speed: 0.6, damage: 0, range: 0, cost: 400, cooldown: 0, color: #d4a017 }, turret: { hp: 200, speed: 0, damage: 20, range: 140, cost: 300, cooldown: 700, color: #7a7a7a } }; class Unit { constructor(type, x, y, owner) { this.type type; // 關(guān)鍵不是每個(gè)單位都復(fù)制一遍所有屬性字段 // 而是共享config里的靜態(tài)數(shù)據(jù) this.config UNIT_TYPES[type]; this.x x; this.y y; this.owner owner; this.hp this.config.hp; this.state idle; // idle / moving / attacking / harvesting / dead this.path []; this.target null; this.attackTimer 0; this.alive true; } }共享配置表的設(shè)計(jì)對(duì)后續(xù)平衡性調(diào)試特別友好。最后我調(diào)數(shù)值平衡的時(shí)候只需要修改UNIT_TYPES里的幾個(gè)數(shù)字整局游戲的體驗(yàn)就會(huì)立刻改變不需要滿代碼庫搜索硬編碼數(shù)值。AI 生成代碼時(shí)很容易在狀態(tài)流轉(zhuǎn)那里犯迷糊所以我專門在 Prompt 里畫了一條行為鏈閑置狀態(tài)下收到移動(dòng)命令就進(jìn)入移動(dòng)移動(dòng)到達(dá)終點(diǎn)附近就回到閑置攻擊范圍內(nèi)出現(xiàn)敵方單位就停下并發(fā)起攻擊攻擊打死目標(biāo)后繼續(xù)移動(dòng)或回到閑置采礦車在礦區(qū)和精煉廠之間來回切換采集狀態(tài)。這樣一條一條寫清楚AI 生成的狀態(tài)機(jī)基本就不會(huì)打架了。實(shí)際運(yùn)行里最容易被忽視的其實(shí)是“死亡”狀態(tài)。單位血量歸零后要立刻從渲染隊(duì)列里移除、從單位列表中刪除、格子占用標(biāo)記清空這三件事必須做成一個(gè)原子操作否則就會(huì)出現(xiàn)“尸體還擋著路”或“死了還能被打”的笑話。2.3 尋路系統(tǒng)BFS 與 A* 的實(shí)際取舍尋路是 RTS 里最容易出戲的環(huán)節(jié)。單位卡墻、轉(zhuǎn)圈、重疊是低級(jí)問題稍微高級(jí)一點(diǎn)的是大量單位同時(shí)尋路時(shí)性能崩盤。我讓 GPT 先寫了一個(gè)基礎(chǔ)版本基于 BFS 的四方向?qū)ぢ?。為什么最初不?A*因?yàn)?BFS 的實(shí)現(xiàn)簡(jiǎn)單到不可能出錯(cuò)而且在小地圖規(guī)模下例如 64×64 的網(wǎng)格BFS 的搜索空間完全可控。function findPath(map, startX, startY, targetX, targetY) { const queue [{ x: startX, y: startY, path: [{ x: startX, y: startY }] }]; const visited new Set(); visited.add(${startX},${startY}); const dirs [{ dx: 1, dy: 0 }, { dx: -1, dy: 0 }, { dx: 0, dy: 1 }, { dx: 0, dy: -1 }]; while (queue.length 0) { const current queue.shift(); if (current.x targetX current.y targetY) return current.path; for (const { dx, dy } of dirs) { const nx current.x dx; const ny current.y dy; if (!map.isWalkable(nx, ny)) continue; if (visited.has(${nx},${ny})) continue; const newPath [...current.path, { x: nx, y: ny }]; queue.push({ x: nx, y: ny, path: newPath }); visited.add(${nx},${ny}); } } return null; // 無法到達(dá) }這里存在一個(gè)真實(shí)工程里必須處理的性能坑[...current.path]在每層擴(kuò)展時(shí)都會(huì)拷貝整個(gè)路徑數(shù)組地圖大的時(shí)候 BFS 的路徑數(shù)組會(huì)不斷膨脹這在單位數(shù)量多時(shí)會(huì)拖垮內(nèi)存。我后來用了一個(gè)經(jīng)典優(yōu)化——不直接把路徑存在隊(duì)列節(jié)點(diǎn)里而是用一個(gè)“前驅(qū)節(jié)點(diǎn)表”搜索完畢后再?gòu)慕K點(diǎn)倒推回溯出整條路徑。把這段優(yōu)化思路作為反饋給 GPT 之后它很快給出了改進(jìn)版本尋路系統(tǒng)的性能直接翻了幾倍。從 BFS 換成 A* 是在多單位尋找目標(biāo)之后發(fā)生的。BFS 在 64×64 地圖上對(duì)單個(gè)單位沒問題但 50 個(gè)單位同時(shí)尋路時(shí)每一幀需要的計(jì)算量就繃不住了BFS 要擴(kuò)展到整個(gè)網(wǎng)格才能確定最短路徑A* 憑借啟發(fā)函數(shù)可以更早收斂。A* 的啟發(fā)函數(shù)我用的曼哈頓距離因?yàn)榈貓D只允許四方向移動(dòng)曼哈頓距離是 完美且一致的啟發(fā)函數(shù)。目前版本的性能足夠支撐一局游戲上百個(gè)單位同時(shí)尋路每幀耗時(shí)穩(wěn)定在幾毫秒級(jí)別配合 Web Worker 異步尋路其實(shí)也不是必須的。2.4 戰(zhàn)斗數(shù)值攻擊命中與傷害結(jié)算的隱藏邏輯戰(zhàn)斗系統(tǒng)做得好不好直接決定這個(gè)“紅警”有沒有魂。最開始 GPT 生成的戰(zhàn)斗代碼極其樸素單位進(jìn)入攻擊距離后直接扣血雙方站著對(duì)擼直到一方倒下。這跟紅警的戰(zhàn)場(chǎng)體驗(yàn)差了十萬八千里。我調(diào)整的思路是把攻擊拆成幾個(gè)階段并逐個(gè)在 Prompt 里說明。首先是接敵判定單位不是一進(jìn)入射程就攻擊而是需要一個(gè)朝向目標(biāo)的過程。其次是攻速窗口每次攻擊后要經(jīng)過冷卻時(shí)間才能發(fā)動(dòng)下一次這自然形成了雙方交火時(shí)的交互節(jié)奏。然后是傷害生效這一步通過命中檢查來決定本輪攻擊是否造成傷害。我沒引入復(fù)雜的彈道和隨機(jī) M iss 率而是測(cè)試了一套更直觀的規(guī)則攻擊動(dòng)畫播到中點(diǎn)時(shí)進(jìn)行一次射線檢測(cè)如果目標(biāo)此刻還在射程范圍內(nèi)就直接扣血。attack(target) { if (this.attackTimer 0) return; // 冷卻中 this.attackTimer this.config.cooldown; const dx this.x - target.x; const dy this.y - target.y; const dist Math.sqrt(dx*dx dy*dy); if (dist this.config.range) { const dmg this.config.damage; target.takeDamage(dmg, this); } else { // 目標(biāo)在攻擊瞬間移出了射程范圍攻擊落空 } }這個(gè)設(shè)計(jì)有一個(gè)讓我很滿意的副產(chǎn)品單位在追擊過程中會(huì)一邊追一邊嘗試攻擊同時(shí)由于攻擊落空的判定存在不會(huì)出現(xiàn)“隔著地圖邊緣的極限拉扯打傷害”這種失衡情況。步兵集群打坦克的數(shù)值被壓得很低因?yàn)椴奖鴨挝凰⑿露?、?shù)量大坦克打步兵則有濺射特效突出反步兵的壓制感。這些數(shù)值不是我憑空想的是我?guī)е鴮?duì)紅警原版的記憶先給 GPT 一版“帶方向感的參數(shù)表”然后自己反復(fù)試玩修改了三四輪才定下來的。血條渲染反饋和受擊閃白也是這個(gè)階段加進(jìn)去的雖然工作量不大但視覺上給玩家的打擊反饋立刻立體了這是涂數(shù)值時(shí)最容易忽略、但玩家感知最強(qiáng)的一層。2.5 AI 對(duì)手讓電腦學(xué)會(huì)筑造基地和偷襲RTS 游戲如果沒有一個(gè)會(huì)反抗的電腦對(duì)手那基本上就只是個(gè)沙盒。紅警的魅力一半在競(jìng)技對(duì)抗一半在與電腦斗智斗勇。AI 模塊的設(shè)計(jì)我分了認(rèn)知層和決策層兩層。認(rèn)知層負(fù)責(zé)給 AI 一個(gè)“有限的視野”——它不能也就是地圖全開的上帝視角而是只知道自己基地周圍以及己方單位視野范圍內(nèi)的信息。我用一個(gè)簡(jiǎn)單的遮蔽算法每個(gè)單位的偵察半徑標(biāo)記出一個(gè)視野集合AI 的尋敵邏輯只能在這個(gè)集合內(nèi)尋找目標(biāo)。決策層負(fù)責(zé)完成典型的 RTS 戰(zhàn)術(shù)動(dòng)作擴(kuò)張?jiān)谫Y源點(diǎn)附近建礦廠、生產(chǎn)維持軍隊(duì)數(shù)量、攻擊當(dāng)兵力達(dá)到一定閾值時(shí)發(fā)起總攻和防守基地被攻擊時(shí)召回部隊(duì)。這個(gè)“偵查→判斷→行動(dòng)”的循環(huán)本質(zhì)上是一個(gè)小型行為樹。我的做法是用一段偽代碼大綱在 Prompt 里描述整個(gè)行為樹的流轉(zhuǎn)條件讓 GPT 用 JavaScript 實(shí)現(xiàn)。以下是簡(jiǎn)化版class AIController { constructor(player) { this.player player; this.state expand; // expand / buildArmy / attack / defend this.army []; this.attackThreshold 8; } update() { // 認(rèn)知層更新可見區(qū)域內(nèi)的敵方單位信息 this.updateVisibility(); // 決策層基于當(dāng)前狀態(tài)和資源情況決定下一步動(dòng)作 if (this.state expand) { if (this.player.money 500) { this.buildHarvesterAndMine(); } else { this.state buildArmy; } } else if (this.state buildArmy) { if (this.army.length this.attackThreshold) { this.state attack; } else if (this.player.money 800) { this.produceUnits(); } else { this.state expand; } } else if (this.state attack) { // 選取一個(gè)可見的敵方建筑作為主目標(biāo) const target this.findBestTarget(); if (target) { this.commandArmyAttack(target); } // 軍隊(duì)傷亡過半則回防休整 if (this.army.length this.attackThreshold * 0.4) { this.state defend; } } } }這套 AI 邏輯不是時(shí)刻最優(yōu)的但非常符合 RTS 的“真實(shí)感”。初期電腦優(yōu)先建經(jīng)濟(jì)中期攢兵后期一波流推過來同時(shí)為了阻止玩家“龜縮憋大招”我加了一個(gè)很狗的條件當(dāng) AI 偵察到玩家單位數(shù)量 3 倍于己方時(shí)AI 會(huì)提前發(fā)動(dòng)騷擾攻擊逼玩家提前接戰(zhàn)。這個(gè)設(shè)計(jì)也讓玩家在低難度下不會(huì)感到電腦作弊在高難度下又能感受到“被針對(duì)”的壓力。我采用了難度分級(jí)的方式去調(diào)節(jié) AI 的擴(kuò)張速度和攻擊閾值而不是直接給 AI 加攻擊力和血量難度增長(zhǎng)完全靠策略強(qiáng)度的抬升玩家玩起來不會(huì)覺得是在打一個(gè)數(shù)值怪物。3. 實(shí)操記錄從零開始到跑通對(duì)局3.1 我是怎么給 GPT 寫 Prompt 的這個(gè)項(xiàng)目里最核心的實(shí)戰(zhàn)技能其實(shí)不是寫代碼而是設(shè)計(jì) Prompt。我總結(jié)了一套“三步式 prompt 模板”對(duì) GPT 類的對(duì)話模型效果特別穩(wěn)定。第一步是定義角色和約束。每條 prompt 我都會(huì)寫明“你是一名資深 RTS 游戲開發(fā)者請(qǐng)用純 JavaScript 實(shí)現(xiàn)……不要依賴任何第三方庫不要使用外部引擎所有代碼必須可以嵌入單個(gè) HTML 文件運(yùn)行”。這個(gè)角色的設(shè)定讓答案的語氣和風(fēng)格直接對(duì)齊了。第二步是描述功能需求但絕不給太模糊的描述。與其說“做一個(gè)尋路系統(tǒng)”不如說“請(qǐng)實(shí)現(xiàn)一個(gè)在二維網(wǎng)格地圖上尋找最短路徑的函數(shù)地圖用 0/1 二維數(shù)組表示可通行/不可通行單位只能上下左右移動(dòng)輸入起點(diǎn)和終點(diǎn)坐標(biāo)輸出路徑坐標(biāo)數(shù)組”。越具體生成代碼的邊緣情況處理越到位。第三步是附上相關(guān)的上下文。在寫單位類的時(shí)候我會(huì)把之前生成的地圖類的完整代碼貼進(jìn)去明確說明“你已經(jīng)實(shí)現(xiàn)了下面的 GameMap 類現(xiàn)在要用它來寫單位移動(dòng)邏輯”。把前序代碼作為上下文喂給 GPTAI 才知道自己現(xiàn)在在哪個(gè)工程里生成的東西才自然接得上。這里面有一個(gè)極有用的技巧當(dāng)發(fā)現(xiàn) GPT 寫出的代碼有問題時(shí)不要去人肉修改代碼而是把報(bào)錯(cuò)信息或錯(cuò)誤行為描述反饋回去讓 GPT 自己改。這個(gè)項(xiàng)目里至少 70% 的 bug 都是通過這種方式消掉的。模型本質(zhì)上是在對(duì)話中被“糾正行為”多輪下來生成的代碼會(huì)越來越貼合你的數(shù)據(jù)結(jié)構(gòu)。3.2 垂直切片的開發(fā)節(jié)奏先跑通再優(yōu)化最初的版本我做了一個(gè)很大的冒險(xiǎn)不使用分號(hào)拼接架構(gòu)而是純粹用垂直切片的迭代方式每一輪都要保證“用戶能玩到新增的內(nèi)容”。第一個(gè)里程碑只有一個(gè)能移動(dòng)的黃色小方塊地圖連網(wǎng)格線都沒有。第二個(gè)里程碑加入地圖渲染和基本尋路點(diǎn)擊地圖任意位置方塊會(huì)自動(dòng)繞過障礙物走過去。這個(gè)階段手感極其粗糙單位移動(dòng)是帶瞬移感的因?yàn)閷ぢ窙]有做平滑插值但基礎(chǔ)框架正確這就是最關(guān)鍵的“從 0 到 1”。第三個(gè)里程碑加入采礦、精煉廠和建筑建造單位的循環(huán)此時(shí)已經(jīng)能體驗(yàn)“攢錢-花錢”的經(jīng)濟(jì)系統(tǒng)了。第四個(gè)里程碑加入戰(zhàn)斗第一個(gè)敵人是固定不動(dòng)的炮塔玩家能造三個(gè)步兵去打它。直到第五個(gè)里程碑我才讓 AI 文明整體運(yùn)轉(zhuǎn)起來——建造、發(fā)展、出兵、戰(zhàn)爭(zhēng)這個(gè)時(shí)候第一場(chǎng)完整對(duì)局才真正跑通。這種開發(fā)節(jié)奏的心得是不要指望 GPT 一次生成一個(gè)完整游戲那就像要求一個(gè)實(shí)習(xí)生第一次上班就把公司的核心項(xiàng)目寫完。你要做的是讓每一輪對(duì)話解決一個(gè)很小的、可驗(yàn)證的問題然后逐步拼接成完整的系統(tǒng)。每次里程碑完成后立刻保存一個(gè)版本這個(gè)版本是可以回頭回滾的安全錨點(diǎn)也不會(huì)因?yàn)樾鹿δ馨牙瞎δ芨惚蓝行睦韷毫Α?.3 性能優(yōu)化的幾個(gè)關(guān)鍵動(dòng)作性能問題是 RTS 從“能玩”到“順暢”之間最大的攔路虎。第一版測(cè)試時(shí)30 個(gè)單位同時(shí)移動(dòng)已經(jīng)開始卡頓幀率掉到 30fps 以下。我抓到三個(gè)核心性能瓶頸。第一個(gè)是頻繁的 Canvas 全屏重繪。最初的代碼在游戲循環(huán)的每一幀都調(diào)用clearRect清空整塊畫布再全部重繪地圖和單位。地圖上的格子數(shù)量多、單位數(shù)量越多每幀重繪消耗就越大。優(yōu)化方式是引入臟矩形機(jī)制——只重繪上一幀和這一幀狀態(tài)發(fā)生變化的區(qū)域靜態(tài)地圖的部分可以預(yù)先渲染成離屏 Canvas每幀只需要把地圖圖塊drawImage過來再把移動(dòng)的單位疊上去。這個(gè)簡(jiǎn)單優(yōu)化直接讓幀率翻了兩倍多。第二個(gè)是單位之間的碰撞檢測(cè)。最初每個(gè)單位每幀都要遍歷所有其他單位判斷是否碰撞30 個(gè)單位就已經(jīng)有約 900 次兩兩距離計(jì)算。我改成網(wǎng)格哈??臻g劃分地圖被切成小格子每個(gè)單位只跟同格子和相鄰格子里的單位做碰撞檢測(cè)檢測(cè)次數(shù)從 O(n2) 降到了近似 O(n)。第三個(gè)是尋路緩存。對(duì)于靜止障礙物地圖同一條路徑通常會(huì)被多個(gè)單位使用我在尋路模塊里加了一層哈希地圖緩存相同起點(diǎn)終點(diǎn)組合的路徑直接復(fù)用。這個(gè)優(yōu)化在多個(gè)步兵同時(shí)去同一個(gè)采礦點(diǎn)的時(shí)候效果奇佳計(jì)算量大幅下降。3.4 開源前的 Code Review 與文檔整理游戲能跑通后離“可以開源”其實(shí)還有一道很大的坎。GitHub 上爛大街的“能跑的 demo”太多了但如果開源的目的不只是展示而是希望別人能參與貢獻(xiàn)或者從中學(xué)習(xí)代碼質(zhì)量和文檔就是必須補(bǔ)的功課。我用了兩天時(shí)間做這件事。先把所有代碼從單 HTML 文件拆成模塊化的多個(gè) JS 文件按照 Units、Map、Pathfinding、Battle、AI、UI 六個(gè)目錄整理。這個(gè)過程很痛苦因?yàn)樽畛鯙榱耸∈潞芏嗪瘮?shù)是全局的拆分時(shí)要梳理依賴關(guān)系但不拆的話后續(xù)任何人接手都會(huì)頭皮發(fā)麻。然后是寫 README。我放棄了冷冰冰的技術(shù)清單式 README而是寫了一個(gè)“項(xiàng)目背后故事 快速試玩 技術(shù)架構(gòu)圖 開發(fā)計(jì)劃”的混合版本。教程型文檔很重要我知道很多人拿到代碼后會(huì)想知道“從哪里開始讀起”所以特別加了一個(gè)“代碼地圖”章節(jié)告訴讀者入口文件是哪幾個(gè)、核心邏輯分別在哪個(gè)目錄。開源許可證我選了 MIT。RTS 游戲題材本身不涉及特殊授權(quán)問題MIT 對(duì)使用者最寬松如果有人想基于這個(gè)做二次開發(fā)或者學(xué)習(xí)改造法律限制最小對(duì)于一個(gè)學(xué)習(xí)向項(xiàng)目這是最合理的選擇。4. 常見問題與排查技巧實(shí)錄4.1 單位卡死在墻角或者繞著目標(biāo)原地轉(zhuǎn)圈這是 RTS 尋路中最經(jīng)典的一類問題。癥狀是單位接到了移動(dòng)命令但到了目標(biāo)點(diǎn)附近后開始原地左右徘徊或者被一個(gè)角落卡住不停地抖。這個(gè)問題的根因通常不是路徑不存在而是單位到達(dá)“路徑終點(diǎn)”的判定方式太苛刻。如果你的到達(dá)判定是“單位坐標(biāo)必須嚴(yán)格等于終點(diǎn)坐標(biāo)”那幾乎一定會(huì)出問題。因?yàn)閱挝灰苿?dòng)是離散的每幀前進(jìn)固定步長(zhǎng)最后一幀幾乎不可能正好落在終點(diǎn)上。標(biāo)準(zhǔn)解法是引入“到達(dá)半徑”單位與終點(diǎn)距離小于一個(gè)閾值比如 6 像素就算到達(dá)。另一個(gè)造成轉(zhuǎn)圈的常見原因是尋路網(wǎng)格的粒度與單位的碰撞半徑不匹配。單位碰撞半徑比網(wǎng)格大但尋路按網(wǎng)格中心行走結(jié)果就是路徑穿過了“單位實(shí)際過不去”的窄縫。這時(shí)候要么把碰撞檢測(cè)半徑調(diào)小要么把尋路網(wǎng)格做膨脹——把所有障礙物的四個(gè)方向都向外擴(kuò)展一格。4.2 大量單位運(yùn)動(dòng)時(shí)互相穿插重疊的詭異行為網(wǎng)上 RTS 游戲 demo 最拉胯的觀感就是一堆單位像幽靈一樣互相穿過。原因就是完全沒有單位之間的碰撞響應(yīng)。但如果你直接給所有單位做嚴(yán)格物理碰撞又會(huì)出現(xiàn)堵車死鎖——前面單位擋路后面單位全部停住。游戲體驗(yàn)反而不如穿插。我的折衷方案是軟碰撞單位之間檢測(cè)到距離過近時(shí)不阻止移動(dòng)而是施加一個(gè)橫向的偏移力讓它們自動(dòng)錯(cuò)開。這實(shí)現(xiàn)起來很像簡(jiǎn)易的斥力模型——兩個(gè)單位互相靠近時(shí)各自向垂直于連線方向偏移一點(diǎn)偏移量跟重疊深度成正比。實(shí)測(cè)下來單位群在移動(dòng)時(shí)能自然形成松散隊(duì)形既不會(huì)穿模也不會(huì)徹底堵死。當(dāng)然軍隊(duì)在進(jìn)攻陣型上的移動(dòng)也可以用編隊(duì)行為做得更漂亮但那個(gè)系統(tǒng)復(fù)雜度會(huì)再上一個(gè)臺(tái)階。我做了一個(gè)簡(jiǎn)化處理選中的多個(gè)單位移動(dòng)到同一目標(biāo)點(diǎn)時(shí)會(huì)自動(dòng)在目標(biāo)點(diǎn)周圍按網(wǎng)格排開而不是全部擠在同一個(gè)中心坐標(biāo)上。這個(gè)補(bǔ)丁讓整隊(duì)的“到達(dá)姿態(tài)”立刻自然了很多。4.3 AI 對(duì)手的經(jīng)濟(jì)崩潰與發(fā)呆循環(huán)AI 最難調(diào)的其實(shí)不是戰(zhàn)斗力而是經(jīng)濟(jì)循環(huán)。初版 AI 經(jīng)常出現(xiàn)這個(gè)問題攢夠了 500 塊錢就建精煉廠但精煉廠建完發(fā)現(xiàn)礦區(qū)太遠(yuǎn)采完一波要跑很久于是生產(chǎn)鏈就斷了然后 AI 進(jìn)入一個(gè)很呆的死循環(huán)——錢不夠造兵資金逐漸枯竭。排查思路是先把 AI 的決策 log 全部打出來觀察它每一幀到底在干什么決策、資源余額是多少。這一看就發(fā)現(xiàn)了AI 的“擴(kuò)張”決策優(yōu)先級(jí)太高資源稍微多一點(diǎn)就全部拿去建新建筑了導(dǎo)致軍事生產(chǎn)被完全擠掉。修復(fù)辦法是給 AI 加入“可負(fù)擔(dān)判斷閾值”——只有當(dāng)余額超過某種建筑成本的 1.5 倍時(shí)才允許建造而不是剛好夠了就動(dòng)工這樣預(yù)留了同時(shí)維持一支小型軍隊(duì)的空間。第二個(gè)修復(fù)是給 AI 加入經(jīng)濟(jì)優(yōu)先級(jí)維持軍隊(duì)在先擴(kuò)張基地在后只有當(dāng)軍隊(duì)規(guī)模達(dá)標(biāo)并且經(jīng)濟(jì)盈余時(shí)才會(huì)考慮多建一個(gè)礦場(chǎng)。調(diào)完之后AI 的暴兵節(jié)奏和資源增長(zhǎng)明顯更有層次感了。4.4 從報(bào)錯(cuò)到修復(fù)一次真實(shí) Bug 的完整排查過程分享一個(gè)我最印象深刻的 bug游戲運(yùn)行幾分鐘后單位開始隨機(jī)消失內(nèi)存占用肉眼可見地飆升。這個(gè) bug 不是立刻出現(xiàn)的它“潛伏”了大概三分鐘才爆發(fā)debug 起來特別痛苦。最初懷疑是內(nèi)存泄漏檢查 Action 里所有數(shù)組的 push 和 splice 都沒發(fā)現(xiàn)問題。后來我用 Chrome Performance 錄制了一段對(duì)局過程發(fā)現(xiàn)內(nèi)存持續(xù)增長(zhǎng)但代碼邏輯里所有創(chuàng)建的對(duì)象好像都有對(duì)應(yīng)的清理步驟。最終定位到罪魁禍?zhǔn)资鞘录O(jiān)聽器泄漏。單位在生產(chǎn)建筑里被創(chuàng)建時(shí)會(huì)綁定一個(gè)“創(chuàng)建完成”的事件監(jiān)聽器但當(dāng)建筑被敵方摧毀時(shí)這個(gè)監(jiān)聽器沒有被移除。當(dāng)時(shí)整個(gè)游戲里其實(shí)同時(shí)存在很多已經(jīng)消失的建筑殘留的監(jiān)聽器仍在響應(yīng)生產(chǎn)事件不斷創(chuàng)建看不到的新單位這些單位占用了坐標(biāo)但本身就崩了。這里我學(xué)到的最重要經(jīng)驗(yàn)是不要相信 AI 自動(dòng)生成的事件訂閱/退訂邏輯這種隱式連接的 bug 是最難通過 README 代碼 review 察覺的。后來我在所有建筑和單位的 destroy 函數(shù)里統(tǒng)一加了解綁邏輯并寫了一行注釋提醒自己。另外一個(gè)排查技巧是在游戲里臨時(shí)加一個(gè) debug 面板實(shí)時(shí)顯示當(dāng)前單位數(shù)量、AI 數(shù)量、事件監(jiān)聽器數(shù)量一旦數(shù)值異常增長(zhǎng)就知道問題大概出在哪個(gè)環(huán)節(jié)了。5. 開源以后社區(qū)反饋和這個(gè)項(xiàng)目還能做什么項(xiàng)目放到 GitHub 之后第一周的反饋超出了我的預(yù)期。Star 數(shù)漲得比我想象中快得多不少人 Fork 下來跑起來之后提了各式各樣的 issue 和 PR。最讓我高興的是有一個(gè)開發(fā)者自己實(shí)現(xiàn)了一個(gè)“多人聯(lián)機(jī)模式”的概念驗(yàn)證基于 WebSocket 做了一個(gè)簡(jiǎn)單的房間同步系統(tǒng)。雖然延遲很高、同步機(jī)制非常粗糙但它證明了這個(gè)項(xiàng)目代碼的可擴(kuò)展性是真實(shí)的——?jiǎng)e人不需要了解全部細(xì)節(jié)就能在自己需要的方向上延展。這個(gè)項(xiàng)目未來的擴(kuò)展空間其實(shí)非常大。我自己心里有幾個(gè)已經(jīng)想清楚的規(guī)劃一是引入一套更豐富的兵種傾向和互相克制機(jī)制讓戰(zhàn)術(shù)縱深更強(qiáng)二是做一套簡(jiǎn)單的戰(zhàn)役模式講一個(gè)完整的故事流程而不只是一張張隨機(jī)地圖三是把整個(gè) AI 策略層做成獨(dú)立的可插拔模塊讓社區(qū)開發(fā)者可以提交自己的 AI 邏輯。如果你也想跑一個(gè)類似的項(xiàng)目我最后想給的一點(diǎn)核心建議是把 GPT 當(dāng)結(jié)對(duì)編程伙伴而不是代碼抄寫員。它會(huì)給你一個(gè)能跑起來的骨架但真正讓游戲“有魂”的細(xì)節(jié)——手感、節(jié)奏、平衡性、意外處理——這些飛躍還是需要你親手一點(diǎn)點(diǎn)調(diào)出來的。用 GPT 做這種完整項(xiàng)目最大的樂趣也正在于此它幫你把天花板抬高了三倍而你自己的品味和判斷力決定了最終能飛多高。