從零開始做一個SLG遊戲(二):用mesh實現簡單的地形

遊資網發表於2019-10-10
從零開始做一個SLG遊戲(二):用mesh實現簡單的地形

本文主要是用mesh實現簡單的地形。暫時先繪製三種地形:高山、平原、水域。

首先要做的是網格的細化:

上一篇已經實現了單個六邊形的繪製,實現方式是將六邊形分割成6個等邊三角形,然後分別繪製。

現在需要將每個三角形再次細化,將一個三角形細化為4個小三角形。

如下圖:

從零開始做一個SLG遊戲(二):用mesh實現簡單的地形
細化原理如下圖:

從零開始做一個SLG遊戲(二):用mesh實現簡單的地形

在上一篇文章中,封裝了三角形繪製的函式:

  1. private void AddTriangle(Vector3 v1, Vector3 v2, Vector3 v3)

  2.         {

  3.                 int count = triangles.Count;

  4.                 vertices.Add(v1);

  5.                 triangles.Add(count++);

  6.                 vertices.Add(v2);

  7.                 triangles.Add(count++);

  8.                 vertices.Add(v3);

  9.                 triangles.Add(count++);

  10.         }
複製程式碼

其中v1,v2,v3如圖所示,而v4,v5,v6分別為三條邊的中點。

所以有:

Vector3 v4 = Vector3.Lerp(v1, v2, 0.5f);
Vector3 v5 = Vector3.Lerp(v2, v3, 0.5f);
Vector3 v6 = Vector3.Lerp(v1, v3, 0.5f);
所以新的4個三角形分別為:(v1, v4, v6)(v4, v2, v5)(v4, v5, v6)(v3, v6, v5)

於是,新寫一個遞迴函式用於細化(原函式保留):

  1.          private void AddTriangle(Vector3 v1, Vector3 v2, Vector3 v3, int time)

  2.         {

  3.                 if (time == 0)

  4.                 {

  5.                         AddTriangle(v1, v2, v3);

  6.                 }

  7.                 else

  8.                 {

  9.                         time--;

  10.                         Vector3 v4 = Vector3.Lerp(v1, v2, 0.5f);

  11.                         Vector3 v5 = Vector3.Lerp(v2, v3, 0.5f);

  12.                         Vector3 v6 = Vector3.Lerp(v1, v3, 0.5f);

  13.                         AddTriangle(v1, v4, v6, time);

  14.                         AddTriangle(v4, v2, v5, time);

  15.                         AddTriangle(v4, v5, v6, time);

  16.                         AddTriangle(v3, v6, v5, time);

  17.                 }

  18.         }

  19.         /// <summary>

  20.         /// 繪製地形

  21.         /// </summary>

  22.         public void Draw(HexTerrian type)

  23.         {

  24.                 ……

  25.                 for (HexDirection dir = HexDirection.NE; dir <= HexDirection.NW; dir++)

  26.                 {

  27.                         Vector3 v1 = HexMetrics.corners[(int)dir];

  28.                         Vector3 v2 = HexMetrics.corners[(int)dir + 1];

  29.                         AddTriangle(center, v1, v2, 2);

  30.                 }

  31.                 UpdateMesh();

  32.         }
複製程式碼

其中time指的是細化次數。

從零開始做一個SLG遊戲(二):用mesh實現簡單的地形

但是,如今生成的圖片還是一個六邊形,與原來沒有變化。原因是,沒有對圖片上的點做擾動處理,所以雖然生成的時候是細化了生成的,但拼在一起還是平的。

現加入擾動處理的函式:

  1. private Vector3 Perturb(Vector3 pos)

  2.         {

  3.                 float level = 0.5f;

  4.                 Vector3 localPos = transform.localPosition + pos;

  5.                 pos.x += level * (Mathf.PerlinNoise(localPos.x, localPos.z) - 0.5f);

  6.                 pos.y += level * (Mathf.PerlinNoise(localPos.x + 1f, localPos.z + 1f) - 0.5f);

  7.                 pos.z += level * (Mathf.PerlinNoise(localPos.x + 2f, localPos.z + 2f) - 0.5f);

  8.                 return pos;

  9.         }
