使用Unity創造動態的2D水體效果

者:Alex Rosenode

在本篇教程中,咱們將使用簡單的物理機制模擬一個動態的2D水體。咱們將使用一個線性渲染器、網格渲染器,觸發器以及粒子的混合體來創造這一水體效果,最終獲得可運用於你下款遊戲的水紋和水花。這裏包含了Unity樣本源,但你應該可以使用任何遊戲引擎以相同的原理執行相似的操做。spring

設置水體管理器編輯器

咱們將使用Unity的一個線性渲染器來渲染咱們的水體表面,並使用這些節點來展示持續的波紋。ide

unity-water-linerenderer(from gamedevelopment)

unity-water-linerenderer(from gamedevelopment)函數

咱們將追蹤每一個節點的位置、速度和加速狀況。爲此,咱們將會使用到陣列。因此在咱們的類頂端將添加以下變量:性能

1
2
3
4
5
float[] xpositions;
float[] ypositions;
float[] velocities;
float[] accelerations;
LineRenderer Body;

LineRenderer將存儲咱們全部的節點,並概述咱們的水體。咱們仍須要水體自己,將使用Meshes來創造。咱們將須要對象來託管這些網格。spa

1
2
GameObject[] meshobjects;
Mesh[] meshes;

咱們還須要碰撞器以便事物可同水體互動:設計

1
GameObject[] colliders;

咱們也存儲了全部的常量:code

1
2
3
4
const float springconstant = 0.02f;
const float damping = 0.04f;
const float spread = 0.05f;
const float z = -1f;

這些常量中的z是咱們爲水體設置的Z位移。咱們將使用-1標註它,這樣它就會呈現於咱們的對象以前(遊戲邦注:你可能想根據本身的需求將其調整爲在對象以前或以後,那你就必須使用Z座標來肯定與之相關的精靈所在的位置)。orm

下一步,咱們將保持一些值:

1
2
3
float baseheight;
float left;
float bottom;

這些就是水的維度。

咱們將須要一些能夠在編輯器中設置的公開變量。首先,咱們將爲水花使用粒子系統:

1
public GameObject splash:

接下來就是咱們將用於線性渲染器的材料:

1
public Material mat:

此外,咱們將爲主要水體使用的網格類型以下:

1
public GameObject watermesh:

咱們想要可以託管全部這些數據的遊戲對象,令其做爲管理器,產出咱們遊戲中的水體。爲此,咱們將編寫SpawnWater()函數。

這個函數將採用水體左邊、跑馬度、頂點以及底部的輸入:

