300行代码实现Minecraft(我的世界)大地图生成
一直以來很多人都比較好奇,《我的世界》里的大地圖是如何隨機生成且還具有無限大小的,那么這一期教程,我就以最簡化的代碼(300行左右)在Unity引擎中實現這一機制。
GIF
運行后,隨機生成角色周圍的地形,且隨著角色的位置變化,動態加載。
在實現之前呢,我們可以先來簡單分析一下這個需求:
我的世界的地圖元素可以分為4個層次
World->Chunk->Block->Face
下面分別來解釋一下這4個層次。
1.Face: 正方體的一個面
2.Block: 6個面組成的一個正方體
3.Chunk: N個正方體組成的一個地圖塊
4.World: 多個地圖塊組成的世界,就是“我的世界”啦。
我們可以看到這4個層次,其實有點類似俄羅斯套娃對吧,一層包含一層。
我們要生成World,那么就是要在這些層次中,一層一層的去處理生成的邏輯, 在World里動態加載Chunk, 在Chunk里生成Block, 在Block里生成Face。
OK ?大概的思路我們已經說完了,接下來我們來拆解一下實現步驟
1.首先我們先實現Chunk的生成,內部會包含 Block的生成,這里會用到simplex noise(一種Perlin噪聲的改進)
有關噪聲的知識,如果讀者沒有接觸過,可以自行網上找找相關資料看看
這里推薦一篇(小姐姐寫的比較細致):http://blog.csdn.net/candycat1992/article/details/50346469
在這個部分我們會寫一個類Chunk.cs, ??(大約200行代碼)
2.接下來我們要通過玩家的位置信息來動態加載Chunk
這個部分我們會寫一個類Player.cs ?(大約100行代碼)
Chunk生成
首先新建一個Unity工程后,導入一些資源,資源包在這里下載:http://pan.baidu.com/s/1hszPgwc
接下來我們在場景中創建一個Cube
然后我們來創建一個Chunk類,并掛到這個Cube上。
打開剛才新建的Chunk.cs,我們來先聲明好Chunk類里需要用到的成員變量
public class Chunk : MonoBehaviour
{
????//Block的類型
????public enum BlockType
????{
????????//空
????????None = 0,
????????//泥土
????????Dirt = 1,
????????//草地
????????Grass = 3,
????????//碎石
????????Gravel = 4,
????}
????//存儲著世界中所有的Chunk
????public static List<Chunk> chunks = new List<Chunk>();
????//每個Chunk的長寬Size
????public static int width = 30;
????//每個Chunk的高度
????public static int height = 30;
????//隨機種子
????public int seed;
????//最小生成高度
????public float baseHeight = 10;
????//噪音頻率(噪音采樣時會用到)
????public float frequency = 0.025f;
????//噪音振幅(噪音采樣時會用到)
????public float amplitude = 1;
????//存儲著此Chunk內的所有Block信息
????BlockType[,,] map;
????//Chunk的網格
????Mesh chunkMesh;
????//噪音采樣時會用到的偏移
????Vector3 offset0;
????Vector3 offset1;
????Vector3 offset2;
????MeshRenderer meshRenderer;
????MeshCollider meshCollider;
????MeshFilter meshFilter;
}
如下:
???void Start ()
????{
????????//初始化時將自己加入chunks列表
????????chunks.Add(this);
//獲取自身相關組件引用
meshRenderer = GetComponent<MeshRenderer>();
meshCollider = GetComponent<MeshCollider>();
meshFilter = GetComponent<MeshFilter>();
????????//初始化地圖
????????InitMap();
????}
????void InitMap()
????{
????????//初始化隨機種子
????????Random.InitState(seed);
????????offset0 = new Vector3(Random.value * 1000, Random.value * 1000, Random.value * 1000);
????????offset1 = new Vector3(Random.value * 1000, Random.value * 1000, Random.value * 1000);
????????offset2 = new Vector3(Random.value * 1000, Random.value * 1000, Random.value * 1000);
????????//初始化Map
????????map = new BlockType[width, height, width];
????????//遍歷map,生成其中每個Block的信息
????????for (int x = 0; x < width; x++)
????????{
????????????for (int y = 0; y < height; y++)
????????????{
????????????????for (int z = 0; z < width; z++)
????????????????{
????????????????????map[x, y, z] = GenerateBlockType(new Vector3(x, y, z) + transform.position);
????????????????}
????????????}
????????}
????????//根據生成的信息,Build出Chunk的網格
????????BuildChunk();
????}
在上面這段代碼中,我們需要注意兩個點
1.這里的map存的是Chunk內每一個Block的信息
2.GenerateBlockType函數和BuildChunk函數,我們還沒有實現
3.我們在Start函數被調用時,便將這個Chunk生成好了
在第二點中說的兩個函數,便是我們接下來生成Chunk的兩個核心步驟
1.生成map信息(每個Block的類型,以及地形的高度信息)
2.構建Chunk用來顯示的網格
那么我們接下來分別看看如何實現這兩步
1.GenerateBlockType
int GenerateHeight(Vector3 wPos)
????{
????????//讓隨機種子,振幅,頻率,應用于我們的噪音采樣結果
????????float x0 = (wPos.x + offset0.x) * frequency;
????????float y0 = (wPos.y + offset0.y) * frequency;
????????float z0 = (wPos.z + offset0.z) * frequency;
????????float x1 = (wPos.x + offset1.x) * frequency * 2;
????????float y1 = (wPos.y + offset1.y) * frequency * 2;
????????float z1 = (wPos.z + offset1.z) * frequency * 2;
????????float x2 = (wPos.x + offset2.x) * frequency / 4;
????????float y2 = (wPos.y + offset2.y) * frequency / 4;
????????float z2 = (wPos.z + offset2.z) * frequency / 4;
????????float noise0 = Noise.Generate(x0, z0, y0) * amplitude;
????????float noise1 = Noise.Generate(x1, z1, y1) * amplitude / 2;
????????float noise2 = Noise.Generate(x2, z2, y2) * amplitude / 4;
????????//在采樣結果上,疊加上baseHeight,限制隨機生成的高度下限
????????return Mathf.FloorToInt(noise0 + noise1 + noise2 + baseHeight);
????}
????BlockType GenerateBlockType(Vector3 wPos)
????{
????????//y坐標是否在Chunk內
????????if (wPos.y >= height)
????????{
????????????return BlockType.None;
????????}
????????//獲取當前位置方塊隨機生成的高度值
????????float genHeight = GenerateHeight(wPos);
????????//當前方塊位置高于隨機生成的高度值時,當前方塊類型為空
????????if (wPos.y > genHeight)
????????{
????????????return BlockType.None;
????????}
????????//當前方塊位置等于隨機生成的高度值時,當前方塊類型為草地
????????else if (wPos.y == genHeight)
????????{
????????????return BlockType.Grass;
????????}
????????//當前方塊位置小于隨機生成的高度值 且 大于 genHeight - 5時,當前方塊類型為泥土
????????else if (wPos.y < genHeight && wPos.y > genHeight - 5)
????????{
????????????return BlockType.Dirt;
????????}
????????//其他情況,當前方塊類型為碎石
????????return BlockType.Gravel;
????}
上面這兩個函數實現了生成Block信息的過程
在上面這段代碼中我們需要注意以下幾點
1.GenerateHeight用于通過噪音來隨機生成每個方塊的高度,這種隨機生成的方式相比其他方式更貼近我們想要的結果。普通的隨機數得到的值都是離散的,均勻分布的結果,而通過simplex noise得到的結果,會是連續的。這樣會獲得更加真實,接近自然的效果。
2. GenerateHeight中那些數字字面量,沒有特殊意義,就是經驗數值,為了生成結果能夠產生更多變化而已。可以自己調整試試看。
3.GenerateHeight中對多個噪聲的生成結果進行了疊加,這是為了混合出理想的結果,具體可以網上檢索查閱噪聲相關資料。
4.GenerateBlockType內,會利用在指定位置隨機生成的高度,來決定當前Block的類型。最內層是巖石,中間混雜著泥土,地表則是草地。
在我們有了地形元素的類型信息后,我們就可以來構建Chunk的網格,以來顯示我們的Chunk了。
接下來我們實現BuildChunk函數
public void BuildChunk()
{
????chunkMesh = new Mesh();
????List<Vector3> verts = new List<Vector3>();
????List<Vector2> uvs = new List<Vector2>();
????List<int> tris = new List<int>();
???
????//遍歷chunk, 生成其中的每一個Block
????for (int x = 0; x < width; x++)
????{
????????for (int y = 0; y < height; y++)
????????{
????????????for (int z = 0; z < width; z++)
????????????{
????????????????BuildBlock(x, y, z, verts, uvs, tris);
????????????}
????????}
????}
???????????????
????chunkMesh.vertices = verts.ToArray();
????chunkMesh.uv = uvs.ToArray();
????chunkMesh.triangles = tris.ToArray();
????chunkMesh.RecalculateBounds();
????chunkMesh.RecalculateNormals();
???
????meshFilter.mesh = chunkMesh;
????meshCollider.sharedMesh = chunkMesh;
}
如上所示,BuildChunk函數內部遍歷了Chunk內的每一個Block,為其生成網格數據,并在最后將生成的數據(頂點,UV, ?索引)提交給了chunkMesh。
接下來我們實現BuildBlock函數
????void BuildBlock(int x, int y, int z, List<Vector3> verts, List<Vector2> uvs, List<int> tris)
????{
????????if (map[x, y, z] == 0) return;
????????BlockType typeid = map[x, y, z];
????????//Left
????????if (CheckNeedBuildFace(x - 1, y, z))
????????????BuildFace(typeid, new Vector3(x, y, z), Vector3.up, Vector3.forward, false, verts, uvs, tris);
????????//Right
????????if (CheckNeedBuildFace(x + 1, y, z))
????????????BuildFace(typeid, new Vector3(x + 1, y, z), Vector3.up, Vector3.forward, true, verts, uvs, tris);
????????//Bottom
????????if (CheckNeedBuildFace(x, y - 1, z))
????????????BuildFace(typeid, new Vector3(x, y, z), Vector3.forward, Vector3.right, false, verts, uvs, tris);
????????//Top
????????if (CheckNeedBuildFace(x, y + 1, z))
????????????BuildFace(typeid, new Vector3(x, y + 1, z), Vector3.forward, Vector3.right, true, verts, uvs, tris);
????????//Back
????????if (CheckNeedBuildFace(x, y, z - 1))
????????????BuildFace(typeid, new Vector3(x, y, z), Vector3.up, Vector3.right, true, verts, uvs, tris);
????????//Front
????????if (CheckNeedBuildFace(x, y, z + 1))
????????????BuildFace(typeid, new Vector3(x, y, z + 1), Vector3.up, Vector3.right, false, verts, uvs, tris);
????}
????bool CheckNeedBuildFace(int x, int y, int z)
????{
????????if (y < 0) return false;
????????var type = GetBlockType(x, y, z);
????????switch (type)
????????{
????????????case BlockType.None:
????????????????return true;
????????????default:
????????????????return false;
????????}
????}
????public BlockType GetBlockType(int x, int y, int z)
????{
????????if (y < 0 || y > height - 1)
????????{
????????????return 0;
????????}
????????//當前位置是否在Chunk內
????????if ((x < 0) || (z < 0) || (x >= width) || (z >= width))
????????{
????????????var id = GenerateBlockType(new Vector3(x, y, z) + transform.position);
????????????return id;
????????}
????????return map[x, y, z];
????}
BuildBlock內,我們分別去構建了一個Block中的每一個Face, 并通過CheckNeedBuildFace來確定,某一面Face是否需要顯示出來,如果不需要,那么就不用去構建這面Face了。也就是說這個檢測,會只把我們可以看到的面,顯示出來。
(不做面優化)
(做了面優化)
我們的角色在地形上時,只能看到最外部的一層面,其實看不到內部的方塊,所以這些看不到的方塊,就沒有必要浪費計算資源了。也正是這個原因,我們不能直接用正方體去隨機生成,而是要像現在這樣,以Face為基本單位來生成。實現這個功能的函數,便是CheckNeedBuildFace。
接下來讓我們完成Chunk部分的最后一步
void BuildFace(BlockType typeid, Vector3 corner, Vector3 up, Vector3 right, bool reversed, List<Vector3> verts, List<Vector2> uvs, List<int> tris)
{
????int index = verts.Count; ???????verts.Add (corner);
????verts.Add (corner + up);
????verts.Add (corner + up + right);
????verts.Add (corner + right);
???
????Vector2 uvWidth = new Vector2(0.25f, 0.25f);
????Vector2 uvCorner = new Vector2(0.00f, 0.75f);
????uvCorner.x += (float)(typeid - 1) / 4;
????uvs.Add(uvCorner);
????uvs.Add(new Vector2(uvCorner.x, uvCorner.y + uvWidth.y));
????uvs.Add(new Vector2(uvCorner.x + uvWidth.x, uvCorner.y + uvWidth.y));
????uvs.Add(new Vector2(uvCorner.x + uvWidth.x, uvCorner.y));
???
????if (reversed)
????{
????????tris.Add(index + 0);
????????tris.Add(index + 1);
????????tris.Add(index + 2);
????????tris.Add(index + 2);
????????tris.Add(index + 3);
????????tris.Add(index + 0);
????}
????else
????{
????????tris.Add(index + 1);
????????tris.Add(index + 0);
????????tris.Add(index + 2);
????????tris.Add(index + 3);
????????tris.Add(index + 2);
????????tris.Add(index + 0);
????}
}
這一步我們構建了正方體其中一面的網格數據,頂點,UV, 索引。這一步實現完后, 如果我們將這個組件掛在我們最初創建的Cube上,并運行,我們即會得到隨機生成的一個Chunk。
2.在世界中動態加載多個Chunk
在實現第二部分之前,我們先在Chunk類中再添加一個函數
????public static Chunk GetChunk(Vector3 wPos)
????{ ???????for (int i = 0; i < chunks.Count; i++)
????????{
????????????Vector3 tempPos = chunks[i].transform.position; ???????????//wPos是否超出了Chunk的XZ平面的范圍
????????????if ((wPos.x < tempPos.x) || (wPos.z < tempPos.z) || (wPos.x >= tempPos.x + 20) || (wPos.z >= tempPos.z + 20))
????????????????continue;
????????????return chunks[i];
????????}
????????return null;
????}
這個函數用于給定一個世界空間的位置,獲取這個指定位置所在的Chunk對象。其中遍歷了chunks列表,并找出對應的chunk返回。這個函數我們將在后面的代碼中用到。
接下來由于動態加載是根據玩家位置的變化來進行的,所以我們首先添加一個Player類
新建一個C#代碼文件:Player.cs,并在其中添加如下代碼:
public class Player : MonoBehaviour
{
????CharacterController cc;
????public float speed = 20;
????public float viewRange = 30;
????public Chunk chunkPrefab;
????private void Start()
????{
????????cc = GetComponent<CharacterController>();
????}
????void Update ()
????{
????????UpdateInput();
????????UpdateWorld();
????}
????void UpdateInput()
????{
????????var h = Input.GetAxis("Horizontal");
????????var v = Input.GetAxis("Vertical");
????????var x = Input.GetAxis("Mouse X");
????????var y = Input.GetAxis("Mouse Y");
????????transform.rotation *= Quaternion.Euler(0f, x, 0f);
????????transform.rotation *= Quaternion.Euler(-y, 0f, 0f);
????????if (Input.GetButton("Jump"))
????????{
????????????cc.Move((transform.right * h + transform.forward * v + transform.up) * speed * Time.deltaTime);
????????}
????????else
????????{
????????????cc.SimpleMove(transform.right * h + transform.forward * v * speed);
????????}
????}
}
這段代碼中有幾點需要注意
1.UpdateWorld我們還沒有實現,這個函數將用來動態生成Chunk。
2.UpdateInput函數中,我們實現了一個最簡單的處理玩家輸入的小模塊(但并不成熟,甚至都沒有做視角的限制,感興趣的可以自己加入更多的處理),其可以根據玩家的鼠標和鍵盤的輸入來控制角色移動和旋轉。
3.控制玩家移動的處理,我們使用了Unity內置的CharacterController組件,這個組件自身就又膠囊體碰撞盒。
在這一步中我們從Update函數中已經看出一些端倪了。這里會每一幀先處理玩家的輸入,然后根據處理后的結果(更新后的玩家位置)來動態加載Chunk。
接下來我們添加最后一個函數UpdateWorld
????void UpdateWorld()
????{
????????for (float x = transform.position.x - viewRange; x < transform.position.x + viewRange; x += Chunk.width)
????????{
????????????for (float z = transform.position.z - viewRange; z < transform.position.z + viewRange; z += Chunk.width)
????????????{
????????????????Vector3 pos = new Vector3(x, 0, z);
????????????????pos.x = Mathf.Floor(pos.x / (float)Chunk.width) * Chunk.width;
????????????????pos.z = Mathf.Floor(pos.z / (float)Chunk.width) * Chunk.width; ???????????????Chunk chunk = Chunk.GetChunk(pos);
????????????????if (chunk != null) continue;
????????????????chunk = (Chunk)Instantiate(chunkPrefab, pos, Quaternion.identity);
????????????}
????????}
????}
這個函數 使用了我們剛才實現過的靜態函數Chunk.GetChunk,來獲取相應位置的chunk, 如果沒有獲取到的話,那么就通過chunkPrefab在相應位置生成一個新的chunk。 這個函數會通過這種方式來動態加載自身周圍的chunk。 viewRange參數可以控制需要加載的范圍。
到這里代碼部分我們就全部實現完了。
接下來我們,添加一個角色對象,并在其上掛載一個CharacterController組件,以及我們的Player組件。
別忘了,還要加上相機哦。
然后是Chunk。
最后我們來看看我們的成果吧:
本期教程兩個文件,總計大約300余行代碼
本期教程工程源碼:https://github.com/meta-42/Minecraft-Unity
總結
以上是生活随笔為你收集整理的300行代码实现Minecraft(我的世界)大地图生成的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: centos7无盘启动_200M Lin
- 下一篇: 2021 各式免費 sorce code