複製程式碼

這裡用的是unity自帶的柏林噪聲函式 Mathf.PerlinNoise(float x,float y),這個演算法會根據x以及y的值生成一個隨機的函式,固定的x和y,生成的隨機值是固定的。所以暫時先用這個做擾動。

Mathf.PerlinNoise(float x,float y)得出的是0到1的一個值。

所以減去0.5。得到的是-0.5到0.5的一個值。

level是一個擾動的引數,擾動後,單個座標最多偏移0.25個單位,這樣顯示出來的就是一個有輕微褶皺的地形圖片。

將在AddTriangle(Vector3 v1, Vector3 v2, Vector3 v3, int time)函式中繪製三角形的部分加入擾動:

  1.          private void AddTriangle(Vector3 v1, Vector3 v2, Vector3 v3, int time)

  2.         {

  3.                 if (time == 0)

  4.                 {

  5.                         AddTriangle(Perturb(v1), Perturb(v2), Perturb(v3));

  6.                 }

  7.                 ………………

  8.         }
複製程式碼

執行後,我們得到如下的圖:

從零開始做一個SLG遊戲(二):用mesh實現簡單的地形

然後再調一下材質的顏色,本遊戲採用的是lowpoly風格,該風格的反射很弱,所以需要把材質上的specular hightlight的鉤去掉,然後再調整一下圖片的顏色:

從零開始做一個SLG遊戲(二):用mesh實現簡單的地形

有一點泥土的感覺了,暫時先這麼用著吧。

下一步要解決的問題是,目前只是一個單面的六邊形,將這個六邊形繪製成一個稜柱,表現會更好一些,當然,稜柱的底就不畫了,反正看不到。

首先寫一個函式用於繪製一個矩形:

  1. /// <summary>

  2.         /// v3  v4

  3.         ///

  4.         /// v1  v2

  5.         /// </summary>

  6.         /// <param name="v1"></param>

  7.         /// <param name="v2"></param>

  8.         /// <param name="v3"></param>

  9.         /// <param name="v4"></param>

  10.         private void AddSquare(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4)

  11.         {

  12.                 AddTriangle(v1, v3, v2);

  13.                 AddTriangle(v3, v4, v2);

  14.         }
複製程式碼

v1,v2,v3,v4的位置備註所示。

如果不考慮細化,那麼繪製邊上的面的函式就可以如下表示:

  1. /// <summary>

  2.         /// 繪製地形

  3.         /// </summary>

  4.         public void Draw(HexTerrian type)

  5.         {

  6.                 ……………………

  7.                 for (HexDirection dir = HexDirection.NE; dir <= HexDirection.NW; dir++)

  8.                 {

  9.                         Vector3 v1 = HexMetrics.corners[(int)dir];

  10.                         Vector3 v2 = HexMetrics.corners[(int)dir + 1];



  11.                         AddTriangle(center, v1, v2, 2);

  12.                         Vector3 v3 = v1 + 5f * Vector3.down;

  13.                         Vector3 v4 = v2 + 5f * Vector3.down;

  14.                         AddSquare(v1, v2, v3, v4);

  15.                 }



  16.                 ……………………

  17.         }
複製程式碼

接下來考慮分形的做法,先看下示意圖:

從零開始做一個SLG遊戲(二):用mesh實現簡單的地形

然後是程式碼:

  1. private void AddEdge(Vector3 v1, Vector3 v2, int time)

  2.         {

  3.                 if (time == 0)

  4.                 {

  5.                         v1 = Perturb(v1);

  6.                         v2 = Perturb(v2);

  7.                         float d = 5f;

  8.                         Vector3 v3 = v1 + d * Vector3.down;

  9.                         Vector3 v4 = v2 + d * Vector3.down;

  10.                         AddSquare(v1, v2, v3, v4);

  11.                 }

  12.                 else

  13.                 {

  14.                         time--;

  15.                         Vector3 v5 = Vector3.Lerp(v1, v2, 0.5f);

  16.                         AddEdge(v1, v5, time);

  17.                         AddEdge(v5, v2, time);

  18.                 }

  19.         }
