理想的過程內容生成(Procedural Content Generation,簡稱PCG)演算法是,只需按下一個鍵就生成整個世界。

但那太困難了,所以我們還沒達到那個境界。在本文中,我們提出的思路也許可以讓讀者離那個理想界境更近一步。

PCG是生成一切,包括背景、聲音、劇情的演算法。是個誘人的想法,對吧?

手動製作遊戲世界既費時間又佔用空間。自《Starflight》和《Elite》的誕生之日起,開發者們就致力於使用電腦完成無限的創意。

粗略地說,開發者們依賴PCG的原因有三:

1、使開發者能夠更快地製作內容。

2、使遊戲對玩家產生即時的反應。

3、減少遊戲內容的佔用空間。

我們還發現了一個隱藏的好處:

4、使開發者可以通過實驗產生更多創意。

在本文中,我們將變談論PCG的歷史、問題、解決方法和我們發現的使用思路。我們的使用方法是在2009年開發簡稱為《Aaaaa!》的遊戲和即將完成的簡稱為《Ugly Baby》的遊戲時發現的。

PCG(from gamasutra)

PCG(from gamasutra)

成功使用於遊戲開發的PCG

首先,有證據表明PCG確實可行實用,除了有時候看起來有些像飛行汽車——理論上成立,實際上不實用。

《Rogue》仍然是PCG運用於遊戲製作的絕佳範例。這款大約完成於1980年的遊戲,使電腦在玩家遊戲時生成幻想的背景、隱藏的房間和彎曲的通道,並且將之先製作好的藥水、敵人和武器填充於上述場景中。這種地下城風格的製作方法很成功(《Hack》、《Moria》、《Larn》、《Nethack》、《Angband》、《Dungeon Siege》、《Dungeon Siege II》、《暗黑破壞神》、《暗黑2》和《暗黑3》等等)也相以容易研究,所以許多開發者都用它製作《Rogue》式的遊戲和許多用於《Rogue》式開發的物品。

Temple of Apshai Trilogy(from gamaustra)

Temple of Apshai Trilogy(from gamaustra)

在《Starflight》中,我們可以探索到的許多星系和上百個世界。各個星系都包含大量行星,所有行星都有自己的特徵(遊戲邦注:例如表面溫度、重力、天氣、大氣、水)。

starflight 2(from gamasutra)

starflight 2(from gamasutra)

那時,尤其令人驚喜的是,玩家可以在星球模組中置入任何以上特徵,使星球的自然環境發生變化,如增加曲折的海岸和起伏的高山、填充礦藏(鋁、鉬等)和根據 海拔和星球型別決定生物(固定的和活動的)的密度和種類。原版可以裝入雙面的5.25英寸的軟碟機中。Braben/Bell的經典之作《Elite》 (1984)的著名之處也許在於,創造了相當於8個星系的星球,讓玩家在其中自由飛行和交易。更近的時候,《孢子》展示了程式性模型生成和動畫。在遊戲中,玩家可以調整生物骨頭的長度和周長,增加四肢、眼睛、耳朵、翅膀等等,使創意成為玩法的一部分。

kkrieger(from gamaustra)

kkrieger(from gamaustra)

另外,數年以前,《kkrieger》讓世界震驚了,因為這個射擊遊戲佔用的磁碟空間甚至比本文還少(僅97280位元組)。

PCG的使用貫穿了整個遊戲的歷史,並且今天仍然在使用。

內容製作工具中的PCG

在我們於2009年開發的遊戲《Aaaaa!》中,我們想探索自動完成任務和輔助創意的工具。

Aaaaa!(from gamasutra)

Aaaaa!(from gamasutra)

你是否曾經一畫素一畫素地畫圖?或者,放置儲存單元?這個過程很沉悶,所以開發者們為數字藝術家創造了更好的工具——現在,你只需要移動滑鼠就可以填充那些畫素點、畫一個填充好的矩形、或渲染一段漸變的文字。

自動工具很重要,因為能節省時間——這些工具通常能夠按我們的想法工作。點選畫布上的一個點,然後點選另一個點,你就填充好一個理想的矩形了。

《Aaaaa!》是一款極限跳傘遊戲,跳傘的地方是懸浮在半空中、高樓林立的未來波士頓。我們手動製作了許多內容,在遊戲編輯器裡,我們放置摩天大樓、大梁、走道、標牌、飛車和巨型土豆等。

從技術上說,自動工具不錯,但我們最終厭倦了——做出來的東西開始讓人感覺千篇一律,部分是因為這個工具讓某些任務變得太簡單,而其他的太困難。