1
2
public void SpawnWater(float Left, float Width, float Top, float Bottom)
{

(雖然這看似有所矛盾,但卻有利於從左往右快速進行關卡設計)

創造節點

如今咱們將找出本身須要多少節點:

1
2
int edgecount = Mathf.RoundToInt(Width) * 5;
int nodecount = edgecount + 1;

咱們將針對每一個單位寬度使用5個節點,以便呈現流暢的移動(你能夠改變這一點以便平衡效率與流暢性)。咱們由此可獲得全部線段,而後須要在末端的節點 + 1。

咱們要作的首件事就是以LineRenderer組件渲染水體:

1
2
3
4
5
Body = gameObject.AddComponent<LineRenderer>();
Body.material = mat;
Body.material.renderQueue = 1000;
Body.SetVertexCount(nodecount);
Body.SetWidth(0.1f, 0.1f);

咱們在此還要作的是選擇材料,並經過選擇渲染隊列中的位置而令其在水面之上渲染。咱們設置正確的節點數據,將線段寬度設爲0.1。

你能夠根據本身所需的線段粗細來改變這一寬度。你可能注意到了SetWidth()須要兩個參數,這是線段開始及末尾的寬度。咱們但願該寬度恆定不變。

如今咱們製做了節點,將初始化咱們全部的頂級變量:

1
2
3
4
5
6
7
8
9
10
11
12
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;

咱們已經有了全部陣列,將控制咱們的數據。

如今要設置咱們陣列的值。咱們將從節點開始:

1
2
3
4
5
6
7
8
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位置設於水體之上,以後一塊兒漸進增長全部節點。由於水面平靜,咱們的速度和加速值最初爲0。

咱們將把LineRenderer (Body)中的每一個節點設爲其正確的位置,以此完成這個循環。

創造網格

這正是它棘手的地方。

咱們有本身的線段,但咱們並無水體自己。咱們要使用網格來製做,以下所示:

1
2
3
for (int i = 0; i < edgecount; i++)
{
meshes[i] = new Mesh();

如今,網格存儲了一系列變量。首個變量至關簡單:它包含了全部頂點(或轉角)。

unity-water-Firstmesh(from gamedevelopment)

unity-water-Firstmesh(from gamedevelopment)

該圖表顯示了咱們所需的網格片斷的樣子。第一個片斷中的頂點被標註出來了。咱們總共須要4個頂點。

1
2
3
4
5
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會選擇咱們想擷取的那部分紋理。在這種狀況下,咱們只想要左上角,右上角,右下角和右下角的紋理。

1
2
3
4
5
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)

unity-water-Tris(from gamedevelopment)

看看含有節點順序標註的轉角。三角形A鏈接節點0,1,以及3,三角形B鏈接節點3,2,1。所以咱們想製做一個包含6個整數的陣列:

1
int[] tris = new int[6] { 0, 1, 3, 3, 2, 0 };

這就創造了咱們的四邊形。如今咱們要設置網格的值。

1
2
3
meshes[i].vertices = Vertices;
meshes[i].uv = UVs;
meshes[i].triangles = tris;

如今咱們已經有了本身的網格,但咱們沒有在場景是渲染它們的遊戲對象。因此咱們將從包括一個網格渲染器和篩網過濾器的watermesh預製件來創造它們。

1
2
3
meshobjects[i] = Instantiate(watermesh,Vector3.zero,Quaternion.identity) as GameObject;
meshobjects[i].GetComponent<MeshFilter>().mesh = meshes[i];
meshobjects[i].transform.parent = transform;

咱們設置了網格,令其成爲水體管理器的子項。

創造碰撞效果

如今咱們還須要本身的碰撞器:

1
2
3
4
5
6
7
8
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>();

至此,咱們製做了方形碰撞器,給它們一個名稱,以便它們會在場景中顯得更整潔一點,而且再次製做水體管理器的每一個子項。咱們將它們的位置設置於兩個節點之點,設置好大小,併爲其添加了WaterDetector類。

如今咱們擁有本身的網格,咱們須要一個函數隨着水體移動進行更新:

1
2
3
4
void UpdateMeshes()
{
for (int i = 0; i < meshes.Length; i++)
{

你可能注意到了這個函數只使用了咱們以前編寫的代碼。惟一的區別在於此次咱們並不須要設置三角形的UV,由於這些仍然保持不變。

咱們的下一步任務是讓水體自己運行。咱們將使用FixedUpdate()遞增地來調整它們。

 

 

1
2
void FixedUpdate()
{

執行物理機制

首先,咱們將把Hooke定律寫Euler方法結合在一塊兒找到新座標、加速和速度。

Hooke定律是F=kx,這裏的F是指由水流產生的力(記住,咱們將把水體表面模擬爲水流),k是指水流的常量,x則是位移。咱們的位移將成爲每一個節點的y座標減去節點的基本高度。

下一步,咱們將添加一個與力的速度成比例的阻尼因素來削弱力。

1
2
3
4
5
6
7
8
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));
}

Euler方法很簡單,咱們只要向速度添加加速,向每幀座標增長速度。

注:我只是假設每一個節點的質量爲1,但你可能會想用:

1
accelerations[i] = -force/mass;

如今咱們將創造波傳播。如下節點是根據Michael Hoffman的教程調整而來的:

1
2
float[] leftDeltas = new float[xpositions.Length];
float[] rightDeltas = new float[xpositions.Length];

在此,咱們要創造兩個陣列。針對每一個節點,咱們將檢查以前節點的高度,以及當前節點的高度,並將兩者差異放入leftDeltas。

以後,咱們將檢查後續節點的高度與當前檢查節點的高度,並將兩者的差異放入rightDeltas(咱們將乘以一個傳播常量來增長全部值)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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];
}
}
}

