-
1 # 就是不說你
-
2 # maimi32709
在本篇教程中,我們將使用簡單的物理機制模擬一個動態的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.AddComponent
回覆列表
在本篇教程中,我們將使用簡單的物理機制模擬一個動態的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.AddComponent<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 < 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 < 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].GetComponent<MeshFilter>().mesh = meshes[i]; meshobjects[i].transform.parent = transform; 我們設定了網格,令其成為水體管理器的子項。 創造碰撞效果 現在我們還需要自己的碰撞器: 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類。 現在我們擁有自己的網格,我們需要一個函式隨著水體移動進行更新: 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; } } 你可能注意到了這個函式只使用了我們之前編寫的程式碼。唯一的區別在於這次我們並不需要設定三角形的UV,因為這些仍然保持不變。 我們的下一步任務是讓水體本身執行。我們將使用FixedUpdate()遞增地來調整它們。 void FixedUpdate() { 執行物理機制 首先,我們將把Hooke定律寫Euler方法結合在一起找到新座標、加速和速度。 Hooke定律是F=kx,這裡的F是指由水流產生的力(記住,我們將把水體表面模擬為水流),k是指水流的常量,x則是位移。我們的位移將成為每個節點的y座標減去節點的基本高度。 下一步,我們將新增一個與力的速度成比例的阻尼因素來削弱力。 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,但你可能會想用: accelerations[i] = -force/mass; 現在我們將創造波傳播。以下節點是根據Michael Hoffman的教程調整而來的: float[] leftDeltas = new float[xpositions.Length]; float[] rightDeltas = new float[xpositions.Length]; 在此,我們要創造兩個陣列。針對每個節點,我們將檢查之前節點的高度,以及當前節點的高度,並將二者差別放入leftDeltas。 之後,我們將檢查後續節點的高度與當前檢查節點的高度,並將二者的差別放入rightDeltas(我們將乘以一個傳播常量來增加所有值)。 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座標,以及它所擊中的任何物體的速度。將其設定為公開狀態,這樣我們可以在之後的碰撞器中呼叫它。 public void Splash(float xpos, float velocity) { 首先,我們應該確保特定的座標位於我們水體的範圍之內: if (xpos >= xpositions[0] && xpos <= xpositions[xpositions.Length-1]) { 然後我們將調整xpos,讓它出現在相對於水體起點的位置上: xpos -= xpositions[0]; 下一步,我們將找到它所接觸的節點。我們可以這樣計算: int index = Mathf.RoundToInt((xpositions.Length-1)*(xpos / (xpositions[xpositions.Length-1] – xpositions[0]))); 這就是它的執行方式: 1.我們選取相對於水體左側邊緣位置的水花位置(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.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的途徑 ,所以為了獲得這一行為,我們必須兩次設定這個值。