日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

300行代码实现Minecraft(我的世界)大地图生成

發布時間:2023/12/31 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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(我的世界)大地图生成的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。