【转】如何使用Unity创造动态的2D水体效果

时间:2022-01-03 15:24:38
原文:http://gamerboom.com/archives/83080

作者:Alex Rose

在本篇教程中,我们将使用简单的物理机制模拟一个动态的2D水体。我们将使用一个线性渲染器、网格渲染器,触发器以及粒子的混合体来创造这一水体效 果,最终得到可运用于你下款游戏的水纹和水花。这里包含了Unity样本源,但你应该能够使用任何游戏引擎以相同的原理执行类似的操作。

设置水体管理器

我们将使用Unity的一个线性渲染器来渲染我们的水体表面,并使用这些节点来展现持续的波纹。

【转】如何使用Unity创造动态的2D水体效果

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创造动态的2D水体效果

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创造动态的2D水体效果

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;

现在我们要设置击中水面的物体的速度,令其与节点速度一致,以样节点就会被该物体拖入深处。

【转】如何使用Unity创造动态的2D水体效果

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的途径 ,所以为了获得这一行为,我们必须两次设置这个值。

现在,我将添加一个你可能想或者不想从脚本中忽略的线段:

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.令粒子系统倾斜,令其总是指向你水体的中心——这样,粒子就不会飞溅到水面。

第二行代码位居坐标的中间点,向上移一点点,并指向粒子发射器。如果你要使用真正宽阔的水体,你可能就不需要这种行为。如果你的水体只是房间中的一个小水池,你可能就会想使用它。所以,你可以根据自己的需要抛弃关于旋转的代码。

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

现在,我们得制作水花,并让它在粒子应该消失之后的片刻再消失。为什么要在之后片刻呢?因为我们的粒子系统会发送出一些连续的粒子阵,所以即使首批粒子只会持续到Time.time + lifetime,我们最终的粒子阵也仍然会存留一小会儿。

没错,我们终于完工了,不是吗?

碰撞检测

错了!我们必须检测我们的对象,否则一切都是徒劳的!

记得我们之前向所有碰撞器添加脚本的情况吗?还记得WaterDetector吗?

我们现在就要把它制作出来!我们在其中只需要一个函数:

