如何使用Unity创建动态2D水体效果
建立一个水体管理者
我们将使用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,所以为了获得这种行为,我们必须设置这个值两次。