2D水体效果

* 参考https://github.com/tutsplus/unity-2d-water-effect

基于线性渲染器(Line Renderer)、网格渲染器(Mesh Renderer),触发器(Trigger)以及粒子来模拟水体效果。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WaterManager : MonoBehaviour {
    // 所有节点的位置、速度及加速度
    float[] xpositions;
    float[] ypositions;
    float[] velocities;
    float[] accelerations;
    // LineRenderer用于保存所有节点及水体的轮廓
    LineRenderer Body;
    // 使用网格来实现水体,并创建游戏对象来使用这些网格
    GameObject[] meshobjects;
    Mesh[] meshes;
    // 为每个游戏对象添加碰撞器
    GameObject[] colliders;

    // 水流速度、衰减度及传播速度
    const float springconstant = 0.02f;
    const float damping = 0.04f;
    const float spread = 0.05f;
    const float z = -1f; // 水体的显示层次,这里设为-1表示会显示在对象前面

    // 水的维度
    float baseheight;
    float left;
    float bottom;

    // 水波四溅效果的粒子系统
    public GameObject splash;

    // 用于Line Renderer的材质
    public Material mat;

    // 用于模拟水体的网格
    public GameObject watermesh

    void Start() {
        //Spawning our water
        SpawnWater(-10,20,0,-3);
    }

    // 在游戏过程中生成水体
    // 参数分别为水体四周的边长
    public void SpawnWater(float Left, float Width, float Top, float Bottom) {
        // 边和节点数量(对每单位宽度的水体使用5个节点,让整个水体运动看起来更平滑)
        int edgecount = Mathf.RoundToInt(Width) * 5;
        int nodecount = edgecount + 1;

        // 使用LineRenderer组件来渲染水体
        Body = gameObject.AddComponent<LineRenderer>();
        Body.material = mat;
        Body.material.renderQueue = 1000; // 通过渲染队列将材质的渲染顺序设为比水体更高
        Body.SetVertexCount(nodecount);
        Body.SetWidth(0.1f, 0.1f);

        // 初始化前面声明的变量
        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;

        // 按节点顺序为各数组赋值。这里将所有的y坐标设为水体上方,让水体各部分紧密排列。速度和加速度都为0表示水体是静止的
        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));
        }

        for (int i = 0; i < edgecount; i++) {
            // 使用网格来实现水体
            meshes[i] = new Mesh();
            // 数组的四个元素按顺序分别表示左上角、右上角、左下角和右下角的顶点位置
            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坐标决定了网格用到的纹理部分。这里简单的使用纹理左上角、右上角、左下角及右下角的部分作为网格显示内容
            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组成
            int[] tris = new int[6] { 0, 1, 3, 3, 2, 0 };
            // 设置网格数据
            meshes[i].vertices = Vertices;
            meshes[i].uv = UVs;
            meshes[i].triangles = tris;
            // 添加游戏对象将其渲染到场景中。利用工程中的watermesh预制创建游戏对象,其中包含Mesh Renderer和Mesh Filter 组件
            meshobjects[i] = Instantiate(watermesh,Vector3.zero,Quaternion.identity) as GameObject;
            meshobjects[i].GetComponent<MeshFilter>().mesh = meshes[i];
            meshobjects[i].transform.parent = transform;

            // 添加盒状碰撞器并统一命名以便于管理,同样将其设为管理器子对象。将碰撞器坐标设为节点中间,设置好大小并添加WaterDetector类
            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);
            // 给碰撞盒添加中心和尺寸,用来实现让物体在水中漂流
            // gameObject.GetComponent<BoxCollider2D>().center = new Vector2(Left + Width / 2, (Top + Bottom) / 2);
            // gameObject.GetComponent<BoxCollider2D>().size = new Vector2(Width, Top - Bottom);
            colliders[i].GetComponent<BoxCollider2D>().isTrigger = true;
            colliders[i].AddComponent<WaterDetector>();
    }

    // 控制水体网格的移动
    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;
        }
    }

    // 在FixedUpdate()函数中添加物理特性让水体可以自行流动
    void FixedUpdate() {
        // 结合胡克定律和欧拉方法获取水体新的坐标、加速度及速度
        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));
        }

        // 实现水浪的传播效果【对于每个节点,都要对比前一个节点与当前节点的高度差并将差值存入leftDeltas】
        float[] leftDeltas = new float[xpositions.Length];
        float[] rightDeltas = new float[xpositions.Length];

        // 比较后一个节点与当前节点的高度差并将差值存入rightDeltas。还需将所有的差值乘以传播速度常量
        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];
                }
            }
            // 根据高度差立即改变速度,但此时只需保存坐标差即可。如果立即改变第一个节点的坐标,同时再去计算第二个节点时第一个坐标已经移动了,这样会影响到后面所有节点的计算
            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];
                }
            }
        }
    }

    // 水波飞溅的效果【检测水波的x坐标及入水物体接触水面时的速度】
    public void Splash(float xpos, float velocity) {
        // 确定水波飞溅的位置是在水体范围内
        if (xpos >= xpositions[0] && xpos <= xpositions[xpositions.Length-1]) {
            // 改变水波的x坐标以获取飞溅位置与水体起始位置间的相对坐标
            xpos -= xpositions[0];
            // 找到落水物体碰撞的节点【先获取飞溅位置与水体左边界的坐标差(xpos),后将该差值除以水体宽度,将飞溅发生位置的分数乘以边数后取整,就得到了离飞溅位置最近的节点索引】
            int index = Mathf.RoundToInt((xpositions.Length-1)*(xpos / (xpositions[xpositions.Length-1] - xpositions[0])));
            // 将入水物体的速度赋给该物体所碰撞的节点,这样节点会被物体压入水体。
            velocities[index] = velocity;

            // 设置飞溅的参数,这个参数是受撞击物体的速度影响的
            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;

            // 水体是室内游泳池就需要添加以下代码
            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);
            // 最后添加飞溅对象,该对象会在粒子被摧毁后一段时间再消失,因为粒子系统发射了大量爆裂的粒子,所以粒子消失所需时间至少是Time.time + lifetime,最后的爆裂的粒子甚至需要更久。
            GameObject splish = Instantiate(splash,position,rotation) as GameObject;
            Destroy(splish, lifetime+0.3f);
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WaterDetector : MonoBehaviour {
    void OnTriggerEnter2D(Collider2D Hit) {
        if (Hit.rigidbody2D != null) {
          transform.parent.GetComponent<WaterManager>().Splash(transform.position.x, Hit.rigidbody2D.velocity.y*Hit.rigidbody2D.mass / 40f);
        }
    }
}

results matching ""

    No results matching ""