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

遊資網發表於2019-06-20
用Unity蓋房子(一):《勇者鬥惡龍:建造者2》遊戲功能的猜想

前言

前段時間一直忙著研究CatLikeCoding的HexMap系列教程,好長時間沒有新開坑寫點小工程了,這次又有了些新點子,與大家分享一下。

現在輪到本期主角出場了:《勇者鬥惡龍:建造者2》(以下簡稱DQB2)

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

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

簡而言之就是:我沉迷了。

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

單就建造而論,DQB2裡的工具就設計的非常實用,比如一次性更換多個同種型別方塊的刷子,一次獲取大量素材的大錘子等,此外還發現了一個十分貼心的功能。

大家都知道建造類遊戲有一個問題,就是玩家上下限差距太大。例如《Minecraft》還有一個別稱叫"別人的世界"。好不容易自己搭建了一個火柴盒,突然看到視訊裡大佬搭建的世界奇觀,突然就失去玩下去的動力了。即便是想仿照大佬的建築複製一遍,所需要的工作量也是驚人的,大多數鹹魚(比如我)就直接放棄了。而在DQB2中這個問題得到極大改善,你可以直接聯機到大佬的島上閒逛參觀,看見喜歡的建築樣式可以直接把設計圖拷貝回來,甚至搭建都不用自己動手,在圖紙規劃地旁邊放上一個裝有材料的箱子,NPC就會自動幫忙建造。這簡直是建造遊戲愛好者的福音,極大的提升了遊戲體驗,同時也讓我對此功能的實現方式產生興趣。

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

那麼關於安利部分就此打住,進入正題。

下面用Unity來對自動建造功能做一個探索,預計內容分為兩篇。第一篇是關於方塊建造遊戲基礎功能在Unity內的實現,第二篇是NPC自動建造系統功能實現方式的猜想。

另外,由於難度直線升高,HexMap系列教程的翻譯進度會稍微放緩,但肯定會繼續更新下去直到完結,這一點可以放心。

1搭建方塊場景

要實現方塊場景的搭建編輯功能,最簡單粗暴的方法是每一個方塊都視為一個單獨的GameObject,每次點選時例項化一個方塊。簡單歸簡單,但這種方式在效率上肯定會有問題,特別是當在較大的地圖上計算物理碰撞而每個方塊都有自己的碰撞器時。我不知道好點的電腦執行起來如何,反正我的老爺機肯定就卡逑了。

剛好這個問題可以參考之前六邊形地圖教程裡的思路,把每一次的編輯看做是對一整塊mesh裡頂點的修改。

(1)獲取方塊放置座標

首先要做的是在滑鼠指向一個位置時,獲取這個位置的方塊座標。即使是粗暴方法這一步也是省不掉的。

為方便起見就設定每個方塊的邊長是Unity裡的標準單位1,那麼無論怎麼轉換,方塊座標都處於方塊的中心位置,座標的小數部分肯定都是是0.5。

  1. public static Vector3 FromWorldPositionToCubePosition(Vector3 position)
  2.     {
  3.         Vector3 resut = Vector3.zero;
  4.         resut.x = position.x > 0 ? (int)position.x * 1f + 0.5f : (int)position.x * 1f - 0.5f;
  5.         resut.y = position.y > 0 ? (int)position.y * 1f + 0.5f : (int)position.y * 1f - 0.5f;
  6.         resut.z = position.z > 0 ? (int)position.z * 1f + 0.5f : (int)position.z * 1f - 0.5f;
  7.         return resut;
  8.     }
複製程式碼

