2011年11月26日 星期六

Unity 在遊戲中改變地形高度


如果我們製作的遊戲可以在遊戲進行中,因為某種撞擊或爆炸而使地面產生凹陷或突起等變形,是不是很酷!但是這該怎麼做呢?使用很多的地板模型去動態拼接嗎?還是利用程式去控制地板 Mesh 頂點的變化?這麼做會不會有拼接不完整或是形狀變得很奇怪的地方呢?如果對自己的電腦圖學演算及本身的程式與建模技術相當有自信的話,可以嘗試用自己的方式完成需求;但如果沒有這些功力,可以考慮使用 Unity 內建的 TerrainData class。


使用 Unity 的 Terrain > Create Terrain 建立地形除了容易學且方便使用外,如果我們點選場景(Scene)視窗 的 tab 下面那個選單選擇 Tex-Wire 來顯示出場景中模型的網格,將會發現 terrain 上的網格是會動態變化的,完全沒有高低起伏的平面,網格的分佈是很平均的,但是當地形有起伏時,Unity 則會自動做些改變使地形更靈活;所以如果要動態的改變地形做些應用,使用 Unity 內建的 Terrain 也是個相當便利的。


完全平面的網格分佈很平均

地形有起伏時會自動改變

然而,我們現在看到的地形編輯是在 Unity Editor 中,那麼如何在遊戲執行期控制它呢?我們可以從官網的 Scripting > Runtime Classes > Terrain 找到,這些地形資料其實是記錄在 terrain.terrainData 中,其中包含了地形高度、貼圖、樹、植被... 等等的資料,詳細內容可查看官網 Scripting > Runtime Classes > TerrainData,既然官方已經提供了這些東西,那麼在執行期要控制地形變化就不難了。

首先,我們來做些準備動作,因為地形是有高度的上下限,所以我們在設計地形時,要先利用 Terrain > Flatten Heightmap 把地面整平提高,這樣才不會讓地形到時候凹陷不下去。另外,就是利用 Terrain > Set Resolution 做些地形設定,例如 Terrain WidthTerrain Length 代表地形的長寬,它們的值儲存在 terrain.terrainData.sizeHeightMap Resolution 可說是等高線控制高度的控制密度,它同時代表 terrain.terrainData.heightmapWidthterrain.terrainData.heightmapHeight 的值。

接下來,因為控制地形資料中高低位置的點是受到地形長寬及 HeightMap Resolution 值的影響,所以我們無法直接從 3D 空間座標去改變它,而必須經過換算去找到正確的控制點;通常建立地形時,預設地形的位置是會在 ( 0 , 0 , 0 ),但依實際需求,我們的地形可能也未必都在固定位置,所以這個相對位置也可能需要換算,經過這些換算,我們就能找出地形內的控制點,並有機會試圖改變它們的高度,以下為換算方式 ...

//地形資料
TerrainData terrainData = terrain.terrainData;

//物件在地形上的相對位置
Vector3 terrainLocalPos = obj.transform.position - terrain.transform.position;

//地形內的控制點
Vector2 controlPos = new Vector2( terrainLocalPos.x / terrainData.size.x * terrainData.heightmapWidth , terrainLocalPos.y / terrainData.size.z * terrainData.heightmapHeight );



找到控制點之後,就可以開始改變高度,改變高度前必須要先取得原本的高度值,而 Unity 的地形高度資料數值是介於 0 ~ 1 之間,所以不管在 Set Resolution 裡設定的 Terrain Height 值是多少,我們都必須要以此為比例去換算為正確的數值;我們可以使用 terrain.terrainData.GetHeight() 取得單一點的高度值或使用  terrain.terrainData.GetHeights() 取得一個範圍的高度值,並利用 terrain.terrainData.SetHeights() 寫入新的高度值。

其中,要特別注意的是 terrain.terrainData.GetHeight() 取得的數值是介於 0Set ResolutionTerrain Height 值之間,terrain.terrainData.GetHeights() 取得的數值是介於 0 ~ 1 之間,而 terrain.terrainData.SetHeights() 寫入的值必須介於 0 ~ 1 之間,如果寫入的數值高於 1 或是低於 0,Unity 的地形系統將會自動調整高度為最高或最低 (0Set ResolutionTerrain Height 值),這樣可能會使得地形變動太過突兀;以下分別列出以供參考..

--- 第一種 ---

//要增加的高度
float addHeight = 1.0f;

//取出原來的高度
float oldHeight = terrainData.GetHeight( (int)controlPos.x , (int)controlPos.y );

//新的高度資料
float[,] newHeightData = new float[1,1] { { ( oldHeight + addHeight ) / terrainData.heightmapScale.y } };

//設定新的高度資料
terrainData.SetHeights( (int)controlPos.x , (int)controlPos.y , newHeightData );

--- 第二種 ---

//要增加的高度
float addHeight = 1.0f;

//取出原來的高度
float[,] oldHeightData = terrainData.GetHeights( (int)controlPos.x , (int)controlPos.y , 1 , 1 );

//新的高度資料
float[,] newHeightData = new float[1,1] { { addHeight / terrainData.heightmapScale.y + oldHeightData[0,0] } };

//設定新的高度資料
terrainData.SetHeights( (int)controlPos.x , (int)controlPos.y , newHeightData );


以上,我們利用簡短的程式碼讓遊戲在執行期間改變地形高低,當然,每次只有一個點改變高度好像有點怪怪的,但依照以上的原理,我們可以輕鬆的去改變地形上任何位置的高度,所以,也可以利用找到的控制點延伸運算出一個範圍的控制點,來做區域性的地形高低變動。