void OnTriggerEnter2D(Collider2D Hit)
{

使用OnTriggerEnter2D()我们可以规定2D刚体进入水体时所发生的情况。如果我们通过了Collider2D的一个参数,就可以找到更多关于该物体的信息:

if (Hit.rigidbody2D != null)
{

我们只需要包含rigidbody2D的物体:

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

现在,我们所有的碰撞器都是水体管理器的子项。所以我们只需要从它们的母体撷取Water组件并从碰撞器的位置调用Splash()。

记住,我说过如果你想让它更具物理准确性,就可以传递速度或动量。这里就需要你传递一者。如果你将对象的Y速度与其质量相乘,就可以得到它的动量。如果你只想使用它的速度,就要从该行代码中去除质量。

最后,你将从某处调用SpawnWater(),如下所示:

void Start()
{
SpawnWater(-10,20,0,-10);
}

现在我们完成了!现在任何含有一个碰撞器并击中水面的rigidbody2D都会创造一个水花,并且波纹还能正确移动。

【转】如何使用Unity创造动态的2D水体效果

Splash2(from gamedevelopment)

额外操作

作为一个额外操作,我还在SpawnWater()之上添加了几行代码。

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;

这几行代码会向水面本身添加一个方体碰撞器。你可以运用自己的知识,以此让物体漂浮在水面。

你将会制作一个称为OnTriggerStay2D()的函数,它有一个Collider2D Hit参数。之后,你可以使用我们之前使用的一个检查物体质量的弹性法则的调整版本,并为你的rigidbody2D添加一个力或速度以便令其漂浮在水面。

总结

在本篇教程中,我们以一些简单的物理代码和一个线性渲染器、网格渲染器、触发器和粒子执行了用于2D游戏的简单模拟水体。也许你会添加波浪起伏的水
体来作为自己下款平台游戏的障碍,准确让你的角色跳入水中或小心地穿过漂浮着的跳板,或者你可能想将它用于航海或冲浪游戏,甚至是一款只是需要玩家跳过水
面的岩石的游戏。总之,祝你好运!(本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦)

Creating Dynamic 2D Water Effects in Unity

by Alex Rose

In this tutorial, we’re going to simulate a dynamic 2D body of water
using simple physics. We will use a mixture of a line renderer, mesh
renderers, triggers and particles to create our effect. The final result
comes complete with waves and splashes, ready to add to your next game.
A Unity (Unity3D) demo source is included, but you should be able to
implement something similar using the same principles in any game
engine.

Related Posts

Make a Splash With Dynamic 2D Water Effects

How to Create a Custom 2D Physics Engine: The Basics and Impulse Resolution

Adding Turbulence to a Particle System

End Result

Here’s what we’re going to end up with. You’ll need the Unity browser plugin to try it out.

Click to create a new object to drop into the water.

Setting Up Our Water Manager

In his tutorial, Michael Hoffman demonstrated how we can model the surface of water with a row of springs.

We’re going to render the top of our water using one of Unity’s line
renderers, and use so many nodes that it appears as a continuous wave.
Create 2D Dynamic Water Effects in Unity (Unity3D)

We’ll have to keep track of the positions, velocities and
accelerations of every node, though. To do that, we’re going to use
arrays. So at the top of our class we’ll add these variables:

float[] xpositions;
float[] ypositions;
float[] velocities;
float[] accelerations;
LineRenderer Body;

The LineRenderer will store all our nodes and outline our body of
water. We still need the water itself, though; we’ll create this with
Meshes. We’re going to need objects to hold these meshes too.

GameObject[] meshobjects;
Mesh[] meshes;

We’re also going to need colliders so that things can interact with our water:

GameObject[] colliders;

And we’ll store all our constants as well:

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

These constants are the same kind as Michael discussed, with the
exception of z—this is our z-offset for our water. We’re going to use -1
for this so that it gets displayed in front of our objects. (You might
want to change this depending on what you want to appear in front and
behind of it; you’re going to have to use the z-coordinate to determine
where sprites sit relative to it.)

Next, we’re going to hold onto some values:

float baseheight;
float left;
float bottom;

These are just the dimensions of the water.

We’re going to need some public variables we can set in the editor,
too. First, the particle system we’re going to use for our splashes:

public GameObject splash:

Next, the material we’ll use for our line renderer (in case you want
to reuse the script for acid, lava, chemicals, or anything else):

public Material mat:

Plus, the kind of mesh we’re going to use for the main body of water:

public GameObject watermesh:

These are all going to be based on prefabs, which are all included in the source files.

We want a game object that can hold all of this data, act as a
manager, and spawn our body of water ingame to specification. To do
that, we’ll write a function called SpawnWater().

This function will take inputs of the left side, the width, the top, and the bottom of the body of water.

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

(Though this seems inconsistent, it acts in the interest of quick level design when building from left to right).

Creating the Nodes

Now we’re going to find out how many nodes we need:

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

We’re going to use five per unit width, to give us smooth motion that
isn’t too demanding. (You can vary this to balance efficiency against
smoothness.) This gives us all our lines, then we need the + 1 for the
extra node on the end.

The first thing we’re going to do is render our body of water with the LineRenderer component:

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

What we’ve also done here is select our material, and set it to
render above the water by choosing its position in the render queue.
We’ve set the correct number of nodes, and set the width of the line to
0.1.

You can vary this depending on how thick you want your line. You may
have noticed that SetWidth() takes two parameters; these are the width
at the start and the end of the line. We want that width to be constant.

Now that we’ve made our nodes, we’ll initialise all our top variables:

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;

So now we have all our arrays, and we’re holding on to our data.

Now to actually set the values of our arrays. We’ll start with the nodes:

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));
}

Here, we set all the y-positions to be at the top of the water, and
then incrementally add all the nodes side by side. Our velocities and
accelerations are zero initially, as the water is still.

We finish the loop by setting each node in our LineRenderer (Body) to their correct position.

Creating the Meshes

Here’s where it gets tricky.

We have our line, but we don’t have the water itself. And the way we
can make this is using Meshes. We’ll start off by creating these:

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

Now, Meshes store a bunch of variables. The first variable is pretty simple: it contains all the vertices (or corners).

Create 2D Dynamic Water Effects in Unity (Unity3D)

The diagram shows what we want our mesh segments to look like. For
the first segment, the vertices are highlighted. We want four in total.

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);

Now, as you can see here, vertex 0 is the top-left, 1 is the
top-right, 2 is the bottom-left, and 3 is the top-right. We’ll need to
remember that for later.