例如,在關卡編輯器中放幾座建築再用得分板加以裝飾,這很容易:

building(from gamasutra)

building(from gamasutra)

然而,創造一些更復雜的東西則需要手動放置物件,這種工作太乏味了。雖然我們可以容忍,手動放置物件(也許需要僱傭更多關卡設計師以及花更多時間),但仍希望有一個更好的解決方法,那就是自動化——例如,有一個指令碼可以生成一縱隊得分板,然後我們再把這些得分板拉成我們需要的形式。

下一步是旋轉得分板並按照隨機的曲線路徑放置:

# Create 40 plates in a sinusoidal pattern:

for i in 0..40:

plate.x = sin(i*freq1)*amplitude

plate.y = sin(i*freq2)* amplitude

sinusoidal paths(from gamasutra)

sinusoidal paths(from gamasutra)

真正有趣的時候是,我們開始將很高的頻率填入其中,於是產生了很荒唐的結果:

something ridiculous(from gamasutra)

something ridiculous(from gamasutra)

完全不能玩的東西冒出來了, 但有趣的東西也出現了,這出乎我們的意料。因為我們埋頭於關卡設計,像這樣的東西讓我們的視野煥然一新。

這是關於“過程生成”的關卡的一個小例子,但不止一次,我們使用像這麼簡單的指令碼做出了令我們開心的東西。這些改變了我們創造關卡的方式,對玩家提出了新挑戰。

不久,我們有了一系列用於創造我們所謂的“關卡骨骼”的通用指令碼。我們大量使用這些指令碼製作讓我們滿意的東西(“啊!我們從來沒想過這種事會發生。真是太棒了!”),然後手動完成關卡的剩餘部分。

hand-creat the rest of the level(from gamasutra)

hand-creat the rest of the level(from gamasutra)

《Aaaaa!》的表現不錯,除了其他獎項,還讓我們獲得了獨立遊戲節提名。所以,我們有了信心,決定在我們的下一款遊戲中大膽嘗試,我們將PCG用於所有的關卡設計。但畢竟,我們還算是體面的程式師,這麼做未免太過簡單了?

PCG是萬靈藥

我們的下一款遊戲《Ugly Baby》玩起來很像《Aaaaa!》,但我們想讓它在執行時,根據玩家提供的媒體,通過演算法生成所有的關卡結構。這個媒體可以是音樂、視訊甚至是一段《獨立宣言》。我們將遊戲形容為:

“與你最喜歡的鼓和貝司作戰,或自由飛行於舞曲唱片的旋律之中。《Ugly Baby》用你的MP3音樂創造一個漂浮的世界,邀請你來戰鬥。”

我們的希望曾是(現在仍是),PCG可以讓我們:

1、生成(本質上)無數的有趣的關卡,且比手動生成的《Aaaaa!》關卡更特別。

2、在執行時,根據玩家自己的媒體生成所有關卡內容。

3、允許玩家通過調整“關卡DNA”參與世界創造的過程,產生他們自己的關卡。

我們想製作一種可以讀取音樂的指令碼,然後產生類似《Aaaaa!》且帶敵人的關卡。本以為我們對漂浮建築和生成藝術的理解可以讓這個過程變得很簡單,然而,在我們才開始基礎工作以前,一個9個月的專案已經變成了24個月。

結果,我們發現了四個主要問題:

1、手動製作的優勢是PCG的弱點。

演算法和手動製作的內容往往優勢互補。如果你要製作大量的山體,且想看看經過侵蝕後的樣子,那麼演算法比手動調整好。雖然你可以也花上一整天的時間自己動手雕刻,再花一點時間在侵蝕工具中執行,但手動操作會讓你嘗試到一些不同的東西。

另一方面,如果你想在山洞的入口處增加一些樹木,手動完成通常比演算法簡單。如果你想在沙灘上寫“HELP”,手動選擇工具然後寫字更容易些。我們認識到這點是經過了艱難的過程:

第1步:寫一個生成關卡框架的演算法。(1小時)

第2步:測試後發現建築物之間的距離太遠;增加密度,還使了一點小技巧防止它們重疊。(15分鐘)

第3步:測試後發現完全行不通;修改路徑使建築物迂迴一些。(15分鐘)

第4-9步:執行、重複。(各15分鐘)

第10步:若上述方法行不通,手動調整反而更簡單。(5分鐘)

