用Unity蓋房子(一)——《勇者斗惡龍:建造者2》游戲功能的猜想

作者:沈琰
前言
前段時間一直忙著研究CatLikeCoding的HexMap系列教程,好長時間沒有新開坑寫點(diǎn)小工程了,這次又有了些新點(diǎn)子,與大家分享一下。
現(xiàn)在輪到本期主角出場了:《勇者斗惡龍:建造者2》(以下簡稱DQB2)

游戲類型是大家都不陌生的開放世界方塊建造類。這類游戲之前也玩過不少,比如《七日殺》、《傳送門騎士》,當(dāng)然還有大名鼎鼎的《Minecraft》,但就個人感覺而言,DQB2在可玩性上要高很多,可以說是此類游戲的集大成之作。并且還融合了一些經(jīng)營模擬養(yǎng)成,RPG戰(zhàn)斗的元素到其中,僅主線任務(wù)就讓我玩得不亦樂乎。
簡而言之就是:我沉迷了。

單就建造而論,DQB2里的工具就設(shè)計的非常實(shí)用,比如一次性更換多個同種類型方塊的刷子,一次獲取大量素材的大錘子等 ,此外還發(fā)現(xiàn)了一個十分貼心的功能。
大家都知道建造類游戲有一個問題,就是玩家上下限差距太大。例如《Minecraft》還有一個別稱叫"別人的世界"。好不容易自己搭建了一個火柴盒,突然看到視頻里大佬搭建的世界奇觀,突然就失去玩下去的動力了。即便是想仿照大佬的建筑復(fù)制一遍,所需要的工作量也是驚人的,大多數(shù)咸魚(比如我)就直接放棄了。而在DQB2中這個問題得到極大改善,你可以直接聯(lián)機(jī)到大佬的島上閑逛參觀,看見喜歡的建筑樣式可以直接把設(shè)計圖拷貝回來,甚至搭建都不用自己動手,在圖紙規(guī)劃地旁邊放上一個裝有材料的箱子,NPC就會自動幫忙建造。這簡直是建造游戲愛好者的福音,極大的提升了游戲體驗(yàn),同時也讓我對此功能的實(shí)現(xiàn)方式產(chǎn)生興趣。

