前言
前段時間一直忙著研究CatLikeCoding的HexMap系列教程,好長時間沒有新開坑寫點小工程了,這次又有了些新點子,與大家分享一下。
現在輪到本期主角出場了:《勇者斗惡龍:建造者2》(以下簡稱DQB2)
?
游戲類型是大家都不陌生的開放世界方塊建造類。這類游戲之前也玩過不少,比如《七日殺》、《傳送門騎士》,當然還有大名鼎鼎的《Minecraft》,但就個人感覺而言,DQB2在可玩性上要高很多,可以說是此類游戲的集大成之作。并且還融合了一些經營模擬養成,RPG戰斗的元素到其中,僅主線任務就讓我玩得不亦樂乎。
簡而言之就是:我沉迷了。
?
單就建造而論,DQB2里的工具就設計的非常實用,比如一次性更換多個同種類型方塊的刷子,一次獲取大量素材的大錘子等,此外還發現了一個十分貼心的功能。
大家都知道建造類游戲有一個問題,就是玩家上下限差距太大。例如《Minecraft》還有一個別稱叫"別人的世界"。好不容易自己搭建了一個火柴盒,突然看到視頻里大佬搭建的世界奇觀,突然就失去玩下去的動力了。即便是想仿照大佬的建筑復制一遍,所需要的工作量也是驚人的,大多數咸魚(比如我)就直接放棄了。而在DQB2中這個問題得到極大改善,你可以直接聯機到大佬的島上閑逛參觀,看見喜歡的建筑樣式可以直接把設計圖拷貝回來,甚至搭建都不用自己動手,在圖紙規劃地旁邊放上一個裝有材料的箱子,NPC就會自動幫忙建造。這簡直是建造游戲愛好者的福音,極大的提升了游戲體驗,同時也讓我對此功能的實現方式產生興趣。
?
那么關于安利部分就此打住,進入正題。
下面用Unity來對自動建造功能做一個探索,預計內容分為兩篇。第一篇是關于方塊建造游戲基礎功能在Unity內的實現,第二篇是NPC自動建造系統功能實現方式的猜想。
另外,由于難度直線升高,HexMap系列教程的翻譯進度會稍微放緩,但肯定會繼續更新下去直到完結,這一點可以放心。
1搭建方塊場景
要實現方塊場景的搭建編輯功能,最簡單粗暴的方法是每一個方塊都視為一個單獨的GameObject,每次點擊時實例化一個方塊。簡單歸簡單,但這種方式在效率上肯定會有問題,特別是當在較大的地圖上計算物理碰撞而每個方塊都有自己的碰撞器時。我不知道好點的電腦運行起來如何,反正我的老爺機肯定就卡逑了。
剛好這個問題可以參考之前六邊形地圖教程里的思路,把每一次的編輯看做是對一整塊mesh里頂點的修改。
(1)獲取方塊放置坐標
首先要做的是在鼠標指向一個位置時,獲取這個位置的方塊坐標。即使是粗暴方法這一步也是省不掉的。
為方便起見就設定每個方塊的邊長是Unity里的標準單位1,那么無論怎么轉換,方塊坐標都處于方塊的中心位置,坐標的小數部分肯定都是是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;? ? } 復制代碼
然后通過屏幕發射射線,換算擊中坐標為方塊坐標,并用Gizmo驗證一下計算是否正確。當然,別忘了新建的測試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)方塊構建
方塊的位置正確無誤之后,下一步就是添加方塊的操作。之前已經說了,要用修改頂點數據的方式來實現這個功能,所以第一步先定義當正方體中心為零點時八個頂點的相對坐標。
?
public static Vector3[] cubeVertex =? ?{? ?? ???//上面四個頂點? ?? ???//左上? ?? ???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),? ?? ???//下面四個頂點? ?? ???//左上? ?? ???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>();? ?? ? }} 復制代碼
由于是正方體,它的三角剖分非常簡單且有規律,所以可以寫一個較為通用的方法來三角化。這樣能使代碼更易讀,且更方便后續功能的添加。
?
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} 復制代碼
頂點和三角形數據填充進去后再刷新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();? ? } 復制代碼
可以看到方塊雖然是一個一個添加的,但數據是表現在一個mesh上的。
(3)刪除方塊
能添加自然就應該能刪除,所以下一步是實現刪除的功能,后續還能擴展成DQB2里的創造師手套搬運功能。
不知道有沒有同學注意到,之前在寫射線坐標轉換成方塊坐標時,代碼里給了一個射線反方向的微小偏移,這是為了防止方塊坐標在某些角度計算到了錯誤的位置。由于現在所有方塊共用的一個碰撞器,所以沒辦法通過碰撞信息來識別點擊的是哪個方塊。那么反過來考慮這個問題,直接通過給射線正方向的偏移,就能讓換算坐標變為當前鼠標指著的方塊坐標。
?
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;? ? } 復制代碼
坐標計算是沒問題,但是該如何告訴mesh刪除這些指定的頂點和三角形呢?
辦法當然是有,射線的RaycastHit結構體里是可以獲取擊中位置的三角形下標和uv坐標的,憑借這些信息已經足夠計算出要刪除的頂點和三角形下標了。
?
但即使能算出來,用腳指頭想也知道會很復雜,咱們不是來做數學題的,所以換個思路。
我們可以這么去思考這個問題:用空的GameObject當做信息載體,在添加方塊時添加這些GameObject到mesh腳本新建的容器里,然后遍歷這個容器來完成三角化。同理,刪除方塊時也根據坐標從容器中找到這個GameObject,然后刪除它并更新mesh。
?
public class CubeInfo : MonoBehaviour{? ? public string cubeName;? ?? ? public Vector3 Position? ? {? ?? ???get? ?? ???{? ?? ?? ?? ?return transform.localPosition;? ?? ???}? ? }} 復制代碼
新建上面的腳本并掛在一個空的GameObject上并拖成預制體,然后在添加方塊的時候實例化這個預制體并加到列表中。
?
public void AddCube(Vector3 position)? ? {? ?? ???CubeInfo cube = Instantiate(CubePrefab, position, Quaternion.identity, transform);? ?? ???Debug.Log("傳入坐標" + position + "||cube本地坐標" + 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設置相鄰方塊與頂點優化
到目前為止添加和刪除方塊都實現了,來考慮一下在兩個方塊相鄰時隱藏接觸面來優化頂點的方法。這一步并不是很必要,優化頂點后并不能帶來明顯的提升,就保持現在這樣也沒問題。但考慮到在后面還要給NPC做尋路功能,獲取方塊之間的相鄰關系是必須的。以此為前提條件的基礎下,那么優化頂點其實就是個順帶的事情。
(1)獲取方塊之間相鄰關系
首先自然就是獲取相鄰關系,在CubeInfo腳本里新建一個數組去存放相鄰方塊的引用關系。基于導航的需要,水平面的每個朝向上還要額外存儲斜上斜下兩個方塊,因此最大相鄰方塊的個數就是3X4+2=14個。把把數組的長度設為14,同時把方向用枚舉保存。
?
public enum CubeNeighborDirection{? ? front,? ? frontUp,? ? frontDown,? ? back,? ? backUp,? ? backDown,? ? left,? ? leftUp,? ? leftDown,? ? right,? ? rightUp,? ? rightDown,? ? up,? ? dowm,} 復制代碼
下一步是寫一個方法,根據當前方塊的坐標和指定方向來推算出這個方向上的方塊坐標。
?
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;? ? } 復制代碼
下一步就是根據這個坐標,在之前保存的所有CubeInfo的容器中找到與之對應的方塊。
?
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;? ? } 復制代碼
然后就可以設置相鄰方塊了,由于方塊的添加有先后,可以在為一個方塊設置其相鄰方塊時在相鄰方塊的相反方向上設置自己為相鄰方塊。但是方向的數量并不對稱,方向的枚舉轉換成int不好找到規律,所以就用笨辦法。
?
? ?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;? ?? ???}? ? } 復制代碼
當然也別忘了在刪除方塊時把相鄰關系也更新一下。
?
public void RemoveNeighbor(CubeNeighborDirection direction)? ? {? ?? ???neighbors[(int)direction] = null;? ? } 復制代碼
現在就能在添加和刪除時設置正確的相鄰關系了,下一步就是優化頂點了。
(2)頂點優化
現在能知道方塊之間的相鄰關系,買手機號碼那在相鄰方塊的方向上隱藏當前面就是一句話的事情了。根據之前表示相鄰方向的枚舉可知,下標為0,3,6,9,12,13的相鄰方塊分別對應前,后,左,右,上,下,接下來就是根據當前三角化的面的朝向來檢測相鄰方塊是否為空。
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);? ?? ?? ?? ???}? ? } 復制代碼
減肥前
減肥成功后
雖然看不出來變化,但表現在數據上還是蠻明顯的。
3方塊的類型UV設置
由于所有方塊都用一個Mesh表示,所以直接改其材質球的顏色是無法區分方塊類型的。那么辦法就是把所有的方塊紋理集合在一張紋理圖上,而根據方塊的類型不同傳入不同的UV坐標。所以首先在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個類型,因為正方形有六個面,設置為六種類型剛好紋理圖就是正方形。
然后就找也好,自己畫也好,搞到一張6乘6正方形的紋理圖,大概就像這樣:
?
隨手畫的,不好看輕噴..
把紋理圖導入Unity中,由于這是像素圖,所以記得修改圖片的Filter Mode為Point,然后把圖片類型改為Sprite。
?
接下來在添加頂點信息時同時把UV信息也添加進去。這里用了一個易于擴展的寫法,之后擴展方塊類型也可以直接修改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圖為正方形,則暫設正方體的類型為n種? ?? ???//v坐標基點: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作為方塊類型選擇,并關聯至腳本里修改方塊類型的枚舉。
?
<p>public void TypeSelect(int type)</p><p></p><p>{</p><p></p><p>cubeType=(M_CubeType)type;</p><p></p><p>}</p> 復制代碼
現在就可以根據選中的類型來方便切換方塊類型了。
?
4方塊旋轉
我們的項目里現在都是方塊,而且由于我畫的UV圖除了最上面都是一個樣子,方塊能不能旋轉無所謂。但原版游戲中不是所有的建造素材都是方塊形狀,其中可能有階梯或者別的不對稱幾何形狀,我們后續也可以往這方面擴展,所以我們還是有必要去實現這個方塊旋轉功能。還是用枚舉來定義方塊的朝向,為方便起見,我們把旋轉的范圍限制在水平面上。
?
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里定義方塊的朝向字段,在添加方塊時將當前朝向一并傳入。
?
??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("傳入坐標" + position + "||cube本地坐標" + cube.transform.localPosition+"type:"+(int)type);? ?? ?? ?? ???SetNeighbors(cube);? ?? ???TriangulateAllCubes();? ? } 復制代碼
使用屬性,在修改方塊朝向枚舉的同時也直接修改實際朝向。
?
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,剛好用來檢測旋轉功能是否正確。(怎么可能是巧合,我肯定是故意的)
?
但是還沒完,別忘了我們之前還為mesh做了"減肥",那么現在旋轉了方塊之后對于需要隱藏面的判定就會出問題,所以這個地方需要修正。干脆直接把這個部分抽成一個函數。
?
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);? ?? ?? ?? ?}? ?? ???}? ? } 復制代碼
然后再檢查一下相鄰時是否會出問題。
?
結束
這期咱們算是把基本的架子搭出來了,可以看到使用簡單粗暴但耗性能的方式一旦換了個思路,其實還是有點麻煩,但這也正是寫這種小工程有意思的地方。
?
文章的代碼貼地有些亂,有興趣的同學還是下載工程研究吧,感謝觀看至此。
總結
以上是生活随笔為你收集整理的用Unity盖房子(一):《勇者斗恶龙:建造者2》游戏功能的猜想的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。