先看实现的最终效果
前言
本文是自己的学习笔记,最近发现一个很有意思的2d水效果,所以把它的实现过程写下来分享给大家。
当在 Unity 中实现带有 Sprite Shape 的 2D 水效果时,首先需要理解 Sprite Shape 和水效果的基本概念和工作原理。Sprite Shape 是 Unity 提供的一种 2D 图形工具,用于创建基于轮廓的精灵形状,并可以根据路径进行变形和填充。而 2D 水效果通常涉及模拟水体的行为,包括波纹、浪花、浮力等物理特性的表现。
总的来说,结合 Sprite Shape 和水效果需要综合运用 Unity 中的图形技术、物理模拟和动画效果,以达到模拟逼真的水体效果。这样的设计可以为游戏场景增添视觉上的沉浸感和互动性,为玩家带来更加生动的游戏体验。
模拟水面的波动效果
新增WaterSpring
public class WaterSpring : MonoBehaviour { public float velocity = 0; public float force = 0; // 当前高度 public float height = 0f; // 目标高度 public float target_height = 0f; // 带有阻尼的弹簧更新 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); } }
新建一个2d球挂载上去
新增WaterShapeController
[SerializeField] private float springstiffness = 0.1f; // 弹簧刚度系数 [SerializeField] private List<WaterSpring> springs = new List<WaterSpring>(); // 所有水弹簧的列表 [SerializeField] private float dampening = 0.03f; // 阻尼系数 public float spread = 0.006f; // 弹簧之间的间隔 void FixedUpdate() { // 对所有水弹簧进行更新 foreach (WaterSpring waterSpringComponent in springs) { waterSpringComponent .WaveSpringUpdate(springstiffness, dampening); } }
新建个空物体,挂载脚本并配置参数
一个球的效果
复制多个球模拟水面波动
效果
制作2d水面
安装插件
新增
配置4个节点
最终效果
修改WaterSpring
public class WaterSpring : MonoBehaviour { private int waveIndex = 0; // 当前水泉所在的曲线上的节点索引 [SerializeField] private static SpriteShapeController spriteShapeController = null; // 水面曲线对应的SpriteShapeController组件 [System.NonSerialized] public float velocity = 0; // 当前水泉的速度 private float force = 0; // 当前水泉的力 [System.NonSerialized] public float height = 0f; // 当前水泉的高度 private float target_height = 0f; // 目标高度 private float resistance = 30f; // 抵抗力,表示当有物体落入水面时,水泉速度增加的系数 // 初始化函数,用于设置当前水泉所在的曲线节点索引、SpriteShapeController组件以及初始化速度、高度 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 OnTriggerEnter2D(Collider2D other) { Debug.Log(2222); if (other.gameObject.tag.Equals("FallingObject")) { FallingObject fallingObject = other.gameObject.GetComponent<FallingObject>(); Rigidbody2D rb = fallingObject.GetComponent<Rigidbody2D>(); var speed = rb.velocity; velocity += speed.y / resistance; } } }
给球挂载脚本,添加触发器和配置区域
修改WaterShapeController,当一个脚本类被标记为 [ExecuteAlways] 时,它的 Update、LateUpdate 和 FixedUpdate 等方法会在编辑模式下以及运行时都被执行,而不仅仅是在播放模式下执行。
[ExecuteAlways] public class WaterShapeController : MonoBehaviour { private int CorsnersCount = 2; // 水形状的边角数 [SerializeField] private SpriteShapeController spriteShapeController; // 水形状控制器 [SerializeField, Header("波浪点预设物体")] private GameObject wavePointPref; [SerializeField, Header("波浪点父物体")] private GameObject wavePoints; [SerializeField, Header("波浪数量")] [Range(1, 100)] private int WavesCount; private List<WaterSpring> springs = new(); // 存储水波浪效果的弹簧节点 [Header("弹簧的强度常数")] public float springStiffness = 0.1f; [Header("阻力,用于减缓弹簧的运动")] public float dampening = 0.03f; [Header("扩散常数,用于将节点的影响传递给它附近的节点")] public float spread = 0.006f; void Start() { } void OnValidate() { // 清空水波浪点和弹簧 StartCoroutine(CreateWaves()); } 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; // 水形状的宽度 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); } } 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); // 设置右切向量 } void FixedUpdate() { foreach (WaterSpring waterSpringComponent in springs) { waterSpringComponent.WaveSpringUpdate(springStiffness, dampening); // 更新弹簧效果 waterSpringComponent.WavePointUpdate(); // 更新节点位置 } UpdateSprings(); // 更新所有弹簧的速度 } 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]; } } } private void Splash(int index, float speed) { if (index >= 0 && index < springs.Count) { springs[index].velocity += speed; } } }
挂载脚本,并配置参数
效果
实现物体落入水中互动效果
修改WaterSpring
//。。。 // 当有物体进入水面时的触发检测函数,如果该物体被标记为FallingObject,则将水泉速度增加一定值 private void OnTriggerEnter2D(Collider2D other) { if (other.gameObject.tag.Equals("FallingObject")) { FallingObject fallingObject = other.gameObject.GetComponent<FallingObject>(); Rigidbody2D rb = fallingObject.GetComponent<Rigidbody2D>(); var speed = rb.velocity; velocity += speed.y / resistance; } }
新增FallingObject脚本,控制物体下落
public class FallingObject : MonoBehaviour { [Header("物体下落的速度")] public float forceAmount = 5f; private Rigidbody2D rb; // 物体的刚体组件 void Start() { rb = GetComponent<Rigidbody2D>(); // 将物体的速度设置为向下的 forceAmount 速度 rb.velocity = Vector3.down * forceAmount; } }
新建个box物体,挂载脚本,配置参数,记得修改物体标签为FallingObject
效果
给水面添加浮力效果
添加Buoyancy Effector 2D(浮力效应器)和多边形碰撞器。浮力效应器用于模拟游戏对象在水中受到的浮力影响,编辑多边形碰撞器配置覆盖整个水体
效果
最终效果
源码
整理好后我会放上了
参考
如果觉得本文实现的效果不错的话,非常推荐大家去支持一下原作者
【视频】https://www.youtube.com/watch?v=69sBjqMtZCc