你好,我們收到了很多關於地圖生成方式的詢問和建議。但如果不先解釋噪聲這個概念,就很難闡述地圖是如何生成的。因此我們需要討論噪聲,因為它們是遊戲的關鍵部分,但我認為我們之前沒有很好地解釋它們實際上是什麼,首先,不要從字面上理解這個概念。我們將來會再次探討星球生成,但現在我們將介紹它的基本概念,以此作為稍後的入門知識。
什麼是噪聲表達?
“表達” 部分
在製作異星工廠時,我們需要決定將哪些內容和物品放置在何處,需要解決的問題只是 X 和 Y 位置。由於地形生成器無法獲知任何已放置的內容,因此需要一些代碼將 X 和 Y 轉換為要放置的圖塊類型,以及需放置的樹木、岩石、資源、裝飾品、懸崖或敵人。
地圖中間 X 和 Y 都為 0 的地方是原點和起始位置。我們希望這裡是陸地,否則你就會被困在水中。我們可以計算距原點的距離,得到一個距離“錐體”,並用它來製作一個圓形島嶼,其中高於某個值的所有東西都是陸地,否則都是水。
(以上圖片未按比例繪製。)
我們並不需要同時更改兩個圖塊,只需要確保我們想要的圖塊出現在我們想要它出現的地方。例如,土地的“權重”始終可以為 1,而當我們希望水出現時,水的權重可以高於 1。
如果我們可以在調用距離函數之前將值添加到 X 和 Y 座標,那麼它會將圓錐體移動到不同的位置。偏置圓錐體可用於製作新的島嶼、為現有島嶼添加一部分面積,或反轉新的圓錐體以從島嶼中取出一大塊面積。
我們還使用了很多算術運算符,例如絕對值、模、指數和三角函數。三角函數可用於旋轉位置,而不僅僅是偏移,這是設計烏爾肯星球的主要工具。
我們可以將這些操作結合起來,例如:test_1 = A + B * C。一個噪聲表達式引用另一個噪聲表達式,例如:test_doubler = test_1 * 2。如果你願意,你可以用它製作一個有趣的麥田圈圖案,但這對於製作自然景觀來說並不是最佳方式。為此我們需要一些噪聲。
“噪聲” 部分
在地形生成中,噪聲並不意味著聲音,它意味著隨機數。
在生成隨機數時,最基本的設置是每次需要時生成完全隨機的數字,這稱為不相干噪聲,與其他點沒有任何關係。如果縮放,這個效果將更隨機,用處非常有限。
相干噪聲則不同。它充分利用了 X 和 Y 座標,使附近的位置具有相似的值。這意味著當你在景觀上移動時,事物會平穩且連貫地變化。
我們使用的主要相干噪聲是來自 FFF-112 (https://factorio.com/blog/post/fff-112) 的基礎噪聲(一種Perlin風格噪聲)。輸出值最終得到近似的特徵尺寸,如果縮小,它與不連貫的噪聲無法區分,但如果放大(使特徵更大),那麼一切都會變得平滑,直到幾乎平坦。
我們可以使用一些大尺寸的噪聲來表示大陸,使用中等尺寸的噪聲來表示島嶼和海灣,以及使用較小的噪聲來進一步分解海岸線,這是分形噪聲背後的基本邏輯。不同大小的多個級別添加在一起,較小的層的影響逐漸減小,因為它們向某些區域添加了較小的細節。
下一種噪聲類型是點噪聲。它在景觀上創建了許多具有一定間距的點,這就是我們通常用於資源放置的方式。每個點實際上都是一個圓錐體,因此我們可以在每塊地的中間擁有更高的豐富度。然後通過添加一些基礎噪聲來擾動資源錐,因此它不是一個完美的圓圈。如果您想了解有關點噪聲的更多信息,我建議您查看 FFF-258 (https://factorio.com/blog/post/fff-258)。
左:在添加噪聲以破壞圓圈之前的瑙維斯資源錐。右:瑙維斯地圖,如您通常看到的那樣。
把它們放在一起
對這些噪聲的控制,並找到有效使用這些噪聲所需的算法很重要。比如說: 點噪聲不僅可以用於資源,還可以用於火山。在海拔上添加了巨大的點錐體來製作主火山體,還可以倒轉圓錐體並“縮小”尖端,將山峰倒轉為熔岩坑。
倒置至關重要,如果你想要一張主要是水的地圖,你可以降低海拔,但這往往會形成島嶼。如果你想要一張主要是水域的地圖,但仍將大部分陸地連接起來怎麼辦?
為此,我們可以使用絕對值將任何負值反彈為正值。如果我們然後將其反轉,所有值都是負值,少量添加值會導致一條窄帶進入正值區域。這使得一系列狹窄的陸地路徑幾乎總是相連的,我們在烏爾肯星球熔岩區域使用這種模式來創造一條穿過迷宮的路。
噪聲工具
在加入 Wube 公司之前,我在為我的太空探索模組開發新行星(現在仍然如此)。在太空探索遊戲中,每種行星類型都會有獨特的地圖生成模式,有點像太空時代中的行星。製作 1 個全新的行星是一項巨大的工作量,因此當開始製作 14 個以上的新類型時,我決定製作一些新工具以使過程更容易。我創造了屬於我自己的一套噪聲工具,它可以做很多事情:
⚙ 自動清潔功能,測試的速度更快。
⚙ 行星切換預設功能,能夠方便預覽其他行星。
⚙ 添加臨時調試滑塊的便捷內聯方式,以便可以從地圖預覽屏幕調整噪聲的數值。
⚙ 最後但並非最不重要的一點是,能夠使用地圖預覽直接可視化噪聲輸出。
噪聲可視化的最後一項很重要。沒有它,系統將無法進行數據的輸出。舉例來說,在添加了一些新代碼後,這些代碼應該使地圖上的一些沙子從黃色變為紅色。但你沒有看到任何紅沙,什麼地方出了錯?可能紅色的區域恰好在水和草下,而不是沙子下。但更有可能的是,如果你探索了一大片區域但仍然沒有找到任何東西,那麼它在某個地方被破壞了,但是在哪裡呢?
噪聲可視化工具允許我們選擇特定的噪聲並將其轉換為預覽屏幕中的圖像。這樣我們就可以看到噪聲的分佈和輸出值之類的東西,比如,如果範圍很大,你需要走 10 公里才能發現任何差異。或者輸出範圍太低,但不會大到足以做出改變,或者一個值可能意外變為負值,與另一個負值相乘並導致其他一些意想不到的問題。
左:瑙維斯地圖,如您通常看到的那樣。右:瑙維斯的海拔。藍色表示海拔低於零,深色值表示深度。黃色表示海拔很高,綠色表示非常高,用於區分水和懸崖。
當處理諸如多個生物群系時,如果僅依靠圖塊變化作為指標,可以看到從一個生物群落到另一個生物群落時發生了變化,但無法判斷變化的速度。通常,柔和的變化率會更好,因此隨著天氣變得乾燥,植被會逐漸消失,但通常無法提前看到這一點。使用可視化工具後,可以判斷生物群系過渡是硬還是軟,它可以顯示多達 255 個不同的值並顯示漸變。
左:烏爾肯星球的海拔。藍色表示海拔低於零,深色值更深。黃色高,綠色非常高。右:烏爾肯上的溫度。黑色表示寒冷,紅色表示溫暖,黃色表示熱。用於放置熱圖塊集。
它的工作方式仍然有點粗糙,因為它作為模組運行,而不是遊戲引擎的一部分。本質上,它獲取遊戲中的所有圖塊,將其地圖顏色更改為從黑色到白色的不同值,或藍色>紅色>綠色,然後它重新分配這些圖塊,使其僅根據可視化的噪聲表達式出現在某個位置。使用藍色表示低於零,其他色調錶示高於零,這使得零線過渡非常清晰,這很重要,因為那往往是我們的海岸線。它在新星球的早期階段或調試階段的作用很大。
所以很自然地,當我開始在 Wube 公司製作行星時,我更新了我的工具,這樣我就可以將它們用於太空時代。我非常有信心地說,使用這些工具,讓我在行星上的工作速度可以提高大約 10 倍。
不僅如此,根尼斯和我一直在研究一些奇特的新噪聲函數,稍後我們將與新行星一起揭示這些函數,新功能有很多設置選項。事實上,我懷疑如果不通過某種方式瞭解我們正在做的事情,我們是否能夠完成新系統的所有功能。當擴展包發佈時,我將發佈 Noise Tools 噪聲工具 2.0。
C++ 中的地圖生成
當第一次合併烏爾肯地圖時,我們注意到啟動遊戲和生成行星地形時速度顯著減慢。雖然這對於玩家來說只是一個輕微的不便,但是每天多次啟動會讓這個問題變得非常明顯。因此,我們要麼必須放棄花哨的地圖生成,要麼讓它更快。
地圖生成可以在多個線程中運行,我們嘗試優化 SIMD 執行的代碼(單指令、多數據),但地圖的運行已經足夠高效,這與我們對烏爾肯星球的觀察結果相符。問題一定出在其他地方。當我越來越深入地研究噪聲表達時,我發現了一些需要改進的地方。
在 C++ 中,噪聲表達式的具體表現為數學運算的抽象語法樹 (AST)。每個噪聲表達式都是一個存儲其子項的類實例。如果您不知道這意味著什麼,只需將其想象為一個可以容納其他袋子的袋子即可。它們可以組合和嵌套,直至達到硬件限制。該結構是根據 Lua 語言對應結構而構建的。創建畫面時,噪聲表達式將編譯為噪聲程序。一般來說,每個NamedNoiseExpression都是程序中的一個過程。過程很有用,因為我們可以在多個步驟中重複使用數據。該過程包含具有已解析的噪聲操作的線性序列,因此可以保證後續操作的子項已被計算。當您需要所有數據時,此結構針對快速計算進行了優化,因此無法輕鬆完成諸如 if 語句之類的更改。此外,在編譯噪聲表達式之前,它們會被遞歸地簡化 —— 如果它們的所有子項都是常數,那它們也可以摺疊成常數。此步驟目前不能儘快完成,因為某些變量取決於地圖的設置。
1.1 中的噪聲表達式生命週期
優化內部結構
系統預計不會被發揮到極限,因此代碼不會刪除重複的相同表達式並單獨分配它們。基礎遊戲分配了 31'000 個對象,烏爾肯分配的數量更多,達到 280'000 個。我添加了一個全局存儲,使用哈希值進行緩存,並且可以在不先構造完整對象的情況下檢索表達式。它將對象數量減少到 5,300 個,並節省了 125 MB 的 RAM內存。
有這麼多重複的表達式肯定意味著還有其他地方不能重用的東西,對嗎?例如,簡化步驟。當它發現有機會應用常量摺疊時,必須重新創建 AST 的整個分支,以用常量替換一個表達式。此步驟創建了許多臨時對象 – 幾乎與永久分配的表達式的數量一樣多(烏爾肯為 200'000)。我想將其合併到編譯步驟中並“及時”簡化表達式。我的嘗試很成功,從低效的噪聲表達式樹創建烏爾肯表面的速度提高了 15 倍。
編寫出最佳的噪聲表達式很困難,我的目標是讓編譯器幫助修改器優化步驟。因此,我嘗試對多個過程中使用的重複表達式進行刪除。新系統會將它們提取到單獨的過程中,以便它們的結果可以在運行時重用。不再需要調用“noise.delimit_procedure()”,因為它是自動的!
然而,事實證明,弄清楚多個過程使用哪些表達式並不容易,並且問題開始出現。如果我想完全刪除重複數據,我就必須犧牲編譯性能。不僅如此,請求運行過程的成本會更高,因為跨過程依賴性是按需計算的。我想這並不重要,因為它仍然微不足道,但是當我們可以優化它並使代碼更簡單時,為什麼要做一些效率低下的事情呢?所以我決定完全刪除程序。我們現在不對每個表面都編寫一個噪聲程序,而是三個(圖塊,懸崖,實體+裝飾物),這些部分是單獨運行的,不會重用過程。運行地圖生成的速度提高了 20%,噪聲程序編譯速度提高了 50%,但結果因噪聲表達式的複雜性而異。
現在您可以將噪聲程序視為具有多個輸出的一個過程,這有其優點。沒有依賴關係,一切都是內聯的。如果不再需要,則將內存分配給另一個噪聲。因此,它就像一個長 C++ 函數,其中堆棧變量被優化掉,儘管這只是一個稍微簡單的版本。
Lua 的格式問題
Lua 噪聲表達式是在 FFF-207(https://www.factorio.com/blog/post/fff-207) 中引入的。儘管許多人聲稱它難以使用,但它為模組製作者提供了極大的靈活性,並允許他們創建獨特且具有氛圍感的地圖。儘管如此,它還是存在一些問題。當轉儲“data.raw”原型表時,很大一部分被噪聲表達式佔據。這是因為格式非常冗長,每個 AST 節點都是一個 Lua 表。創建如此多的單獨字符串和表也會影響性能。
如果我們不使用噪聲庫提供的 Lua 函數和元表,則格式將如下所示,僅計算“x + 5”。
不可讀?想象一下將它與其他函數和操作乘以 100 倍。雖然噪聲庫隱藏了這一點,但性能損失和“損壞”仍然存在,所以我們決定改變它。我的任務是讓格式用數學表達。也就是說,創建一個可以將字符串表達式處理為 AST 的解析器。起初,我專注於儘可能節省性能,從而實現整體設計。功能齊全,但難以測試、維護和進一步擴展。然後,我開始閱讀其他解析器是如何做到這一點的。我考慮使用外部語法工具來生成解析器,但我認為不值得花時間學習和使用它,並且生成的代碼可能不是最佳的。最後,我決定採用內部解決方案。
解析器分為三個邏輯部分。
1. Tokenizer,處理字符流並將各個字符組分類為標記類型。運算符字符集基於 Lua,但有一些例外,同時支持 C++ 和 Lua 語法。除常規數字外,還支持科學記數法,也可根據需要添加其他格式。
2. PostfixTokenizer,它將中綴標記轉換為後綴表示法。此步驟負責遵循運算符優先級規則並確保結果表達式明確。它使用調度場算法的修改版本來處理數據。
3. NoiseExpressionParser,它採用後綴標記並將其轉換為噪聲表達式樹 (AST)。
我想將儘可能多的代碼移至 C++,以避免過多 Lua 字符串連接。因此,我擴展了噪聲表達式並定義了噪聲函數。我還添加了對未暴露於全局列表的局部噪聲函數和對表達式的支持。關於這些改進我還有很多話要說,但它更適合編寫文檔,不成為一篇有趣的博客文章。
所以現在所有的噪聲表達式都是從字符串中解析出來的,使用的 Lua 表在 2.0 中被刪除了。噪聲表達式在遊戲初始化期間所用的時間減少了 50%,因此遊戲現在的加載速度加快了 20%。
進一步改進
改進地圖生成的旅程並沒有就此結束。定義的噪聲函數的引入意味著 AST 對結果沒有任何影響,我使用算術恆等式實現了部分常量摺疊,因此像“1*x + 0”這樣的表達式被摺疊為“x”,並且不會針對每個塊進行計算。
此外,我注意到我們沒有充分利用基礎噪聲(類似 Perlin 的噪聲函數)。一些特殊情況(x=x,y=y)被優化,因為我們知道我們可以對網格進行改進。我們可以重用中等圖塊值,因此它比通用實現快 5 倍。當我們想要使用 x 和 y 參數偏移網格時,它將不再被解釋為特殊情況。添加單獨的偏移參數進一步提高了性能。
我在地圖生成方面做了更多工作,但並非所有內容都適合寫成博客文章。例如,我刪除了噪聲層原型,添加了更多噪聲函數,並進行了一些其他調整。然而,我不得不做出一些妥協。一些噪聲函數被刪除,包括陣列構造。如果需要的話,可以向新的解析器添加數組支持。
成果
自第一代烏爾肯星球原型被創造出以來,已經經歷了多次迭代。其單獨的噪聲表達也得到了優化。加上 C++ 的改進,每個圖塊的加載時間原本需要 18.35 毫秒,現在只需 2.83 毫秒。這是我們非常滿意的結果。
我相信您很想知道這一切與 1.1 版本相比如何:
⚙ 基礎遊戲初始化原型的速度提高了 7%,花在噪聲表達上的時間減少了 87%。
⚙ 瑙維斯噪聲程序的編譯速度提高了 85%。
⚙ 由於刪除了不分程序,噪聲操作減少了(6'016 → 2'233)。
⚙ 這使我們的圖塊生成速度平均提高了 25%(4.8 ms → 3.58 ms)。
大約 90% 的噪聲表達引擎是從頭開始重寫的,我估計花了大約 4 個月的時間在編寫地圖生成的 C++ 上。這是非常值得的,因為我們不僅擁有更快的系統,而且更易於維護,我們可以根據需要輕鬆添加新的噪聲表達類型。設計它是一個有趣的挑戰。該系統對於地圖生成來說可能有點過度設計,但至少我們有一個堅實的基礎,如果我們願意的話,我們可以在其他項目中重用它。
#steam遊戲# #自動化# #基地建設# #開發日誌#