Minecraft大地圖生成續(xù)集——小鋤頭挖起來

作者:皮皮關(guān)馬遙
上次由Meta42編寫的《300行代碼實(shí)現(xiàn)Minecraft(我的世界)大地圖生成》受到了大量點(diǎn)贊,并獲得了成就

原文鏈接:300行代碼實(shí)現(xiàn)Minecraft(我的世界)大地圖生成
我仔細(xì)閱讀并研究了文章中的代碼,做出了一點(diǎn)改進(jìn)——在地圖上點(diǎn)擊會(huì)挖掉一個(gè)方塊。這個(gè)新功能本身改動(dòng)非常小,但是可能恰好是讀者們萌妹以求的功能,所以把方法分享給大家,拋磚引玉,自己動(dòng)手做出Minecraft指日可待 ????
另外,為了符合本專欄誨人不倦的特點(diǎn),本文分兩部分講,第一部分帶著萌新們盡可能看懂Meta42大神用到的代碼建模技術(shù),第二部分做代碼分析并改動(dòng),讓地圖可以被挖出坑來。高手們可以直接翻看第二部分。

1、用代碼建立3D模型的原理
我們很難通過一篇很短的文章學(xué)會(huì)程序3D建模的方法。但是我想通過簡(jiǎn)單的圖片和說明,讓大家理解一些基本的概念,從而大概看懂Minecraft的大地圖生成的大致步驟。
如何用腳本畫出一個(gè)3D立方體呢?
1、確定某個(gè)頂點(diǎn)的位置,由于邊長(zhǎng)固定,其他頂點(diǎn)位置很容易確定。
2、畫六個(gè)面,我們先從一個(gè)正方形面開始看,其他面是一樣的。
3、畫三角形,一個(gè)正方形面由兩個(gè)三角形構(gòu)成,如圖:

對(duì)應(yīng)工程中的代碼為:
void BuildFace(BlockType typeid, Vector3 corner, Vector3 up, Vector3 right, bool reversed, List<Vector3> verts, List<Vector2> uvs, List<int> tris)
{
// index是之前畫過的所有的頂點(diǎn)的總數(shù),因?yàn)檫€有其它面的存在
int index = verts.Count;
// up和right分別是向上和向右的單位方向向量,正方形很容易確定四個(gè)頂點(diǎn)的位置
verts.Add (corner);
verts.Add (corner + up);
verts.Add (corner + up + right);
verts.Add (corner + right);
// UV坐標(biāo)和貼圖有關(guān), 我們先不管它
Vector2 uvWidth = new Vector2(0.25f, 0.25f);
…………省略UV的部分…………
// 頂點(diǎn)順序不同,就朝向不同的方向(3D的面只能從一個(gè)方向看,另一個(gè)方向看不見)
// 比如立方體和上面朝上,下面朝下。上面只能從上面看見,下面要從下面才能看見
// 如果一個(gè)三角形的頂點(diǎn)0 1 2是順時(shí)針,1 0 2就是逆時(shí)針。
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);
}
// 以上代碼把頂點(diǎn)都加入verts列表,把頂點(diǎn)序號(hào)都放入了tris列表。之后由其他函數(shù)
// 把所有的頂點(diǎn)和序號(hào)信息都傳給渲染器
}
我給這段代碼加上了詳細(xì)的注釋,可以看到,要點(diǎn)就是頂點(diǎn)和頂點(diǎn)順序,以下說明頂點(diǎn)順序存在的意義:

比如這個(gè)正方形,有兩個(gè)三角形,理論上來說一共有6個(gè)頂點(diǎn)。但是由于3D模型有大量頂點(diǎn)是公共的,所以渲染的時(shí)候都是盡量復(fù)用頂點(diǎn)提高效率。上圖只需要4個(gè)頂點(diǎn)既可,但是要再用一個(gè)列表指定順序,比如0、1、2、2、3、0,就可以畫出兩個(gè)三角面了。那么如果逆時(shí)針畫1、0、2、3、2、0,也能畫出來。但是這個(gè)面的方向是反的。
什么是面的正反方向呢?我們只要在Unity里隨便拉一個(gè)Plane平面,然后從高處往下和低處往上看,自然就明白了。

