前言
原始資料是油管上的視頻教程,鏈接戳我,播主是南非小哥Sebastian Lague,專門在油管上做Procedural Generation和Unity的相關教程視頻。Unity官方也把這個系列加到了推薦教程中。"Procedural Cave Generation"這個系列已經完結,自己跟著后面做了一遍,收獲頗多,這里做個整理和翻譯,也算幫助自己理解。他還有一個正在連載的系列“Landmass Generation”,動態生成3D Terrain,等完結了也可以考慮做個整理。
這個整理以理解和介紹背景知識為主,會貼部分代碼,想看詳細代碼的可以看他的Github項目主頁。部分代碼小哥是一筆帶過,可能看完你知道怎么做,但為什么這么做理解起來可能有些困難,我也盡量找出相關資料輔助理解,Let's Start!
Cellular automata(細胞自動機)
Cellular Automata最早是馮諾曼依大爺提出的離散數學模型,詳細的信息可以參考Wiki,在洞穴生成里面我們只需要借鑒這個模型的三個特點:
- 一個由多個格子Cell組成的N維網格(這里只要用到2維網格)
- 每格Cell狀態有限(這里只取兩個狀態,每格值是0-空地,或者1-墻)
- 網格按照某種規則演變,每格Cell狀態變化受周圍格子狀態的影響而變化
背景知識就介紹這么多,接下來開始一步步實現。
隨機生成2維網格
根據上面介紹的Cellular automata第一和第二條規則,在Unity中建立一個腳本"MapGenerator"負責二維網格的實現。
public int width;
public int height;
public string seed;
public bool useRandomSeed;
[Range(0,100)]
public int randomFillPercent;
int[,] map;
width和height為可設置的地圖大小。生成地圖的規則也很簡單,設置一個randomFillPercent值,對每一點進行遍歷,隨機取值,如果小于randomFillPercent,將該點設置為1,否則設置為0。一般設置randomFillPercent為50左右。
考慮到有時候我們需要能夠存儲和重新生成相同的地圖,所以在初始化網格時并不是完全隨機,而是設置一個seed,進行偽隨機生成。
void RandomFillMap() {
if (useRandomSeed) {
seed = Time.time.ToString();
}
System.Random pseudoRandom = new System.Random(seed.GetHashCode());
for (int x = 0; x < width; x ++) {
for (int y = 0; y < height; y ++) {
if (x == 0 || x == width-1 || y == 0 || y == height -1) {
map[x,y] = 1; //設置邊緣固定為墻
} else {
map[x,y] = (pseudoRandom.Next(0,100) < randomFillPercent)? 1 : 0;
}
}
}
}
首次生成的圖可能是這個樣子,別著急,接下來根據Cellular Automata的第三個特征處理網格。
應用規則處理網格
Cellular Automata網格的處理規則并不是固定的,比較經典的如Conway's Game of Life生命游戲的規則,不過我們這里處理規則比較簡單:
- 統計當前格子Cell周圍8個網格狀態為1(墻)的總和S
- 如果S大于4,則把Cell設為1。如果S小于4,則把Cell設為0。
- 如果S等于4,則Cell值保持不變。
void SmoothMap() {
for (int x = 0; x < width; x ++) {
for (int y = 0; y < height; y ++) {
int neighbourWallTiles = GetSurroundingWallCount(x,y);
if (neighbourWallTiles > 4)
map[x,y] = 1;
else if (neighbourWallTiles < 4)
map[x,y] = 0;
}
}
}
int GetSurroundingWallCount(int gridX, int gridY) {
int wallCount = 0;
for (int neighbourX = gridX - 1; neighbourX <= gridX + 1; neighbourX ++) {
for (int neighbourY = gridY - 1; neighbourY <= gridY + 1; neighbourY ++) {
if (IsInMapRange(neighbourX, neighbourY)) {
// 統計周圍8個點的情況,請參考Moore neighborhood(https://en.wikipedia.org/wiki/Moore_neighborhood)
if (neighbourX != gridX || neighbourY != gridY) {
wallCount += map[neighbourX, neighbourY];
}
}
else {
wallCount ++;
}
}
}
return wallCount;
}
循環上述步驟5次,可以看到地圖的變化如下:
如果你對其他處理規則感興趣,可以查閱下面兩個鏈接:
1.Generate Random Cave Levels Using Cellular Automata
2.Procedural Level Generation in Games using a Cellular Automaton
規則是先設定一個DeathLimit(如3)和BirthLimit(如4):
- 統計當前Cell周圍為1(墻)的值S
- 如果Cell為1(墻),S值小于DeathLimit,則設置Cell為0
- 如果Cell為0(空地),S值大于BirthLimit,則設Cell為1
需要解決的問題
雖然目前可以生成一個賣相不錯的地圖,但還存留一些問題:
- 地圖中依然存在小塊的空地集合或墻集合。
- 大塊的空地并不確保互相連通。
要解決這兩個問題,可以參考下面這篇文章,在生成規則上做一些優化
Cellular Automata Method for Generating Random Cave-Like Levels
也可以參考Procedural Cave Generation這個系列教程里,Sebastian小哥引入的“房間Room”的概念,去除過小的房間,然后對空房間進行連接,這個是part2要講的部分。
注:原始教程中,講完本文的內容,Sebastian小哥先去講了怎么在Unity里生成二維網格的Mesh,然后再回頭講房間連接,這里我先換個順序,把和網格處理相關的內容一塊說了,再把Mesh生成放到最后說。