當咱們集齊全部的高度數據時,咱們最後就能夠派上用場了。咱們沒法查看到最右端的節點右側,或者最大左端的節點左側,所以基條件就是i > 0以及i < xpositions.Length – 1。

所以,要注意咱們在一個循環中包含整片代碼,並運行它8次。這是由於咱們想以少許而屢次的時間運行這一過程,而不是進行一次大型運算,由於這會削弱流動性。

添加水花

如今咱們已經有了流動的水體,下一步就須要讓它濺起水花!

爲此,咱們要增長一個稱爲Splash()的函數,它會檢查水花的X座標,以及它所擊中的任何物體的速度。將其設置爲公開狀態,這樣咱們能夠在以後的碰撞器中調用它。

1
2
public void Splash(float xpos, float velocity)
{

首先,咱們應該確保特定的座標位於咱們水體的範圍以內:

1
2
if (xpos >= xpositions[0] && xpos <= xpositions[xpositions.Length-1])
{

而後咱們將調整xpos,讓它出如今相對於水體起點的位置上:

1
xpos -= xpositions[0];

下一步,咱們將找到它所接觸的節點。咱們能夠這樣計算:

1
int index = Mathf.RoundToInt((xpositions.Length-1)*(xpos / (xpositions[xpositions.Length-1] – xpositions[0])));

這就是它的運行方式:

1.咱們選取相對於水體左側邊緣位置的水花位置(xpos)。

2.咱們將相對於水體左側邊緣的的右側位置進行劃分。

3.這讓咱們知道了水花所在的位置。例如,位於水體四分之三處的水花的值就是0.75。

4.咱們將把這一數字乘以邊緣的數量,這就能夠獲得咱們水花最接近的節點。

1
velocities[index] = velocity;

如今咱們要設置擊中水面的物體的速度,令其與節點速度一致,以樣節點就會被該物體拖入深處。

Particle-System(from gamedevelopment)

Particle-System(from gamedevelopment)

注:你能夠根據本身的需求改變這條線段。例如,你能夠將其速度添加到當前速度,或者使用動量而非速度,併除以你節點的質量。

如今,咱們想製做一個將產生水花的粒子系統。咱們早點定義,將其稱爲「splash」。要確保不要讓它與Splash()相混淆。

首先,咱們要設置水花的參,以便調整物體的速度:

1
2
3
4
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;

在此,咱們要選取粒子,設置它們的生命週期,以避免他們擊中水面就快速消失,而且根據它們速度的直角設置速度(爲小小的水花增長一個常量)。

你可能會看着代碼心想,「爲何要兩次設置startSpeed?」你這樣想沒有錯,問題在於,咱們使用一個起始速度設置爲「兩個常量間的隨機數」 這種粒子系統(Shuriken)。不幸的是,咱們並無太多以腳本訪問Shuriken的途徑 ,因此爲了得到這一行爲,咱們必須兩次設置這個值。

如今,我將添加一個你可能想或者不想從腳本中忽略的線段:

1
2
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粒子擊中你的物體時不會被破壞,因此若是你想確保它們不會在你的物體面前着陸,你能夠採用兩種對策:

1.令其固定在背景(你能夠經過將Z座標設爲5來實現)

2.令粒子系統傾斜,令其老是指向你水體的中心——這樣,粒子就不會飛濺到水面。

第二行代碼位居座標的中間點,向上移一點點,並指向粒子發射器。若是你要使用真正寬闊的水體,你可能就不須要這種行爲。若是你的水體只是房間中的一個小水池,你可能就會想使用它。因此,你能夠根據本身的須要拋棄關於旋轉的代碼。

1
2
3
4
GameObject splish = Instantiate(splash,position,rotation) as GameObject;
Destroy(splish, lifetime+0.3f);
}
}