那么關(guān)于安利部分就此打住,進(jìn)入正題。
下面用Unity來對自動建造功能做一個探索,預(yù)計內(nèi)容分為兩篇。第一篇是關(guān)于方塊建造游戲基礎(chǔ)功能在Unity內(nèi)的實(shí)現(xiàn),第二篇是NPC自動建造系統(tǒng)功能實(shí)現(xiàn)方式的猜想。
另外,由于難度直線升高,HexMap系列教程的翻譯進(jìn)度會稍微放緩,但肯定會繼續(xù)更新下去直到完結(jié),這一點(diǎn)可以放心。
1 搭建方塊場景
要實(shí)現(xiàn)方塊場景的搭建編輯功能,最簡單粗暴的方法是每一個方塊都視為一個單獨(dú)的GameObject,每次點(diǎn)擊時實(shí)例化一個方塊。簡單歸簡單,但這種方式在效率上肯定會有問題,特別是當(dāng)在較大的地圖上計算物理碰撞而每個方塊都有自己的碰撞器時。我不知道好點(diǎn)的電腦運(yùn)行起來如何,反正我的老爺機(jī)肯定就卡逑了。
剛好這個問題可以參考之前六邊形地圖教程里的思路,把每一次的編輯看做是對一整塊mesh里頂點(diǎn)的修改。
(1)獲取方塊放置坐標(biāo)
首先要做的是在鼠標(biāo)指向一個位置時,獲取這個位置的方塊坐標(biāo)。即使是粗暴方法這一步也是省不掉的。
為方便起見就設(shè)定每個方塊的邊長是Unity里的標(biāo)準(zhǔn)單位1,那么無論怎么轉(zhuǎn)換,方塊坐標(biāo)都處于方塊的中心位置,坐標(biāo)的小數(shù)部分肯定都是是0.5。
public static Vector3 FromWorldPositionToCubePosition(Vector3 position)
??? {
??????? Vector3 resut = Vector3.zero;
??????? resut.x = position.x > 0 ? (int)position.x * 1f + 0.5f : (int)position.x * 1f - 0.5f;
??????? resut.y = position.y > 0 ? (int)position.y * 1f + 0.5f : (int)position.y * 1f - 0.5f;
??????? resut.z = position.z > 0 ? (int)position.z * 1f + 0.5f : (int)position.z * 1f - 0.5f;
??????? return resut;
??? }
?
然后通過屏幕發(fā)射射線,換算擊中坐標(biāo)為方塊坐標(biāo),并用Gizmo驗(yàn)證一下計算是否正確。當(dāng)然,別忘了新建的測試plane上要有碰撞器,不然射線檢測不會起作用。
??? bool GetMouseRayPoint(out Vector3 addCubePosition)
??? {
??????? Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
??????? RaycastHit hitInfo;
??????? if (Physics.Raycast(ray, out hitInfo))
??????? {
?
??????????? Debug.DrawRay(hitInfo.point, Vector3.up, Color.red);
?
??????????? addCubePosition = CubeMetrics.FromWorldPositionToCubePosition(hitInfo.point - ray.direction * 0.001f);
??????????
??????????? return true;
??????? }
??????? addCubePosition = Vector3.zero;
??????? return false;
??? }
?
?? private void OnDrawGizmos()
??? {
???????
??????? if (GetMouseRayPoint(out Vector3 cubePosition)
??????? {
??????????? Gizmos.DrawWireCube(cubePosition, Vector3.one);
??????? }
???????
??? }
?

(2)方塊構(gòu)建
方塊的位置正確無誤之后,下一步就是添加方塊的操作。之前已經(jīng)說了,要用修改頂點(diǎn)數(shù)據(jù)的方式來實(shí)現(xiàn)這個功能,所以第一步先定義當(dāng)正方體中心為零點(diǎn)時八個頂點(diǎn)的相對坐標(biāo)。
public static Vector3[] cubeVertex =
?? {
??????? //上面四個頂點(diǎn)
??????? //左上
??????? new Vector3(-0.5f,0.5f,0.5f),
??????? //右上
??????? new Vector3(0.5f,0.5f,0.5f),
??????? //右下
??????? new Vector3 (0.5f,0.5f,-0.5f),
??????? //左下
??????? new Vector3(-0.5f,0.5f,-0.5f),
??????? //下面四個頂點(diǎn)
??????? //左上
??????? new Vector3(-0.5f,-0.5f,0.5f),
??????? //右上
??????? new Vector3(0.5f,-0.5f,0.5f),
?????? ?//右下
??????? new Vector3(0.5f,-0.5f,-0.5f),
??????? //左下
??????? new Vector3(-0.5f,-0.5f,-0.5f)
??? };
?
然后為整個mesh新建一個類,用來處理方塊的形狀問題。
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class CubeMesh : MonoBehaviour
{
??? Mesh cubeMesh;
??? MeshCollider meshCollider;
?
?
??? List<Vector3> vertices;
??? List<int> triangles;
?
?private void Awake()
{
??? {
??????? GetComponent<MeshFilter>().mesh = cubeMesh = new Mesh();
??????? meshCollider = gameObject.AddComponent<MeshCollider>();??????
??????? vertices = new List<Vector3>();
??????? triangles = new List<int>();??
??? }
}
?
由于是正方體,它的三角剖分非常簡單且有規(guī)律,所以可以寫一個較為通用的方法來三角化。這樣能使代碼更易讀,且更方便后續(xù)功能的添加。
public void TriaggulateCube(Vector3 p)
??? {
?????? Vector3 v1 = p + CubeMetrics.cubeVertex[0];
?????? Vector3 v2 = p + CubeMetrics.cubeVertex[1];
?????? Vector3 v3 = p + CubeMetrics.cubeVertex[2];
???? ??Vector3 v4 = p + CubeMetrics.cubeVertex[3];
?????? Vector3 v5 = p + CubeMetrics.cubeVertex[4];
?????? Vector3 v6 = p + CubeMetrics.cubeVertex[5];
?????? Vector3 v7 = p + CubeMetrics.cubeVertex[6];
?????? Vector3 v8 = p + CubeMetrics.cubeVertex[7];
?
???? ???for (int i = 0; i < 6; i++)
??????? {????????
????????? AddCubeSurface(v1, v2, v3, v4,v5, v6, v7, v8,(CubeSurface)i);??????
??????? }
??? }
?
?void AddCubeSurface(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4,
???????????????????? Vector3 v5, Vector3 v6, Vector3 v7, Vector3 v8)
??? {
??????? switch (suface)
??????? {
??????????? case CubeSurface.up:????????
??????????????? AddSurfaceQuad(v1, v2, v3, v4);
??????????????? break;
??????????? case CubeSurface.down:
??????????????? AddSurfaceQuad(v6, v5, v8, v7);
??????????????? break;
??????????? case CubeSurface.left:
??????????????? AddSurfaceQuad(v1, v4, v8, v5);
??????????????? break;
??????????? case CubeSurface.right:
??????????????? AddSurfaceQuad(v3, v2, v6, v7);
??????????????? break;
??????????? case CubeSurface.front:
??????????????? AddSurfaceQuad(v2, v1, v5, v6);
??????????????? break;
??????????? case CubeSurface.back:
??????????????? AddSurfaceQuad(v4, v3, v7, v8);
??????????????? break;
??????? }
??? }
?
void AddSurfaceQuad(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4)
??? {
??????? int vertexIndex = vertices.Count;
??????? vertices.Add(v1); vertices.Add(v2); vertices.Add(v3); vertices.Add(v4);
??????? triangles.Add(vertexIndex); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 2);
??????? triangles.Add(vertexIndex); triangles.Add(vertexIndex + 2); triangles.Add(vertexIndex + 3);
??? }
?
public enum CubeSurface
{
??? front, right, back, left, up, down
}
?
頂點(diǎn)和三角形數(shù)據(jù)填充進(jìn)去后再刷新mesh。
? public void Apply()
??? {
??????? cubeMesh.SetVertices(vertices);
??????? cubeMesh.SetTriangles(triangles, 0);
??????? cubeMesh.RecalculateNormals();
??????? meshCollider.sharedMesh = cubeMesh;
?
???????
??? }
?
??? public void Clear()
??? {
??????? vertices.Clear();
??????? triangles.Clear();
??????? cubeMesh.Clear();
??? }
?


可以看到方塊雖然是一個一個添加的,但數(shù)據(jù)是表現(xiàn)在一個mesh上的。
(3)刪除方塊
能添加自然就應(yīng)該能刪除,所以下一步是實(shí)現(xiàn)刪除的功能,后續(xù)還能擴(kuò)展成DQB2里的創(chuàng)造師手套搬運(yùn)功能。
不知道有沒有同學(xué)注意到,之前在寫射線坐標(biāo)轉(zhuǎn)換成方塊坐標(biāo)時,代碼里給了一個射線反方向的微小偏移,這是為了防止方塊坐標(biāo)在某些角度計算到了錯誤的位置。由于現(xiàn)在所有方塊共用的一個碰撞器,所以沒辦法通過碰撞信息來識別點(diǎn)擊的是哪個方塊。那么反過來考慮這個問題,直接通過給射線正方向的偏移,就能讓換算坐標(biāo)變?yōu)楫?dāng)前鼠標(biāo)指著的方塊坐標(biāo)。
bool GetMouseRayPoint(out Vector3 addCubePosition, out Vector3 removeCubePosition)
??? {
??????? Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
??????? RaycastHit hitInfo;
??????? if (Physics.Raycast(ray, out hitInfo))
??????? {
?
??????????? Debug.DrawRay(hitInfo.point, Vector3.up, Color.red);
?
??????????? addCubePosition = CubeMetrics.FromWorldPositionToCubePosition(hitInfo.point - ray.direction * 0.001f);
??????????? removeCubePosition = CubeMetrics.FromWorldPositionToCubePosition(hitInfo.point + ray.direction * 0.001f);
??????????? return true;
??????? }
??????? addCubePosition = Vector3.zero;
??????? removeCubePosition = Vector3.zero;
??????? return false;
??? }
?