然後通過螢幕發射射線,換算擊中座標為方塊座標,並用Gizmo驗證一下計算是否正確。當然,別忘了新建的測試plane上要有碰撞器,不然射線檢測不會起作用。

  1.   bool GetMouseRayPoint(out Vector3 addCubePosition)
  2.     {
  3.         Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
  4.         RaycastHit hitInfo;
  5.         if (Physics.Raycast(ray, out hitInfo))
  6.         {

  7.             Debug.DrawRay(hitInfo.point, Vector3.up, Color.red);

  8.             addCubePosition = CubeMetrics.FromWorldPositionToCubePosition(hitInfo.point - ray.direction * 0.001f);
  9.            
  10.             return true;
  11.         }
  12.         addCubePosition = Vector3.zero;
  13.         return false;
  14.     }

  15.    private void OnDrawGizmos()
  16.     {
  17.         
  18.         if (GetMouseRayPoint(out Vector3 cubePosition)
  19.         {
  20.             Gizmos.DrawWireCube(cubePosition, Vector3.one);
  21.         }
  22.         
  23.     }
複製程式碼

用Unity蓋房子(一):《勇者鬥惡龍:建造者2》遊戲功能的猜想
紅線即滑鼠射線擊中位置

(2)方塊構建

方塊的位置正確無誤之後,下一步就是新增方塊的操作。之前已經說了,要用修改頂點資料的方式來實現這個功能,所以第一步先定義當正方體中心為零點時八個頂點的相對座標。

  1. public static Vector3[] cubeVertex =
  2.    {
  3.         //上面四個頂點
  4.         //左上
  5.         new Vector3(-0.5f,0.5f,0.5f),
  6.         //右上
  7.         new Vector3(0.5f,0.5f,0.5f),
  8.         //右下
  9.         new Vector3 (0.5f,0.5f,-0.5f),
  10.         //左下
  11.         new Vector3(-0.5f,0.5f,-0.5f),
  12.         //下面四個頂點
  13.         //左上
  14.         new Vector3(-0.5f,-0.5f,0.5f),
  15.         //右上
  16.         new Vector3(0.5f,-0.5f,0.5f),
  17.         //右下
  18.         new Vector3(0.5f,-0.5f,-0.5f),
  19.         //左下
  20.         new Vector3(-0.5f,-0.5f,-0.5f)
  21.     };
複製程式碼

然後為整個mesh新建一個類,用來處理方塊的形狀問題。

  1. [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
  2. public class CubeMesh : MonoBehaviour
  3. {
  4.     Mesh cubeMesh;
  5.     MeshCollider meshCollider;


  6.     List<Vector3> vertices;
  7.     List<int> triangles;

  8. private void Awake()
  9. {
  10.     {
  11.         GetComponent<MeshFilter>().mesh = cubeMesh = new Mesh();
  12.         meshCollider = gameObject.AddComponent<MeshCollider>();      
  13.         vertices = new List<Vector3>();
  14.         triangles = new List<int>();   
  15.     }
  16. }
複製程式碼

由於是正方體,它的三角剖分非常簡單且有規律,所以可以寫一個較為通用的方法來三角化。這樣能使程式碼更易讀,且更方便後續功能的新增。

  1. public void TriaggulateCube(Vector3 p)
  2.     {
  3.        Vector3 v1 = p + CubeMetrics.cubeVertex[0];
  4.        Vector3 v2 = p + CubeMetrics.cubeVertex[1];
  5.        Vector3 v3 = p + CubeMetrics.cubeVertex[2];
  6.        Vector3 v4 = p + CubeMetrics.cubeVertex[3];
  7.        Vector3 v5 = p + CubeMetrics.cubeVertex[4];
  8.        Vector3 v6 = p + CubeMetrics.cubeVertex[5];
  9.        Vector3 v7 = p + CubeMetrics.cubeVertex[6];
  10.        Vector3 v8 = p + CubeMetrics.cubeVertex[7];

  11.         for (int i = 0; i < 6; i++)
  12.         {         
  13.           AddCubeSurface(v1, v2, v3, v4,v5, v6, v7, v8,(CubeSurface)i);      
  14.         }
  15.     }

  16. void AddCubeSurface(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4,
  17.                      Vector3 v5, Vector3 v6, Vector3 v7, Vector3 v8)
  18.     {
  19.         switch (suface)
  20.         {
  21.             case CubeSurface.up:         
  22.                 AddSurfaceQuad(v1, v2, v3, v4);
  23.                 break;
  24.             case CubeSurface.down:
  25.                 AddSurfaceQuad(v6, v5, v8, v7);
  26.                 break;
  27.             case CubeSurface.left:
  28.                 AddSurfaceQuad(v1, v4, v8, v5);
  29.                 break;
  30.             case CubeSurface.right:
  31.                 AddSurfaceQuad(v3, v2, v6, v7);
  32.                 break;
  33.             case CubeSurface.front:
  34.                 AddSurfaceQuad(v2, v1, v5, v6);
  35.                 break;
  36.             case CubeSurface.back:
  37.                 AddSurfaceQuad(v4, v3, v7, v8);
  38.                 break;
  39.         }
  40.     }

  41. void AddSurfaceQuad(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4)
  42.     {
  43.         int vertexIndex = vertices.Count;
  44.         vertices.Add(v1); vertices.Add(v2); vertices.Add(v3); vertices.Add(v4);
  45.         triangles.Add(vertexIndex); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 2);
  46.         triangles.Add(vertexIndex); triangles.Add(vertexIndex + 2); triangles.Add(vertexIndex + 3);
  47.     }

  48. public enum CubeSurface
  49. {
  50.     front, right, back, left, up, down
  51. }
複製程式碼

頂點和三角形資料填充進去後再重新整理mesh。

  1. public void Apply()
  2.     {
  3.         cubeMesh.SetVertices(vertices);
  4.         cubeMesh.SetTriangles(triangles, 0);
  5.         cubeMesh.RecalculateNormals();
  6.         meshCollider.sharedMesh = cubeMesh;
  7.   
  8.         
  9.     }

  10.     public void Clear()
  11.     {
  12.         vertices.Clear();
  13.         triangles.Clear();
  14.         cubeMesh.Clear();
  15.     }
複製程式碼

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

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

可以看到方塊雖然是一個一個新增的,但資料是表現在一個mesh上的。

(3)刪除方塊

能新增自然就應該能刪除,所以下一步是實現刪除的功能,後續還能擴充套件成DQB2裡的創造師手套搬運功能。

不知道有沒有同學注意到,之前在寫射線座標轉換成方塊座標時,程式碼裡給了一個射線反方向的微小偏移,這是為了防止方塊座標在某些角度計算到了錯誤的位置。由於現在所有方塊共用的一個碰撞器,所以沒辦法通過碰撞資訊來識別點選的是哪個方塊。那麼反過來考慮這個問題,直接通過給射線正方向的偏移,就能讓換算座標變為當前滑鼠指著的方塊座標。

  1. bool GetMouseRayPoint(out Vector3 addCubePosition, out Vector3 removeCubePosition)
  2.     {
  3.         Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
  4.         RaycastHit hitInfo;
  5.         if (Physics.Raycast(ray, out hitInfo))
  6.         {

  7.             Debug.DrawRay(hitInfo.point, Vector3.up, Color.red);

  8.             addCubePosition = CubeMetrics.FromWorldPositionToCubePosition(hitInfo.point - ray.direction * 0.001f);
  9.             removeCubePosition = CubeMetrics.FromWorldPositionToCubePosition(hitInfo.point + ray.direction * 0.001f);
  10.             return true;
  11.         }
  12.         addCubePosition = Vector3.zero;
  13.         removeCubePosition = Vector3.zero;
  14.         return false;
  15.     }
複製程式碼

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

座標計算是沒問題,但是該如何告訴mesh刪除這些指定的頂點和三角形呢?

辦法當然是有,射線的RaycastHit結構體裡是可以獲取擊中位置的三角形下標和uv座標的,憑藉這些資訊已經足夠計算出要刪除的頂點和三角形下標了。

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

但即使能算出來,用腳指頭想也知道會很複雜,我們們不是來做數學題的,所以換個思路。

我們可以這麼去思考這個問題:用空的GameObject當做資訊載體,在新增方塊時新增這些GameObject到mesh指令碼新建的容器裡,然後遍歷這個容器來完成三角化。同理,刪除方塊時也根據座標從容器中找到這個GameObject,然後刪除它並更新mesh。

  1. public class CubeInfo : MonoBehaviour
  2. {
  3.     public string cubeName;   

  4.     public Vector3 Position
  5.     {
  6.         get
  7.         {
  8.             return transform.localPosition;
  9.         }
  10.     }
  11. }
複製程式碼

新建上面的指令碼並掛在一個空的GameObject上並拖成預製體,然後在新增方塊的時候例項化這個預製體並加到列表中。

  1. public void AddCube(Vector3 position)
  2.     {
  3.         CubeInfo cube = Instantiate(CubePrefab, position, Quaternion.identity, transform);
  4.         Debug.Log("傳入座標" + position + "||cube本地座標" + cube.transform.localPosition+"type:"+(int)type);
  5.       
  6.         Allcubes.Add(cube);
  7.         TriangulateAllCubes();
  8.     }

  9. void TriangulateAllCubes()
  10.     {
  11.         Clear();
  12.         foreach (var c in Allcubes)
  13.         {
  14.             TriaggulateCube(c);
  15.         }
  16.         Apply();
  17.     }
複製程式碼

這樣一來刪除方塊的方法也容易寫了。

  1. public void RemoveCube(Vector3 positon)
  2.     {
  3.         CubeInfo cube;
  4.         if (GetCubeByPosition(positon, out cube))
  5.         {
  6.             Allcubes.Remove(cube);
  7.             Destroy(cube.gameObject);
  8.             TriangulateAllCubes();
  9.         }
  10.     }

  11. bool GetCubeByPosition(Vector3 position, out CubeInfo resutCube)
  12.     {
  13.         for (int i = 0; i < Allcubes.Count; i++)
  14.         {
  15.             if (Allcubes[i].Position == position)
  16.             {
  17.                 resutCube = Allcubes[i];
  18.                 return true;
  19.             }
  20.         }
  21.         resutCube = null;
  22.         return false;
  23.     }
複製程式碼

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

2設定相鄰方塊與頂點優化

到目前為止新增和刪除方塊都實現了,來考慮一下在兩個方塊相鄰時隱藏接觸面來優化頂點的方法。這一步並不是很必要,優化頂點後並不能帶來明顯的提升,就保持現在這樣也沒問題。但考慮到在後面還要給NPC做尋路功能,獲取方塊之間的相鄰關係是必須的。以此為前提條件的基礎下,那麼優化頂點其實就是個順帶的事情。

(1)獲取方塊之間相鄰關係

首先自然就是獲取相鄰關係,在CubeInfo指令碼里新建一個陣列去存放相鄰方塊的引用關係。基於導航的需要,水平面的每個朝向上還要額外儲存斜上斜下兩個方塊,因此最大相鄰方塊的個數就是3X4+2=14個。把把陣列的長度設為14,同時把方向用列舉儲存。

  1. public enum CubeNeighborDirection
  2. {
  3.     front,
  4.     frontUp,
  5.     frontDown,

  6.     back,
  7.     backUp,
  8.     backDown,

  9.     left,
  10.     leftUp,
  11.     leftDown,

  12.     right,
  13.     rightUp,
  14.     rightDown,

  15.     up,
  16.     dowm,
  17. }
複製程式碼

下一步是寫一個方法,根據當前方塊的座標和指定方向來推算出這個方向上的方塊座標。

  1. public static Vector3 GetCubePosByDirection(Vector3 pos,CubeNeighborDirection direction)
  2.     {  
  3.         switch (direction)
  4.         {
  5.             case CubeNeighborDirection.front:
  6.                 pos += Vector3.forward;
  7.                 break;
  8.             case CubeNeighborDirection.frontUp:
  9.                 pos += Vector3.forward + Vector3.up;
  10.                 break;
  11.             case CubeNeighborDirection.frontDown:
  12.                 pos += Vector3.forward + Vector3.down;
  13.                 break;
  14.             case CubeNeighborDirection.back:
  15.                 pos += Vector3.back;
  16.                 break;
  17.             case CubeNeighborDirection.backUp:
  18.                 pos += Vector3.back + Vector3.up;
  19.                 break;
  20.             case CubeNeighborDirection.backDown:
  21.                 pos += Vector3.back + Vector3.down;
  22.                 break;
  23.             case CubeNeighborDirection.left:
  24.                 pos += Vector3.left;
  25.                 break;
  26.             case CubeNeighborDirection.leftUp:
  27.                 pos += Vector3.left + Vector3.up;
  28.                 break;
  29.             case CubeNeighborDirection.leftDown:
  30.                 pos += Vector3.left + Vector3.down;
  31.                 break;
  32.             case CubeNeighborDirection.right:
  33.                 pos += Vector3.right;
  34.                 break;
  35.             case CubeNeighborDirection.rightUp:
  36.                 pos += Vector3.right + Vector3.up;
  37.                 break;
  38.             case CubeNeighborDirection.rightDown:
  39.                 pos += Vector3.right + Vector3.down;
  40.                 break;
  41.             case CubeNeighborDirection.up:
  42.                 pos += Vector3.up;
  43.                 break;
  44.             case CubeNeighborDirection.dowm:
  45.                 pos += Vector3.down;
  46.                 break;               
  47.         }
  48.         return pos;
  49.     }
複製程式碼

下一步就是根據這個座標,在之前儲存的所有CubeInfo的容器中找到與之對應的方塊。

  1. bool GetCubeByDirection(Vector3 position, CubeNeighborDirection direction, out CubeInfo resutCube)
  2.     {
  3.         CubeInfo cube;
  4.         if (GetCubeByPosition(CubeMetrics.GetCubePosByDirection(position, direction), out cube))
  5.         {
  6.             resutCube = cube;
  7.             return true;
  8.         }
  9.         resutCube = cube;
  10.         return false;
  11.     }

  12.     bool GetCubeByPosition(Vector3 position, out CubeInfo resutCube)
  13.     {
  14.         for (int i = 0; i < Allcubes.Count; i++)
  15.         {
  16.             if (Allcubes[i].Position == position)
  17.             {
  18.                 resutCube = Allcubes[i];
  19.                 return true;
  20.             }

  21.         }
  22.         resutCube = null;
  23.         return false;
  24.     }
複製程式碼

然後就可以設定相鄰方塊了,由於方塊的新增有先後,可以在為一個方塊設定其相鄰方塊時在相鄰方塊的相反方向上設定自己為相鄰方塊。但是方向的數量並不對稱,方向的列舉轉換成int不好找到規律,所以就用笨辦法。

  1.   public void SetNeighbor(CubeNeighborDirection direction,CubeInfo cube)
  2.     {
  3.         neighbors[(int)direction] = cube;
  4.         cube.neighbors[(int)CubeMetrics.GetOppositeDirection(direction)] = this;
  5.     }
  6.   public static CubeNeighborDirection GetOppositeDirection(CubeNeighborDirection direction)
  7.     {
  8.         switch(direction)
  9.         {
  10.             case CubeNeighborDirection.front:
  11.                 return CubeNeighborDirection.back;
  12.             case CubeNeighborDirection.frontUp:
  13.                 return CubeNeighborDirection.backDown;
  14.             case CubeNeighborDirection.frontDown:
  15.                 return CubeNeighborDirection.backUp;

  16.             case CubeNeighborDirection.back:
  17.                 return CubeNeighborDirection.front;
  18.             case CubeNeighborDirection.backUp:
  19.                 return CubeNeighborDirection.frontDown;
  20.             case CubeNeighborDirection.backDown:
  21.                 return CubeNeighborDirection.frontUp;

  22.             case CubeNeighborDirection.left:
  23.                 return CubeNeighborDirection.right;
  24.             case CubeNeighborDirection.leftUp:
  25.                 return CubeNeighborDirection.rightDown;
  26.             case CubeNeighborDirection.leftDown:
  27.                 return CubeNeighborDirection.rightUp;

  28.             case CubeNeighborDirection.right:
  29.                 return CubeNeighborDirection.left;
  30.             case CubeNeighborDirection.rightUp:
  31.                 return CubeNeighborDirection.leftDown;
  32.             case CubeNeighborDirection.rightDown:
  33.                 return CubeNeighborDirection.leftUp;

  34.             case CubeNeighborDirection.up:
  35.                 return CubeNeighborDirection.dowm;
  36.             case CubeNeighborDirection.dowm:
  37.                 return CubeNeighborDirection.up;

  38.             default:
  39.                 return direction;
  40.         }
  41.     }
複製程式碼

當然也別忘了在刪除方塊時把相鄰關係也更新一下。

  1.   public void RemoveNeighbor(CubeNeighborDirection direction)
  2.     {
  3.         neighbors[(int)direction] = null;
  4.     }
複製程式碼

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

現在就能在新增和刪除時設定正確的相鄰關係了,下一步就是優化頂點了。

(2)頂點優化

現在能知道方塊之間的相鄰關係,那在相鄰方塊的方向上隱藏當前面就是一句話的事情了。根據之前表示相鄰方向的列舉可知,下標為0,3,6,9,12,13的相鄰方塊分別對應前,後,左,右,上,下,接下來就是根據當前三角化的面的朝向來檢測相鄰方塊是否為空。

  1. public void TriaggulateCube(Vector3 p)
  2.     {
  3.        Vector3 v1 = p + CubeMetrics.cubeVertex[0];
  4.        Vector3 v2 = p + CubeMetrics.cubeVertex[1];
  5.        Vector3 v3 = p + CubeMetrics.cubeVertex[2];
  6.        Vector3 v4 = p + CubeMetrics.cubeVertex[3];
  7.        Vector3 v5 = p + CubeMetrics.cubeVertex[4];
  8.        Vector3 v6 = p + CubeMetrics.cubeVertex[5];
  9.        Vector3 v7 = p + CubeMetrics.cubeVertex[6];
  10.        Vector3 v8 = p + CubeMetrics.cubeVertex[7];

  11.         for (int i = 0; i < 6; i++)
  12.         {   
  13.             if (i == 0 && cube.neighbors[0]) { continue; }
  14.             else if (i == 1 && cube.neighbors[3]) { continue; }
  15.             else if (i == 2 && cube.neighbors[6]) { continue; }
  16.             else if (i == 3 && cube.neighbors[9]) { continue; }
  17.             else if (i == 4 && cube.neighbors[12]) { continue; }
  18.             else if (i == 5 && cube.neighbors[13]) { continue; }      
  19.           AddCubeSurface(v1, v2, v3, v4,v5, v6, v7, v8,(CubeSurface)i);      
  20.         }
  21.     }
複製程式碼

用Unity蓋房子(一):《勇者鬥惡龍:建造者2》遊戲功能的猜想
減肥前

用Unity蓋房子(一):《勇者鬥惡龍:建造者2》遊戲功能的猜想
減肥成功後

雖然看不出來變化,但表現在資料上還是蠻明顯的。

3方塊的型別UV設定

由於所有方塊都用一個Mesh表示,所以直接改其材質球的顏色是無法區分方塊型別的。那麼辦法就是把所有的方塊紋理集合在一張紋理圖上,而根據方塊的型別不同傳入不同的UV座標。所以首先在CubeInfo裡定義方塊型別的列舉欄位。

  1. ublic class CubeInfo : MonoBehaviour
  2. {
  3.     public string cubeName;
  4.     public CubeInfo[] neighbors;

  5.     public M_CubeType type;
  6. }
  7. public enum M_CubeType
  8. {
  9.     test1,
  10.     test2,
  11.     test3,
  12.     test4,
  13.     test5,
  14.     test6
  15. }
複製程式碼


先暫且用test佔位,後面再來考慮具體的型別。至於為什麼是6個型別,因為正方形有六個面,設定為六種型別剛好紋理圖就是正方形。

然後就找也好,自己畫也好,搞到一張6乘6正方形的紋理圖,大概就像這樣:

用Unity蓋房子(一):《勇者鬥惡龍:建造者2》遊戲功能的猜想
隨手畫的,不好看輕噴..

把紋理圖匯入Unity中,由於這是畫素圖,所以記得修改圖片的Filter Mode為Point,然後把圖片型別改為Sprite。

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

接下來在新增頂點資訊時同時把UV資訊也新增進去。這裡用了一個易於擴充套件的寫法,之後擴充套件方塊型別也可以直接修改TypeCount的值,很方便。

  1. void AddCubeSurface(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4,
  2.                         Vector3 v5, Vector3 v6, Vector3 v7, Vector3 v8,
  3.                         CubeSurface suface, M_CubeType type,int TypeCount)
  4.     {
  5.         //正方體為六個面,若使UV圖為正方形,則暫設正方體的型別為n種
  6.         //v座標基點:0~5/n

  7.         float uCoordinate = ((int)suface * 1.0f) / 6.0f;
  8.         float vCoordinate=((int)type*1.0f)/TypeCount*1.0f;
  9.      
  10.         Vector2 uvBasePoint=new Vector2(uCoordinate,vCoordinate);

  11.         switch (suface)
  12.         {
  13.             case CubeSurface.up:         
  14.                 AddSurfaceQuad(v1, v2, v3, v4,uvBasePoint,TypeCount);
  15.                 break;
  16.             case CubeSurface.down:
  17.                 AddSurfaceQuad(v6, v5, v8, v7,uvBasePoint, TypeCount);
  18.                 break;
  19.             case CubeSurface.left:
  20.                 AddSurfaceQuad(v1, v4, v8, v5,uvBasePoint, TypeCount);
  21.                 break;
  22.             case CubeSurface.right:
  23.                 AddSurfaceQuad(v3, v2, v6, v7,uvBasePoint, TypeCount);
  24.                 break;
  25.             case CubeSurface.front:
  26.                 AddSurfaceQuad(v2, v1, v5, v6,uvBasePoint, TypeCount);
  27.                 break;
  28.             case CubeSurface.back:
  29.                 AddSurfaceQuad(v4, v3, v7, v8,uvBasePoint, TypeCount);
  30.                 break;
  31.         }
  32.     }

  33. void AddSurfaceQuad(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector2 uvDp,int uvCount)
  34.     {
  35.         AddQuad(v1, v2, v3, v4);
  36.         AddQuadUV(uvDp,uvCount);
  37.     }

  38. void AddQuadUV(Vector2 uvBasePoint,int TypeCount)
  39.     {
  40.         float deltaU = 1f / 6.0f;
  41.         float deltaV = 1f / TypeCount*1.0f;
  42.         Vector2 uv1 = new Vector2(uvBasePoint.x, uvBasePoint.y + deltaV);
  43.         Vector2 uv2 = new Vector2(uvBasePoint.x + deltaU, uvBasePoint.y + deltaV);
  44.         Vector2 uv3 = new Vector2(uvBasePoint.x + deltaU, uvBasePoint.y);
  45.         Vector2 uv4 = uvBasePoint;
  46.         uvs.Add(uv1); uvs.Add(uv2); uvs.Add(uv3); uvs.Add(uv4);
  47.     }
複製程式碼

在場景裡新建6個toggle作為方塊型別選擇,並關聯至指令碼里修改方塊型別的列舉。

  1. public void TypeSelect(int type)
  2.     {
  3.         cubeType = (M_CubeType)type;
  4.     }
複製程式碼

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

現在就可以根據選中的型別來方便切換方塊型別了。

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

4方塊旋轉

我們的專案裡現在都是方塊,而且由於我畫的UV圖除了最上面都是一個樣子,方塊能不能旋轉無所謂。但原版遊戲中不是所有的建造素材都是方塊形狀,其中可能有階梯或者別的不對稱幾何形狀,我們後續也可以往這方面擴充套件,所以我們還是有必要去實現這個方塊旋轉功能。還是用列舉來定義方塊的朝向,為方便起見,我們把旋轉的範圍限制在水平面上。

  1. bool OrientateControl()
  2.     {
  3.         CubeOrientate temp = Orientate;
  4.         if (Input.GetKeyDown(KeyCode.Q))
  5.         {
  6.             Orientate = (int)Orientate == 0 ? (CubeOrientate)3 : (CubeOrientate)Orientate - 1;
  7.         }
  8.         else if (Input.GetKeyDown(KeyCode.E))
  9.         {
  10.             Orientate = (int)Orientate == 3 ? (CubeOrientate)0 : (CubeOrientate)Orientate + 1;
  11.         }

  12.         if(temp!=Orientate)
  13.         {
  14.             return true;
  15.         }

  16.         return false;
  17.     }

  18.     void Update()
  19.     {
  20.       
  21.         if(OrientateControl())
  22.         {
  23.             preview.UpdateCube(cubeType, Orientate);
  24.         }
  25.       
  26.     }

  27. public enum CubeOrientate
  28. {
  29.     front, right, back, left
  30. }
複製程式碼

然後在CubeInfo裡定義方塊的朝向欄位,在新增方塊時將當前朝向一併傳入。


  1. <blockquote>public CubeOrientate Orientate
複製程式碼

剛才很巧合的畫了一個六面不同的UV,剛好用來檢測旋轉功能是否正確。(怎麼可能是巧合,我肯定是故意的)

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

但是還沒完,別忘了我們之前還為mesh做了"減肥",那麼現在旋轉了方塊之後對於需要隱藏面的判定就會出問題,所以這個地方需要修正。乾脆直接把這個部分抽成一個函式。

  1. public bool CanHideSurface(CubeSurface surface)
  2.     {
  3.         if((int)surface<4)
  4.         {
  5.             int temp = (int)surface -(int)orientate;
  6.             if(temp<0)
  7.             {
  8.                 temp += 4;
  9.             }
  10.             switch((CubeOrientate)temp)
  11.             {
  12.                 case CubeOrientate.front:
  13.                     return neighbors[0];
  14.                 case CubeOrientate.back:
  15.                     return neighbors[3];
  16.                 case CubeOrientate.left:
  17.                     return neighbors[6];
  18.                 case CubeOrientate.right:
  19.                     return neighbors[9];
  20.                 default:
  21.                     return false;
  22.             }
  23.         }
  24.         else if((int)surface == 4)
  25.         {
  26.             return neighbors[12];
  27.         }
  28.         else
  29.         {
  30.             return neighbors[13];
  31.         }
  32.         
  33.     }
  34. }
  35.     void TriaggulateCube(CubeInfo cube)
  36.     {
  37.         TransformToCubeVertices(cube);

  38.         for (int i = 0; i < 6; i++)
  39.         {
  40.             if (!cube.CanHideSurface((CubeSurface)i))
  41.             {
  42.                 AddCubeSurface(tempCubeVertices[0], tempCubeVertices[1], tempCubeVertices[2], tempCubeVertices[3],
  43.                                tempCubeVertices[4], tempCubeVertices[5], tempCubeVertices[6], tempCubeVertices[7],
  44.                               (CubeSurface)i, cube.type,6);
  45.             }
  46.         }
  47.     }
複製程式碼

然後再檢查一下相鄰時是否會出問題。

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

結束

這期我們們算是把基本的架子搭出來了,可以看到使用簡單粗暴但耗效能的方式一旦換了個思路,其實還是有點麻煩,但這也正是寫這種小工程有意思的地方。

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

文章的程式碼貼地有些亂,有興趣的同學還是下載工程研究吧,感謝觀看至此。

本期工程地址:https://github.com/tank1018702/CubeBuilder

有意參與線下游戲開發學習的童鞋,歡迎訪問http://levelpp.com/。

作者:沈琰
專欄地址:https://zhuanlan.zhihu.com/p/61067078

相關文章