AI 教學 12 min read

AI 生遊戲地圖為什麼亂?先排關卡拓撲:循環、鑰匙與鎖的產生器實測

關卡拓撲是先決定區域怎麼連、玩家怎麼走,再畫地圖外觀的設計方法,源自《Unexplored》的 cyclic generation。我把它做成網頁產生器,和「只給數量」的隨機地圖對照:拓撲版 1,500 組測試 0 失敗,隨機版 500 張有 211 張走不到 Boss——給 AI 地圖要先給連線,不是只給數量。

AI 生遊戲地圖為什麼亂?先排關卡拓撲:循環、鑰匙與鎖的產生器實測
本文目錄 · 8
懶人包
  • 只跟 AI 說「做一張 30 區的地圖」,等於只給數量、沒給動線。我用程式模擬這種做法跑 500 張:211 張走不到 Boss,只有 93 張每一區都去得到。
  • 關卡拓撲先決定「區域怎麼連」,再決定「長什麼樣子」。理論來自 Joris Dormans 在《Unexplored》(2017)用的 cyclic generation:用循環、鑰匙與門、單向通道組關卡。
  • 我做了一個產生器,順序是主線 → 循環(鑰匙環、替代路線、單向捷徑)→ 支線,再用模擬玩家的方式驗證整張圖走不走得通。
  • 1,500 組測試 0 失敗:Boss 一定走得到、每一區都去得到,而且把鑰匙拿掉後 Boss 就到不了,代表每道鎖都真的擋路。
  • 產生器最後輸出一份把每一區、每條路都寫死的結構化提示詞,讓生圖 AI 只負責畫美術。這一步還沒實測生圖結果,文末照實列出。

Sosuke Guo 在 Threads 分享過一段心得:以前叫 GPT「做一張有 30 個怪物區域的地圖」,成果都不好;後來 GPT 提到「關卡拓撲設計」,他才發現地圖要先規劃路線、區域之間怎麼連、玩家怎麼探索,先把最基礎的關卡圖建起來再往上擴充(原文)。

這段話點出一件事:數量是規格,動線才是結構。我把這個想法做成一個網頁產生器,左邊模擬「只給數量」,右邊「先排拓撲」,再用測試把兩邊的差距量出來。線上可以直接玩:關卡拓撲產生器。

只給「30 個區域」的地圖會缺什麼?

「30 個區域」只回答了「有幾個」。它沒說起點在哪、Boss 在哪、哪幾區之間有路、玩家先去哪裡、什麼東西擋住進度。這些全部留給生成的那一方自己猜。

為了把差距變成數字,我寫了一個對照組來模擬這種做法:在畫面上隨機撒 30 個點,每個點連到最近的 2 個鄰居,第一個點當起點、最後一個點當 Boss。看起來路很多、很熱鬧,但沒有人設計過動線。

下面是線上產生器在 seed 2 的畫面。左邊這張走不到 Boss,整張圖斷成兩塊;右邊從左往右難度遞增,兩道上鎖的門、一條藍色虛線的單向捷徑都看得到:

關卡拓撲產生器 seed 2:左邊只給數量的隨機地圖走不到 Boss,右邊先排拓撲的地圖有主線、兩道門鎖與一條單向捷徑

跑 500 個 seed(每張 30 區)的結果:

指標只給數量(對照組)先排拓撲
走不到 Boss211 / 5000 / 500
每一區都去得到93 / 500500 / 500
平均斷成幾塊2.591
相鄰兩區難度最大落差(1 到 5 級)3.141.09
對照組是我用程式模擬的「只給數量」,不是 GPT 實際畫出來的圖。它量到的是「沒有結構時會發生什麼事」,不是在評某個模型。

關卡拓撲是什麼?跟畫地圖差在哪?

拓撲只管「誰跟誰相連、要怎麼通過」,不管距離和外觀。同一張拓撲可以畫成森林、地城或太空站,玩家的體驗骨架都一樣。

