從零開始做一個SLG遊戲(二):用mesh實現簡單的地形
本文主要是用mesh實現簡單的地形。暫時先繪製三種地形:高山、平原、水域。
首先要做的是網格的細化:
上一篇已經實現了單個六邊形的繪製,實現方式是將六邊形分割成6個等邊三角形,然後分別繪製。
現在需要將每個三角形再次細化,將一個三角形細化為4個小三角形。
如下圖:
在上一篇文章中,封裝了三角形繪製的函式:
- private void AddTriangle(Vector3 v1, Vector3 v2, Vector3 v3)
- {
- int count = triangles.Count;
- vertices.Add(v1);
- triangles.Add(count++);
- vertices.Add(v2);
- triangles.Add(count++);
- vertices.Add(v3);
- triangles.Add(count++);
- }
其中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)
於是,新寫一個遞迴函式用於細化(原函式保留):
- private void AddTriangle(Vector3 v1, Vector3 v2, Vector3 v3, int time)
- {
- if (time == 0)
- {
- AddTriangle(v1, v2, v3);
- }
- else
- {
- time--;
- Vector3 v4 = Vector3.Lerp(v1, v2, 0.5f);
- Vector3 v5 = Vector3.Lerp(v2, v3, 0.5f);
- Vector3 v6 = Vector3.Lerp(v1, v3, 0.5f);
- AddTriangle(v1, v4, v6, time);
- AddTriangle(v4, v2, v5, time);
- AddTriangle(v4, v5, v6, time);
- AddTriangle(v3, v6, v5, time);
- }
- }
- /// <summary>
- /// 繪製地形
- /// </summary>
- public void Draw(HexTerrian type)
- {
- ……
- for (HexDirection dir = HexDirection.NE; dir <= HexDirection.NW; dir++)
- {
- Vector3 v1 = HexMetrics.corners[(int)dir];
- Vector3 v2 = HexMetrics.corners[(int)dir + 1];
- AddTriangle(center, v1, v2, 2);
- }
- UpdateMesh();
- }
其中time指的是細化次數。
但是,如今生成的圖片還是一個六邊形,與原來沒有變化。原因是,沒有對圖片上的點做擾動處理,所以雖然生成的時候是細化了生成的,但拼在一起還是平的。
現加入擾動處理的函式:
- private Vector3 Perturb(Vector3 pos)
- {
- float level = 0.5f;
- Vector3 localPos = transform.localPosition + pos;
- pos.x += level * (Mathf.PerlinNoise(localPos.x, localPos.z) - 0.5f);
- pos.y += level * (Mathf.PerlinNoise(localPos.x + 1f, localPos.z + 1f) - 0.5f);
- pos.z += level * (Mathf.PerlinNoise(localPos.x + 2f, localPos.z + 2f) - 0.5f);
- return pos;
- }
這裡用的是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)函式中繪製三角形的部分加入擾動:
- private void AddTriangle(Vector3 v1, Vector3 v2, Vector3 v3, int time)
- {
- if (time == 0)
- {
- AddTriangle(Perturb(v1), Perturb(v2), Perturb(v3));
- }
- ………………
- }
執行後,我們得到如下的圖:
然後再調一下材質的顏色,本遊戲採用的是lowpoly風格,該風格的反射很弱,所以需要把材質上的specular hightlight的鉤去掉,然後再調整一下圖片的顏色:
有一點泥土的感覺了,暫時先這麼用著吧。
下一步要解決的問題是,目前只是一個單面的六邊形,將這個六邊形繪製成一個稜柱,表現會更好一些,當然,稜柱的底就不畫了,反正看不到。
首先寫一個函式用於繪製一個矩形:
- /// <summary>
- /// v3 v4
- ///
- /// v1 v2
- /// </summary>
- /// <param name="v1"></param>
- /// <param name="v2"></param>
- /// <param name="v3"></param>
- /// <param name="v4"></param>
- private void AddSquare(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4)
- {
- AddTriangle(v1, v3, v2);
- AddTriangle(v3, v4, v2);
- }
v1,v2,v3,v4的位置備註所示。
如果不考慮細化,那麼繪製邊上的面的函式就可以如下表示:
- /// <summary>
- /// 繪製地形
- /// </summary>
- public void Draw(HexTerrian type)
- {
- ……………………
- for (HexDirection dir = HexDirection.NE; dir <= HexDirection.NW; dir++)
- {
- Vector3 v1 = HexMetrics.corners[(int)dir];
- Vector3 v2 = HexMetrics.corners[(int)dir + 1];
- AddTriangle(center, v1, v2, 2);
- Vector3 v3 = v1 + 5f * Vector3.down;
- Vector3 v4 = v2 + 5f * Vector3.down;
- AddSquare(v1, v2, v3, v4);
- }
- ……………………
- }
接下來考慮分形的做法,先看下示意圖:
然後是程式碼:
- private void AddEdge(Vector3 v1, Vector3 v2, int time)
- {
- if (time == 0)
- {
- v1 = Perturb(v1);
- v2 = Perturb(v2);
- float d = 5f;
- Vector3 v3 = v1 + d * Vector3.down;
- Vector3 v4 = v2 + d * Vector3.down;
- AddSquare(v1, v2, v3, v4);
- }
- else
- {
- time--;
- Vector3 v5 = Vector3.Lerp(v1, v2, 0.5f);
- AddEdge(v1, v5, time);
- AddEdge(v5, v2, time);
- }
- }
因為v3,v4是通過v1,v2計算得來的,所以輸入就只需要v1,v2就行了。
因為點的擾動是最後計算的,所以會和六邊形的邊重合,不會出現錯位。
效果如下圖:
接下來是河流和高山地塊。
先定義一個地形型別的列舉:
- public enum HexTerrian
- {
- Water,
- Plain,
- Mountain,
- }
然後在HexCell類中加入地形型別,用於表示當前地形的型別,同時定義一個材質的陣列用於儲存不同地形對應的材質,並在繪製函式中加入地形引數:
- public class HexCell : MonoBehaviour {
- ……
- public HexTerrian terr;
- public Material[] materials;
- ……
- public void Draw(HexTerrian type)
- {
- terrianType = type;
- GetComponent<Renderer>().material = materials[(int)type];
- ……
- }
將做好的3個地形材質拖到陣列上,記得和列舉一一對應。
先處理湖泊地形,湖泊和平原相比,只在於水平面將會比平原低一點:
- ………
- private readonly float deep = 5;//方塊厚度
- private readonly float waterLevel = 2;//水平面離地距離
- public int fractalTime = 3;//細化次數
- ………
- for (HexDirection dir = HexDirection.NE; dir <= HexDirection.NW; dir++)
- {
- Vector3 v1 = HexMetrics.corners[(int)dir];
- Vector3 v2 = HexMetrics.corners[(int)dir + 1];
- switch (type)
- {
- case HexTerrian.Water:
- if (dir == HexDirection.NE)
- {
- center -= waterLevel * Vector3.up;
- }
- v1 -= waterLevel * Vector3.up;
- v2 -= waterLevel * Vector3.up;
- break;
- case HexTerrian.Plain:
- break;
- }
- AddTriangle(center, v1, v2, fractalTime);
- AddEdge(v1, v2, fractalTime);
- }
- ……
邊緣六個面繪製的時候也要記得短一些:
- private void AddEdge(Vector3 v1, Vector3 v2, int time)
- {
- if (time == 0)
- {
- v1 = Perturb(v1);
- v2 = Perturb(v2);
- float d = (terrianType == HexTerrian.Water) ? (deep - waterLevel) : deep;
- Vector3 v3 = v1 + d * Vector3.down;
- Vector3 v4 = v2 + d * Vector3.down;
- AddSquare(v1, v2, v3, v4);
- }
- ……
- }
接下來繪製高山,對於高山,先簡單處理一下:將頂點提高一些,然後細化一下。
- ……
- case HexTerrian.Mountain:
- if (dir == HexDirection.NE)
- {
- center += 5f * Vector3.up;
- }
- break;
- ……
然後測試一下地形效果,隨機地形在這篇文章就不做了,主要是把所有的地形都顯示出來,所以隨便寫一下吧:
- foreach (HexCell c in hexCells)
- {
- int height = (c.Pos.x + c.Pos.y) % 3;
- c.Draw((HexTerrian)height);
- }
於是,得到了封面上的效果圖:
相關閱讀:
從零開始做一個SLG遊戲(一):六邊形網格
從零開始做一個SLG遊戲(二):用mesh實現簡單的地形
作者:觀復
專欄地址:https://zhuanlan.zhihu.com/p/44683990
相關文章
- 從零開始做一個SLG遊戲(三):用unity繪製圖形遊戲Unity
- 從零開始做一個SLG遊戲(一):六邊形網格遊戲
- 從零開始做一個SLG遊戲(七):遊戲系統以及配置表遊戲
- 從零開始做一個SLG遊戲(八):配置表載入元件遊戲元件
- 從零開始做一個SLG遊戲(六):UI系統擴充套件遊戲UI套件
- 從零開始做一個SLG遊戲(四):UI系統之主介面搭建遊戲UI
- 從零開始做一個SLG遊戲(五):UI系統之彈窗功能遊戲UI
- 從零開始:用REACT寫一個格鬥遊戲(二)React遊戲
- 從零開始實現放置遊戲(一)遊戲
- 從零開始實現一個RPC框架(二)RPC框架
- 從零開始實現一個簡易的Java MVC框架(二)--實現Bean容器JavaMVC框架Bean
- 從零開始:用REACT寫一個格鬥遊戲(一)React遊戲
- 從零開始實現一個簡易的Java MVC框架JavaMVC框架
- 從零開始實現一個RPC框架(零)RPC框架
- 從零開始實現放置遊戲(一):整體框架搭建遊戲框架
- 從零開始實現一個簡易的Java MVC框架(七)–實現MVCJavaMVC框架
- 從零開始實現一個簡易的Java MVC框架(三)--實現IOCJavaMVC框架
- 從零開始實現一個簡易的Java MVC框架(四)--實現AOPJavaMVC框架
- 從零開始實現一個簡易的Java MVC框架(七)--實現MVCJavaMVC框架
- 從零開始實現一個RPC框架(一)RPC框架
- 從零點五開始用Unity做半個2D戰棋小遊戲(一)Unity遊戲
- 從零開始的Java RASP實現(二)Java
- 從零實現一個RPC框架系列文章(二):11個類實現簡單RPCRPC框架
- 從零開始實現一個RPC框架(四)RPC框架
- 從零開始實現一個RPC框架(五)RPC框架
- 從零開始實現一個RPC框架(三)RPC框架
- 從0開始用python寫一個命令列小遊戲(二)Python命令列遊戲
- 從零開始實現簡單 RPC 框架 4:註冊中心RPC框架
- 從零開始實現一個自己的指令碼引擎指令碼
- 從零開始 實現一個自己的指令碼引擎指令碼
- 從零開始用 proxy 實現 Mobx
- 從零開始的Java RASP實現(一)Java
- 從零開始實現一個分散式RPC框架分散式RPC框架
- 從零開始實現一個IDL+RPC框架RPC框架
- 從零開始實現一個簡易的Java MVC框架(八)–製作StarterJavaMVC框架
- 從零開始實現一個簡易的Java MVC框架(八)--製作StarterJavaMVC框架
- 從零開始實現放置遊戲(六):Excel批量匯入遊戲Excel
- 從零開始實現一個React(四):非同步的setStateReact非同步