使用Unity实现动态2D水效果

http://forum.china.unity3d.com/thread-16044-1-1.html


在这片教程里面我们将会用简单的物理效果来模拟动态的2D水效果。我们将会使用Line Renderer,Mesh Renderer,触发器(Trigger)和粒子来创造这个水效果。最终的的效果将会包含波浪和水花溅起的特效,你可以直接加入自己的游戏中。你可以在文章的结尾下载此工程。当然,本文中使用的制作原理可以应用于任何游戏引擎之中。

最终效果
本教程要实现的最终效果如下:


设置水管理器
第一步就是使用Unity的线段渲染器(Line Renderer)和一些节点来实现水浪的形状。如下图:


然后还要跟踪所有节点的位置、速度及加速度。这些信息使用数组来存储,在类的最上面添加以下代码:
[C#]  纯文本查看  复制代码
?
float [] xpositions;
float [] ypositions;
float [] velocities;
float [] accelerations;
LineRenderer Body;
LineRenderer用来保存所有节点及水体的轮廓。接下来使用网格来实现水体,还需创建游戏对象来使用这些网格。添加以下代码:
[C#]  纯文本查看  复制代码
?
GameObject[] meshobjects;
Mesh[] meshes;
为了让物体可以与水交互,还需为每个游戏对象添加碰撞器:
[C#]  纯文本查看  复制代码
?
GameObject[] colliders;
还要定义一些常量:
[C#]  纯文本查看  复制代码
?
const float springconstant = 0.02f;
const float damping = 0.04f;
const float spread = 0.05f;
const float z = -1f;
前三个常量用来控制水流速度、衰减度及传播速度,最后的z值用于控制水体的显示层次,这里设为-1表示会显示在对象前面。大家也可根据自己的需求进行调整。

还要设置一些值:
[C#]  纯文本查看  复制代码
?
float baseheight;
float left;
float bottom;
这三个变量定义了水的维度。

还要定义一些可以在编辑器中修改的公共变量,首先是制作水波四溅效果所需的粒子系统:
[C#]  纯文本查看  复制代码
?
public GameObject splash:
接下来是用于Line Renderer的材质:
[C#]  纯文本查看  复制代码
?
public Material mat:
还有用于模拟水体的网格:
[C#]  纯文本查看  复制代码
?
public GameObject watermesh:
这些资源均可在工程中获取。另外还需要一个管理器,保存所有数据并在游戏过程中生成水体。下面创建SpwanWater()函数来实现该功能。

该函数的参数分别为水体四周的边长:
[C#]  纯文本查看  复制代码
?
public void SpawnWater( float Left, float Width, float Top, float Bottom)
{}
创建节点
下面决定总共需要的节点数量:
[C#]  纯文本查看  复制代码
?
int edgecount = Mathf.RoundToInt(Width) * 5;
int nodecount = edgecount + 1;
这里对每单位宽度的水体使用5个节点,让整个水体运动看起来更平滑。你也可以自己权衡性能与平滑效果来选择合适的节点数量。这样就能得到所有的边数了,顶点数在此基础上加1。

下面使用LineRenderer组件来渲染水体:
[C#]  纯文本查看  复制代码
?
Body = gameObject.AddComponent<LineRenderer>();
Body.material = mat;
Body.material.renderQueue = 1000;
Body.SetVertexCount(nodecount);
Body.SetWidth(0.1f, 0.1f);
同时这里还通过渲染队列将材质的渲染顺序设为比水体更高。设置了节点总数,并将线段宽度设为0.1。

你也可以自己设置线段宽度,SetWidth()函数有两个参数,分别是线段的起始宽度和结束宽度,设为一样就表示线段宽度固定。

节点创建好后初始化上面声明的变量:
[C#]  纯文本查看  复制代码
?
positions = 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;
现在所有的数组都初始化好,也拿到了所需的数据。下面就为各数组赋值,从节点开始:
[C#]  纯文本查看  复制代码
?
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将各节点设置到正确的位置。

创建网格
现在有了水波线段,下面就使用网格来实现水体。先添加以下代码:
[C#]  纯文本查看  复制代码
?
for ( int i = 0; i < edgecount; i++)
{
     meshes[i] = new Mesh();
}
网格中也保存了一堆变量,第一个就是所有的顶点。


上图展示了网格片段的理想显示效果。第一个片段的顶点高亮显示,共有4个。
[C#]  纯文本查看  复制代码
?
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);
数组的四个元素按顺序分别表示左上角、右上角、左下角和右下角的顶点位置。

网格所需的第二个数据就是UV坐标。UV坐标决定了网格用到的纹理部分。这里简单的使用纹理左上角、右上角、左下角及右下角的部分作为网格显示内容。
[C#]  纯文本查看  复制代码
?
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、0组成。所以定义一个顶点索引数组顺序包含这些索引:
[C#]  纯文本查看  复制代码
?
int [] tris = new int [6] { 0, 1, 3, 3, 2, 0 };
四边形定义好了,下面来设置网格数据。
[C#]  纯文本查看  复制代码
?
meshes[i].vertices = Vertices;
meshes[i].uv = UVs;
meshes[i].triangles = tris;
网格设置好了,还需添加游戏对象将其渲染到场景中。利用工程中的watermesh预制创建游戏对象,其中包含Mesh Renderer和Mesh Filter 组件。
[C#]  纯文本查看  复制代码
?
meshobjects[i] = Instantiate(watermesh,Vector3.zero,Quaternion.identity) as GameObject;
meshobjects[i].GetComponent<MeshFilter>().mesh = meshes[i];
meshobjects[i].transform.parent = transform;
将网格对象设为水管理器的子对象以便于管理。

创建碰撞器
下面添加碰撞器:
[C#]  纯文本查看  复制代码
?
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类。

下面添加函数来控制水体网格的移动:
[C#]  纯文本查看  复制代码
?
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()函数中添加物理特性让水体可以自行流动。
[C#]  纯文本查看  复制代码
?
void FixedUpdate()
{}

添加物理特性
首先是结合胡克定律和欧拉方法获取水体新的坐标、加速度及速度。

胡克定律即 F = kx,F是指由水浪产生的力(这里的水体模型就是由一排水浪组成),k指水体强度系数,x是偏移距离。这里的偏移距离就是各节点的y坐标减去节点的基本高度。

接下来添加一个与速度成比例的阻尼因子形成水面的阻力。

[C#]  纯文本查看  复制代码
?
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));
         }
欧拉方法很简单,就是在每帧用加速度更新速度然后用速度更新位置。

注意这里每个节点的作用力原子数量为1,你也可以改为其它值,这样加速度就是:
[C#]  纯文本查看  复制代码
?
accelerations[i] = -force/mass;
下面实现水浪的传播效果。
[C#]  纯文本查看  复制代码
?
float [] leftDeltas = new float [xpositions.Length];
float [] rightDeltas = new float [xpositions.Length];
这里创建了两个数组,对于每个节点,都要对比前一个节点与当前节点的高度差并将差值存入leftDeltas。

然后还要比较后一个节点与当前节点的高度差并将差值存入rightDeltas。还需将所有的差值乘以传播速度常量。
[C#]  纯文本查看  复制代码
?
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];
         }
     }
}
可以根据高度差立即改变速度,但此时只需保存坐标差即可。如果立即改变第一个节点的坐标,同时再去计算第二个节点时第一个坐标已经移动了,这样会影响到后面所有节点的计算。
[C#]  纯文本查看  复制代码
?
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];
     }
}
到此就获得了所有的高度数据,可以应用到最终效果了。由于最左与最右的节点不会动,所以需要改变坐标是第一个至倒数第二个节点。

这里将所有代码放在一个循环,共运行八次。这样做的目的是希望多次运行但计算量小,而非计算量过大从而导致效果不够流畅。

添加水波飞溅的效果
现在已经实现了水的流动,下面来实现水波飞溅的效果。添加函数Splash()用于检测水波的x坐标及入水物体接触水面时的速度。将该函数设为公有的以供后续的碰撞器调用。
[C#]  纯文本查看  复制代码
?
public void Splash( float xpos, float velocity)
{}
首先需要确定水波飞溅的位置是在水体范围内:
[C#]  纯文本查看  复制代码
?
if (xpos >= xpositions[0] && xpos <= xpositions[xpositions.Length-1])
{}
然后改变水波的x坐标以获取飞溅位置与水体起始位置间的相对坐标:
[C#]  纯文本查看  复制代码
?
expos -= xpositions[0];
然后找到落水物体碰撞的节点。计算方法如下:
[C#]  纯文本查看  复制代码
?
int index = Mathf.RoundToInt((xpositions.Length-1)*(xpos / (xpositions[xpositions.Length-1] - xpositions[0])));
步骤如下:
首先获取飞溅位置与水体左边界的坐标差(xpos)。

然后将该差值除以水体宽度。

这样就得到了飞溅发生位置的分数,例如飞溅发生在水体宽度的3/4处就会返回0.75。

将该分数乘以边数后取整,就得到了离飞溅位置最近的节点索引。
[C#]  纯文本查看  复制代码
?
velocities[index] = velocity;
下面将入水物体的速度赋给该物体所碰撞的节点,这样节点会被物体压入水体。

注意:你可以按自己的需求来更改上面的代码。例如,你可以将节点速度与物体速度相加,或者使用动量除以节点的作用原子数量而非直接使用速度。


下面实现产生水花的粒子系统。将该对象命名为“splash”,别跟Splash()搞混了,后者是一个函数。

首先,我们需要设置飞溅的参数,这个参数是受撞击物体的速度影响的。
[C#]  纯文本查看  复制代码
?
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中的更多内容,所以这里设置两次startSpeed。

下面增加的几行代码可能不是必须的:
[C#]  纯文本查看  复制代码
?
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.让粒子系统总是朝向水体中心,这样就不会飞溅到边缘以外。

第二行代码获取坐标中点,稍微上移,并让粒子发射器指向该点。如果你的水体够宽,就不需要进行该设置。如果你的水体是室内游泳池就需要用到该脚本。

[C#]  纯文本查看  复制代码
?
GameObject splish = Instantiate(splash,position,rotation) as GameObject;
Destroy(splish, lifetime+0.3f);
现在添加了飞溅对象,该对象会在粒子被摧毁后一段时间再消失,因为粒子系统发射了大量爆裂的粒子,所以粒子消失所需时间至少是Time.time + lifetime,最后的爆裂的粒子甚至需要更久。

碰撞检测
最后还需对物体进行碰撞检测,之前为所有的碰撞器都添加了WaterDetector脚本,在该脚本中添加下面的函数:
[C#]  纯文本查看  复制代码
?
void OnTriggerEnter2D(Collider2D Hit)
{}
在OnTriggerEnter2D()中实现2D Rigid Body与水体碰撞产生的效果。传入Collider2D类型的参数可获取更多关于碰撞物体的信息。需要该物体带有Rigidbody2D组件:
[C#]  纯文本查看  复制代码
?
if (Hit.rigidbody2D != null )
{
       transform.parent.GetComponent<Water>().Splash(transform.position.x, Hit.rigidbody2D.velocity.y*Hit.rigidbody2D.mass / 40f);
     }
}
所有碰撞器都是water manager的子对象。所以直接从碰撞器父节点获取Water组件并调用Splash()函数。如果希望物理效果更精确,可以使用动量而非速度。注意在这里也该为对应的属性即可。如果要获取物体动量,就将其速度乘以mass。如果只用速度,就将代码中的mass删掉。

在Start()函数中调用SpawnWater():
[C#]  纯文本查看  复制代码
?
void Start()
{
     SpawnWater(-10,20,0,-10);
}
到此就完成了,所有带有rigidbody2D和碰撞器的物体都可以撞击水面并产生水波飞溅的效果,并且水波也会正常流动。


加分练习
在SpawnWater()函数中添加以下代码:
[C#]  纯文本查看  复制代码
?
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类型的参数,用与之前一样的方式检测物体的作用力原子数量,然后为rigidbody2D添加力或速度让物体漂流在水中。

总结
本教程主要教大家使用Unity 2D模拟简单的2D水效果,用到了一点简单的物理知识以及Line Renderer、Mesh Renderer、触发器和粒子。教程不难,但理论知识都是适用的,希望大家发挥自己的想象力将其用到实际项目中。

原文链接: http://gamedevelopment.tutsplus. ... edtutorials_sidebar

原文作者:
Alex Rose

本文版权归Unity官方中文论坛所有,转载请注明来源forum.china.unity3d.com)。


http://download.csdn.net/download/onafioo/9966532


  • 4
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
实现滑动屏幕效果可以按照以下步骤进行: 1. 创建一个 Plane,将其缩放为适当大小,以适应屏幕大小。 2. 创建一个材质,并将其 Shader 设置为“Mobile/Diffuse”。 3. 将材质的纹理贴图设置为波图。 4. 在 Plane 上添加一个脚本,用于控制波的运动。 5. 使用 Touch 事件监听屏幕滑动事件。 6. 根据滑动方向和幅度计算出波的速度和振幅。 7. 将速度和振幅传递给 Shader,控制波的运动。 以下是一个示例脚本,用于实现滑动屏幕效果: ```csharp using UnityEngine; using System.Collections; public class WaterController : MonoBehaviour { public float speed = 1.0f; public float amplitude = 0.1f; private Material mat; void Start () { mat = GetComponent<Renderer>().material; } void Update () { if (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Moved) { Vector2 deltaPos = Input.GetTouch(0).deltaPosition; Vector2 direction = deltaPos.normalized; float distance = deltaPos.magnitude; float velocity = distance * speed; float wave = Mathf.Sin(Time.time * velocity) * amplitude; mat.SetVector("_Direction", new Vector4(direction.x, direction.y, 0, 0)); mat.SetFloat("_Wave", wave); } } } ``` 在 Shader 中,可以使用以下代码来实现效果: ```csharp Shader "Custom/Water" { Properties { _MainTex ("Texture", 2D) = "white" {} _Direction ("Direction", Vector) = (1, 0, 0, 0) _Wave ("Wave", Range(0, 1)) = 0 } SubShader { Pass { Tags { "RenderType"="Opaque" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; float4 _Direction; float _Wave; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { float2 p = i.uv - 0.5; float distance = length(p) * 2; float2 direction = normalize(_Direction.xy); float dotProduct = dot(p, direction); float wave = sin(dotProduct * distance * _Wave); return tex2D(_MainTex, i.uv + wave * direction * _Wave); } ENDCG } } FallBack "Diffuse" } ``` 在 Shader 中,使用 _Direction 和 _Wave 控制波的运动。_Direction 表示波的运动方向,_Wave 表示波的振幅。在 frag 函数中,使用 sin 函数根据 _Direction 和 _Wave 计算出波的波动,最后使用 tex2D 函数获取纹理贴图的颜色并返回。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值