坐標(biāo)計算是沒問題,但是該如何告訴mesh刪除這些指定的頂點(diǎn)和三角形呢?
辦法當(dāng)然是有,射線的RaycastHit結(jié)構(gòu)體里是可以獲取擊中位置的三角形下標(biāo)和uv坐標(biāo)的,憑借這些信息已經(jīng)足夠計算出要刪除的頂點(diǎn)和三角形下標(biāo)了。

但即使能算出來,用腳指頭想也知道會很復(fù)雜,咱們不是來做數(shù)學(xué)題的,所以換個思路。
我們可以這么去思考這個問題:用空的GameObject當(dāng)做信息載體,在添加方塊時添加這些GameObject到mesh腳本新建的容器里,然后遍歷這個容器來完成三角化。同理,刪除方塊時也根據(jù)坐標(biāo)從容器中找到這個GameObject,然后刪除它并更新mesh。
public class CubeInfo : MonoBehaviour
{
??? public string cubeName;??
?
??? public Vector3 Position
??? {
??????? get
??????? {
??????????? return transform.localPosition;
??????? }
??? }
}
?
新建上面的腳本并掛在一個空的GameObject上并拖成預(yù)制體,然后在添加方塊的時候?qū)嵗@個預(yù)制體并加到列表中。
public void AddCube(Vector3 position)
??? {
??????? CubeInfo cube = Instantiate(CubePrefab, position, Quaternion.identity, transform);
??????? Debug.Log("傳入坐標(biāo)" + position + "||cube本地坐標(biāo)" + cube.transform.localPosition+"type:"+(int)type);
??????
??????? Allcubes.Add(cube);
??????? TriangulateAllCubes();
??? }
?
?void TriangulateAllCubes()
??? {
??????? Clear();
?????? ?foreach (var c in Allcubes)
??????? {
??????????? TriaggulateCube(c);
??????? }
??????? Apply();
??? }
?
這樣一來刪除方塊的方法也容易寫了。
public void RemoveCube(Vector3 positon)
??? {
??????? CubeInfo cube;
??????? if (GetCubeByPosition(positon, out cube))
??????? {
??????????? Allcubes.Remove(cube);
??????????? Destroy(cube.gameObject);
??????????? TriangulateAllCubes();
??????? }
??? }
?
?bool GetCubeByPosition(Vector3 position, out CubeInfo resutCube)
??? {
??????? for (int i = 0; i < Allcubes.Count; i++)
??????? {
??????????? if (Allcubes[i].Position == position)
??????????? {
??????????????? resutCube = Allcubes[i];
??????????????? return true;
??????????? }
??????? }
??????? resutCube = null;
??????? return false;
??? }
?