如今,咱們得製做水花,並讓它在粒子應該消失以後的片刻再消失。爲何要在以後片刻呢?由於咱們的粒子系統會發送出一些連續的粒子陣,因此即便首批粒子只會持續到Time.time + lifetime,咱們最終的粒子陣也仍然會存留一小會兒。

沒錯,咱們終於完工了,不是嗎?

碰撞檢測

錯了!咱們必須檢測咱們的對象,不然一切都是徒勞的!

記得咱們以前向全部碰撞器添加腳本的狀況嗎?還記得WaterDetector嗎?

咱們如今就要把它製做出來!咱們在其中只須要一個函數:

1
2
void OnTriggerEnter2D(Collider2D Hit)
{

使用OnTriggerEnter2D()咱們能夠規定2D剛體進入水體時所發生的狀況。若是咱們經過了Collider2D的一個參數,就能夠找到更多關於該物體的信息:

1
2
if (Hit.rigidbody2D != null )
{

咱們只須要包含rigidbody2D的物體:

1
2
3
transform.parent.GetComponent<Water>().Splash(transform.position.x, Hit.rigidbody2D.velocity.y*Hit.rigidbody2D.mass / 40f);
}
}

如今,咱們全部的碰撞器都是水體管理器的子項。因此咱們只須要從它們的母體擷取Water組件並從碰撞器的位置調用Splash()。

記住,我說過若是你想讓它更具物理準確性,就能夠傳遞速度或動量。這裏就須要你傳遞一者。若是你將對象的Y速度與其質量相乘,就能夠獲得它的動量。若是你只想使用它的速度,就要從該行代碼中去除質量。

最後,你將從某處調用SpawnWater(),以下所示:

1
2
3
4
void Start()
{
SpawnWater(-10,20,0,-10);
}

如今咱們完成了!如今任何含有一個碰撞器並擊中水面的rigidbody2D都會創造一個水花,而且波紋還能正確移動。

Splash2(from gamedevelopment)

Splash2(from gamedevelopment)

額外操做

做爲一個額外操做,我還在SpawnWater()之上添加了幾行代碼。

1
2
3
gameObject.AddComponent<BoxCollider2D>();
gameObject.GetComponent<BoxCollider2D>().center = new Vector2(Left + Width / 2, (Top + Bottom) / 2);
gameObject.GetComponent<BoxCollider2D>().size = new Vector2(Width, Top – Bottom);

這幾行代碼會向水面自己添加一個方體碰撞器。你能夠運用本身的知識,以此讓物體漂浮在水面。

你將會製做一個稱爲OnTriggerStay2D()的函數,它有一個Collider2D Hit參數。以後,你可使用咱們以前使用的一個檢查物體質量的彈性法則的調整版本,併爲你的rigidbody2D添加一個力或速度以便令其漂浮在水面。

總結

在本篇教程中,咱們以一些簡單的物理代碼和一個線性渲染器、網格渲染器、觸發器和粒子執行了用於2D遊戲的簡單模擬水體。也許你會添加波浪起伏的水 體來做爲本身下款平臺遊戲的障礙,準確讓你的角色跳入水中或當心地穿過漂浮着的跳板,或者你可能想將它用於航海或衝浪遊戲,甚至是一款只是須要玩家跳過水 面的岩石的遊戲。總之,祝你好運!

 

原文連接:如何使用Unity創造動態的2D水體效果

相關文章
相關標籤/搜索