複製程式碼

因為v3,v4是通過v1,v2計算得來的,所以輸入就只需要v1,v2就行了。

因為點的擾動是最後計算的,所以會和六邊形的邊重合,不會出現錯位。

效果如下圖:

從零開始做一個SLG遊戲(二):用mesh實現簡單的地形

接下來是河流和高山地塊。

先定義一個地形型別的列舉:

  1. public enum HexTerrian

  2. {

  3.         Water,

  4.         Plain,

  5.         Mountain,

  6. }
複製程式碼

然後在HexCell類中加入地形型別,用於表示當前地形的型別,同時定義一個材質的陣列用於儲存不同地形對應的材質,並在繪製函式中加入地形引數:

  1. public class HexCell : MonoBehaviour {

  2.         ……

  3.         public HexTerrian terr;

  4.         public Material[] materials;

  5.         ……

  6.         public void Draw(HexTerrian type)

  7.         {

  8.                 terrianType = type;

  9.                 GetComponent<Renderer>().material = materials[(int)type];

  10.         ……

  11. }
複製程式碼

將做好的3個地形材質拖到陣列上,記得和列舉一一對應。

先處理湖泊地形,湖泊和平原相比,只在於水平面將會比平原低一點:

  1. ………

  2.         private readonly float deep = 5;//方塊厚度

  3.         private readonly float waterLevel = 2;//水平面離地距離

  4.         public int fractalTime = 3;//細化次數

  5. ………

  6.                 for (HexDirection dir = HexDirection.NE; dir <= HexDirection.NW; dir++)

  7.                 {

  8.                         Vector3 v1 = HexMetrics.corners[(int)dir];

  9.                         Vector3 v2 = HexMetrics.corners[(int)dir + 1];

  10.                         switch (type)

  11.                         {

  12.                                 case HexTerrian.Water:

  13.                                         if (dir == HexDirection.NE)

  14.                                         {

  15.                                                 center -= waterLevel * Vector3.up;

  16.                                         }

  17.                                         v1 -= waterLevel * Vector3.up;

  18.                                         v2 -= waterLevel * Vector3.up;

  19.                                         break;

  20.                                 case HexTerrian.Plain:

  21.                                         break;

  22.                         }

  23.                         AddTriangle(center, v1, v2, fractalTime);

  24.                         AddEdge(v1, v2, fractalTime);

  25.                 }

  26. ……
複製程式碼

邊緣六個面繪製的時候也要記得短一些:

  1. private void AddEdge(Vector3 v1, Vector3 v2, int time)

  2.         {

  3.                 if (time == 0)

  4.                 {

  5.                         v1 = Perturb(v1);

  6.                         v2 = Perturb(v2);

  7.                         float d = (terrianType == HexTerrian.Water) ? (deep - waterLevel) : deep;

  8.                         Vector3 v3 = v1 + d * Vector3.down;

  9.                         Vector3 v4 = v2 + d * Vector3.down;

  10.                         AddSquare(v1, v2, v3, v4);

  11.                 }

  12.                 ……

  13.         }
複製程式碼

接下來繪製高山,對於高山,先簡單處理一下:將頂點提高一些,然後細化一下。

  1. ……

  2. case HexTerrian.Mountain:

  3. if (dir == HexDirection.NE)

  4. {

  5.         center += 5f * Vector3.up;

  6. }

  7. break;

  8. ……
複製程式碼

然後測試一下地形效果,隨機地形在這篇文章就不做了,主要是把所有的地形都顯示出來,所以隨便寫一下吧:

  1.                 foreach (HexCell c in hexCells)

  2.                 {

  3.                         int height = (c.Pos.x + c.Pos.y) % 3;

  4.                         c.Draw((HexTerrian)height);

  5.                 }
複製程式碼

於是,得到了封面上的效果圖:

從零開始做一個SLG遊戲(二):用mesh實現簡單的地形

相關閱讀:

從零開始做一個SLG遊戲(一):六邊形網格
從零開始做一個SLG遊戲(二):用mesh實現簡單的地形

作者:觀復
專欄地址:
https://zhuanlan.zhihu.com/p/44683990

相關文章