iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Claude AI

血汗同事:我和 Claude Code 僱了一隊 AI,做出 Web MMO ARPG系列 第 12

Day 11|不拼 tile 了,改整幅 AI 母畫的世界

  • 分享至 

  • xImage
  •  

Whisper Isle 遊戲連結(PC Only):whisperisle.app(註:專案每日持續高速演進,撰文當下線上已推進至 v0.2.4)

Yes

昨天在 Day 10 聊了島嶼從 80×80 蓋到 180×180 的三次重建,以及地圖尺寸問題這件事。地基定了,下一個問題馬上浮出來:這 180×180 格的地面,要用什麼畫出來?開工以來我們一直走的是傳統做法:32×32 的 tile 拼地面,再用生圖工具逐件生成岩壁、樹叢、木箱這類 prop 散佈上去。到了 8 月初,島上已經擺了 609 件 prop,光岩壁素材就有 56 件。

但從玩家角度來看 AI 的組合,大概就是亂做一通。

一、拼裝目前不可行

我最初的懷疑方向是錯的。我懷疑的是 web 平台:是不是瀏覽器就做不出有層次感的地形?但讓 AI 查詢了主流的開發做法:

  1. 崖壁這種大型連續結構,靠生圖逐件拼,接縫、剪影重複、光向不一致,全是不可行的放大版。
  2. 岩石素材擺上去了,碰撞沒跟上,玩家一走就穿幫。
  3. 構圖交給 AI 逐件排,它做不到(現階段視覺美感不足)。

它同時查了先例:柏德之門用的是整幅預渲染背景(原生 5120×3480)加上預渲染八方向 sprite 的角色與怪物,再用資料檔標出不可走區。Pillars of Eternity 也是整幅背景,Maya 渲成品、深度、法線、顏色四層。

這一點很關鍵:柏德之門的架構,跟我們的角色管線是同構的。 從 Meshy 到 Blender 八方向 sprite,就是那條線。差別只在地面:人家先畫一整幅再切塊儲存,我們是拼零件當場組。再對照生圖工具的能力邊界:整幅構圖是它的強項(HUD 那張參照圖本身就是證據),可拼接的零件是它的弱項(56 件素材的下場)。結論很單純,把 AI 換到它強的那一側。

二、先畫後標

原本的流程是我先排 layout,標好碼頭、泊位、NPC 站位,再請生圖照座標畫。改成:

imagegen 自由生底圖
  → 我目視迭代定稿
  → 照定稿的畫,反向標定遊戲空間資料
     (手塗可走遮罩/重標 spawn、NPC 站位、泊位、出入口)
  → 編譯 walkable 寫回 isle.json
  → 前景切分(樹冠、屋簷、崖壁上緣另存透明層,插進 y-sort)

架構上守住的是不變項:server 的 180×180 walkable grid、碰撞、尋路、怪物區、多人同步,零改動。server 對「畫」零感知,它只看遮罩編譯出來的格子。Gate 1 是一張合成驗證圖,零 code:把底圖候選跟現役的騎士、狼、NPC sprite 用影像工具疊成假截圖,我看角色會不會「浮」在畫上。角色來自 Meshy,底圖來自 imagegen,兩台生成器的風格要咬合。

結果是咬合過了,代價是一個暖調 tint。場景內的角色統一乘上 0xffe3bf,把 Meshy 的冷灰 sprite 拉向暖調底圖。2026-08-04 凌晨 00:08,港區整幅底圖上機(commit 756d594):一張 2x 銳化母帶 webp,1.52MB,96px 羽化邊,manifest 裡寫死世界座標 (3900, 4200) 與 3072×2048 的擺放契約,client 與地圖工具共用這一份真相。

