在這片教程里面我們將會(huì)用簡(jiǎn)單的物理效果來(lái)模擬動(dòng)態(tài)的2D水效果。我們將會(huì)使用Line Renderer,Mesh Renderer,觸發(fā)器(Trigger)和粒子來(lái)創(chuàng)造這個(gè)水效果。最終的的效果將會(huì)包含波浪和水花濺起的特效,你可以直接加入自己的游戲中。你可以在文章的結(jié)尾下載此工程。當(dāng)然,本文中使用的制作原理可以應(yīng)用于任何游戲引擎之中。
最終效果
本教程要實(shí)現(xiàn)的最終效果如下:
設(shè)置水管理器
第一步就是使用Unity的線段渲染器(Line Renderer)和一些節(jié)點(diǎn)來(lái)實(shí)現(xiàn)水浪的形狀。如下圖:
然后還要跟蹤所有節(jié)點(diǎn)的位置、速度及加速度。這些信息使用數(shù)組來(lái)存儲(chǔ),在類(lèi)的最上面添加以下代碼:
[C#] 純文本查看 復(fù)制代碼 float[] xpositions;
float[] ypositions;
float[] velocities;
float[] accelerations;
LineRenderer Body;
|
LineRenderer用來(lái)保存所有節(jié)點(diǎn)及水體的輪廓。接下來(lái)使用網(wǎng)格來(lái)實(shí)現(xiàn)水體,還需創(chuàng)建游戲?qū)ο髞?lái)使用這些網(wǎng)格。添加以下代碼:
[C#] 純文本查看 復(fù)制代碼 GameObject[] meshobjects;
Mesh[] meshes;
|
為了讓物體可以與水交互,還需為每個(gè)游戲?qū)ο筇砑优鲎财鳎?br>
還要定義一些常量:
[C#] 純文本查看 復(fù)制代碼 const float springconstant = 0.02f;
const float damping = 0.04f;
const float spread = 0.05f;
const float z = -1f;
|
前三個(gè)常量用來(lái)控制水流速度、衰減度及傳播速度,最后的z值用于控制水體的顯示層次,這里設(shè)為-1表示會(huì)顯示在對(duì)象前面。大家也可根據(jù)自己的需求進(jìn)行調(diào)整。
還要設(shè)置一些值:
[C#] 純文本查看 復(fù)制代碼 float baseheight;
float left;
float bottom;
|
這三個(gè)變量定義了水的維度。
還要定義一些可以在編輯器中修改的公共變量,首先是制作水波四濺效果所需的粒子系統(tǒng):
[C#] 純文本查看 復(fù)制代碼 public GameObject splash:
|
接下來(lái)是用于Line Renderer的材質(zhì):
還有用于模擬水體的網(wǎng)格:
[C#] 純文本查看 復(fù)制代碼 public GameObject watermesh:
|
這些資源均可在工程中獲取。另外還需要一個(gè)管理器,保存所有數(shù)據(jù)并在游戲過(guò)程中生成水體。下面創(chuàng)建SpwanWater()函數(shù)來(lái)實(shí)現(xiàn)該功能。
該函數(shù)的參數(shù)分別為水體四周的邊長(zhǎng):
[C#] 純文本查看 復(fù)制代碼 public void SpawnWater(float Left, float Width, float Top, float Bottom)
{}
|
創(chuàng)建節(jié)點(diǎn)
下面決定總共需要的節(jié)點(diǎn)數(shù)量:
[C#] 純文本查看 復(fù)制代碼 int edgecount = Mathf.RoundToInt(Width) * 5;
int nodecount = edgecount + 1;
|
這里對(duì)每單位寬度的水體使用5個(gè)節(jié)點(diǎn),讓整個(gè)水體運(yùn)動(dòng)看起來(lái)更平滑。你也可以自己權(quán)衡性能與平滑效果來(lái)選擇合適的節(jié)點(diǎn)數(shù)量。這樣就能得到所有的邊數(shù)了,頂點(diǎn)數(shù)在此基礎(chǔ)上加1。
下面使用LineRenderer組件來(lái)渲染水體:
[C#] 純文本查看 復(fù)制代碼 Body = gameObject.AddComponent<LineRenderer>();
Body.material = mat;
Body.material.renderQueue = 1000;
Body.SetVertexCount(nodecount);
Body.SetWidth(0.1f, 0.1f);
|
同時(shí)這里還通過(guò)渲染隊(duì)列將材質(zhì)的渲染順序設(shè)為比水體更高。設(shè)置了節(jié)點(diǎn)總數(shù),并將線段寬度設(shè)為0.1。
你也可以自己設(shè)置線段寬度,SetWidth()函數(shù)有兩個(gè)參數(shù),分別是線段的起始寬度和結(jié)束寬度,設(shè)為一樣就表示線段寬度固定。
節(jié)點(diǎn)創(chuàng)建好后初始化上面聲明的變量:
[C#] 純文本查看 復(fù)制代碼 positions = 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;
|
現(xiàn)在所有的數(shù)組都初始化好,也拿到了所需的數(shù)據(jù)。下面就為各數(shù)組賦值,從節(jié)點(diǎn)開(kāi)始:
[C#] 純文本查看 復(fù)制代碼 for (int i = 0; i < 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坐標(biāo)設(shè)為水體上方,讓水體各部分緊密排列。速度和加速度都為0表示水體是靜止的。
循環(huán)結(jié)束后就通過(guò)LineRenderer將各節(jié)點(diǎn)設(shè)置到正確的位置。
創(chuàng)建網(wǎng)格
現(xiàn)在有了水波線段,下面就使用網(wǎng)格來(lái)實(shí)現(xiàn)水體。先添加以下代碼:
[C#] 純文本查看 復(fù)制代碼 for (int i = 0; i < edgecount; i++)
{
meshes[i] = new Mesh();
}
|
網(wǎng)格中也保存了一堆變量,第一個(gè)就是所有的頂點(diǎn)。
上圖展示了網(wǎng)格片段的理想顯示效果。第一個(gè)片段的頂點(diǎn)高亮顯示,共有4個(gè)。
[C#] 純文本查看 復(fù)制代碼 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);
|
數(shù)組的四個(gè)元素按順序分別表示左上角、右上角、左下角和右下角的頂點(diǎn)位置。
網(wǎng)格所需的第二個(gè)數(shù)據(jù)就是UV坐標(biāo)。UV坐標(biāo)決定了網(wǎng)格用到的紋理部分。這里簡(jiǎn)單的使用紋理左上角、右上角、左下角及右下角的部分作為網(wǎng)格顯示內(nèi)容。
[C#] 純文本查看 復(fù)制代碼 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);
|
現(xiàn)在需要用到之前定義的數(shù)據(jù)。網(wǎng)格是由三角形組成的,而一個(gè)四邊形可由兩個(gè)三角形組成,所以這里要告訴網(wǎng)格如何繪制三角形。
按節(jié)點(diǎn)順序觀察各角,三角形A由節(jié)點(diǎn)0、1、3組成,三角形B由節(jié)點(diǎn)3、2、0組成。所以定義一個(gè)頂點(diǎn)索引數(shù)組順序包含這些索引:
[C#] 純文本查看 復(fù)制代碼 int[] tris = new int[6] { 0, 1, 3, 3, 2, 0 };
|
四邊形定義好了,下面來(lái)設(shè)置網(wǎng)格數(shù)據(jù)。
[C#] 純文本查看 復(fù)制代碼 meshes[i].vertices = Vertices;
meshes[i].uv = UVs;
meshes[i].triangles = tris;
|
網(wǎng)格設(shè)置好了,還需添加游戲?qū)ο髮⑵滗秩镜綀?chǎng)景中。利用工程中的watermesh預(yù)制創(chuàng)建游戲?qū)ο?,其中包含Mesh Renderer和Mesh Filter 組件。
[C#] 純文本查看 復(fù)制代碼 meshobjects[i] = Instantiate(watermesh,Vector3.zero,Quaternion.identity) as GameObject;
meshobjects[i].GetComponent<MeshFilter>().mesh = meshes[i];
meshobjects[i].transform.parent = transform;
|
將網(wǎng)格對(duì)象設(shè)為水管理器的子對(duì)象以便于管理。
創(chuàng)建碰撞器
下面添加碰撞器:
[C#] 純文本查看 復(fù)制代碼 colliders[i] = new GameObject();
colliders[i].name = "Trigger";
colliders[i].AddComponent<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].GetComponent<BoxCollider2D>().isTrigger = true;
colliders[i].AddComponent<WaterDetector>();
|
添加盒狀碰撞器并統(tǒng)一命名以便于管理,同樣將其設(shè)為管理器子對(duì)象。將碰撞器坐標(biāo)設(shè)為節(jié)點(diǎn)中間,設(shè)置好大小并添加WaterDetector類(lèi)。
下面添加函數(shù)來(lái)控制水體網(wǎng)格的移動(dòng):
[C#] 純文本查看 復(fù)制代碼 void UpdateMeshes()
{
for (int i = 0; i < meshes.Length; 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;
}
}
|
該函數(shù)與上面的幾乎一樣,只是不需再設(shè)置三角形和UV。
下一步是在FixedUpdate()函數(shù)中添加物理特性讓水體可以自行流動(dòng)。
添加物理特性
首先是結(jié)合胡克定律和歐拉方法獲取水體新的坐標(biāo)、加速度及速度。
胡克定律即 F = kx,F(xiàn)是指由水浪產(chǎn)生的力(這里的水體模型就是由一排水浪組成),k指水體強(qiáng)度系數(shù),x是偏移距離。這里的偏移距離就是各節(jié)點(diǎn)的y坐標(biāo)減去節(jié)點(diǎn)的基本高度。
接下來(lái)添加一個(gè)與速度成比例的阻尼因子形成水面的阻力。
[C#] 純文本查看 復(fù)制代碼 for (int i = 0; i < xpositions.Length ; 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));
}
|
歐拉方法很簡(jiǎn)單,就是在每幀用加速度更新速度然后用速度更新位置。
注意這里每個(gè)節(jié)點(diǎn)的作用力原子數(shù)量為1,你也可以改為其它值,這樣加速度就是:
[C#] 純文本查看 復(fù)制代碼 accelerations[i] = -force/mass;
|
下面實(shí)現(xiàn)水浪的傳播效果。
[C#] 純文本查看 復(fù)制代碼 float[] leftDeltas = new float[xpositions.Length];
float[] rightDeltas = new float[xpositions.Length];
|
這里創(chuàng)建了兩個(gè)數(shù)組,對(duì)于每個(gè)節(jié)點(diǎn),都要對(duì)比前一個(gè)節(jié)點(diǎn)與當(dāng)前節(jié)點(diǎn)的高度差并將差值存入leftDeltas。
然后還要比較后一個(gè)節(jié)點(diǎn)與當(dāng)前節(jié)點(diǎn)的高度差并將差值存入rightDeltas。還需將所有的差值乘以傳播速度常量。
[C#] 純文本查看 復(fù)制代碼 for (int j = 0; j < 8; j++)
{
for (int i = 0; i < xpositions.Length; i++)
{
if (i > 0)
{
leftDeltas[i] = spread * (ypositions[i] - ypositions[i-1]);
velocities[i - 1] += leftDeltas[i];
}
if (i < xpositions.Length - 1)
{
rightDeltas[i] = spread * (ypositions[i] - ypositions[i + 1]);
velocities[i + 1] += rightDeltas[i];
}
}
}
|
可以根據(jù)高度差立即改變速度,但此時(shí)只需保存坐標(biāo)差即可。如果立即改變第一個(gè)節(jié)點(diǎn)的坐標(biāo),同時(shí)再去計(jì)算第二個(gè)節(jié)點(diǎn)時(shí)第一個(gè)坐標(biāo)已經(jīng)移動(dòng)了,這樣會(huì)影響到后面所有節(jié)點(diǎn)的計(jì)算。
[C#] 純文本查看 復(fù)制代碼 for (int i = 0; i < xpositions.Length; i++)
{
if (i > 0)
{
ypositions[i-1] += leftDeltas[i];
}
if (i < xpositions.Length - 1)
{
ypositions[i + 1] += rightDeltas[i];
}
}
|
到此就獲得了所有的高度數(shù)據(jù),可以應(yīng)用到最終效果了。由于最左與最右的節(jié)點(diǎn)不會(huì)動(dòng),所以需要改變坐標(biāo)是第一個(gè)至倒數(shù)第二個(gè)節(jié)點(diǎn)。
這里將所有代碼放在一個(gè)循環(huán),共運(yùn)行八次。這樣做的目的是希望多次運(yùn)行但計(jì)算量小,而非計(jì)算量過(guò)大從而導(dǎo)致效果不夠流暢。
添加水波飛濺的效果
現(xiàn)在已經(jīng)實(shí)現(xiàn)了水的流動(dòng),下面來(lái)實(shí)現(xiàn)水波飛濺的效果。添加函數(shù)Splash()用于檢測(cè)水波的x坐標(biāo)及入水物體接觸水面時(shí)的速度。將該函數(shù)設(shè)為公有的以供后續(xù)的碰撞器調(diào)用。
[C#] 純文本查看 復(fù)制代碼 public void Splash(float xpos, float velocity)
{}
|
首先需要確定水波飛濺的位置是在水體范圍內(nèi):
[C#] 純文本查看 復(fù)制代碼 if (xpos >= xpositions[0] && xpos <= xpositions[xpositions.Length-1])
{}
|
然后改變水波的x坐標(biāo)以獲取飛濺位置與水體起始位置間的相對(duì)坐標(biāo):
然后找到落水物體碰撞的節(jié)點(diǎn)。計(jì)算方法如下:
[C#] 純文本查看 復(fù)制代碼 int index = Mathf.RoundToInt((xpositions.Length-1)*(xpos / (xpositions[xpositions.Length-1] - xpositions[0])));
|
步驟如下:
首先獲取飛濺位置與水體左邊界的坐標(biāo)差(xpos)。
然后將該差值除以水體寬度。
這樣就得到了飛濺發(fā)生位置的分?jǐn)?shù),例如飛濺發(fā)生在水體寬度的3/4處就會(huì)返回0.75。
將該分?jǐn)?shù)乘以邊數(shù)后取整,就得到了離飛濺位置最近的節(jié)點(diǎn)索引。
[C#] 純文本查看 復(fù)制代碼 velocities[index] = velocity;
|
下面將入水物體的速度賦給該物體所碰撞的節(jié)點(diǎn),這樣節(jié)點(diǎn)會(huì)被物體壓入水體。
注意:你可以按自己的需求來(lái)更改上面的代碼。例如,你可以將節(jié)點(diǎn)速度與物體速度相加,或者使用動(dòng)量除以節(jié)點(diǎn)的作用原子數(shù)量而非直接使用速度。
下面實(shí)現(xiàn)產(chǎn)生水花的粒子系統(tǒng)。將該對(duì)象命名為“splash”,別跟Splash()搞混了,后者是一個(gè)函數(shù)。
首先,我們需要設(shè)置飛濺的參數(shù),這個(gè)參數(shù)是受撞擊物體的速度影響的。
[C#] 純文本查看 復(fù)制代碼 float lifetime = 0.93f + Mathf.Abs(velocity)*0.07f;
splash.GetComponent<ParticleSystem>().startSpeed = 8+2*Mathf.Pow(Mathf.Abs(velocity),0.5f);
splash.GetComponent<ParticleSystem>().startSpeed = 9 + 2 * Mathf.Pow(Mathf.Abs(velocity), 0.5f);
splash.GetComponent<ParticleSystem>().startLifetime = lifetime;
|
這里已經(jīng)設(shè)置了粒子系統(tǒng),并設(shè)定好生命周期,以免在物體撞擊水面后粒子消失過(guò)早,并將粒子速度設(shè)置為撞擊速度的立方(加上一個(gè)常數(shù),這樣較小力度的飛濺也會(huì)有效果)。
上面設(shè)置兩次startSpeed的原因是,這里使用Shuriken來(lái)實(shí)現(xiàn)的粒子系統(tǒng),它設(shè)定粒子的起始速度是兩個(gè)隨機(jī)常量之間,但我們通過(guò)腳本無(wú)法操作Shuriken中的更多內(nèi)容,所以這里設(shè)置兩次startSpeed。
下面增加的幾行代碼可能不是必須的:
[C#] 純文本查看 復(fù)制代碼 Vector3 position = new Vector3(xpositions[index],ypositions[index]-0.35f,5);
Quaternion rotation = Quaternion.LookRotation(new Vector3(xpositions[Mathf.FloorToInt(xpositions.Length / 2)], baseheight + 8, 5) - position);
|
Shuriken粒子在與物體碰撞后不會(huì)立即被摧毀,所以要確保粒子不會(huì)顯示在物體前方,有兩種辦法:
1.將它們固定在背景上,例如將其坐標(biāo)的z值設(shè)為5。
2.讓粒子系統(tǒng)總是朝向水體中心,這樣就不會(huì)飛濺到邊緣以外。
第二行代碼獲取坐標(biāo)中點(diǎn),稍微上移,并讓粒子發(fā)射器指向該點(diǎn)。如果你的水體夠?qū)?,就不需要進(jìn)行該設(shè)置。如果你的水體是室內(nèi)游泳池就需要用到該腳本。
[C#] 純文本查看 復(fù)制代碼 GameObject splish = Instantiate(splash,position,rotation) as GameObject;
Destroy(splish, lifetime+0.3f);
|
現(xiàn)在添加了飛濺對(duì)象,該對(duì)象會(huì)在粒子被摧毀后一段時(shí)間再消失,因?yàn)榱W酉到y(tǒng)發(fā)射了大量爆裂的粒子,所以粒子消失所需時(shí)間至少是Time.time + lifetime,最后的爆裂的粒子甚至需要更久。
碰撞檢測(cè)
最后還需對(duì)物體進(jìn)行碰撞檢測(cè),之前為所有的碰撞器都添加了WaterDetector腳本,在該腳本中添加下面的函數(shù):
[C#] 純文本查看 復(fù)制代碼 void OnTriggerEnter2D(Collider2D Hit)
{}
|
在OnTriggerEnter2D()中實(shí)現(xiàn)2D Rigid Body與水體碰撞產(chǎn)生的效果。傳入Collider2D類(lèi)型的參數(shù)可獲取更多關(guān)于碰撞物體的信息。需要該物體帶有Rigidbody2D組件:
[C#] 純文本查看 復(fù)制代碼 if (Hit.rigidbody2D != null)
{
transform.parent.GetComponent<Water>().Splash(transform.position.x, Hit.rigidbody2D.velocity.y*Hit.rigidbody2D.mass / 40f);
}
}
|
所有碰撞器都是water manager的子對(duì)象。所以直接從碰撞器父節(jié)點(diǎn)獲取Water組件并調(diào)用Splash()函數(shù)。如果希望物理效果更精確,可以使用動(dòng)量而非速度。注意在這里也該為對(duì)應(yīng)的屬性即可。如果要獲取物體動(dòng)量,就將其速度乘以mass。如果只用速度,就將代碼中的mass刪掉。
在Start()函數(shù)中調(diào)用SpawnWater():
[C#] 純文本查看 復(fù)制代碼 void Start()
{
SpawnWater(-10,20,0,-10);
}
|
到此就完成了,所有帶有rigidbody2D和碰撞器的物體都可以撞擊水面并產(chǎn)生水波飛濺的效果,并且水波也會(huì)正常流動(dòng)。
加分練習(xí)
在SpawnWater()函數(shù)中添加以下代碼:
[C#] 純文本查看 復(fù)制代碼 gameObject.AddComponent<BoxCollider2D>();
gameObject.GetComponent<BoxCollider2D>().center = new Vector2(Left + Width / 2, (Top + Bottom) / 2);
gameObject.GetComponent<BoxCollider2D>().size = new Vector2(Width, Top - Bottom);
gameObject.GetComponent<BoxCollider2D>().isTrigger = true;
|
上面的代碼就是為水體添加碰撞器,然后利用本教程學(xué)到的知識(shí)就可以讓物體在水中漂流。
添加OnTriggerStay2D()函數(shù)同樣帶有一個(gè)Collider2D類(lèi)型的參數(shù),用與之前一樣的方式檢測(cè)物體的作用力原子數(shù)量,然后為rigidbody2D添加力或速度讓物體漂流在水中。
|