2 設(shè)置相鄰方塊與頂點(diǎn)優(yōu)化
到目前為止添加和刪除方塊都實(shí)現(xiàn)了,來考慮一下在兩個方塊相鄰時隱藏接觸面來優(yōu)化頂點(diǎn)的方法。這一步并不是很必要,優(yōu)化頂點(diǎn)后并不能帶來明顯的提升,就保持現(xiàn)在這樣也沒問題。但考慮到在后面還要給NPC做尋路功能,獲取方塊之間的相鄰關(guān)系是必須的。以此為前提條件的基礎(chǔ)下,那么優(yōu)化頂點(diǎn)其實(shí)就是個順帶的事情。
(1)獲取方塊之間相鄰關(guān)系
首先自然就是獲取相鄰關(guān)系,在CubeInfo腳本里新建一個數(shù)組去存放相鄰方塊的引用關(guān)系?;趯?dǎo)航的需要,水平面的每個朝向上還要額外存儲斜上斜下兩個方塊,因此最大相鄰方塊的個數(shù)就是3X4+2=14個。把把數(shù)組的長度設(shè)為14,同時把方向用枚舉保存。
public enum CubeNeighborDirection
{
??? front,
??? frontUp,
??? frontDown,
?
??? back,
??? backUp,
??? backDown,
?
??? left,
??? leftUp,
??? leftDown,
?
??? right,
??? rightUp,
??? rightDown,
?
??? up,
??? dowm,
}
?
下一步是寫一個方法,根據(jù)當(dāng)前方塊的坐標(biāo)和指定方向來推算出這個方向上的方塊坐標(biāo)。
public static Vector3 GetCubePosByDirection(Vector3 pos,CubeNeighborDirection direction)
??? {?
??????? switch (direction)
??????? {
??????????? case CubeNeighborDirection.front:
??????????????? pos += Vector3.forward;
??????????????? break;
??????????? case CubeNeighborDirection.frontUp:
??????????????? pos += Vector3.forward + Vector3.up;
??????????????? break;
??????????? case CubeNeighborDirection.frontDown:
??????????????? pos += Vector3.forward + Vector3.down;
??????????????? break;
??????????? case CubeNeighborDirection.back:
??????????????? pos += Vector3.back;
??????????????? break;
??????????? case CubeNeighborDirection.backUp:
??????????????? pos += Vector3.back + Vector3.up;
??????????????? break;
??????????? case CubeNeighborDirection.backDown:
??????????????? pos += Vector3.back + Vector3.down;
??????????????? break;
??????????? case CubeNeighborDirection.left:
??????????????? pos += Vector3.left;
??????????????? break;
??????????? case CubeNeighborDirection.leftUp:
??????????????? pos += Vector3.left + Vector3.up;
??????????????? break;
??????????? case CubeNeighborDirection.leftDown:
??????????????? pos += Vector3.left + Vector3.down;
??????????????? break;
??????????? case CubeNeighborDirection.right:
??????????????? pos += Vector3.right;
??????????????? break;
??????????? case CubeNeighborDirection.rightUp:
??????????????? pos += Vector3.right + Vector3.up;
??????????????? break;
??????????? case CubeNeighborDirection.rightDown:
??????????????? pos += Vector3.right + Vector3.down;
??????????????? break;
??????????? case CubeNeighborDirection.up:
??????????????? pos += Vector3.up;
??????????????? break;
??????????? case CubeNeighborDirection.dowm:
??????????????? pos += Vector3.down;
??????????????? break;??????????????
??????? }
??????? return pos;
??? }
?
下一步就是根據(jù)這個坐標(biāo),在之前保存的所有CubeInfo的容器中找到與之對應(yīng)的方塊。
??? bool GetCubeByDirection(Vector3 position, CubeNeighborDirection direction, out CubeInfo resutCube)
??? {
??????? CubeInfo cube;
??????? if (GetCubeByPosition(CubeMetrics.GetCubePosByDirection(position, direction), out cube))
??????? {
??????????? resutCube = cube;
??????????? return true;
??????? }
??????? resutCube = cube;
??? ????return false;
??? }
?
??? bool GetCubeByPosition(Vector3 position, out CubeInfo resutCube)
??? {
??????? for (int i = 0; i < Allcubes.Count; i++)
??????? {
??????????? if (Allcubes[i].Position == position)
??????????? {
??????????????? resutCube = Allcubes[i];
??????????????? return true;
??????????? }
?
??????? }
??????? resutCube = null;
??????? return false;
??? }
?
然后就可以設(shè)置相鄰方塊了,由于方塊的添加有先后,可以在為一個方塊設(shè)置其相鄰方塊時在相鄰方塊的相反方向上設(shè)置自己為相鄰方塊。但是方向的數(shù)量并不對稱,方向的枚舉轉(zhuǎn)換成int不好找到規(guī)律,所以就用笨辦法。
??? public void SetNeighbor(CubeNeighborDirection direction,CubeInfo cube)
??? {
??????? neighbors[(int)direction] = cube;
??????? cube.neighbors[(int)CubeMetrics.GetOppositeDirection(direction)] = this;
??? }
? public static CubeNeighborDirection GetOppositeDirection(CubeNeighborDirection direction)
??? {
??????? switch(direction)
??????? {
??????????? case CubeNeighborDirection.front:
??????????????? return CubeNeighborDirection.back;
??????????? case CubeNeighborDirection.frontUp:
??????????????? return CubeNeighborDirection.backDown;
??????????? case CubeNeighborDirection.frontDown:
??????????????? return CubeNeighborDirection.backUp;
?
??????????? case CubeNeighborDirection.back:
??????????????? return CubeNeighborDirection.front;
??????????? case CubeNeighborDirection.backUp:
??????????????? return CubeNeighborDirection.frontDown;
??????????? case CubeNeighborDirection.backDown:
????? ??????????return CubeNeighborDirection.frontUp;
?
??????????? case CubeNeighborDirection.left:
??????????????? return CubeNeighborDirection.right;
??????????? case CubeNeighborDirection.leftUp:
??????????????? return CubeNeighborDirection.rightDown;
?????? ?????case CubeNeighborDirection.leftDown:
??????????????? return CubeNeighborDirection.rightUp;
?
??????????? case CubeNeighborDirection.right:
??????????????? return CubeNeighborDirection.left;
??????????? case CubeNeighborDirection.rightUp:
????????????? ??return CubeNeighborDirection.leftDown;
??????????? case CubeNeighborDirection.rightDown:
??????????????? return CubeNeighborDirection.leftUp;
?
??????????? case CubeNeighborDirection.up:
??????????????? return CubeNeighborDirection.dowm;
??????????? case CubeNeighborDirection.dowm:
??????????????? return CubeNeighborDirection.up;
?
??????????? default:
??????????????? return direction;
??????? }
??? }
?
當(dāng)然也別忘了在刪除方塊時把相鄰關(guān)系也更新一下。
? public void RemoveNeighbor(CubeNeighborDirection direction)
??? {
??????? neighbors[(int)direction] = null;
??? }
?

