300行代码实现Minecraft(我的世界)大地图生成
一直以來(lái)很多人都比較好奇,《我的世界》里的大地圖是如何隨機(jī)生成且還具有無(wú)限大小的,那么這一期教程,我就以最簡(jiǎn)化的代碼(300行左右)在Unity引擎中實(shí)現(xiàn)這一機(jī)制。
GIF
運(yùn)行后,隨機(jī)生成角色周圍的地形,且隨著角色的位置變化,動(dòng)態(tài)加載。
在實(shí)現(xiàn)之前呢,我們可以先來(lái)簡(jiǎn)單分析一下這個(gè)需求:
我的世界的地圖元素可以分為4個(gè)層次
World->Chunk->Block->Face
下面分別來(lái)解釋一下這4個(gè)層次。
1.Face: 正方體的一個(gè)面
2.Block: 6個(gè)面組成的一個(gè)正方體
3.Chunk: N個(gè)正方體組成的一個(gè)地圖塊
4.World: 多個(gè)地圖塊組成的世界,就是“我的世界”啦。
我們可以看到這4個(gè)層次,其實(shí)有點(diǎn)類似俄羅斯套娃對(duì)吧,一層包含一層。
我們要生成World,那么就是要在這些層次中,一層一層的去處理生成的邏輯, 在World里動(dòng)態(tài)加載Chunk, 在Chunk里生成Block, 在Block里生成Face。
OK ?大概的思路我們已經(jīng)說(shuō)完了,接下來(lái)我們來(lái)拆解一下實(shí)現(xiàn)步驟
1.首先我們先實(shí)現(xiàn)Chunk的生成,內(nèi)部會(huì)包含 Block的生成,這里會(huì)用到simplex noise(一種Perlin噪聲的改進(jìn))
有關(guān)噪聲的知識(shí),如果讀者沒有接觸過(guò),可以自行網(wǎng)上找找相關(guān)資料看看
這里推薦一篇(小姐姐寫的比較細(xì)致):http://blog.csdn.net/candycat1992/article/details/50346469
在這個(gè)部分我們會(huì)寫一個(gè)類Chunk.cs, ??(大約200行代碼)
2.接下來(lái)我們要通過(guò)玩家的位置信息來(lái)動(dòng)態(tài)加載Chunk
這個(gè)部分我們會(huì)寫一個(gè)類Player.cs ?(大約100行代碼)
Chunk生成
首先新建一個(gè)Unity工程后,導(dǎo)入一些資源,資源包在這里下載:http://pan.baidu.com/s/1hszPgwc
接下來(lái)我們?cè)趫?chǎng)景中創(chuàng)建一個(gè)Cube
然后我們來(lái)創(chuàng)建一個(gè)Chunk類,并掛到這個(gè)Cube上。
打開剛才新建的Chunk.cs,我們來(lái)先聲明好Chunk類里需要用到的成員變量
public class Chunk : MonoBehaviour
{
????//Block的類型
????public enum BlockType
????{
????????//空
????????None = 0,
????????//泥土
????????Dirt = 1,
????????//草地
????????Grass = 3,
????????//碎石
????????Gravel = 4,
????}
????//存儲(chǔ)著世界中所有的Chunk
????public static List<Chunk> chunks = new List<Chunk>();
????//每個(gè)Chunk的長(zhǎng)寬Size
????public static int width = 30;
????//每個(gè)Chunk的高度
????public static int height = 30;
????//隨機(jī)種子
????public int seed;
????//最小生成高度
????public float baseHeight = 10;
????//噪音頻率(噪音采樣時(shí)會(huì)用到)
????public float frequency = 0.025f;
????//噪音振幅(噪音采樣時(shí)會(huì)用到)
????public float amplitude = 1;
????//存儲(chǔ)著此Chunk內(nèi)的所有Block信息
????BlockType[,,] map;
????//Chunk的網(wǎng)格
????Mesh chunkMesh;
????//噪音采樣時(shí)會(huì)用到的偏移
????Vector3 offset0;
????Vector3 offset1;
????Vector3 offset2;
????MeshRenderer meshRenderer;
????MeshCollider meshCollider;
????MeshFilter meshFilter;
}
如下:
???void Start ()
????{
????????//初始化時(shí)將自己加入chunks列表
????????chunks.Add(this);
//獲取自身相關(guān)組件引用
meshRenderer = GetComponent<MeshRenderer>();
meshCollider = GetComponent<MeshCollider>();
meshFilter = GetComponent<MeshFilter>();
????????//初始化地圖
????????InitMap();
????}
????void InitMap()
????{
????????//初始化隨機(jī)種子
????????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,生成其中每個(gè)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);
????????????????}
????????????}
????????}
????????//根據(jù)生成的信息,Build出Chunk的網(wǎng)格
????????BuildChunk();
????}
在上面這段代碼中,我們需要注意兩個(gè)點(diǎn)
1.這里的map存的是Chunk內(nèi)每一個(gè)Block的信息
2.GenerateBlockType函數(shù)和BuildChunk函數(shù),我們還沒有實(shí)現(xiàn)
3.我們?cè)赟tart函數(shù)被調(diào)用時(shí),便將這個(gè)Chunk生成好了
在第二點(diǎn)中說(shuō)的兩個(gè)函數(shù),便是我們接下來(lái)生成Chunk的兩個(gè)核心步驟
1.生成map信息(每個(gè)Block的類型,以及地形的高度信息)
2.構(gòu)建Chunk用來(lái)顯示的網(wǎng)格
那么我們接下來(lái)分別看看如何實(shí)現(xiàn)這兩步
1.GenerateBlockType
int GenerateHeight(Vector3 wPos)
????{
????????//讓隨機(jī)種子,振幅,頻率,應(yīng)用于我們的噪音采樣結(jié)果
????????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;
????????//在采樣結(jié)果上,疊加上baseHeight,限制隨機(jī)生成的高度下限
????????return Mathf.FloorToInt(noise0 + noise1 + noise2 + baseHeight);
????}
????BlockType GenerateBlockType(Vector3 wPos)
????{
????????//y坐標(biāo)是否在Chunk內(nèi)
????????if (wPos.y >= height)
????????{
????????????return BlockType.None;
????????}
????????//獲取當(dāng)前位置方塊隨機(jī)生成的高度值
????????float genHeight = GenerateHeight(wPos);
????????//當(dāng)前方塊位置高于隨機(jī)生成的高度值時(shí),當(dāng)前方塊類型為空
????????if (wPos.y > genHeight)
????????{
????????????return BlockType.None;
????????}
????????//當(dāng)前方塊位置等于隨機(jī)生成的高度值時(shí),當(dāng)前方塊類型為草地
????????else if (wPos.y == genHeight)
????????{
????????????return BlockType.Grass;
????????}
????????//當(dāng)前方塊位置小于隨機(jī)生成的高度值 且 大于 genHeight - 5時(shí),當(dāng)前方塊類型為泥土
????????else if (wPos.y < genHeight && wPos.y > genHeight - 5)
????????{
????????????return BlockType.Dirt;
????????}
????????//其他情況,當(dāng)前方塊類型為碎石
????????return BlockType.Gravel;
????}
上面這兩個(gè)函數(shù)實(shí)現(xiàn)了生成Block信息的過(guò)程
在上面這段代碼中我們需要注意以下幾點(diǎn)
1.GenerateHeight用于通過(guò)噪音來(lái)隨機(jī)生成每個(gè)方塊的高度,這種隨機(jī)生成的方式相比其他方式更貼近我們想要的結(jié)果。普通的隨機(jī)數(shù)得到的值都是離散的,均勻分布的結(jié)果,而通過(guò)simplex noise得到的結(jié)果,會(huì)是連續(xù)的。這樣會(huì)獲得更加真實(shí),接近自然的效果。
2. GenerateHeight中那些數(shù)字字面量,沒有特殊意義,就是經(jīng)驗(yàn)數(shù)值,為了生成結(jié)果能夠產(chǎn)生更多變化而已。可以自己調(diào)整試試看。
3.GenerateHeight中對(duì)多個(gè)噪聲的生成結(jié)果進(jìn)行了疊加,這是為了混合出理想的結(jié)果,具體可以網(wǎng)上檢索查閱噪聲相關(guān)資料。
4.GenerateBlockType內(nèi),會(huì)利用在指定位置隨機(jī)生成的高度,來(lái)決定當(dāng)前Block的類型。最內(nèi)層是巖石,中間混雜著泥土,地表則是草地。
在我們有了地形元素的類型信息后,我們就可以來(lái)構(gòu)建Chunk的網(wǎng)格,以來(lái)顯示我們的Chunk了。
接下來(lái)我們實(shí)現(xiàn)BuildChunk函數(shù)
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, 生成其中的每一個(gè)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函數(shù)內(nèi)部遍歷了Chunk內(nèi)的每一個(gè)Block,為其生成網(wǎng)格數(shù)據(jù),并在最后將生成的數(shù)據(jù)(頂點(diǎn),UV, ?索引)提交給了chunkMesh。
接下來(lái)我們實(shí)現(xiàn)BuildBlock函數(shù)
????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;
????????}
????????//當(dāng)前位置是否在Chunk內(nèi)
????????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內(nèi),我們分別去構(gòu)建了一個(gè)Block中的每一個(gè)Face, 并通過(guò)CheckNeedBuildFace來(lái)確定,某一面Face是否需要顯示出來(lái),如果不需要,那么就不用去構(gòu)建這面Face了。也就是說(shuō)這個(gè)檢測(cè),會(huì)只把我們可以看到的面,顯示出來(lái)。
(不做面優(yōu)化)
(做了面優(yōu)化)
我們的角色在地形上時(shí),只能看到最外部的一層面,其實(shí)看不到內(nèi)部的方塊,所以這些看不到的方塊,就沒有必要浪費(fèi)計(jì)算資源了。也正是這個(gè)原因,我們不能直接用正方體去隨機(jī)生成,而是要像現(xiàn)在這樣,以Face為基本單位來(lái)生成。實(shí)現(xiàn)這個(gè)功能的函數(shù),便是CheckNeedBuildFace。
接下來(lái)讓我們完成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);
????}
}
這一步我們構(gòu)建了正方體其中一面的網(wǎng)格數(shù)據(jù),頂點(diǎn),UV, 索引。這一步實(shí)現(xiàn)完后, 如果我們將這個(gè)組件掛在我們最初創(chuàng)建的Cube上,并運(yùn)行,我們即會(huì)得到隨機(jī)生成的一個(gè)Chunk。
2.在世界中動(dòng)態(tài)加載多個(gè)Chunk
在實(shí)現(xiàn)第二部分之前,我們先在Chunk類中再添加一個(gè)函數(shù)
????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;
????}
這個(gè)函數(shù)用于給定一個(gè)世界空間的位置,獲取這個(gè)指定位置所在的Chunk對(duì)象。其中遍歷了chunks列表,并找出對(duì)應(yīng)的chunk返回。這個(gè)函數(shù)我們將在后面的代碼中用到。
接下來(lái)由于動(dòng)態(tài)加載是根據(jù)玩家位置的變化來(lái)進(jìn)行的,所以我們首先添加一個(gè)Player類
新建一個(gè)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);
????????}
????}
}
這段代碼中有幾點(diǎn)需要注意
1.UpdateWorld我們還沒有實(shí)現(xiàn),這個(gè)函數(shù)將用來(lái)動(dòng)態(tài)生成Chunk。
2.UpdateInput函數(shù)中,我們實(shí)現(xiàn)了一個(gè)最簡(jiǎn)單的處理玩家輸入的小模塊(但并不成熟,甚至都沒有做視角的限制,感興趣的可以自己加入更多的處理),其可以根據(jù)玩家的鼠標(biāo)和鍵盤的輸入來(lái)控制角色移動(dòng)和旋轉(zhuǎn)。
3.控制玩家移動(dòng)的處理,我們使用了Unity內(nèi)置的CharacterController組件,這個(gè)組件自身就又膠囊體碰撞盒。
在這一步中我們從Update函數(shù)中已經(jīng)看出一些端倪了。這里會(huì)每一幀先處理玩家的輸入,然后根據(jù)處理后的結(jié)果(更新后的玩家位置)來(lái)動(dòng)態(tài)加載Chunk。
接下來(lái)我們添加最后一個(gè)函數(shù)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);
????????????}
????????}
????}
這個(gè)函數(shù) 使用了我們剛才實(shí)現(xiàn)過(guò)的靜態(tài)函數(shù)Chunk.GetChunk,來(lái)獲取相應(yīng)位置的chunk, 如果沒有獲取到的話,那么就通過(guò)chunkPrefab在相應(yīng)位置生成一個(gè)新的chunk。 這個(gè)函數(shù)會(huì)通過(guò)這種方式來(lái)動(dòng)態(tài)加載自身周圍的chunk。 viewRange參數(shù)可以控制需要加載的范圍。
到這里代碼部分我們就全部實(shí)現(xiàn)完了。
接下來(lái)我們,添加一個(gè)角色對(duì)象,并在其上掛載一個(gè)CharacterController組件,以及我們的Player組件。
別忘了,還要加上相機(jī)哦。
然后是Chunk。
最后我們來(lái)看看我們的成果吧:
本期教程兩個(gè)文件,總計(jì)大約300余行代碼
本期教程工程源碼:https://github.com/meta-42/Minecraft-Unity
總結(jié)
以上是生活随笔為你收集整理的300行代码实现Minecraft(我的世界)大地图生成的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: centos7无盘启动_200M Lin
- 下一篇: 2021 各式免費 sorce code