交互水 使用模拟Springs 和 Sprite Shape 的 Unity 交互式 2D 水。 本教程以类似的方式制作,您可以通过不同的示例逐步了解如何创建 2D 水。
最终效果为玩家跳入水中有水面波纹,游泳的时候水面前高后低
实现思路来源
【【Unity教程搬运】Unity中带有Sprite Shape的2D水教程】https://www.bilibili.com/video/BV1MM4y1r7JU?vd_source=0c56e77847d28781d8cdd412054492d9
这里添加在有玩家游泳的情况下的水体扰动。
2D物理组件 Buoyancy Effector 2D浮力效应 Buoyancy Effector 2D 浮力效应:在特定区域内没有力度效果。 Use Collider Mask 使用碰撞遮罩,默认勾选,碰撞遮罩可以用于忽略某些层的碰撞效果,比如一块玻璃,如果是光Layer就可以穿过去,如果是物体Layer就会反弹。 Collider Mask 碰撞遮罩,用于设置哪些层产生碰撞效果。 Density 密度,从而影响对撞机的行为:那些有更高的密度,密度较低的浮动,和那些相同的流体密度表现 Surface Level 定义了浮力液体的表面。当一个对象超过这条线,不应用力。当一个物体相交或完全低于这条线,力被重新应用。 Linear Drag 线性阻力,如果物体在水上弹来弹去的,就是阻力给小了,这时候加点阻力就好了。 Angular Drag 角度阻力 Flow Angle 流动力在世界坐标的角度的方向,这个方向就是水流的方向。 Flow Magnitude 流动力大小。结合流体角,这个指定的水平力大小,相当于水的流速。 Flow Variation 随机流动力大小
给一个区域添加水的浮力效果
Render Shape Controller 给2D图片塑性的关键性性组件
在场景管理器后者assets文件中选择新建一个SpriteShapeProfile,有两个选项框进行操作,Open和Close,二者具体的区别如下图所示
获取点 spritshapeController是改变形状的组件,类型是SpriteShapeController,Spline类型中保存着顶点,可以使用数组形式的索引进行设置操作
1 2 Spline waterSpline = spriteShapeController.spline; int waterPointsCount = waterSpline.GetPointCount();
在close的类型下
点的获取遵循数组的常规定义,从0开始作为第一个
1 2 3 4 5 6 7 Vector3 waterTopLeftCorner = waterSpline.GetPosition(1 ); Vector3 waterTopRightCorner = waterSpline.GetPosition(2 );
注意:
使用spline的GetPosition()方法获取到的vector3的位置是基于SpriteShapeController所在的游戏对象的局部坐标系,也就是模型坐标系
并且在本例子将局部坐标抓换为世界坐标和在水中进行游泳的物体进行计算的时候会出现索引越界 的情况,因此使用的是将世界坐标转为局部坐标的方式
世界坐标和局部坐标的相互转换 二者的参数和返回类型一样,需要注意的是需要通过相关的局部坐标系的Transform 来调用TransformPoint方法
即A为世界坐标系,B为的目标或这源头局部坐标系。
世界到局部
1 Vector3 objectPos = transform.InverseTransformPoint(waterSwimInDetect.transform.position);
局部到世界
1 transform.TransformPoint(waterSwimDetect.transform.position);
波纹点生成 Unity事件 Editor运行时才会被自动调用的函数
Unity实现的事件函数
1 2 3 4 5 6 void OnValidate () { StartCoroutine(CreateWaves()); }
在序列化的数值被更改后,会自动调用该函数,
为避免歧义,当脚本中以下属性值在Unity中被可视化改变后会调用以上函数
1 2 [SerializeField ] private float swimWaveCreateDistance = 0.2f ; [SerializeField ] private float swimWaveEndDistance = 0.1f ;
删除、生成点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 IEnumerator CreateWaves () { foreach (Transform child in wavePoints.transform) { StartCoroutine(Destroy(child.gameObject)); } yield return null ; SetWaves(); yield return null ; } IEnumerator Destroy (GameObject go ) { yield return null ; DestroyImmediate(go); } private void SetWaves () { Spline waterSpline = spriteShapeController.spline; int waterPointsCount = waterSpline.GetPointCount(); for (int i = CorsnersCount; i < waterPointsCount - CorsnersCount; i++) { waterSpline.RemovePointAt(CorsnersCount); } Vector3 waterTopLeftCorner = waterSpline.GetPosition(1 ); Vector3 waterTopRightCorner = waterSpline.GetPosition(2 ); float waterWidth = waterTopRightCorner.x - waterTopLeftCorner.x; pointOriginPositionY = waterTopLeftCorner.y; float spacingPerWave = waterWidth / (WavesCount+1 ); for (int i = WavesCount; i > 0 ; i--) { int index = CorsnersCount; float xPosition = waterTopLeftCorner.x + (spacingPerWave*i); Vector3 wavePoint = new Vector3(xPosition, waterTopLeftCorner.y, waterTopLeftCorner.z); waterSpline.InsertPointAt(index, wavePoint); waterSpline.SetHeight(index, 0.1f ); waterSpline.SetCorner(index, false ); waterSpline.SetTangentMode(index, ShapeTangentMode.Continuous); } springs = new (); for (int i = 0 ; i <= WavesCount+1 ; i++) { int index = i + 1 ; Smoothen(waterSpline, index); GameObject wavePoint = Instantiate(wavePointPref, wavePoints.transform, false ); wavePoint.transform.localPosition = waterSpline.GetPosition(index); WaterSpring waterSpring = wavePoint.GetComponent<WaterSpring>(); waterSpring.Init(spriteShapeController); springs.Add(waterSpring); } spline = waterSpline; }
平滑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private void Smoothen (Spline waterSpline, int index ){ Vector3 position = waterSpline.GetPosition(index); Vector3 positionPrev = position; Vector3 positionNext = position; if (index > 1 ) { positionPrev = waterSpline.GetPosition(index-1 ); } if (index - 1 <= WavesCount) { positionNext = waterSpline.GetPosition(index+1 ); } Vector3 forward = gameObject.transform.forward; float scale = Mathf.Min((positionNext - position).magnitude, (positionPrev - position).magnitude) * 0.33f ; Vector3 leftTangent = (positionPrev - position).normalized * scale; Vector3 rightTangent = (positionNext - position).normalized * scale; SplineUtility.CalculateTangents(position, positionPrev, positionNext, forward, scale, out rightTangent, out leftTangent); waterSpline.SetLeftTangent(index, leftTangent); waterSpline.SetRightTangent(index, rightTangent); }
代码解释
1 Vector3 position = waterSpline.GetPosition(index);
1 2 3 4 5 6 if (index > 1 ) { positionPrev = waterSpline.GetPosition(index-1 ); } if (index - 1 <= WavesCount) { positionNext = waterSpline.GetPosition(index+1 ); }
获取当前点的前一个和后一个点的位置,如果它们存在的话。
1 float scale = Mathf.Min((positionNext - position).magnitude, (positionPrev - position).magnitude) * 0.33f ;
计算当前点到前后点的最小距离,并乘以0.33来得到切线长度的缩放因子。
1 2 Vector3 leftTangent = (positionPrev - position).normalized * scale; Vector3 rightTangent = (positionNext - position).normalized * scale;
计算当前点的左右切线向量,基于前后点的位置和缩放因子。
1 SplineUtility.CalculateTangents(position, positionPrev, positionNext, forward, scale, out rightTangent, out leftTangent);
使用SplineUtility
类的方法来优化左右切线的计算。
1 2 waterSpline.SetLeftTangent(index, leftTangent); waterSpline.SetRightTangent(index, rightTangent);
将计算出的左右切线应用到样条曲线的当前点,以平滑曲线。
波纹模拟 注意,该部分有两个不同实现点
跳水部分顶点的位置改变是通过和顶点位置同步的WaterSpring游戏对象进行控制
游泳则是直接改变Shape的顶点
总结就是前者有同步的游戏对象位置和单独的弹簧模拟,后者没有
WaterSpringController的执行代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 void FixedUpdate () { JumpInWatetWaveChange(); SwimWaveChange(); UpdateSprings(); } private void JumpInWatetWaveChange () { foreach (WaterSpring waterSpringComponent in springs) { waterSpringComponent.WaveSpringUpdate(springStiffness, dampening); waterSpringComponent.WavePointUpdate(); } } private void SwimWaveChange () { for (int i = 2 ; i <= WavesCount + 2 ; ++i) { int index = i; if (waterSwimInDetect.isObjectInWater) { if (MathF.Abs(waterSwimInDetect.rb.velocity.x) < 0.04f || MathF.Abs(waterSwimInDetect.rb.velocity.y)>0.09f ) return ; Vector3 objectPos = transform.InverseTransformPoint(waterSwimInDetect.transform.position); Vector3 pointPos = spline.GetPosition(index); float distance = pointPos.x - objectPos.x; pointOriginPositionY = pointPos.y; bool isTooFar = Mathf.Abs(distance) > swimWaveCreateDistance; bool isTooClose = Mathf.Abs(distance) < swimWaveEndDistance; bool isSlow = Mathf.Abs(waterSwimInDetect.rb.velocity.x) < 0.004f ; if (isTooFar || isSlow || isTooClose) { pointPos.y = pointOriginPositionY; spline.SetPosition(index, pointPos); } else { float disturbance = ((MathF.Cos(distance)+1 )/2 * heightFactor * waterSwimInDetect.rb.velocity.x) * MathF.Sign(distance); pointPos.y = pointOriginPositionY + disturbance; spline.SetPosition(index, pointPos); } } } } private void UpdateSprings () { int count = springs.Count; float [] left_deltas = new float [count]; float [] right_deltas = new float [count]; for (int i = 0 ; i < count; i++) { if (i > 0 ) { left_deltas[i] = spread * (springs[i].height - springs[i-1 ].height); springs[i-1 ].velocity += left_deltas[i]; } if (i < springs.Count - 1 ) { right_deltas[i] = spread * (springs[i].height - springs[i+1 ].height); springs[i+1 ].velocity += right_deltas[i]; } } }
SpringWater中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.U2D;public class WaterSpring : MonoBehaviour { public float velocity = 0 ; public float force = 0 ; public float height = 0f ; private float target_height = 0f ; public Transform springTransform; [SerializeField ] private SpriteShapeController spriteShapeController = null ; private int waveIndex = 0 ; private List<WaterSpring> springs = new (); private float resistance = 40f ; public void Init (SpriteShapeController ssc ) { var index = transform.GetSiblingIndex(); waveIndex = index+1 ; spriteShapeController = ssc; velocity = 0 ; height = transform.localPosition.y; target_height = transform.localPosition.y; } public void WaveSpringUpdate (float springStiffness, float dampening ) { height = transform.localPosition.y; var x = height - target_height; var loss = -dampening * velocity; force = - springStiffness * x + loss; velocity += force; var y = transform.localPosition.y; transform.localPosition = new Vector3(transform.localPosition.x, y+velocity, transform.localPosition.z); } public void WavePointUpdate () { if (spriteShapeController != null ) { Spline waterSpline = spriteShapeController.spline; Vector3 wavePosition = waterSpline.GetPosition(waveIndex); waterSpline.SetPosition(waveIndex, new Vector3(wavePosition.x, transform.localPosition.y, wavePosition.z)); } } private void OnCollisionEnter2D (Collision2D other ) { if (other.gameObject.tag.Equals("FallingObject" )) { FallingObject fallingObject = other.gameObject.GetComponent<FallingObject>(); Rigidbody2D rb = fallingObject.rigidbody2D; var speed = rb.velocity; velocity += speed.y/resistance; } } }
跳水 更新控制
1 2 3 4 5 6 7 8 9 private void JumpInWatetWaveChange (){ foreach (WaterSpring waterSpringComponent in springs) { waterSpringComponent.WaveSpringUpdate(springStiffness, dampening); waterSpringComponent.WavePointUpdate(); } }
弹簧模拟 WaveSpringUpdate
该方法模拟了一个简单的弹簧系统,用于更新某个对象(可能是水面或波浪)的高度。以下是代码的逐行解释:
1 public void WaveSpringUpdate (float springStiffness, float dampening ) {
定义了一个公共方法WaveSpringUpdate
,它接受两个参数:springStiffness
(弹簧刚度)和dampening
(阻尼)。
1 height = transform.localPosition.y;
获取当前对象在Y轴上的位置,并将其存储在变量height
中。
1 var x = height - target_height;
计算当前高度与目标高度target_height
之间的差值,并存储在变量x
中。
1 var loss = -dampening * velocity;
计算阻尼力,即阻尼乘以当前速度的负值,并存储在变量loss
中。
1 force = - springStiffness * x + loss;
计算总的力,即弹簧力(由弹簧刚度乘以高度差)加上阻尼力。
1 var y = transform.localPosition.y;
再次获取当前对象在Y轴上的位置,并存储在变量y
中。
1 transform.localPosition = new Vector3(transform.localPosition.x, y+velocity, transform.localPosition.z);
更新对象的位置,将速度加到Y轴的位置上,从而在Y轴方向上移动对象。
总的来说,这个方法模拟了一个简单的弹簧-阻尼系统,其中:
springStiffness
决定了弹簧恢复到原始位置的力的大小。
dampening
决定了系统减速到停止的速度。
height
是当前对象在Y轴上的位置。
target_height
是对象想要达到的目标高度。
velocity
是对象在Y轴上的速度。
force
是作用在对象上的总力。
这个方法在每一帧被调用时,都会根据弹簧力和阻尼力更新对象的位置,模拟波浪或弹簧的运动。
以上内容总结于智普清言
波动扩散 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private void UpdateSprings () { int count = springs.Count; float [] left_deltas = new float [count]; float [] right_deltas = new float [count]; for (int i = 0 ; i < count; i++) { if (i > 0 ) { left_deltas[i] = spread * (springs[i].height - springs[i-1 ].height); springs[i-1 ].velocity += left_deltas[i]; } if (i < springs.Count - 1 ) { right_deltas[i] = spread * (springs[i].height - springs[i+1 ].height); springs[i+1 ].velocity += right_deltas[i]; } } }
在WaterSpringController中根据传播因子spread ,对没有直接被改变速度的WaterSpring取周边被直接影响速度,下一帧就会在FixedUpdate 函数中自行模拟弹簧运动
游泳 具体见注释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 private void SwimWaveChange () { for (int i = 2 ; i <= WavesCount + 2 ; ++i) { int index = i; if (waterSwimInDetect.isObjectInWater) { if (MathF.Abs(waterSwimInDetect.rb.velocity.x) < 0.04f || MathF.Abs(waterSwimInDetect.rb.velocity.y)>0.09f ) return ; Vector3 objectPos = transform.InverseTransformPoint(waterSwimInDetect.transform.position); Vector3 pointPos = spline.GetPosition(index); float distance = pointPos.x - objectPos.x; pointOriginPositionY = pointPos.y; bool isTooFar = Mathf.Abs(distance) > swimWaveCreateDistance; bool isTooClose = Mathf.Abs(distance) < swimWaveEndDistance; bool isSlow = Mathf.Abs(waterSwimInDetect.rb.velocity.x) < 0.004f ; if (isTooFar || isSlow || isTooClose) { pointPos.y = pointOriginPositionY; spline.SetPosition(index, pointPos); } else { float disturbance = ((MathF.Cos(distance)+1 )/2 * heightFactor * waterSwimInDetect.rb.velocity.x) * MathF.Sign(distance); pointPos.y = pointOriginPositionY + disturbance; spline.SetPosition(index, pointPos); } } } }
注意 跳水的模拟需要在游泳前面,具体原因参照在波纹模拟最开始的提示
1 2 3 4 5 6 7 void FixedUpdate (){ JumpInWatetWaveChange(); SwimWaveChange(); UpdateSprings(); }
跳水在后面的话,因为游泳没有改变WaterSpring游戏对象的位置,最终游泳的波动会被跳水更新中的以下代码根据游戏对象的位置强制更新
1 2 3 4 5 6 7 8 9 public void WavePointUpdate () { if (spriteShapeController != null ) { Spline waterSpline = spriteShapeController.spline; Vector3 wavePosition = waterSpline.GetPosition(waveIndex); waterSpline.SetPosition(waveIndex, new Vector3(wavePosition.x, transform.localPosition.y, wavePosition.z)); } }
物体游泳通过的区域需要抚平,想要在游泳的时候保留跳水波动的效果,就使用以下注释为 非常重要 的部分代码,将跳水影响的高度重新赋值给指定顶点
1 2 3 4 5 6 7 8 9 10 11 12 Vector3 pointPos = spline.GetPosition(index); float distance = pointPos.x - objectPos.x; pointOriginPositionY = pointPos.y; bool isTooFar = Mathf.Abs(distance) > swimWaveCreateDistance;bool isTooClose = Mathf.Abs(distance) < swimWaveEndDistance;bool isSlow = Mathf.Abs(waterSwimInDetect.rb.velocity.x) < 0.004f ;
最终效果
补充 这个版本还有部分缺陷,游泳产生的扰动不和跳水的弹簧波动有交互,即,你游你的,我震我的。日后修正。
注意
在以上示例中使用到的OnValidate()Unity事件函数在打包后调用会出现错误,因此要如果要将效果加入到打包工程中,应将该函数中的对应代码加入到Awake、Start或者OEnable中去