在本篇教程中,我們將使用簡單的物理機制模擬壹個動態的2D水體。我們將使用壹個線性渲染器、網格渲染器,觸發器以及粒子的混合體來創造這壹水體效果,最終得到可運用於妳下款遊戲的水紋和水花。這裏包含了Unity樣本源,但妳應該能夠使用任何遊戲引擎以相同的原理執行類似的操作。
設置水體管理器
我們將使用Unity的壹個線性渲染器來渲染我們的水體表面,並使用這些節點來展現持續的波紋。
unity-water-linerenderer(from gamedevelopment)
我們將追蹤每個節點的位置、速度和加速情況。為此,我們將會使用到陣列。所以在我們的類頂端將添加如下變量:
float[] xpositions;
float[] ypositions;
float[] velocities;
float[] accelerations;
LineRenderer Body;
LineRenderer將存儲我們所有的節點,並概述我們的水體。我們仍需要水體本身,將使用Meshes來創造。我們將需要對象來托管這些網格。
GameObject[] meshobjects;
Mesh[] meshes;
我們還需要碰撞器以便事物可同水體互動:
GameObject[] colliders;
我們也存儲了所有的常量:
const float springconstant = 0.02f;
const float damping = 0.04f;
const float spread = 0.05f;
const float z = -1f;
這些常量中的z是我們為水體設置的Z位移。我們將使用-1標註它,這樣它就會呈現於我們的對象之前(遊戲邦註:妳可能想根據自己的需求將其調整為在對象之前或之後,那妳就必須使用Z坐標來確定與之相關的精靈所在的位置)。
下壹步,我們將保持壹些值:
float baseheight;
float left;
float bottom;
這些就是水的維度。
我們將需要壹些可以在編輯器中設置的公開變量。首先,我們將為水花使用粒子系統:
public GameObject splash:
接下來就是我們將用於線性渲染器的材料:
public Material mat:
此外,我們將為主要水體使用的網格類型如下:
public GameObject watermesh:
我們想要能夠托管所有這些數據的遊戲對象,令其作為管理器,產出我們遊戲中的水體。為此,我們將編寫SpawnWater()函數。
這個函數將采用水體左邊、跑馬度、頂點以及底部的輸入:
public void SpawnWater(float Left, float Width, float Top, float Bottom)
{
(雖然這看似有所矛盾,但卻有利於從左往右快速進行關卡設計)
創造節點
現在我們將找出自己需要多少節點:
int edgecount = Mathf.RoundToInt(Width) * 5;
int nodecount = edgecount + 1;
我們將針對每個單位寬度使用5個節點,以便呈現流暢的移動(妳可以改變這壹點以便平衡效率與流暢性)。我們由此可得到所有線段,然後需要在末端的節點 + 1。
我們要做的首件事就是以LineRenderer組件渲染水體:
Body = gameObject.AddComponentlt;LineRenderer;();
Body.material = mat;
Body.material.renderQueue = 1000;
Body.SetVertexCount(nodecount);
Body.SetWidth(0.1f, 0.1f);
我們在此還要做的是選擇材料,並通過選擇渲染隊列中的位置而令其在水面之上渲染。我們設置正確的節點數據,將線段寬度設為0.1。
妳可以根據自己所需的線段粗細來改變這壹寬度。妳可能註意到了SetWidth()需要兩個參數,這是線段開始及末尾的寬度。我們希望該寬度恒定不變。
現在我們制作了節點,將初始化我們所有的頂級變量:
xpositions = new float[nodecount];
ypositions = new float[nodecount];
velocities = new float[nodecount];
accelerations = new float[nodecount];
meshobjects = new GameObject[edgecount];
meshes = new Mesh[edgecount];
colliders = new GameObject[edgecount];
baseheight = Top;
bottom = Bottom;
left = Left;
我們已經有了所有陣列,將控制我們的數據。
現在要設置我們陣列的值。我們將從節點開始:
for (int i = 0; i lt; nodecount; i++)
{
ypositions[i] = Top;
xpositions[i] = Left + Width * i / edgecount;
accelerations[i] = 0;
velocities[i] = 0;
Body.SetPosition(i, new Vector3(xpositions[i], ypositions[i], z));
}
在此,我們將所有Y位置設於水體之上,之後壹起漸進增加所有節點。因為水面平靜,我們的速度和加速值最初為0。
我們將把LineRenderer (Body)中的每個節點設為其正確的位置,以此完成這個循環。
創造網格
這正是它棘手的地方。
我們有自己的線段,但我們並沒有水體本身。我們要使用網格來制作,如下所示:
for (int i = 0; i lt; edgecount; i++)
{
meshes[i] = new Mesh();
現在,網格存儲了壹系列變量。首個變量相當簡單:它包含了所有頂點(或轉角)。
unity-water-Firstmesh(from gamedevelopment)
該圖表顯示了我們所需的網格片段的樣子。第壹個片段中的頂點被標註出來了。我們總***需要4個頂點。
Vector3[] Vertices = new Vector3[4];
Vertices[0] = new Vector3(xpositions[i], ypositions[i], z);
Vertices[1] = new Vector3(xpositions[i + 1], ypositions[i + 1], z);
Vertices[2] = new Vector3(xpositions[i], bottom, z);
Vertices[3] = new Vector3(xpositions[i+1], bottom, z);
現在如妳所見,頂點0處於左上角,1處於右上角,2是左下角,3是右下角。我們之後要記住。
網格所需的第二個性能就是UV。網格擁有紋理,UV會選擇我們想擷取的那部分紋理。在這種情況下,我們只想要左上角,右上角,右下角和右下角的紋理。
Vector2[] UVs = new Vector2[4];
UVs[0] = new Vector2(0, 1);
UVs[1] = new Vector2(1, 1);
UVs[2] = new Vector2(0, 0);
UVs[3] = new Vector2(1, 0);
現在我們又需要這些數據了。網格是由三角形組成的,我們知道任何四邊形都是由兩個三角形組成的,所以現在我們需要告訴網格它如何繪制這些三角形。
unity-water-Tris(from gamedevelopment)
看看含有節點順序標註的轉角。三角形A連接節點0,1,以及3,三角形B連接節點3,2,1。因此我們想制作壹個包含6個整數的陣列:
int[] tris = new int[6] { 0, 1, 3, 3, 2, 0 };
這就創造了我們的四邊形。現在我們要設置網格的值。
meshes[i].vertices = Vertices;
meshes[i].uv = UVs;
meshes[i].triangles = tris;
現在我們已經有了自己的網格,但我們沒有在場景是渲染它們的遊戲對象。所以我們將從包括壹個網格渲染器和篩網過濾器的watermesh預制件來創造它們。
meshobjects[i] = Instantiate(watermesh,Vector3.zero,Quaternion.identity) as GameObject;
meshobjects[i].GetComponentlt;MeshFilter;().mesh = meshes[i];
meshobjects[i].transform.parent = transform;
我們設置了網格,令其成為水體管理器的子項。
創造碰撞效果
現在我們還需要自己的碰撞器:
colliders[i] = new GameObject();
colliders[i].name = “Trigger”;
colliders[i].AddComponentlt;BoxCollider2D;();
colliders[i].transform.parent = transform;
colliders[i].transform.position = new Vector3(Left + Width * (i + 0.5f) / edgecount, Top – 0.5f, 0);
colliders[i].transform.localScale = new Vector3(Width / edgecount, 1, 1);
colliders[i].GetComponentlt;BoxCollider2D;().isTrigger = true;
colliders[i].AddComponentlt;WaterDetector;();
至此,我們制作了方形碰撞器,給它們壹個名稱,以便它們會在場景中顯得更整潔壹點,並且再次制作水體管理器的每個子項。我們將它們的位置設置於兩個節點之點,設置好大小,並為其添加了WaterDetector類。
現在我們擁有自己的網格,我們需要壹個函數隨著水體移動進行更新:
void UpdateMeshes()
{
for (int i = 0; i lt; meshes.Lenh; i++)
{
Vector3[] Vertices = new Vector3[4];
Vertices[0] = new Vector3(xpositions[i], ypositions[i], z);
Vertices[1] = new Vector3(xpositions[i+1], ypositions[i+1], z);
Vertices[2] = new Vector3(xpositions[i], bottom, z);
Vertices[3] = new Vector3(xpositions[i+1], bottom, z);
meshes[i].vertices = Vertices;
}
}
妳可能註意到了這個函數只使用了我們之前編寫的代碼。唯壹的區別在於這次我們並不需要設置三角形的UV,因為這些仍然保持不變。
我們的下壹步任務是讓水體本身運行。我們將使用FixedUpdate()遞增地來調整它們。
void FixedUpdate()
{
執行物理機制
首先,我們將把Hooke定律寫Euler方法結合在壹起找到新坐標、加速和速度。
Hooke定律是F=kx,這裏的F是指由水流產生的力(記住,我們將把水體表面模擬為水流),k是指水流的常量,x則是位移。我們的位移將成為每個節點的y坐標減去節點的基本高度。
下壹步,我們將添加壹個與力的速度成比例的阻尼因素來削弱力。
for (int i = 0; i lt; xpositions.Lenh ; i++)
{
float force = springconstant * (ypositions[i] – baseheight) + velocities[i]*damping ;
accelerations[i] = -force;
ypositions[i] += velocities[i];
velocities[i] += accelerations[i];
Body.SetPosition(i, new Vector3(xpositions[i], ypositions[i], z));
}
Euler方法很簡單,我們只要向速度添加加速,向每幀坐標增加速度。
註:我只是假設每個節點的質量為1,但妳可能會想用:
accelerations[i] = -force/mass;
現在我們將創造波傳播。以下節點是根據Michael Hoffman的教程調整而來的:
float[] leftDeltas = new float[xpositions.Lenh];
float[] rightDeltas = new float[xpositions.Lenh];
在此,我們要創造兩個陣列。針對每個節點,我們將檢查之前節點的高度,以及當前節點的高度,並將二者差別放入leftDeltas。
之後,我們將檢查後續節點的高度與當前檢查節點的高度,並將二者的差別放入rightDeltas(我們將乘以壹個傳播常量來增加所有值)。
for (int j = 0; j lt; 8; j++)
{
for (int i = 0; i lt; xpositions.Lenh; i++)
{
if (i ; 0)
{
leftDeltas[i] = spread * (ypositions[i] – ypositions[i-1]);
velocities[i - 1] += leftDeltas[i];
}
if (i lt; xpositions.Lenh – 1)
{
rightDeltas[i] = spread * (ypositions[i] – ypositions[i + 1]);
velocities[i + 1] += rightDeltas[i];
}
}
}
當我們集齊所有的高度數據時,我們最後就可以派上用場了。我們無法查看到最右端的節點右側,或者最大左端的節點左側,因此基條件就是i ; 0以及i lt; xpositions.Lenh – 1。
因此,要註意我們在壹個循環中包含整片代碼,並運行它8次。這是因為我們想以少量而多次的時間運行這壹過程,而不是進行壹次大型運算,因為這會削弱流動性。
添加水花
現在我們已經有了流動的水體,下壹步就需要讓它濺起水花!
為此,我們要增加壹個稱為Splash()的函數,它會檢查水花的X坐標,以及它所擊中的任何物體的速度。將其設置為公開狀態,這樣我們可以在之後的碰撞器中調用它。
public void Splash(float xpos, float velocity)
{
首先,我們應該確保特定的坐標位於我們水體的範圍之內:
if (xpos ;= xpositions[0] ?xpos lt;= xpositions[xpositions.Lenh-1])
{
然後我們將調整xpos,讓它出現在相對於水體起點的位置上:
xpos -= xpositions[0];
下壹步,我們將找到它所接觸的節點。我們可以這樣計算:
int index = Mathf.RoundToInt((xpositions.Lenh-1)*(xpos / (xpositions[xpositions.Lenh-1] – xpositions[0])));
這就是它的運行方式:
我們選取相對於水體左側邊緣位置的水花位置(xpos)。
2.我們將相對於水體左側邊緣的的右側位置進行劃分。
3.這讓我們知道了水花所在的位置。例如,位於水體四分之三處的水花的值就是0.75。
4.我們將把這壹數字乘以邊緣的數量,這就可以得到我們水花最接近的節點。
velocities[index] = velocity;
現在我們要設置擊中水面的物體的速度,令其與節點速度壹致,以樣節點就會被該物體拖入深處。
Particle-System(from gamedevelopment)
註:妳可以根據自己的需求改變這條線段。例如,妳可以將其速度添加到當前速度,或者使用動量而非速度,並除以妳節點的質量。
現在,我們想制作壹個將產生水花的粒子系統。我們早點定義,將其稱為“splash”。要確保不要讓它與Splash()相混淆。
首先,我們要設置水花的參,以便調整物體的速度:
float lifetime = 0.93f + Mathf.Abs(velocity)*0.07f;
splash.GetComponentlt;ParticleSystem;().startSpeed = 8+2*Mathf.Pow(Mathf.Abs(velocity),0.5f);
splash.GetComponentlt;ParticleSystem;().startSpeed = 9 + 2 * Mathf.Pow(Mathf.Abs(velocity), 0.5f);
splash.GetComponentlt;ParticleSystem;().startLifetime = lifetime;
在此,我們要選取粒子,設置它們的生命周期,以免他們擊中水面就快速消失,並且根據它們速度的直角設置速度(為小小的水花增加壹個常量)。
妳可能會看著代碼心想,“為什麽要兩次設置startSpeed?”妳這樣想沒有錯,問題在於,我們使用壹個起始速度設置為“兩個常量間的隨機數”這種粒子系統(Shuriken)。不幸的是,我們並沒有太多以腳本訪問Shuriken的途徑 ,所以為了獲得這壹行為,我們必須兩次設置這個值。