三、全島 24 幅

  • 舊 prop 全剝,609 件歸零。 畫裡都有了。
  • 原本手寫的五條跨區 connector 退役。 縫合改由畫幅本身的語意供給:港村走廊在 c1r2 幅、村東門接廊在 c2r2、溪線縱貫在 seam-wb。
  • 新增一個 ash 分類器。 火山區的落塵灰坪「暗但開闊」,原本 wild-dark 和 volcano 兩個門檻都吃不到,整幅 c3r1 曾被誤判成岩石不可走。新規則:中性近無彩(飽和度 ≤ 0.14)、中低明度的灰面,算可走。
  • 跨幅交接帶補六條 DIRT 接橋多邊形,帶寬至少 220 世界像素。窄了不行:等距 4 鄰接、128px 步距下,窄帶會踩出互不相鄰的棋盤格。這句是實測教訓,寫進了註解。

驗證不是看圖,是跑真實 BFS:walkpoint 斷點 0,六個怪區可達率 81/81、55/72、453/479、178/180、562/586、778/779,全島總可達 10,376 格。還有一個橋咽喉性測試:把橋面禁走,東岸全域必須不可達,證明橋真的是唯一通路。

四、畫和碰撞

在拼 tile 的年代,畫面和碰撞來自同一份資料:tile 是什麼,可不可走就是什麼。整幅母畫上機之後,這個關係斷了。
畫是真相 A。 玩家看到的橋、深溝、岸線、崖壁,全在畫裡。
碰撞是真相 B。 它是 apply_scene_paint.pyclassify_* 這組分類器,從畫的顏色反推出來的:綠的是草、藍的是水、灰的是岩、暗灰的是落塵坪。

兩套真相的縫,就在「反推」這兩個字。分類器只看顏色,不看語意。一條橋在畫裡是木頭色,它可能判成 DIRT 可走,也可能判成 ROCK 牆。一道深溝是暗灰色,落塵分類器一看,暗但開闊,判可走。玩家走過去,掉進畫裡的溝。

29 張場景圖用 Promise.all 並行載入,每張載完就 push 進陣列,完成序取代了 manifest 序。z 序契約「底毯在前、hero 在後」只寫在註解裡,沒落在 code。哪張底毯比港區 hero 幅晚載完,就蓋掉整個港村。單幅時代這個序沒差,24 幅入庫後它變成潛在的地雷。

修法一行:載入完成後按 manifest key 序 sort 回來。

// assetLoader.ts(commit df22d3a):完成序 ≠ z 序,載完後按 manifest 契約排回
const scenePaintOrder = new Map(scenePaintEntries.map(([key], i) => [key, i]));
result.scenePaints.sort(
  (a, b) => (scenePaintOrder.get(a.key) ?? 0) - (scenePaintOrder.get(b.key) ?? 0),
);

五、10 張重畫

一個獨立的 design-agent,24 幅全看,另外對 9 個鄰接熱點做像素掃描量測:岸線的海色偵測、道路的暖亮偵測,再拿 layout 參考圖仲裁偏差歸誰。被退的 10 幅,每一幅都附 pin 值,直接寫進重生成 prompt 的硬幾何約束段。舉兩張:

  • 港村走廊主動線斷路。 村西門交點畫在 y≈6150,村莊畫幅的饋入帶在 5810–5920,差了 250 到 330 像素。兩張圖各自都對,接起來路就斷了。
  • 雙岸線。 底段岸線畫在 4356,真相是 5154,差 798 像素。玩家會看到兩條海岸。

重點是報告裡的一句話:這些 FIX 全是視覺鬼影級,可走性已經被接橋 poly 保住,重畫是為了顏值,不阻擋遊玩。也就是說,蓋章側先用人工多邊形把路接通,畫幅慢慢重生。

還有一條重生成紀律,後來變成整個專案的素材原則:一律回乾淨基底加原始附件重生成,絕不拿上一版輸出當基底。 生圖工具拿自己的輸出再改,會把上一版的錯一起繼承。

結語:質感上了一個檔次

整幅母畫這條路,畫面質感確實上了一個檔次,重複且破碎的 tile 感消失了。但「空間資料整批重標」跟「畫與碰撞不同源」是兩件事,前者是一次性成本,後者是持續。這就是 AI 產線的一個真相:它把生成的邊際成本壓到接近零,品管與一致性的成本一毛都沒少,只是從前面搬到了後面。


上一篇
Day 10|開始蓋島嶼
系列文
血汗同事:我和 Claude Code 僱了一隊 AI,做出 Web MMO ARPG12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言