明白了BuildFace函數(shù)在干嘛,再看BuildBlock函數(shù)就簡(jiǎn)單了,因?yàn)锽uildBlock函數(shù)就是調(diào)用了6次BuildFace函數(shù),分別畫立方體的上下左右前后六個(gè)面而已。但是在畫面之前,先判斷了該面是不是可以跳過不畫:
if (CheckNeedBuildFace(x - 1, y, z))
BuildFace(typeid, new Vector3(x, y, z), Vector3.up, Vector3.forward, false, verts, uvs, tris);
這個(gè)CheckNeedBuildFace的原理是:如果這個(gè)面朝左,而這個(gè)立方體的左邊不是空的(有其他方塊),那么就不畫了,因?yàn)橥婕铱床坏健O喾慈绻筮吺强盏?,它就必須要畫出來了?/p>
然后呢,BuildChunk來調(diào)用BuildBlock生成每一個(gè)方塊,這個(gè)流程大部分是固定的寫法,我也寫了詳細(xì)注釋。
public void BuildChunk()
{
// 新建一個(gè)Mesh,也就是網(wǎng)格
chunkMesh = new Mesh();
// 建立三個(gè)List,存放所有的頂點(diǎn)、UV、序號(hào)。數(shù)量可能非常多
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);
}
}
}
// 把List里面的所有信息送到網(wǎng)格里面
chunkMesh.vertices = verts.ToArray();
chunkMesh.uv = uvs.ToArray();
chunkMesh.triangles = tris.ToArray();
//固定寫法,網(wǎng)格刷新并賦值給相應(yīng)的Unity組件meshFilter和meshCollider
chunkMesh.RecalculateBounds();
chunkMesh.RecalculateNormals();
meshFilter.mesh = chunkMesh;
meshCollider.sharedMesh = chunkMesh;
}
到此為止就差不多啦,作為暫時(shí)不想深入建模細(xì)節(jié)的讀者,以上解說加上注釋,已經(jīng)足夠說明用代碼來組裝網(wǎng)格的整體思路了。如果你通過閱讀以上一段內(nèi)容對(duì)Mesh的構(gòu)建和使用有了一點(diǎn)感性的認(rèn)識(shí),我就算沒白寫這么多????
2、分析代碼結(jié)構(gòu),加入挖坑邏輯
其實(shí)Meta42給出的建模方法,是完全支持Minecraft那樣的挖坑效果的。實(shí)現(xiàn)挖坑不難,但是分析代碼的過程必不可少。我花了不少時(shí)間搞清楚了原來的代碼邏輯,而最終的修改比想象中簡(jiǎn)單的多。
首先,整個(gè)Unity工程的核心腳本文件只有一個(gè),就是Chunk.cs。Chunk類代表了Chunk對(duì)象,也就是20 * 20方塊組成的一個(gè)大格子。我們的代碼全都是針對(duì)某個(gè)Chunk本身進(jìn)行操作的,但是注意,只有個(gè)別static字段和方法,是全局唯一的,它們是:
public static List<Chunk> chunks = new List<Chunk>();
public static Chunk GetChunk(Vector3 wPos)
chunks列表整個(gè)世界只有一個(gè),用于存放所有Chunk。而某一個(gè)Chunk中的所有方塊Block,則是存儲(chǔ)在這個(gè)字段里:
BlockType[,,] map;
它是一個(gè)三維數(shù)組,BlockType是一個(gè)簡(jiǎn)單的枚舉,可以取值為空格None、泥土格Dirt、草格Grass和巖石格Gravel。
簡(jiǎn)單地說,整個(gè)世界是由一系列Chunk組成,每個(gè)Chunk里有個(gè)20*20*20的三維數(shù)組,存放著每一個(gè)格子Block的情況。我們只要修改BlockType[,,] map這個(gè)數(shù)組里面某一個(gè)格子map[i,j,k] = None,它就變成了空(也就是被挖掉了),同理,只要設(shè)置某個(gè)坐標(biāo)為Grass,它就變成了草地,就是這么簡(jiǎn)單。
注:修改完map的數(shù)據(jù)后,只要調(diào)用BuildChunk() 方法即可生效。所以挖坑方法很簡(jiǎn)單:
public bool Dig(int x, int y, int z)
{
map[x, y, z] = BlockType.None;
BuildChunk();
return true;
}
有了這個(gè)Dig函數(shù),理論上你就可以直接指定x y z挖坑了?,F(xiàn)在還缺一步鼠標(biāo)點(diǎn)擊操作。
3、鼠標(biāo)操作挖坑
我們不在原來的Player.cs腳本里亂改了,新建一個(gè)HitBlock.cs來處理鼠標(biāo)點(diǎn)擊。該腳本掛在Player的GameObject上。
public class HitBlock : MonoBehaviour {
void Update () {
if (Input.GetMouseButtonDown(0))
{
// 3D游戲點(diǎn)擊屏幕都是發(fā)射射線來做,不再贅述
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hitt = new RaycastHit();
Physics.Raycast(ray, out hitt, 1000);
if (hitt.transform != null)
{
// 根據(jù)3D空間中的點(diǎn)坐標(biāo),獲得對(duì)應(yīng)的大Chunk對(duì)象,Chunk里已經(jīng)提供了該方法
Debug.Log("hitt.point="+hitt.point);
Chunk chunk = Chunk.GetChunk(hitt.point);
// chunk.transform.position 其實(shí)是整塊Chunk底面最角上一點(diǎn)的坐標(biāo),也就是該Chunk的原點(diǎn)
// hitt.point是世界坐標(biāo),減去chunk的起點(diǎn),就是對(duì)該chunk來說的局部坐標(biāo)
Vector3 v = hitt.point - chunk.transform.position;
// Unity可以直接獲取碰撞點(diǎn)的法線
// 根據(jù)法線方向就知道了你點(diǎn)擊的是側(cè)面還是上面。這個(gè)地方坐標(biāo)換算需要仔細(xì)考慮
if(Mathf.Abs(hitt.normal.y)>0.01)
{
Debug.Log("上表面");
chunk.Dig((int)v.x, (int)v.y-1, (int)v.z);
}
else
{
Debug.Log("側(cè)面");
chunk.Dig((int)v.x, (int)v.y, (int)v.z);
}
// 注:我沒有實(shí)現(xiàn)底面,其實(shí)確實(shí)存在從下往上挖的情況,再說吧 XD
}
}
}
}
這個(gè)腳本的要點(diǎn):
1、發(fā)射射線,獲得點(diǎn)擊的點(diǎn)的3D坐標(biāo)。
2、用Chunk.GetChunk()函數(shù)可以查找到點(diǎn)擊的點(diǎn)是對(duì)應(yīng)哪個(gè)Chunk。注:Chunk.GetChunk函數(shù)是遍歷所有Chunk查找的,顯然沒有必要、可以優(yōu)化,不過對(duì)本文來說無所謂。
3、Unity可以直接獲得碰撞點(diǎn)的法線,這個(gè)功能太好用了。對(duì)方塊來說,如果法線向上,就是上表面,法線向下,就是下表面,否則就是四個(gè)側(cè)面。
4、根據(jù)是上面還是側(cè)面,可以算出到底玩家想挖哪一個(gè)方塊。調(diào)用之前寫的Chunk.Dig方法即可。
完成 ????~~看看效果:

總結(jié)
本文初衷是修改和擴(kuò)展大家喜愛的一個(gè)游戲原型,但是寫完發(fā)現(xiàn)大部分篇幅都是對(duì)上一篇內(nèi)容的講解和分析,不過這樣也挺好,符合咱們專欄的定位。
其實(shí)修改和維護(hù)別人寫的代碼,也是一項(xiàng)很常見的工作。改進(jìn)別人的代碼其實(shí)不比自己從頭編寫簡(jiǎn)單,所以這項(xiàng)工作也不太受人歡迎,但是相信我,這項(xiàng)工作非常重要????。
本篇中3D模型生成的技術(shù)屬于高級(jí)技術(shù),但是其實(shí)難度并不很大,希望此文能引起游戲開發(fā)愛好者們的興趣。 (?????) ??
對(duì)游戲開發(fā)感興趣的同學(xué),歡迎圍觀我們:【Levelplay皮皮關(guān)游戲開發(fā)教育】 ,會(huì)定期更新各種教程干貨,更有別具一格的線下小班教育。
我們的官網(wǎng)地址:http://levelpp.com/
我們的游戲開發(fā)技術(shù)交流群:610475807
我們的微信公眾號(hào):皮皮關(guān)