:用React+Three.js+Rapier重寫3D娃娃機(jī)項目)
1. 項目緣起一個被遺忘的“半成品”與AI的邂逅五年前我沉迷于用代碼創(chuàng)造一些“無用但有趣”的東西。當(dāng)時Three.js正火物理引擎也讓人著迷于是心血來潮想做一個在線的、可交互的娃娃機(jī)。想法很美好用React搭界面Three.js渲染3D場景再找個物理引擎模擬抓取的真實感。我吭哧吭哧搞了幾天實現(xiàn)了基礎(chǔ)的場景搭建、模型加載和一個極其簡陋的抓取動畫。然后……就沒有然后了。項目文件夾靜靜地躺在硬盤角落成了典型的“從入門到放棄”的標(biāo)本。直到最近AI編程工具的風(fēng)潮讓我重新審視這些陳年舊碼??粗莻€粗糙的、爪子穿模、邏輯混亂的娃娃機(jī)項目一個念頭冒了出來如果讓現(xiàn)在的AI來“重寫”它會怎么樣不是讓它從零生成而是基于我五年前的思路和殘破的代碼用今天更成熟的技術(shù)棧和AI的理解能力重新構(gòu)建一個完整、可玩、代碼優(yōu)雅的項目。這不僅僅是一次代碼翻新更像是一場跨越時間的對話看看五年間我的技術(shù)認(rèn)知與AI的代碼能力能碰撞出什么火花。于是我花了半天時間以“AI輔助重寫”為核心將這個老項目徹底復(fù)活。2. 技術(shù)棧選型與AI工具的角色定位五年前我的技術(shù)選擇帶著明顯的探索和拼湊痕跡。今天重做首先要明確一個清晰、現(xiàn)代且高效的技術(shù)棧。AI在這里不僅是寫代碼的“手”更是提供選型建議、規(guī)避歷史陷阱的“顧問”。2.1 前端框架堅守React但擁抱全新開發(fā)范式項目主體是瀏覽器內(nèi)的3D交互應(yīng)用React依然是構(gòu)建復(fù)雜UI狀態(tài)的首選其組件化模型與3D場景中的對象管理有天然的契合度。但五年前我用的是React Class Component和一堆散亂的setState狀態(tài)流像一團(tuán)亂麻。AI的輔助建議與實施我向AI工具如Cursor或Claude Code描述了項目需求一個需要精細(xì)控制3D物體狀態(tài)、抓取流程、游戲分?jǐn)?shù)和UI聯(lián)動的應(yīng)用。AI幾乎一致推薦使用React Hooks TypeScript的組合。Hooks尤其是useState,useEffect,useRef,useReducer能讓狀態(tài)和副作用邏輯更清晰、更內(nèi)聚。例如娃娃機(jī)的抓取臂狀態(tài)、計時器、獎品池管理都可以用自定義Hook封裝與3D渲染邏輯解耦。TypeScript的引入是本次重寫的關(guān)鍵提升。我為整個項目定義了清晰的接口InterfaceIClawMachineState、IPrizeItem、IPhysicsWorld等。AI在根據(jù)我的自然語言描述生成代碼片段時TypeScript能立刻提供類型約束和智能提示極大減少了因類型錯誤導(dǎo)致的運(yùn)行時Bug。當(dāng)我說“創(chuàng)建一個控制抓鉤上下移動的函數(shù)”時AI生成的代碼已經(jīng)包含了參數(shù)類型speed: number和返回類型Promisevoid使得代碼意圖一目了然。2.2 3D渲染核心Three.js的版本升級與最佳實踐五年前我用的是Three.js r89很多API如今已變。AI幫我快速梳理了版本差異并直接采用了最新的穩(wěn)定版本如r162。重寫不是簡單替換版本號而是在AI的幫助下應(yīng)用當(dāng)前社區(qū)公認(rèn)的最佳實踐。場景圖Scene Graph管理過去我把所有模型機(jī)器、爪子、獎品都直接add到scene里管理混亂。AI建議并幫我重構(gòu)為更清晰的層級結(jié)構(gòu)// 使用Group進(jìn)行邏輯分組 const machineGroup new THREE.Group(); const clawGroup new THREE.Group(); const prizesGroup new THREE.Group(); machineGroup.add(cabinetModel, glassModel); clawGroup.add(armModel, clawModel); prizesGroup.add(prize1, prize2, prize3); scene.add(machineGroup, clawGroup, prizesGroup);這樣移動爪子只需操作clawGroup旋轉(zhuǎn)整個機(jī)器只需操作machineGroup邏輯清晰便于AI理解和后續(xù)維護(hù)。資源管理與加載當(dāng)年用THREE.TextureLoader和THREE.GLTFLoader都是直接內(nèi)聯(lián)沒有加載狀態(tài)和錯誤處理。AI幫我引入了LoadingManager和異步加載模式并生成了加載進(jìn)度條UI的配套代碼提升了用戶體驗。性能優(yōu)化AI提示我對于靜態(tài)的娃娃機(jī)柜體模型應(yīng)使用THREE.StaticCopy屬性或在加載時合并幾何體以減少draw call。對于大量重復(fù)的獎品模型建議使用實例化網(wǎng)格THREE.InstancedMesh這在五年前我完全沒概念。2.3 物理引擎從“紙片”動畫到真實模擬舊項目最大的敗筆是物理。抓取動畫是用Tween.js做的簡單線性運(yùn)動爪子與獎品是“檢測矩形框重疊”這種2D游戲邏輯毫無真實感。AI的決策支持過程我向AI提出了需求“需要為3D娃娃機(jī)添加真實的抓取物理包括碰撞檢測、抓取判定、獎品被抓起后的擺動?!?AI沒有直接推薦某個引擎而是列出了幾個選項并分析了在Web環(huán)境下的優(yōu)劣Cannon.js純JavaScript輕量API直接但功能相對基礎(chǔ)社區(qū)活躍度一般。Ammo.jsBullet物理引擎的Emscripten移植功能強(qiáng)大工業(yè)級但包體積大初始化慢。Rapier.rs編譯為WebAssembly新興引擎性能極好API現(xiàn)代但中文資料相對少??紤]到本項目并非需要極度復(fù)雜的物理特性如流體、軟體而是注重實時交互性能和開發(fā)體驗AI最終建議我嘗試Rapier。它WASM的特性保證了性能且其TypeScript類型定義完善與我的技術(shù)棧契合。AI不僅給出了建議還生成了初始化的樣板代碼import * as RAPIER from dimforge/rapier3d; // 初始化物理世界 const world new RAPIER.World({ x: 0.0, y: -9.81, z: 0.0 }); // 創(chuàng)建地面和碰撞體 const groundColliderDesc RAPIER.ColliderDesc.cuboid(10, 0.1, 10); world.createCollider(groundColliderDesc); // 在Three.js對象和Rapier剛體之間建立關(guān)聯(lián) const rigidBodyDesc RAPIER.RigidBodyDesc.dynamic().setTranslation(x, y, z); const rigidBody world.createRigidBody(rigidBodyDesc); const colliderDesc RAPIER.ColliderDesc.ball(radius); world.createCollider(colliderDesc, rigidBody); // 在渲染循環(huán)中更新物理世界 function animate() { world.step(); // 同步Three.js對象位置到物理剛體位置 const position rigidBody.translation(); threeJsObj.position.set(position.x, position.y, position.z); requestAnimationFrame(animate); }這段由AI生成的代碼清晰地勾勒出了整合物理引擎的核心鏈路我只需在此基礎(chǔ)上將我的3D模型與對應(yīng)的剛體、碰撞體綁定即可。2.4 AI編程工具Cursor作為核心“協(xié)作者”整個重寫過程我主要使用Cursor作為AI編程助手。它的優(yōu)勢在于深度集成在IDE中能直接分析整個項目上下文包括我五年前的爛代碼提供基于上下文的建議。我的工作流是需求拆解與描述我用自然語言在Cursor里描述一個功能點(diǎn)比如“我想讓爪子下落時如果碰到獎品就停止并閉合然后上升”。代碼生成與審查Cursor會生成一段包含物理檢測、狀態(tài)變更的TypeScript函數(shù)。我并非直接采納而是仔細(xì)閱讀理解其邏輯例如它使用了Rapier的world.intersectionsWith進(jìn)行碰撞檢測。迭代與調(diào)試如果生成的代碼運(yùn)行不如預(yù)期比如抓取判定太靈敏我會繼續(xù)對話“抓取判定需要更精確只有當(dāng)爪子的兩個鉗口都接觸到獎品時才觸發(fā)抓取。” Cursor會修改代碼可能引入更詳細(xì)的射線檢測或形狀查詢。代碼重構(gòu)對于AI生成的一些冗長函數(shù)我會命令它“將這個函數(shù)重構(gòu)將物理檢測和狀態(tài)更新邏輯分離?!?它便能給出更模塊化的代碼。AI在此過程中扮演的不是替代者而是一個知識淵博、不知疲倦的結(jié)對編程伙伴。它快速將我的意圖轉(zhuǎn)化為語法正確的代碼草案而我負(fù)責(zé)把握整體架構(gòu)、業(yè)務(wù)邏輯的正確性和代碼最終質(zhì)量。3. 核心功能模塊的重構(gòu)與AI實現(xiàn)有了清晰的技術(shù)棧和工具重寫工作就可以系統(tǒng)性地展開。我將娃娃機(jī)拆解為幾個核心模塊逐一用AI輔助重構(gòu)。3.1 游戲狀態(tài)管理從混亂到清晰舊項目狀態(tài)散落在各個角落難以維護(hù)。我決定使用useReducerHook來集中管理游戲狀態(tài)。我向AI描述了狀態(tài)機(jī)游戲狀態(tài)IDLE待機(jī) - COIN_INSERTED投幣 - CLAW_MOVING移動爪子 - CLAW_DESCENDING下爪 - CLAW_GRASPING抓取中 - CLAW_ASCENDING上升 - RESULT判定結(jié)果 - IDLEAI為我生成了狀態(tài)和動作Action的類型定義以及reducer函數(shù)的基本骨架。我在此基礎(chǔ)上填充了每個狀態(tài)轉(zhuǎn)換的具體邏輯。例如當(dāng)派發(fā)‘GRASP_COMPLETE’動作時reducer會計算是否抓取成功并更新獎品列表和玩家分?jǐn)?shù)。這種集中式狀態(tài)管理讓UI如按鈕禁用狀態(tài)、提示文字和3D場景動畫的同步變得異常簡單。3.2 抓取物理與交互真實感的核心這是本次重寫的重中之重也是AI輔助價值最高的部分。物理實體綁定首先需要為Three.js場景中的每個可交互物體爪子、獎品創(chuàng)建對應(yīng)的Rapier剛體RigidBody和碰撞體Collider。我讓AI編寫了一個工具函數(shù)createPhysicsEntity它接收一個Three.js的Mesh對象和物理參數(shù)質(zhì)量、形狀、類型返回一個包含rigidBody和collider的對象并自動將兩者的位置同步。這避免了手動維護(hù)兩套坐標(biāo)的麻煩。抓取邏輯實現(xiàn)下爪過程不再是簡單的動畫。AI幫我實現(xiàn)的邏輯是爪子一個Dynamic剛體在CLAW_DESCENDING狀態(tài)下被施加一個向下的力rigidBody.applyForce。在每一幀物理更新后檢測爪子碰撞體clawCollider與所有獎品碰撞體的交集world.contactPair(clawCollider, prizeCollider)。如果檢測到接觸且接觸點(diǎn)數(shù)量或深度滿足“抓取”閾值則切換狀態(tài)到CLAW_GRASPING。此時AI生成的代碼會創(chuàng)建一個物理約束將獎品剛體與爪子剛體通過一個FixedImpulseJoint或BallJoint連接起來。這就模擬了爪子“抓住”獎品的物理效果獎品會隨著爪子上升而自然擺動而不是僵直地粘在一起。上升到頂后解除物理約束并根據(jù)獎品最終位置是否落入出貨口判定游戲結(jié)果。這個過程中AI快速幫我處理了復(fù)雜的Rapier API調(diào)用并解釋了不同關(guān)節(jié)Joint類型的區(qū)別讓我能做出合適的選擇。用戶輸入與控制我保留了舊項目的“方向鍵移動爪子”設(shè)計。AI將鍵盤事件監(jiān)聽與物理引擎結(jié)合生成的控制代碼不再是直接設(shè)置position.x而是對爪子的剛體施加一個沖量applyImpulse或設(shè)置速度setLinvel使得移動帶有慣性手感更真實。3.3 3D模型與動畫優(yōu)化五年前的模型是網(wǎng)上隨便找的.obj文件材質(zhì)粗糙。這次我使用AI圖像生成工具如Midjourney或Stable Diffusion的輔助設(shè)計功能重新生成了更卡通、更精致的娃娃機(jī)和獎品概念圖然后借助Blender快速建模導(dǎo)出為.glb格式。在加載環(huán)節(jié)我遇到了一個經(jīng)典問題“glb模型為什么到three.js里打開全是黑的”舊項目也遇到過當(dāng)時沒解決。這次我直接問AI。AI給出了系統(tǒng)性的排查清單檢查光源場景中是否添加了AmbientLight環(huán)境光和DirectionalLight平行光沒有光源模型就是黑的。檢查相機(jī)位置相機(jī)是否在模型內(nèi)部或被模型遮擋使用camera.lookAt()指向場景中心。檢查模型材質(zhì)某些.glb模型的材質(zhì)可能需要THREE.sRGBEncoding或特定的渲染器配置。AI建議我嘗試在加載后遍歷模型材質(zhì)并設(shè)置material.needsUpdate true。檢查模型縮放模型可能過大或過小超出視錐體。添加一個BoxHelper來可視化模型邊界。通過AI的指引我迅速定位到問題是光源不足和材質(zhì)編碼問題并得以解決。AI還建議我為抓取臂的張開/閉合添加骨骼動畫THREE.AnimationMixer讓動作更流暢并生成了混合動畫剪輯的基礎(chǔ)代碼。3.4 UI與游戲邏輯集成使用ReactUI層變得非常簡單。我創(chuàng)建了GameControlPanel組件顯示狀態(tài)、幣數(shù)、分?jǐn)?shù)以及控制按鈕。狀態(tài)通過Context或Props從主reducer傳遞下來。AI幫我快速搭建了這些UI組件并確保了樣式與3D畫風(fēng)的統(tǒng)一。對于“開始游戲”、“移動”等按鈕事件它們只是派發(fā)對應(yīng)的Action如‘MOVE_LEFT’由reducer和物理系統(tǒng)去響應(yīng)實現(xiàn)了完美的關(guān)注點(diǎn)分離。4. 開發(fā)、構(gòu)建與部署的現(xiàn)代化改造五年前我用的是create-react-app的早期版本構(gòu)建配置一無所知。現(xiàn)在我用ViteTypeScript作為構(gòu)建工具鏈速度極快。Vite配置AI幫我初始化了項目并解決了TypeScript裝飾器解析的問題。當(dāng)我在tsconfig.json中遇到“選項‘baseUrl’已棄用”的警告時AI解釋了這是TypeScript 7.0的變更并給出了遷移到‘rootDir’和路徑映射paths的正確配置方法。調(diào)試與性能分析AI建議我使用Three.js的調(diào)試庫drei和物理引擎的調(diào)試渲染器將碰撞體以線框形式可視化這對于調(diào)試抓取判定至關(guān)重要。在性能方面AI提示我使用stats.js監(jiān)控幀率并注意在組件卸載時正確銷毀Three.js的渲染器、場景和物理世界防止內(nèi)存泄漏。部署項目是純前端的部署極其簡單。AI建議我可以使用Vercel、Netlify或GitHub Pages。我選擇了Vercel只需關(guān)聯(lián)Git倉庫它就能自動識別Vite項目并進(jìn)行部署。AI還生成了必要的vercel.json配置雖然本項目基本不需要并提醒我注意構(gòu)建輸出目錄是dist。5. 半日重構(gòu)經(jīng)驗、教訓(xùn)與AI的邊界從打開舊項目到新版本上線總共耗時約半天。這個過程中AI的貢獻(xiàn)是顛覆性的但也暴露出其局限性。AI帶來的效率提升是驚人的知識檢索與代碼生成我不再需要反復(fù)搜索“Three.js如何創(chuàng)建陰影”、“Rapier怎么創(chuàng)建碰撞傳感器”用自然語言描述AI就能給出可直接使用或稍作修改的代碼塊。代碼重構(gòu)與規(guī)范化AI能迅速將過程式代碼重構(gòu)為聲明式、模塊化的函數(shù)組件和自定義Hook并強(qiáng)制加入TypeScript類型代碼質(zhì)量遠(yuǎn)超我五年前的水平。問題排查與調(diào)試遇到“模型全黑”、“物理對象穿透”等問題時向AI描述現(xiàn)象它能給出結(jié)構(gòu)化的排查思路節(jié)省了大量盲目試錯的時間。但AI并非萬能我的核心作用無可替代架構(gòu)設(shè)計與決策AI不會主動說“我們應(yīng)該用useReducer來管理狀態(tài)”。它需要我提出清晰、正確的指令。整體應(yīng)用架構(gòu)、模塊劃分、數(shù)據(jù)流設(shè)計仍然依賴我的經(jīng)驗和判斷。業(yè)務(wù)邏輯理解娃娃機(jī)抓取的“玄學(xué)”手感如爪子的抓力、獎品的滑動需要微調(diào)參數(shù)。AI可以幫我寫參數(shù)調(diào)節(jié)的代碼框架但“怎樣算好玩”這個感覺需要我親自測試和調(diào)整。AI無法理解“趣味性”這種主觀體驗。錯誤處理與邊界情況AI生成的代碼往往是“理想路徑”。當(dāng)網(wǎng)絡(luò)加載失敗、物理引擎初始化出錯、用戶快速連續(xù)點(diǎn)擊時需要我手動添加健壯的錯誤處理和狀態(tài)保護(hù)。AI可能會遺漏這些邊緣情況。代碼審查與優(yōu)化對AI生成的代碼我必須逐行審查。有時它會使用已棄用的API或?qū)懗鲂阅懿患训膶懛ㄈ缭阡秩狙h(huán)中創(chuàng)建新對象。我需要識別并糾正這些。一個具體的踩坑案例在實現(xiàn)爪子抓取獎品時AI最初生成的代碼是在檢測到碰撞的瞬間就將獎品剛體的類型從Dynamic改為KinematicPositionBased并直接設(shè)置其位置跟隨爪子。這導(dǎo)致了詭異的“抽搐”現(xiàn)象。我意識到這是物理模擬和直接設(shè)置位置的沖突。經(jīng)過幾輪與AI的對話調(diào)試我們最終采用了前面提到的物理關(guān)節(jié)Joint方案這才是符合物理規(guī)律的實現(xiàn)。這個過程讓我深刻體會到AI提供的是“素材”和“可能性”而工程師的價值在于“選擇”和“判斷”。這次“AI重寫”實驗結(jié)果遠(yuǎn)超預(yù)期。一個死氣沉沉的老項目在半天內(nèi)脫胎換骨擁有了現(xiàn)代化的代碼結(jié)構(gòu)、真實的物理交互和不錯的視覺效果。它證明了在當(dāng)前階段AI編程工具已經(jīng)成為前端開發(fā)者尤其是從事3D、游戲等復(fù)雜交互領(lǐng)域開發(fā)的強(qiáng)大“倍增器”。它極大地壓縮了“查找資料-理解API-編寫樣板代碼”的循環(huán)讓開發(fā)者能更專注于核心創(chuàng)意、架構(gòu)設(shè)計和用戶體驗打磨。然而它沒有改變編程的本質(zhì)——解決問題的邏輯思維。AI更像是一本超級智能的、能對話的API文檔和代碼片段生成器。駕馭它需要你清楚地知道自己要去哪里以及如何下達(dá)正確的指令。對于新手它可能讓你更快地做出東西但也可能讓你在不理解原理的情況下堆砌代碼對于有經(jīng)驗的開發(fā)者它則能解放生產(chǎn)力讓你從繁瑣的重復(fù)勞動中解脫出來去挑戰(zhàn)更有價值的問題。我的這個“新”娃娃機(jī)就是這種新工作流下一個有趣的產(chǎn)物。