遊戲設計裡把這件事做得很完整的,是 Joris Dormans 為 2017 年的《Unexplored》寫的關卡產生器,通稱 cyclic dungeon generation。Boris the Brave 的拆解文整理了幾個重點:

  • 每層的骨幹是一個從起點到目標的大循環,起點和目標把循環切成兩條弧,兩條弧可以放不同的障礙。
  • 《Unexplored》有 24 種主要循環型別,大部分會擺放鑰匙、門和單向通道(文中叫 valve)。
  • 主循環之後再加小循環,當成主線旁邊的短繞路。
  • 整個產生器靠約 5,000 條「圖形替換」規則,從抽象節點一步步換成具體的房間、敵人和謎題。
另一個關鍵概念是鑰匙與鎖。Boris the Brave 在 Lock and Key Dungeons 裡把它抽象化:鎖是任何擋住進度的東西,鑰匙是任何讓進度往前的東西。道具、能力升級、密碼、劇情旗標都算。

循環比樹狀結構好的地方在於,玩家不必走進死巷再原路折返。Tommy Thompson 在 Game Developer 的介紹文裡舉了門鎖的例子:A 路被鎖住,玩家只好走 B 路,在 B 路上撿到能開門的鑰匙。

我的產生器怎麼排拓撲?

《Unexplored》的 24 種循環和 5,000 條規則我沒有照搬。產生器只取三種最常見的循環,重點放在「排的順序」:

主線
起點 → Hub → Boss

循環
鑰匙環 / 替代路線 / 單向捷徑

支線
剩下的區域做成寶物死路

模擬玩家
撿鑰匙、開門、走捷徑

分難度、排版面

輸出結構化提示詞

  1. 主線:先拉一條從起點到 Boss 的路,長度約為總區域數的 30%(至少 5 區)。主線三分之一處設為據點(Hub)。
  2. 循環:依序加入三種循環,數量由「循環數」滑桿決定。
  3. 支線:還沒用完的區域接成 1 到 3 區的死路,尾端放寶物,讓走進死路有回報。
  4. 難度:用「離起點幾步」切成 1 到 5 級,Boss 固定 5 級。
三種循環的設計如下:
循環怎麼接玩家體驗
鑰匙環主線後段某扇門上鎖,鑰匙放在前段繞出去的一條環上被門擋住,回頭繞一圈拿鑰匙
替代路線從主線某點分岔,繞 2 到 4 區後,接回主線往前 2 到 3 步的位置短路直衝,長路有寶物
單向捷徑從主線後段連回 Hub,只能從後段那端打開推進到後面後,回補給不用整條走回去

怎麼確認每道鎖都真的擋路?

拓撲排好不等於能玩。產生器裡有一個 solve(),用最單純的方式模擬玩家:能走的路都走,撿到鑰匙就能開對應的門,捷徑只能從遠端那一頭通過,一直重複到沒有新區域可以去為止。

測試對每張圖檢查四件事:區域數正確、Boss 走得到、每一區都去得到,還有最容易漏掉的一條——把某把鑰匙拿掉之後,Boss 必須到不了。如果拿掉鑰匙還是走得到,那道鎖就只是裝飾。

// 每道鎖都必須真的擋路:沒有鑰匙時 Boss 到不了
for (const e of t.edges.filter((x) => x.kind === 'lock')) {
  const noKey = { ...t, nodes: t.nodes.map((nd) => (nd.type === 'key' && nd.key === e.key ? { ...nd, type: 'zone' } : nd)) };
  if (Topo.solve(noKey).bossReachable) errs.push(`lock K${e.key} bypassable`);
}

這條檢查抓到過兩個 bug:

  • 替代路線的跨距剛好越過上鎖的門,玩家從旁邊繞過去,鎖形同虛設。
  • 鑰匙環接回主線的位置,剛好跨過另一道鎖。
兩個都是同一種問題:一段繞路跟一道鎖的範圍重疊。修法是記錄每段繞路的起訖位置,加鎖和加繞路時都檢查不能重疊。

修完後跑 500 個 seed,每個 seed 測 12、30、60 區三種大小,每張 4 個循環:

$ node test.mjs
cases=1500 failures=0
topo  n=30 avg: loops=3.68 deadEnds=6.00 maxTierJump=1.09 bossUnreachable=0/500
naive n=30 avg: loops=11.74 deadEnds=0.00 maxTierJump=3.14 bossUnreachable=211/500

