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

unity-water-linerenderer(from gamedevelopment)
我们将追踪每个节点的位置、速度和加速情况。为此,我们将会使用到阵列。所以在我们的类顶端将添加如下变量:
| 
 1 
2 
3 
4 
5 
 | 
float[] xpositions;float[] ypositions;float[] velocities;float[] accelerations;LineRenderer Body; | 
LineRenderer将存储我们所有的节点,并概述我们的水体。我们仍需要水体本身,将使用Meshes来创造。我们将需要对象来托管这些网格。
| 
 1 
2 
 | 
GameObject[] meshobjects;Mesh[] meshes; | 
我们还需要碰撞器以便事物可同水体互动:
| 
 1 
 | 
GameObject[] colliders; | 
我们也存储了所有的常量:
| 
 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坐标来确定与之相关的精灵所在的位置)。
下一步,我们将保持一些值:
| 
 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(); | 
现在,网格存储了一系列变量。首个变量相当简单:它包含了所有顶点(或转角)。

unity-water-Firstmesh(from gamedevelopment)
该图表显示了我们所需的网格片段的样子。第一个片段中的顶点被标注出来了。我们总共需要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); | 
现在我们又需要这些数据了。网格是由三角形组成的,我们知道任何四边形都是由两个三角形组成的,所以现在我们需要告诉网格它如何绘制这些三角形。
unity-water-Tris(from gamedevelopment)
看看含有节点顺序标注的转角。三角形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; | 
现在我们要设置击中水面的物体的速度,令其与节点速度一致,以样节点就会被该物体拖入深处。
Particle-System(from gamedevelopment)
注:你可以根据自己的需求改变这条线段。例如,你可以将其速度添加到当前速度,或者使用动量而非速度,并除以你节点的质量。
现在,我们想制作一个将产生水花的粒子系统。我们早点定义,将其称为“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都会创造一个水花,并且波纹还能正确移动。
Splash2(from gamedevelopment)
额外操作
作为一个额外操作,我还在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水体效果
使用Unity创造动态的2D水体效果的更多相关文章
- 【转】如何使用Unity创造动态的2D水体效果
		
原文:http://gamerboom.com/archives/83080 作者:Alex Rose 在本篇教程中,我们将使用简单的物理机制模拟一个动态的2D水体.我们将使用一个线性渲染器.网格渲染 ...
 - 使用Unity实现动态2D水效果
		
http://forum.china.unity3d.com/thread-16044-1-1.html 在这片教程里面我们将会用简单的物理效果来模拟动态的2D水效果.我们将会使用Line Rende ...
 - jQuery动态提示消息框效果
		
效果预览:http://keleyi.com/keleyi/phtml/jqtexiao/2.htm 原文:http://keleyi.com/a/bjac/hxv86dyi.htm <!DOC ...
 - jQuery实现鼠标移到元素上动态提示消息框效果
		
当光标移动到某些元素上时,会弹出像tips的提示框,这种效果想必大家都有见到过吧,下面有个不错的示例,大家可以感受下 当光标移动到某些元素上时,会弹出像tips的提示框. 复制代码代码如下: < ...
 - Unity的NGUI插件篇——入场效果
		
Unity的NGUI插件篇--入场效果 入场效果 入场效果须要借助于NGUI提供的TweenPosition类来完毕.为了说明此类的用法.本节将使会解说两个演示样例.本文选自 大学霸 <NGU ...
 - Unity牛逼的2D纹理功能
		
[Unity牛逼的2D纹理功能] 1.可直接将贴图生成成为Cubemap. 2.自动生成Mipmap. 3.查看纹理被当前场景哪些对象引用.在Project窗口中,右击图像,选择 参考:file:// ...
 - Unity Shader 2D水流效果
		
水流的模拟主要运用了顶点变换和纹理动画的结合: 顶点变换中,利用正弦函数模拟河流的大致形态,例如波长,振幅等. 纹理动画中,将纹理坐标朝某一方向持续滚动以形成流动的效果. 脚本如下: Shader & ...
 - Unity Lightmap动态加载研究
		
什么情况下需要Lightmap? 移动平台上目前暂时还不能开实时光影效果,会卡成幻灯片.所以就需要将光影烘焙到贴图上. 什么情况下需要动态加载Lightmap? 1.当项目抛弃了Unity的多场景模式 ...
 - Unity shader学习之屏幕后期处理效果之高斯模糊
		
高斯模糊,见 百度百科. 也使用卷积来实现,每个卷积元素的公式为: 其中б是标准方差,一般取值为1. x和y分别对应当前位置到卷积中心的整数距离. 由于需要对高斯核中的权重进行归一化,即使所有权重相加 ...
 
随机推荐
- php上传图片---初级版
			
没有样式,没有淘宝的那种放大截取大小的效果,只是实现了图片上传的功能. 图片超过100k,会出现内部错误服务器错误,需要手动更改配置文件里的MaxRequestLen属性. 下面粘上代码: <? ...
 - 记<<ssh穿透防火墙连接内网的机器(不用路由端口映射)>>
			
场景: 在家连接公司的内网服务器. 需求: 不用设置端口映射在家用putty登录公司内网服务器. 条件: 有一台公网服务器做转发,有开放端口的控制权.(公网服务器可以是阿里云ECS, 腾讯云主机这样的 ...
 - Mac上git的安装配置与使用简述
			
Mac下git搭建及使用 之前就只是经常在GitHubs上下载代码,也没注意怎么上传项目.一开始对git都没什么了解花了几个小时去小补了下知识.如果有需要可以转去这里学习:[GIT使用简易指南] (h ...
 - Ibatis学习总结3--SQL Map XML 映射文件
			
在前面的例子中,只使用了 SQL Map 最简单的形式.SQL Map 的结构中还有其他更多 的选项.这里是一个 mapped statement 较复杂的例子,使用了更多的特性. <sqlMa ...
 - .map文件的作用以及在chorme下会报错找不到jquery-1.10.2.min.map文件,404 的原因
			
source map文件是js文件压缩后,文件的变量名替换对应.变量所在位置等元信息数据文件,一般这种文件和min.js主文件放在同一个目录下. 比如压缩后原变量是map,压缩后通过变量替换规则可能会 ...
 - poj 3734         矩阵快速幂+YY
			
题目原意:N个方块排成一列,每个方块可涂成红.蓝.绿.黄.问红方块和绿方块都是偶数的方案的个数. sol:找规律列递推式+矩阵快速幂 设已经染完了i个方块将要染第i+1个方块. a[i]=1-i方块中 ...
 - PHP邮件注入攻击技术
			
1. 简介 如 今,互联网的使用急剧上升,但绝大多数互联网用户没有安全知识背景.大多数的人都会使用互联网通过邮件Email的方式和他人进行通信.出于这个原因,大 多数网站允许他们的用户联系他们,向网站 ...
 - 两个大的整数的运算(java)
			
import java.math.BigInteger; public class BigInt { BigInteger m1; BigInteger m2; BigInteger m3; BigI ...
 - 如何在 Arch Linux 的终端里设定 WiFi 网络
			
如果你使用的是其他 Linux 发行版 而不是 Arch CLI,那么可能会不习惯在终端里设置 WiFi.尽管整个过程有点简单,不过我还是要讲一下.在这篇文章里,我将带领新手们通过一步步的设置向导,把 ...
 - UVA116Unidirectional TSP(DP+逆推)
			
http://acm.hust.edu.cn/vjudge/problem/viewProblem.action?id=18206 题意:M*N的数阵,从左边一列到右边一列走过的数的和的最小.并输出路 ...