者:Alex Rosenode
在本篇教程中,咱们将使用简单的物理机制模拟一个动态的2D水体。咱们将使用一个线性渲染器、网格渲染器,触发器以及粒子的混合体来创造这一水体效果,最终获得可运用于你下款游戏的水纹和水花。这里包含了Unity样本源,但你应该可以使用任何游戏引擎以相同的原理执行相似的操做。spring
设置水体管理器编辑器
咱们将使用Unity的一个线性渲染器来渲染咱们的水体表面,并使用这些节点来展示持续的波纹。ide
咱们将追踪每一个节点的位置、速度和加速状况。为此,咱们将会使用到阵列。因此在咱们的类顶端将添加以下变量:性能
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();
|
如今,网格存储了一系列变量。首个变量至关简单:它包含了全部顶点(或转角)。
该图表显示了咱们所需的网格片断的样子。第一个片断中的顶点被标注出来了。咱们总共须要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);
|
如今咱们又须要这些数据了。网格是由三角形组成的,咱们知道任何四边形都是由两个三角形组成的,因此如今咱们须要告诉网格它如何绘制这些三角形。
看看含有节点顺序标注的转角。三角形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;
|
如今咱们要设置击中水面的物体的速度,令其与节点速度一致,以样节点就会被该物体拖入深处。
注:你能够根据本身的需求改变这条线段。例如,你能够将其速度添加到当前速度,或者使用动量而非速度,并除以你节点的质量。
如今,咱们想制做一个将产生水花的粒子系统。咱们早点定义,将其称为“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都会创造一个水花,而且波纹还能正确移动。
额外操做
做为一个额外操做,我还在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水体效果