如何使用Unity创建动态2D水体效果

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

建立一个水体管理者

我们将使用Unity的线性渲染器来渲染水面,并使用这些节点来显示连续的波纹。

unity-water-linerenderer(来自游戏开发)

我们将跟踪每个节点的位置、速度和加速度。为此,我们将使用数组。因此,下面的变量将被添加到我们的类的顶部:

float[]x位置;

float[]y positions;

浮动[]速度;

浮动[]加速度;

LineRenderer正文;

LineRenderer将存储我们所有的节点,并勾勒出我们的水体。我们仍然需要水体本身,它将使用网格来创建。我们将需要对象来托管这些网格。

game object[]mesh objects;

Mesh[]网格;

我们还需要对撞机,这样物体就可以和水体相互作用:

GameObject[]碰撞器;

我们还存储了所有的常数:

常量浮动弹簧常量= 0.02f

常数浮动阻尼= 0.04f

常浮点差= 0.05f

常量浮点z =-1f;

这些常数中的z是我们为水体设定的z位移。我们会用-1来标记,这样它就会出现在我们的对象前面(游戏注意:你可能要根据自己的需要调整到对象的前面或者后面,那么你必须用Z坐标来确定相关向导的位置)。

接下来,我们将保留一些值:

float baseheight

向左浮动;

浮动底部;

这些是水的维度。

我们将需要一些可以在编辑器中设置的公共变量。首先,我们将为飞溅使用一个粒子系统:

公共游戏对象飞溅:

接下来是我们将用于线性渲染器的材质:

公共材料垫:

此外,我们将对主要水体使用以下网格类型:

公共游戏对象:

我们想要一个游戏对象,可以托管所有这些数据,并使其成为一个管理器,以产生我们游戏中的水体。为此,我们将编写SpawnWater()函数。

该函数将接受水体左侧、疾驰、顶点和底部的输入:

公共void SpawnWater(浮动左、浮动宽、浮动顶、浮动底)