所以,我們陷入了一種工作模式,先寫了一個不錯的指令碼,無盡地調整,然後達到區域性最大值:初始指令碼可以生成一種型別的指令碼佈局,我們花了數個小時尋找其中最好的排列,而不是檢查其他地方。我們隨後發現“無聊”和“有趣”之間的差距很小,但要實現二者之間的跨越,即對演算法做實驗或在演算法中擴充有趣的細節可能很費時間。

解決方法:有時候,實驗最好是手動操作;我們可以從製作的東西中吸取經驗教訓,然後用演算法模擬手動做出來看上去不錯的東西。

2、製作重複的內容很容易。

從Alex Norton的帖子AltDevBlogADay中借用一段話:

為什麼世界有邊界?程式生成程式碼在過去25年並未改變多少。人們仍然侷限於使用碎片、方塊和斑點做所有東西,這也太雷同了,簡直就像程式生成的內容。對於許多程式師而言,看起來其實就是程式生成的。

如果我們不認真看,我們不會發現我們一次又一次看到的東西都是相同的。例如,在《Ugly Baby》中,玩以下關卡前15秒還覺得有趣,之後就無聊了:

Ugly baby (from gamasutra)

Ugly baby (from gamasutra)

因為每道關卡大約是5分鐘長,這意味在整個關卡過程中,我們得切換好幾次。一個常用的解決方法是,定期地置換出不同的演算法,但那樣做可能比較不諧調——想象一下在邊界上突然中斷的PCG森林。另一個解決方法是擴充演算法,在沿途放上新花樣,使不同物件之間漸進地改變,但我們又遇到問題了,這也是下文要說到的。

3、PCG演算法變得更加有表現力,但複雜的演算法看似越來越與設計脫節。

生成簡單關卡的演算法很容易寫——但因為我們做的東西更復雜了,執行也變得更加困難,並且這種困難是不成比例的。例如,在《Ugly Baby》中,起初我們成功地寫出在地圖上分散建築物的指令碼。然而,擴充指令碼時發生了以下對話:

“看起來很棒;如果我們將這些聚為群集會怎麼樣呢?”

“好吧,但太規律了,還要混雜一點。”

“新增一些得分板、隧道、移動的平臺和扇葉。”

“哎呀,得分板和建築物交叉了。移開一點。將扇葉主在隧道的中間,除非當時還有移動的平臺,或如果之前隧道中間就有扇葉,因為那樣太無聊了。”

“好吧,現在,什麼都不管用了。”

相同地,看到3D角色在草地上來回走動,遊戲開發新手會說:“嘿,我會做MMORPG了,真簡單!”我們認為,如果簡單的指令碼能產生有趣的結果,我們只需要不斷地用適當的東西擴充演算法就行了。但是,有趣的建築物開始需要維持機制和認真設計的其他內容。

4、有趣的PCG通常產生的結果是1)愚蠢的和2)無聊的內容。

我們見過的最簡單的隨機名稱生成器如下:

# Generate random letters, yo:

for i in 1..random_number():

name += random_ character()