The second property that meshes need is UVs. Meshes have textures,
and the UVs choose which part of the textures we want to grab. In this
case, we just want the top-left, top-right, bottom-left, and
bottom-right corners of our texture.

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);

Now we need those numbers from before again. Meshes are made up of
triangles, and we know that any quadrilateral can be made of two
triangles, so now we need to tell the mesh how it should draw those
triangles.

Create 2D Dynamic Water Effects in Unity (Unity3D)

Look at the corners with the node order labelled. Triangle A connects
nodes 0, 1 and 3; Triangle B connects nodes 3, 2 and 0. Therefore, we
want to make an array that contains six integers, reflecting exactly
that:

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

This creates our quadrilateral. Now we set the mesh values.

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

Now, we have our meshes, but we don’t have Game Objects to render
them in the scene. So we’re going to create them from our watermesh
prefab which contains a Mesh Renderer and Mesh Filter.

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

We set the mesh, and we set it to be the child of the water manager, to tidy things up.

Creating Our Collisions

Now we want our collider too:

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>();

Here, we’re making box colliders, giving them a name so they’re a bit
tidier in the scene, and making them each children of the water manager
again. We set their position to be halfway between the nodes, set their
size, and add a WaterDetector class to them.

Now that we have our mesh, we need a function to update it as the water moves:

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;
}
}

You might notice that this function just uses the code we wrote
before. The only difference is that this time we don’t have to set the
tris and UVs, because these stay the same.

Our next task is to make the water itself work. We’ll use FixedUpdate() to modify them all incrementally.

void FixedUpdate()
{
Implementing the Physics

First, we’re going to combine Hooke’s Law with the Euler method to find the new positions, accelerations and velocities.

So, Hooke’s Law is F=kx, where F is the force produced by a spring
(remember, we’re modelling the surface of the water as a row of
springs), k is the spring constant, and x is the displacement. Our
displacement is simply going to be the y-position of each node minus the
base height of the nodes.

Next, we add a damping factor proportional to the velocity of the force to dampen the force.

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));
}

The Euler method is simple; we just add the acceleration to the velocity and the velocity to the position, every frame.

Note: I just assumed the mass of each node was 1 here, but you’ll want to use:

accelerations[i] = -force/mass;

if you want a different mass for your nodes.

Tip: For precise physics, we would use Verlet integration, but
because we’re adding damping, we can only use the Euler method, which is
a lot quicker to calculate. Generally, though, the Euler method will
exponentially introduce kinetic energy from nowhere into your physics
system, so don’t use it for anything precise.

Now we’re going to create wave propagation. The following code is adapted from Michael Hoffman’s tutorial.

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

Here, we create two arrays. For each node, we’re going to check the
height of the previous node against the height of the current node and
put the difference into leftDeltas.

Then, we’ll check the height of the subsequent node against the
height of the node we’re checking, and put that difference into
rightDeltas. (We’ll also multiply all values by a spread constant).

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];
}
}
}

We can change the velocities based on the height difference
immediately, but we should only store the differences in positions at
this point. If we changed the position of the first node straight off
the bat, by the time we looked at the second node, the first node will
have already moved, so that’ll ruin all our calculations.

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];
}
}

So once we’ve collected all our height data, we can apply it at the
end. We can’t look to the right of the node at the far right, or to the
left of the node at the far left, hence the conditions i

> 0 and i < xpositions.Length – 1.

Also, note that we contained this whole code in a loop, and ran it
eight times. This is because we want to run this process in small doses
multiple times, rather than one large calculation, which would be a lot
less fluid.

Adding Splashes

Now we have water that flows, and it shows. Next, we need to be able to disturb the water!

For this, let’s add a function called Splash(), which will check the
x-position of the splash, and the velocity of whatever is hitting it. It
should be public so that we can call it from our colliders later.

