M7:效能優化與打包——meshopt 壓縮、幾何合併、動畫 LOD
玩法、BOSS、juice 都到位後,最後一關是效能與體積——要在一般筆電穩定 60fps,首次載入也不能讓人等太久。
先量,再改
優化的鐵則是先有數據。做了一個按 ` 鍵開的 debug 面板,顯示 FPS、draw calls、三角面數、敵人數、粒子數。用 performance.now() 量真實幀率(不受 timeScale 影響)。沒有這個面板,優化就是瞎猜。
模型壓縮:meshopt 砍半
四個角色 GLB 原本各約 4.5MB、共 22MB。檢視後發現體積不在貼圖(只有 14KB 的小圖集),而在幾十個骨骼動畫 clip。
用 gltf-transform 的 meshopt 壓縮:
gltf-transform meshopt input.glb output.glb
# Knight.glb 3.66MB → 1.86MB(-49%)
載入端在 GLTFLoader 掛上 MeshoptDecoder 就能解。四個角色從 18MB 壓到 9MB。再把沒用到的模型(一個沒用的法師 + 一堆沒引用的地城件)移出打包範圍——資源從 23MB 降到 11MB。
幾何合併:draw call 砍半
一個房間有 80+ 個獨立 mesh(地磚、牆、柱、桶),每個都是一次 draw call。但它們共用同一張 KayKit 圖集。
用 BufferGeometryUtils.mergeGeometries 把共用貼圖的靜態 mesh 烘焙世界矩陣後合併成單一 mesh:
// 把地磚、非門牆面各自合併;門面保留獨立(門板要能開)
const merged = mergeGeometries(geometries, false);
draw call 從 ~102 降到 ~40–60。實作上有個保險:任何環節失敗就退回原模組(不合併),確保畫面絕不破。物理碰撞體維持各自獨立、不受影響。
動畫 LOD:遠處敵人降頻
同屏很多敵人時,骨骼動畫(skinning + mixer 更新)是 CPU 大戶。但遠處的敵人動畫畫質根本看不清——所以按距離降頻:
// 距玩家越遠,mixer 更新越疏(累積 dt,間隔到才一次更新)
animator.setLod(dist > 18 ? 2 : dist > 11 ? 1 : 0);
近處每幀更新(流暢),遠處每 2–3 幀一次(省 CPU)。關鍵:戰鬥命中判定走真實 dt 的計時器,不受動畫 LOD 影響——所以遠處敵人看起來動作稍頓,但攻擊時機完全正確。
物件池全覆蓋
傷害數字、XP 寶石、粒子全部用物件池——遊戲進行中幾乎零 new、零 GC 停頓。這對避免「每隔幾秒卡一下」的 GC 尖峰很重要。
打包
vite build 出純靜態 dist/,WASM(Rapier)已 base64 內嵌,免特殊伺服器設定。JS bundle gzip 後約 1MB(大半是 Rapier 的 WASM)。整包丟任何靜態主機(Cloudflare Pages、Vercel、GitHub Pages)都能跑。
到這裡,遊戲技術上完工了:玩法完整、有 BOSS、有 juice、效能達標、可打包部署。最後一篇不談程式,談一個這個專案讓我反覆想到的問題:當 AI 成為素材的主要使用者,素材庫會變成什麼樣子?