它有可能生成任何你能想象得到的幻想的、科幻的或者寶寶的名字——假如讓它重複足夠的次數,它有可能輸出“Captain Rock McSpectacular”這樣的了不起的名稱。但更多時候,它產生的是像“ergihwe`=-ufaw38o72wenufse”這樣對你完全無用的垃圾。

在《Ugly Baby》中,我們做了類似的東西——一個相對不受約束的演算法,產生隨機方塊,然後圍繞中心軸產生一個簡單、可玩的隧道。這個演算法過程如下:

1、從庫中選擇一個隨機3D模型(方塊、三角形、曲線)。

2、選擇一個數字N和一個數字M。

3、創造N個平均間隔的環狀模型,各個環狀中有M個模型實體。

4、返回第1步。

在第一次計算中,結果是:

Ugly Baby(from gamasutra)

Ugly Baby(from gamasutra)

看上去很有趣,確實有用——它是個隧道!再次執行相同的指令碼,生成了一個有趣的序列,有毛髮、手指樣的東西,伴隨著另一個隧道:

Ugly Baby 2(from gamasutra)

Ugly Baby(from gamasutra)

也很有趣,出人意料,仍然有用。玩家可以在其中飛行。第三次計算:

Ugly Baby 3(from gamasutra)

Ugly Baby(from gamasutra)

這次產生了一個完全不能通過的路徑,組成的多邊形太多,幀速率跟不上。雖然很漂亮,也許有些時候能用上,但沒有形成隧道。

解決方法之一是,限制引數。在剛才那個命名生成器的例子中,也許我們可以構思一句語法(子音+母音+子音+母音+子音),或儲存一份常用的姓氏和名稱,然後把它們串起來(例如“Billy Margaret Smith”)。類似地,在我們的隧道例子中,我們大概只需要建立一些用不多於某個數量的方塊組成的隧道,或限制多邊基本方塊的數量。

另一個問題是:

結果,我們挑選了一些有趣的分支,有可能讓我們自己都感到驚喜(再見啦,Captain Rock McSpectacular)。

我們很快用特例補充好演算法,但演算法無法執行。

構成關卡的模組化方法

在我們開發《Ugly Baby》的三年時間裡,我們並沒有解決所有問題,但我們用簡單的、模組化的概念組合成複雜的東西時,我們收穫了一些成功的經驗。

Ugly Baby (from gamasutra)

Ugly Baby (from gamasutra)

以程式設計的方式為《Aaaaa!》製作關卡骨骼時,我們成功了,這讓我們感到愉快,所以我們返回檢視根源。製作看上去有趣的東西其實相當容易,就是重複簡單結構。舉例說明,一個由方塊組成的球體:

這一次,不是把演算法變得複雜,而是重複修改輸出的結果:

1、生成方塊組成的球體。

2、給塊賦予顏色(如,調整飽和度,然後根據物件圍繞中心軸的位置來選擇色相)。

3、改變方塊的穿插。

4、只具現化在球體的某個部分的物件。

最終的結果完全不像個球體:

Ugly Baby (from gamasutra)

Ugly Baby (from gamasutra)

這個簡單的概念讓我們感興趣的有三點:首先,我們可以填入稍微不同的引數,就得到非常不同的幾何體;其次,在《Ugly Baby》中,我們可以把這些引數與音訊關聯,這樣關卡的外觀就會隨著音樂改變;最後,修改路徑獨立於基礎結構,所以我們只需對一些方塊使用上述方法就可以得到完全新奇而(也許)有用的東西。

有希望,所以我們把這個辦法規範了一下。《Ugly Baby》的關卡生成器由三個模組組成:

1、定序器是產生球體、柱體、網格、圓柱等東西的模組。

2、選擇器返回滿足一系列條件的所有物件,如位於飛機的某一側,或大於麵包箱的所有物件。

3、更改器根據物件的特點使之發生變化。例如,根據位置改變顏色或根據方向縮放比例。

以上三個模組生效的結果如圖:

step 1(from gamasutra)

step 1(from gamasutra)

第1步:玩家會沿著線性路徑飛行,所以我們先簡單地製作一個柱體,其組成的方塊沿著落下的中心軸。

程式碼:

# Instantiate the column:

sequencer_column = sequencer.Column()

queue = sequencer_column.iterate()

step 2(from gamasutra)

step 2(from gamasutra)

第2步:這個關卡隧道有點像鐵路射擊遊戲,所以我們再做一個更像隧道的隧道。我們將簡單柱體置換出六個。這需要設定一些基本的引數,如方塊與方塊之間的垂直距離和圍繞著中心軸的方塊柱體的數量。

程式碼:

# Instantiate the cylinder:

sequencer_cylinder = sequencer.Cylinder(layer_delta=4, blocks=6)

queue = sequencer_cylinder.iterate()

step 3(from gamasutra)

step 3(from gamasutra)

第3步:改變定序器生成的各個方塊的大小。

程式碼:

# Change every piece’s scale:

mutator.scale(queue, [1, 4, 1])

step 4(from gamaustra)

step 4(from gamaustra)

第4步:我們寫了一個更改器,它可以使方塊的一個面朝向Z軸,然後把這個屬性賦予所有方塊。

程式碼:

# Turn pieces to face the player’s falling (z) axis:

mutator.face_axis(queue)

step 5(from gamasutra)

step 5(from gamasutra)

第5步:一個“Every-N”的選擇器節點可以把填入其中的每N塊抓出來。這裡,我們希望每四個塊選擇一次,然後用更改器將其變為紅色。

程式碼:

# Get a list of every 4th pieces that comes into the queue:

every_4th_piece = selector.every_n(queue, 4)

# Turn those pieces reddish:

mutator.set_color(every_4th_piece, [255, 32, 0])

step 6(from gamasutra)

step 6(from gamasutra)

第6步:最後,在垂直距離上調整為曲線方向。

程式碼:

# Pan from -45..45 depending on a piece’s position along the player’s falling axis:

mutator.cyclic_rotate(queue, freq=0.1, low=[-45, 0, 0], high=[45, 0, 0])

從這裡開始,我們可以做到以下事情:

1、因為這些效果是各自分離的,我們可以將物件置換進或置換出。這種模組化操作可以幫助我們嘗試新的東西和新的圖案。

2、《Ugly Baby》的畫面與音樂相關。我們知道在音樂播放到某個點時玩家的所在,我們可以根據這一點,看音訊訊號構建物件。

例如,在音樂的高音部分壓縮隧道或根據聲音訊號的高頻部分改變塊的顏色。

3、我們可以允許玩家調整某些值來創造他們自己的關卡。比如,他們不想要6根柱體,而要2根呢?或者,如果他們想讓方塊更肥大一點呢?

更改器引數上的小調整引起的關卡變化並不大。這裡,我們只演示了柱體的數量和大小,以及組成柱體的基礎模型:

columns(from gamasutra)

columns(from gamasutra)

我們又做了一些變體(如,下圖左邊的網格狀的圖案),然後組合起來(下圖右邊的網格再加上環狀物)。

more abstract things(from gamasutra)

more abstract things(from gamasutra)

對於抽象物體,這個方法很管用,但我們還想看看對於更有組織的結構,這個方法是否行得通。

用於有機模型製作的模組化方法

在《Ugly Baby》中,我們用更有機的模型補充抽象的幾何關卡設計。也就是說,我們在Maya中創造了一個影像的、基於節點的東西,叫作DING。以下是製作一隻昆蟲的過程。

table1(from gamaustra)

table1(from gamaustra)

我們首先生成一個圓柱。上圖是兩張截圖——第一張是顯示了圖解(左邊的節點創造了一個圓柱;右邊的節點顯示圓柱)。

第二張圖顯示了結果模型。DING創造幾何體,Maya顯示幾何體(之後將它輸入有檔案格局(FBX))。

table2(from gamasutra)

table2(from gamasutra)

之後我們把圓柱調整成近似錐形體,有些地方做得肥大一點,有些地方做得瘦小一點。上圖所示的是錐形體的側檢視——我們手動調整,先擠出胖的部分,再縮小面,再擠出細長的腳。

table3(from gamasutra)

table3(from gamasutra)

我們翻轉節點(紅色),將其調整成垂直方向,然後新增“環”節點,這樣就做成了帶6足的環狀物。

table4(from gamasutra)

我們再新增球體結點,然後合併(布林合併結點)到環狀物上。這個東西有些像蜘蛛了。

table5(from gamasutra)

table5(from gamasutra)

在我們做尖尖的腳以前,要新增整個物體的輪廓線結點。這條輪廓線夾在整個模型的中間。我們還要對模型再細分,使它更平滑一些。

table6(from gamaustra)

table6(from gamaustra)

最後,沿著垂直軸壓低前部。這東西現在看起來有點像一隻遍蝨。

因為模型不是在執行時生成的,所以它不會對音樂產生反應,而這是關卡構成的方式。但它確實達到了我們的兩個目標,一方面它使我們能夠快速製作有趣的內容(這個方法對我們團隊當中不熟悉Maya的成員很管用),另一方面,它使我們可以做大量實驗。因為所有這些操作都是無損的,所以我們可以增加或減少環狀物的足數目,改變周線外形(如下圖所示),或甚至置換出柱體或球形基本體。下一步,我們要做的是隨機化。

ticks(from gamasutra)

ticks(from gamasutra)

值得注意的是,基於圖解的工具用途很多。我們對《Ugly Baby》的關卡做抽象處理,然後實體化敵人,但在2011年的《Aaaaa!》,我們想要更寫實的材質,因此我們使用了Spiral Graphics出品的基於節點的Genetica(遊戲邦注:這是一個製作無縫材質的編輯器)。

結論

雖然“模組化”的方法並不算新鮮,且聯絡節點也沒有解決所有關卡生成的問題,如交叉物件或無意義的輸出,但它幫助我們提高了效率,使我們在挑出無用的部分後仍然能得到有用的輸出結果。

同樣地,我們能夠利用PCG的優勢(更快的內容生成、動態內容、更小的佔用空間和增加創意)同時迴避它的缺點(避免大量程式碼,使程式碼便於管理等)。雖然PCG不是萬靈藥,但我們認為我們可以利用它成功地製作有趣的內容。

我們不是PCG的專家,但我們希望本文能給讀者提供有益的參考。

via:遊戲邦/gamerboom.com