【Unity小技巧】Unity中实现带有Sprite Shape的2D水效果(附项目源码)

简介: 【Unity小技巧】Unity中实现带有Sprite Shape的2D水效果(附项目源码)

先看实现的最终效果

前言

本文是自己的学习笔记,最近发现一个很有意思的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


目录
相关文章
|
3月前
|
编译器 vr&ar 图形学
从零开始的unity3d入门教程(五)---- 基于Vuforia的AR项目
这是一篇Unity3D结合Vuforia实现增强现实(AR)项目的入门教程,涵盖了环境配置、Vuforia账户注册、Target数据集创建、Unity项目设置、AR程序配置、Android环境配置以及最终在手机上测试运行的全过程。
从零开始的unity3d入门教程(五)---- 基于Vuforia的AR项目
|
3月前
|
图形学 C#
超实用!深度解析Unity引擎,手把手教你从零开始构建精美的2D平面冒险游戏,涵盖资源导入、角色控制与动画、碰撞检测等核心技巧,打造沉浸式游戏体验完全指南
【8月更文挑战第31天】本文是 Unity 2D 游戏开发的全面指南,手把手教你从零开始构建精美的平面冒险游戏。首先,通过 Unity Hub 创建 2D 项目并导入游戏资源。接着,编写 `PlayerController` 脚本来实现角色移动,并添加动画以增强视觉效果。最后,通过 Collider 2D 组件实现碰撞检测等游戏机制。每一步均展示 Unity 在 2D 游戏开发中的强大功能。
161 6
|
3月前
|
API 开发工具 vr&ar
PicoVR Unity SDK⭐️一、SDK下载、项目设置与程序初始配置
PicoVR Unity SDK⭐️一、SDK下载、项目设置与程序初始配置
|
5月前
|
图形学
【unity小技巧】Unity中实现一个战斗连击连招系统,可以动态添加减少连击连招段数功能
【unity小技巧】Unity中实现一个战斗连击连招系统,可以动态添加减少连击连招段数功能
112 0
|
5月前
|
存储 图形学
【unity小技巧】unity事件系统创建通用的对象交互的功能
【unity小技巧】unity事件系统创建通用的对象交互的功能
52 0
|
5月前
|
图形学
【unity小技巧】unity通过代码进行更改后处理效果
【unity小技巧】unity通过代码进行更改后处理效果
68 0
|
5月前
|
图形学
【unity小技巧】unity3D寻路指示轨迹预测
【unity小技巧】unity3D寻路指示轨迹预测
69 0
|
5月前
|
图形学
【推荐100个unity插件之19】武器拖尾特效插件——Pocket RPG Weapon Trails(2d 3d通用)
【推荐100个unity插件之19】武器拖尾特效插件——Pocket RPG Weapon Trails(2d 3d通用)
92 0
|
5月前
|
图形学
【制作100个unity游戏之29】使用unity复刻经典游戏《愤怒的小鸟》(完结,附带项目源码)(下)
【制作100个unity游戏之29】使用unity复刻经典游戏《愤怒的小鸟》(完结,附带项目源码)(下)
83 0
|
2月前
|
测试技术 C# 图形学
掌握Unity调试与测试的终极指南:从内置调试工具到自动化测试框架,全方位保障游戏品质不踩坑,打造流畅游戏体验的必备技能大揭秘!
【9月更文挑战第1天】在开发游戏时,Unity 引擎让创意变为现实。但软件开发中难免遇到 Bug,若不解决,将严重影响用户体验。调试与测试成为确保游戏质量的最后一道防线。本文介绍如何利用 Unity 的调试工具高效排查问题,并通过 Profiler 分析性能瓶颈。此外,Unity Test Framework 支持自动化测试,提高开发效率。结合单元测试与集成测试,确保游戏逻辑正确无误。对于在线游戏,还需进行压力测试以验证服务器稳定性。总之,调试与测试贯穿游戏开发全流程,确保最终作品既好玩又稳定。
93 4