現(xiàn)在就能在添加和刪除時設(shè)置正確的相鄰關(guān)系了,下一步就是優(yōu)化頂點(diǎn)了。
(2)頂點(diǎn)優(yōu)化
現(xiàn)在能知道方塊之間的相鄰關(guān)系,那在相鄰方塊的方向上隱藏當(dāng)前面就是一句話的事情了。根據(jù)之前表示相鄰方向的枚舉可知,下標(biāo)為0,3,6,9,12 , 13的相鄰方塊分別對應(yīng)前,后,左,右,上,下,接下來就是根據(jù)當(dāng)前三角化的面的朝向來檢測相鄰方塊是否為空。
public void TriaggulateCube(Vector3 p)
??? {
?????? Vector3 v1 = p + CubeMetrics.cubeVertex[0];
?????? Vector3 v2 = p + CubeMetrics.cubeVertex[1];
?????? Vector3 v3 = p + CubeMetrics.cubeVertex[2];
?????? Vector3 v4 = p + CubeMetrics.cubeVertex[3];
?????? Vector3 v5 = p + CubeMetrics.cubeVertex[4];
?????? Vector3 v6 = p + CubeMetrics.cubeVertex[5];
?????? Vector3 v7 = p + CubeMetrics.cubeVertex[6];
?????? Vector3 v8 = p + CubeMetrics.cubeVertex[7];
?
??????? for (int i = 0; i < 6; i++)
??????? { ??
??????????? if (i == 0 && cube.neighbors[0]) { continue; }
??????????? else if (i == 1 && cube.neighbors[3]) { continue; }
??????????? else if (i == 2 && cube.neighbors[6]) { continue; }
??????????? else if (i == 3 && cube.neighbors[9]) { continue; }
? ??????????else if (i == 4 && cube.neighbors[12]) { continue; }
??????????? else if (i == 5 && cube.neighbors[13]) { continue; }?????
????????? AddCubeSurface(v1, v2, v3, v4,v5, v6, v7, v8,(CubeSurface)i);??????
??????? }
??? }
?