循環數比較多,為什麼反而比較差?

看數字會發現對照組的循環比較多(平均 11.74 對 3.68)。這些循環是「連最近的 2 個鄰居」順手產生的三角形,相鄰幾個點互相連起來,沒有一個是為了讓玩家繞路拿鑰匙而存在。

死路也一樣。拓撲版平均有 6 條死路,對照組是 0。拓撲版的死路是刻意做的支線,尾端放寶物;對照組沒有死路,是因為每個點都至少連了兩條線,不代表動線比較好。

所以判斷一張關卡圖,我看的是「每個循環、每條死路有沒有用途」,而不是數量本身。這也是只給數量會出問題的原因:數量對了,用途全靠運氣。

排好的拓撲怎麼交給生圖 AI?

產生器最後一步把拓撲轉成英文提示詞,列出每一區的角色、難度,以及每一條路是普通道路、上鎖的門還是單向捷徑。下面是 12 區、seed 2 的輸出:

Top-down game world map, theme: Japanese night village in a forest, lanterns, sakura. Follow this level topology EXACTLY; do not add or remove roads.
Progression flows from left (easy, tier 1) to right (hard, tier 5).
Zones:
- Z1: 起點 / tier 1
- Z2: 據點(Hub) / tier 1
- Z3: 怪物區 / tier 2
- Z4: 怪物區 / tier 3
- Z5: Boss / tier 5
- Z6: 怪物區 / tier 1
- Z7: 鑰匙區 / tier 2 (holds key K1)
- Z8: 怪物區 / tier 2
- Z9: 寶物區 / tier 3
- Z10: 怪物區 / tier 4
- Z11: 怪物區 / tier 4
- Z12: 寶物區 / tier 5
Roads:
- Z1 <-> Z2
- Z2 <-> Z3
- Z3 <-> Z4
- Z4 <-> Z5: gate locked by key K1
- Z1 <-> Z6
- Z6 <-> Z7
- Z7 <-> Z2
- Z2 <-> Z8
- Z8 <-> Z9
- Z9 <-> Z10
- Z10 <-> Z4
- Z4 -> Z2: one-way shortcut back to hub
- Z4 <-> Z11
- Z11 <-> Z12

這份提示詞的分工很清楚:結構由程式決定、由測試保證;生圖 AI 只負責「長什麼樣子」。在網頁上調區域數、循環數,按「換一張」直到動線順眼,再按「複製」把提示詞帶走。

這個做法目前的限制

  • 生圖那一步還沒驗。這次沒有把提示詞實際丟給生圖模型,所以還不知道模型會不會照著每一條路畫。圖片模型對「Z4 跟 Z5 之間有一道門」這種精確連線的服從度,需要另外做 A/B 對照才能下結論。
  • 對照組是模擬的。「隨機撒點、連最近鄰居」代表的是沒有結構的極端情況,真實的 AI 生圖可能更好,也可能有其他種類的錯。
  • 循環型別是簡化版。只做了三種,沒有《Unexplored》那種循環套循環、多把鑰匙開一道門、Boss 守門之類的變化。
  • 版面是分層排列。座標只是為了好讀,從左到右照步數排開,不是真的地形配置。要接到 RPG Maker 或 Tiled,還要另外寫一層把拓撲轉成 tilemap。

這招還能用在哪裡?

「先把結構用程式定死,再請 AI 補外觀」不只適用遊戲地圖。流程圖、UI 頁面之間的跳轉、課程大綱的先修關係,本質上都是「誰接誰、什麼條件才能往下走」的圖。這類東西交給 AI 之前,我會先問自己一句:我給的是數量,還是連線?

想自己動手的話,先開線上產生器按幾次「換一張」,比對左右兩邊的指標,再把右邊的提示詞丟給你慣用的生圖工具試試看。

author
陳
陳彥彤

AI 工程師 · AI 顧問。Java 後端 8 年、AI 工程師 2 年。AI 內訓 · AI 導入顧問 · 前後端與雲端培訓。

support

覺得文章有用可以到 GitHub 給個 star,或是透過信箱聊聊 AI 內訓、AI 導入顧問或前後端 / 雲端培訓。

related

相關文章