public void Splash(float xpos, float velocity)
{

First, we need to make sure that the specified position is actually within the bounds of our water:

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

And then we’ll change xpos so it gives us the position relative to the start of the body of water:

xpos -= xpositions[0];

Next, we’re going to find out which node it’s touching. We can calculate that like this:

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

So, here’s what going on here:

We take the position of the splash relative to the position of the left edge of the water (xpos).

We divide this by the position of the right edge relative to the position of the left edge of the water.

This gives us a fraction that tells us where the splash is. For
instance, a splash three-quarters of the way along the body of water
would give a value of 0.75.

We multiply this by the number of edges and round this number, which gives us the node our splash was closest to.

velocities[index] = velocity;

Now we set the velocity of the object that hit our water to that node’s velocity, so that it gets dragged down by the object.

Note: You could change this line to whatever suits you. For instance,
you could add the velocity to its current velocity, or you could use
momentum instead of velocity and divide by your node’s mass.

Create 2D Dynamic Water Effects in Unity (Unity3D)

Now we want to make a particle system that’ll produce the splash. We
defined that earlier; it’s called “splash” (creatively enough). Be sure
not to confuse it with Splash(). The one I’ll be using is included in
the source files.

First, we want to set the parameters of the splash to change with the velocity of the object.

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;

Here, we’ve taken our particles, set their lifetime so they won’t die
shortly after they hit the surface of the water, and set their speed to
be based on the square of their velocity (plus a constant, for small
splashes).

You may be looking at that code and thinking, “Why has he set the
startSpeed twice?”, and you’d be right to wonder that. The problem is,
we’re using a particle system (Shuriken, provided with the project) that
has its start speed set to “random between two constants”.
Unfortunately, we don’t have much access over Shuriken by scripts, so to
get that behaviour to work we have to set the value twice.

Now I’m going to add a line that you may or may not want to omit from your script:

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 particles won’t be destroyed when they hit your objects, so
if you want to make sure they aren’t going to land in front of your
objects, you can take two measures:

Stick them in the background. (You can tell this by the z-position being 5).

Tilt the particle system to always point towards the center of your
body of water—this way, the particles won’t splash onto the land.

The second line of code takes the midpoint of the positions, moves
upwards a bit, and points the particle emitter towards it. I’ve included
this behaviour in the demo. If you’re using a really wide body of
water, you probably don’t want this behaviour. If your water is in a
small pool inside a room, you may well want to use it. So, feel free to
scrap that line about rotation.

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

Now, we make our splash, and tell it to die a little after the
particles are due to die. Why a little afterwards? Because our particle
system sends out a few sequential bursts of particles, so even though
the first batch only last till Time.time + lifetime, our final bursts
will still be around a little after that.

Yes! We’re finally done, right?

Collision Detection

Wrong! We need to detect our objects, or this was all for nothing!

Remember we added that script to all our colliders before? The one called WaterDetector?

Well we’re going to make it now! We only want one function in it:

void OnTriggerEnter2D(Collider2D Hit)
{

Using OnTriggerEnter2D(), we can specify what happens whenever a 2D
Rigid Body enters our body of water. If we pass a parameter of
Collider2D we can find more information about that object.

if (Hit.rigidbody2D != null)
{

We only want objects that contain a rigidbody2D.

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

Now, all of our colliders are children of the water manager. So we
just grab the Water component from their parent and call Splash(), from
the position of the collider.

Remember again, I said you could either pass velocity or momentum, if
you wanted it to be more physically accurate? Well here’s where you
have to pass the right one. If you multiply the object’s y-velocity by
its mass, you’ll have its momentum. If you just want to use its
velocity, get rid of the mass from that line.

Finally, you’ll want to call SpawnWater() from somewhere. Let’s do it at launch:

void Start()
{
SpawnWater(-10,20,0,-10);
}

And now we’re done! Now any rigidbody2D with a collider that hits the
water will create a splash, and the waves will move correctly.
Create 2D Dynamic Water Effects in Unity (Unity3D)

Bonus Exercise

As an extra bonus, I’ve added a few lines of code to the top of SpawnWater().

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;

These lines of code will add a box collider to the water itself. You
can use this to make things float in your water, using what you’ve
learnt.

You’ll want to make a function called OnTriggerStay2D() which takes a
parameter of Collider2D Hit. Then, you can use a modified version of
the spring formula we used before that checks the mass of the object,
and add a force or velocity to your rigidbody2D to make it float in the
water.

Make a Splash

In this tutorial, we implemented a simple water simulation for use in
2D games with simple physics code and a line renderer, mesh renderers,
triggers and particles. Perhaps you will add wavy bodies of fluid water
as an obstacle to your next platformer, ready for your characters to
dive into or carefully cross with floating stepping stones, or maybe you
could use this in a sailing or windsurfing game, or even a game where
you simply skip rocks across the water from a sunny beach. Good
luck!(source:gamedevelopment