雖然看不出來變化,但表現(xiàn)在數(shù)據(jù)上還是蠻明顯的。
3 方塊的類型UV設(shè)置
由于所有方塊都用一個Mesh表示,所以直接改其材質(zhì)球的顏色是無法區(qū)分方塊類型的。那么辦法就是把所有的方塊紋理集合在一張紋理圖上,而根據(jù)方塊的類型不同傳入不同的UV坐標(biāo)。所以首先在CubeInfo里定義方塊類型的枚舉字段。
ublic class CubeInfo : MonoBehaviour
{
??? public string cubeName;
??? public CubeInfo[] neighbors;
?
??? public M_CubeType type;
}
public enum M_CubeType
{
??? test1,
??? test2,
??? test3,
??? test4,
??? test5,
??? test6
}
?
先暫且用test占位,后面再來考慮具體的類型。至于為什么是6個類型,因?yàn)檎叫斡辛鶄€面,設(shè)置為六種類型剛好紋理圖就是正方形。
然后就找也好,自己畫也好,搞到一張6乘6正方形的紋理圖,大概就像這樣:

把紋理圖導(dǎo)入Unity中,由于這是像素圖,所以記得修改圖片的Filter Mode為Point,然后把圖片類型改為Sprite。

接下來在添加頂點(diǎn)信息時同時把UV信息也添加進(jìn)去。這里用了一個易于擴(kuò)展的寫法,之后擴(kuò)展方塊類型也可以直接修改TypeCount的值,很方便。
??? void AddCubeSurface(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4,
??????????????????????? Vector3 v5, Vector3 v6, Vector3 v7, Vector3 v8,
??????????????????????? CubeSurface suface, M_CubeType type,int TypeCount)
??? {
??????? //正方體為六個面,若使UV圖為正方形,則暫設(shè)正方體的類型為n種
??????? //v坐標(biāo)基點(diǎn):0~5/n
?
??????? float uCoordinate = ((int)suface * 1.0f) / 6.0f;
??????? float vCoordinate=((int)type*1.0f)/TypeCount*1.0f;
????
??????? Vector2 uvBasePoint=new Vector2(uCoordinate,vCoordinate);
?
??????? switch (suface)
??????? {
??????????? case CubeSurface.up:????????
??????????????? AddSurfaceQuad(v1, v2, v3, v4,uvBasePoint,TypeCount);
??????????????? break;
??????????? case CubeSurface.down:
??????????????? AddSurfaceQuad(v6, v5, v8, v7,uvBasePoint, TypeCount);
??????????????? break;
??????????? case CubeSurface.left:
??????????????? AddSurfaceQuad(v1, v4, v8, v5,uvBasePoint, TypeCount);
??????????????? break;
??????????? case CubeSurface.right:
??????????????? AddSurfaceQuad(v3, v2, v6, v7,uvBasePoint, TypeCount);
??????????????? break;
??????????? case CubeSurface.front:
??????????????? AddSurfaceQuad(v2, v1, v5, v6,uvBasePoint, TypeCount);
??????????????? break;
??????????? case CubeSurface.back:
??????????????? AddSurfaceQuad(v4, v3, v7, v8,uvBasePoint, TypeCount);
??????????????? break;
??????? }
??? }
?
?void AddSurfaceQuad(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector2 uvDp,int uvCount)
??? {
??????? AddQuad(v1, v2, v3, v4);
??????? AddQuadUV(uvDp,uvCount);
??? }
?
?void AddQuadUV(Vector2 uvBasePoint,int TypeCount)
??? {
??????? float deltaU = 1f / 6.0f;
??????? float deltaV = 1f / TypeCount*1.0f;
??????? Vector2 uv1 = new Vector2(uvBasePoint.x, uvBasePoint.y + deltaV);
??????? Vector2 uv2 = new Vector2(uvBasePoint.x + deltaU, uvBasePoint.y + deltaV);
??????? Vector2 uv3 = new Vector2(uvBasePoint.x + deltaU, uvBasePoint.y);
??????? Vector2 uv4 = uvBasePoint;
??????? uvs.Add(uv1); uvs.Add(uv2); uvs.Add(uv3); uvs.Add(uv4);
??? }
?
在場景里新建6個toggle作為方塊類型選擇,并關(guān)聯(lián)至腳本里修改方塊類型的枚舉。
??? public void TypeSelect(int type)
??? {
??????? cubeType = (M_CubeType)type;
??? }
?