{

(虽然这看似矛盾,但有利于从左到右快速关卡设计。)

创建节点

现在我们将找出我们需要多少个节点:

int edgecount = Mathf。圆点(宽度)* 5;

int node count = edge count+1;

我们将使用每单位宽度5个节点来呈现平滑的移动(你可以改变这一点来平衡效率和流畅性)。我们可以由此得到所有的线段,然后需要最后的节点+1。

我们需要做的第一件事是用LineRenderer组件渲染水体:

Body =游戏对象。AddComponent & ltLineRenderer & gt();

Body.material = mat

body . material . render queue = 1000;

身体。SetVertexCount(节点计数);

身体。SetWidth(0.1f,0.1f);

这里我们要做的就是选择材质,通过选择渲染队列中的位置,渲染到水面以上。我们设置正确的节点数据,并将线段宽度设置为0.1。

你可以根据你需要的线段的粗细来改变这个宽度。您可能已经注意到,SetWidth()需要两个参数,它们是线段起点和终点的宽度。我们希望宽度保持不变。

现在我们已经创建了节点,我们将初始化所有的顶级变量:

x positions = new float[node count];

ypositions = new float[node count];

速度=新浮点[节点计数];

accelerations = new float[node count];

mesh objects = new game object[edge count];

meshes =新网格[edge count];

colliders = new game object[edge count];

baseheight = Top

bottom =底部;

左=左;

我们已经拥有了控制数据的所有阵列。

现在我们需要设置数组的值。我们将从节点开始:

for(int I = 0;我& ltnodecounti++)

{

y positions[I]= Top;

x positions[I]= Left+Width * I/edge count;

加速度[I]= 0;

速度[I]= 0;

身体。SetPosition(i,新向量3(xpositions[i],ypositions[i],z));

}

在这里,我们将所有的Y位置设置在水体上方,然后将所有的节点一起逐渐增加。因为水面平静,我们的速度和加速度值最初为零。

我们将通过将LineRenderer (Body)中的每个节点设置到正确的位置来完成这个循环。

创建网格

这是棘手的部分。

我们有自己的线段,却没有水体本身。我们将使用网格来制作它,如下所示:

for(int I = 0;我& ltedgecounti++)

{

Mesh[I]= new Mesh();

现在,网格存储了一系列变量。第一个变量非常简单:它包含所有顶点(或角)。

unity-water-Firstmesh(来自游戏开发)

图表显示了我们需要的网格片段的样子。标记第一个剪辑中的顶点。我们总是需要四个顶点。

Vector3[]顶点=新vector 3[4];

顶点[0] =新向量3(x位置[i],y位置[i],z);

顶点[1] =新向量3(x位置[i + 1],y位置[i + 1],z);

顶点[2] =新向量3(x位置[i],底部,z);

顶点[3] =新向量3(x位置[i+1],底部,z);

现在你可以看到,顶点0在左上角,1在右上角,2在左下角,3在右下角。我们以后要记住。

电网要求的第二个性能是紫外线。网格有纹理,UV会选择我们要提取的纹理。在这种情况下,我们只需要左上角,右上角,右下角和右下角的纹理。

Vector2[] UVs =新vector 2[4];

UVs[0] =新向量2(0,1);

UVs[1] =新向量2(1,1);

UVs[2] =新向量2(0,0);

UVs[3] =新向量2(1,0);

现在我们又需要这些数据了。网格是由三角形组成的,我们知道任何四边形都是由两个三角形组成的,所以现在我们需要告诉网格如何画这些三角形。

unity-water-Tris(来自游戏开发)

看有节点顺序标签的角。三角形A连接节点0,1和3,三角形B连接节点3,2,1。所以我们想做一个包含六个整数的数组:

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

这就产生了我们的四边形。现在我们需要设置网格的值。

网格[i]。顶点=顶点;

网格[i]。uv = UVs

网格[i]。三角形= tris

现在我们有了自己的网格,但是我们不在场景中渲染他们的游戏对象。因此,我们将从watermesh预制部件创建它们,包括网格渲染器和屏幕过滤器。

meshobjects[i] =实例化(watermesh,Vector3.zero,四元数. identity)为GameObject

meshobjects[i]。GetComponent & lt网格过滤器& gt().mesh = meshs[I];

mesh objects[I]. transform . parent = transform;

我们设置了一个网格,使其成为水体管理器的一个子项。

制造碰撞效果

现在我们需要自己的对撞机:

colliders[I]= new game object();

对撞机[i]。name = " Trigger

对撞机[i]。AddComponent & ltBoxCollider2D & gt();

colliders[I]. transform . parent = transform;

colliders[I]. transform . position = new vector 3(Left+Width *(I+0.5f)/edge count,Top–0.5f,0);

colliders[I]. transform . local scale = new vector 3(Width/edge count,1,1);

对撞机[i]。GetComponent & ltBoxCollider2D & gt().isTrigger = true

对撞机[i]。AddComponent & lt水检测器& gt();

在这一点上,我们制作了方形碰撞器,给它们起了一个名字,这样它们在场景中会显得更整洁,并再次制作了水体管理器的每个子项。我们将它们的位置设置在两个节点的点上,设置它们的大小,并向它们添加WaterDetector类。

现在我们有了自己的网格,我们需要一个函数随着水的流动而更新:

void UpdateMeshes()

{

for(int I = 0;我& lt网格。长度;i++)

{

Vector3[]顶点=新vector 3[4];

顶点[0] =新向量3(x位置[i],y位置[i],z);

顶点[1] =新向量3(x位置[i+1],y位置[i+1],z);

顶点[2] =新向量3(x位置[i],底部,z);

顶点[3] =新向量3(x位置[i+1],底部,z);

网格[i]。顶点=顶点;

}

}

你可能注意到了,这个函数只使用了我们之前写的代码。唯一不同的是,这次我们不需要设置三角形UV,因为这些还是一样的。

我们的下一个任务是让水自己流动起来。我们将使用FixedUpdate()来逐步调整它们。

void固定更新()

{

执行物理机制

首先,我们将胡克定律和欧拉方法结合起来,寻找新的坐标、加速度和速度。

胡克定律为F=kx,其中F指水流产生的力(记住,我们将水面模拟为水流),k指水流常数,x为位移。我们的位移将是每个节点的y坐标减去节点的基本高度。

接下来,我们将添加一个与力的速度成比例的阻尼因子来削弱力。

for(int I = 0;我& ltx定位。长度;i++)

{

浮力=弹簧常数*(位置[I]–基底高度)+速度[I]*阻尼;

加速度[I]=-力;

ypositions[i] +=速度[I];

速度[i] +=加速度[I];

身体。SetPosition(i,新向量3(xpositions[i],ypositions[i],z));

}

欧拉法很简单,我们只需要给速度加上加速度,把速度增加到每一帧的坐标上。

注意:我只是假设每个节点的质量是1,但是你可能想用:

加速度[I]=-力/质量;

现在我们将创建波传播。以下节点改编自迈克尔·霍夫曼的教程:

float[] leftDeltas =新float[xpositions。长度];

float[] rightDeltas =新float[xpositions。长度];

这里,我们将创建两个数组。对于每个节点,我们将检查前一个节点的高度和当前节点的高度,并将它们之间的差异放入leftDeltas。

之后,我们将检查后续节点的高度和当前节点的高度,并将它们之间的差值放入rightDeltas(我们将它乘以一个传播常数以增加所有值)。

for(int j = 0;j & lt8;j++)

{

for(int I = 0;我& ltx定位。长度;i++)

{

如果(i & gt0)

{

left deltas[I]= spread *(y positions[I]–y positions[I-1]);

速度[I-1]+= left deltas[I];

}

如果(我& ltx定位。长度–1)

{

right deltas[I]= spread *(y positions[I]–y positions[I+1]);

速度[I+1]+= right deltas[I];

}

}

}

当我们收集了所有的高度数据后,我们终于可以派上用场了。我们看不到最右边节点的右边,也看不到最左边节点的左边,所以基条件是I >;0和I位置。长度–1 .

因此,请注意,我们在一个循环中包含了一段完整的代码,并运行了8次。这是因为我们希望在少量时间内运行这个过程,而不是大型操作,因为它会削弱流动性。

添加喷雾

现在我们有了流动的水体,下一步就是让它飞溅起来!

为此,我们需要添加一个名为Splash()的函数,该函数将检查喷雾的x坐标以及它击中的任何对象的速度。公开它,这样我们以后就可以在碰撞器中调用它。

公共空隙飞溅(浮动xpos,浮动速度)

{

首先要保证具体坐标在我们水体的范围内:

if(xpos & gt;= x positions[0]& amp;& ampxpos & lt= xpositions[xpositions。长度-1])

{

然后我们将调整xpos,使其出现在相对于水体起点的位置:

xpos-= x positions[0];

接下来,我们将找到它接触的节点。我们可以这样计算:

int index = Mathf。round point((x positions。长度-1)*(xpos/(x positions[x positions。length-1]–x positions[0])));

它是这样工作的:

1.我们选择相对于水体左边缘位置的溅水位置(xpos)。

2.我们将相对于水体的左边缘来划分右边的位置。

这让我们知道水花在哪里。例如,位于水体四分之三处的喷雾的值为0.75。

我们将这个数乘以边数,这样就可以得到我们飞溅的最近节点。

速度[指数] =速度;

现在我们需要将物体撞击水面的速度设置为与节点的速度一致,这样节点就会被物体拖向深处。

粒子系统(来自游戏开发)

注意:您可以根据自己的需要更改此线段。例如,您可以将它的速度添加到当前速度,或者使用动量而不是速度,然后除以节点的质量。

现在,我们想做一个能产生水花的粒子系统。我们之前定义过,叫“飞溅”。务必不要将其与Splash()混淆。

首先,我们应该设置喷雾的参数来调整物体的速度:

浮动寿命= 0.93f + Mathf。Abs(速度)* 0.07f

飞溅。GetComponent & ltParticleSystem & gt().startSpeed = 8+2*Mathf。Pow(Mathf。Abs(速度),0.5f);

飞溅。GetComponent & ltParticleSystem & gt().startSpeed = 9 + 2 * Mathf。Pow(Mathf。Abs(速度),0.5f);

飞溅。GetComponent & ltParticleSystem & gt().startLifetime = lifetime

在这里,我们需要选择粒子,设置它们的生命周期,以防止它们在撞击水面时迅速消失,并根据它们的速度的直角来设置它们的速度(小水花加一个常数)。

你可能会看着代码想,“为什么要设置startSpeed两次?”你这样想没有错。问题是我们用的是一个粒子系统(手里剑),它的初速度设置为“两个常数之间的随机数”。不幸的是,我们没有太多的方法通过脚本访问Shuriken,所以为了获得这种行为,我们必须设置这个值两次。