現(xiàn)在就可以根據(jù)選中的類型來方便切換方塊類型了。

4 方塊旋轉(zhuǎn)
我們的項(xiàng)目里現(xiàn)在都是方塊,而且由于我畫的UV圖除了最上面都是一個樣子,方塊能不能旋轉(zhuǎn)無所謂。但原版游戲中不是所有的建造素材都是方塊形狀,其中可能有階梯或者別的不對稱幾何形狀,我們后續(xù)也可以往這方面擴(kuò)展,所以我們還是有必要去實(shí)現(xiàn)這個方塊旋轉(zhuǎn)功能。還是用枚舉來定義方塊的朝向,為方便起見,我們把旋轉(zhuǎn)的范圍限制在水平面上。
bool OrientateControl()
??? {
??????? CubeOrientate temp = Orientate;
??????? if (Input.GetKeyDown(KeyCode.Q))
??????? {
??????????? Orientate = (int)Orientate == 0 ? (CubeOrientate)3 : (CubeOrientate)Orientate - 1;
??????? }
??????? else if (Input.GetKeyDown(KeyCode.E))
??????? {
??????????? Orientate = (int)Orientate == 3 ? (CubeOrientate)0 : (CubeOrientate)Orientate + 1;
??????? }
?
??????? if(temp!=Orientate)
??????? {
??????????? return true;
??????? }
?
??????? return false;
??? }
?
??? void Update()
??? {
??????
??????? if(OrientateControl())
??????? {
??????????? preview.UpdateCube(cubeType, Orientate);
??????? }
??????
??? }
?
public enum CubeOrientate
{
??? front, right, back, left
}
?
然后在CubeInfo里定義方塊的朝向字段,在添加方塊時將當(dāng)前朝向一并傳入。
? public void AddCube(Vector3 position, M_CubeType type,CubeOrientate orientate)
??? {
? ??????CubeInfo cube = Instantiate(CubePrefab, position, Quaternion.identity, transform);
??????? cube.type = type;
??????? cube.Orientate = orientate;
??????? Debug.Log("傳入坐標(biāo)" + position + "||cube本地坐標(biāo)" + cube.transform.localPosition+"type:"+(int)type);
?? ????
?
??????? SetNeighbors(cube);
?
??????? TriangulateAllCubes();
??? }
?
使用屬性,在修改方塊朝向枚舉的同時也直接修改實(shí)際朝向。
public CubeOrientate Orientate
??? {
??????? get
??????? {
??????????? return orientate;
??????? }
??????? set
??????? {
???????????
??????????? switch(value)
??????????? {
????????? ??????case CubeOrientate.front:
??????????????????? transform.forward = Vector3.forward;
??????????????????? break;
??????????????? case CubeOrientate.back:
??????????????????? transform.forward = Vector3.back;
??????????????????? break;
??????????????? case CubeOrientate.left:
??????????????????? transform.forward = Vector3.left;
??????????????????? break;
??????????????? case CubeOrientate.right:
??????????????????? transform.forward = Vector3.right;
??????????????????? break;
??????????? }
??????????? orientate = value;
??????? }
??? }
?
剛才很巧合的畫了一個六面不同的UV,剛好用來檢測旋轉(zhuǎn)功能是否正確。(怎么可能是巧合,我肯定是故意的)

但是還沒完,別忘了我們之前還為mesh做了"減肥",那么現(xiàn)在旋轉(zhuǎn)了方塊之后對于需要隱藏面的判定就會出問題,所以這個地方需要修正。干脆直接把這個部分抽成一個函數(shù)。
public bool CanHideSurface(CubeSurface surface)
??? {
??????? if((int)surface<4)
??????? {
??????????? int temp = (int)surface -(int)orientate;
??????????? if(temp<0)
??????????? {
??????????????? temp += 4;
??????????? }
??????????? switch((CubeOrientate)temp)
??????????? {
??????????????? case CubeOrientate.front:
??????????????????? return neighbors[0];
??????????????? case CubeOrientate.back:
??????????????????? return neighbors[3];
??????????????? case CubeOrientate.left:
??????????????????? return neighbors[6];
??????????????? case CubeOrientate.right:
??????????????????? return neighbors[9];
??????????????? default:
??????????????????? return false;
??????????? }
??????? }
??????? else if((int)surface == 4)
??????? {
??????????? return neighbors[12];
??????? }
??????? else
??????? {
??????????? return neighbors[13];
??????? }
???????
??? }
}
??? void TriaggulateCube(CubeInfo cube)
??? {
??????? TransformToCubeVertices(cube);
?
??????? for (int i = 0; i < 6; i++)
??? ????{
??????????? if (!cube.CanHideSurface((CubeSurface)i))
??????????? {
??????????????? AddCubeSurface(tempCubeVertices[0], tempCubeVertices[1], tempCubeVertices[2], tempCubeVertices[3],
?????????????????????????????? tempCubeVertices[4], tempCubeVertices[5], tempCubeVertices[6], tempCubeVertices[7],
????????????????????????????? (CubeSurface)i, cube.type,6);
??????????? }
??????? }
??? }
?
然后再檢查一下相鄰時是否會出問題。

結(jié)束
這期咱們算是把基本的架子搭出來了,可以看到使用簡單粗暴但耗性能的方式一旦換了個思路,其實(shí)還是有點(diǎn)麻煩,但這也正是寫這種小工程有意思的地方。

文章的代碼貼地有些亂,有興趣的同學(xué)還是下載工程研究吧,感謝觀看至此。
本期工程地址:https://link.zhihu.com/?target=https%3A//github.com/tank1018702/CubeBuilder
想系統(tǒng)學(xué)習(xí)游戲開發(fā)的童鞋,歡迎訪問:http://levelpp.com/???
另有專業(yè)開發(fā)交(gao)流(ji)群等待大家強(qiáng)勢插入:869551769