美文网首页
[Unity]2D颜料泼溅效果

[Unity]2D颜料泼溅效果

作者: pamisu | 来源:发表于2021-02-18 08:29 被阅读0次

    做一个类似于《INK》的2D颜料泼溅效果:

    INK 效果

    表面与颜料

    利用模板测试,让颜料污渍能在物体表面上重叠显示且不超出物体轮廓。在物体表面的Shader中,总是通过模板测试并替换参考值,同时裁剪掉透明度为0的像素;颜料污渍的Shader中,如果与参考值相等则通过测试。

    项目用了URP,这里直接将Sprite-Lit-Default.shader拷贝两份出来修改。

    被染色表面Surface-Lit.shader

    SubShader中加入Stencil配置

    SubShader
    {
        Tags {"Queue" = "Transparent" "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" }
    
        Stencil
        {
            Ref 2
            Comp Always
            Pass Replace
        }
        ...
    

    透明度裁剪在原来的Sprite-Lit-Default.shader中已经做好了所以不用再写。

    颜料污渍Stain-Lit.shader

    SubShader
    {
        Tags {"Queue" = "Transparent" "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" }
    
        Stencil
        {
            Ref 2
            Comp Equal
        }
        ...
    

    建一个场景测试效果,Tilemap使用Surface-Lit材质,污渍Sprite使用Stain材质,二者在同一个Sorting Layer,Tilemap的Order in Layer需要比污渍Sprite小。

    Tilemap:

    颜料污渍:

    颜料污渍已经可以显示在Tilemap上并且不会超出其轮廓。

    喷溅

    添加一把玩具水枪,简单写一个发射颜料子弹的逻辑,子弹爆开后围绕当前位置随机生成多个颜料污渍预制体。颜料污渍预制体用一张1像素的白色图片,通过缩放来显示出不同大小的污渍。子弹中设置多种颜色随机应用到子弹与污渍的SpriteRenderer。

    Stain Radius为喷溅半径,Stain Scale为最大污渍缩放。

    编写StainGenerator类用来生成颜料污渍:

    public class StainGenerator
    {
        /// <summary>
        /// 在中心点生成向四周发散的污渍
        /// </summary>
        /// <param name="prefab">污渍预制体</param>
        /// <param name="color">颜色</param>
        /// <param name="position">中心点位置</param>
        /// <param name="direction">冲击方向</param>
        /// <param name="scale">污渍缩放</param>
        /// <param name="radius">污渍分布半径</param>
        public static void Generate(GameObject prefab, Color color, Vector3 position, Vector3 direction, float scale, float radius)
        {
            // 以position为中心分裂到若干个方向,每个分裂的角度随机
            int splitNum = Random.Range(4, 9);  // 分裂数量随机,数值暂时写死
            Vector3[] splitDirs = new Vector3[splitNum];
            float angleDelta = 360f / splitNum;
            for (int i = 0; i < splitDirs.Length; i++)
            {
                var lastDir = i == 0? direction : splitDirs[i - 1];
                var angle = RandomNum(angleDelta, .2f);
                splitDirs[i] = Quaternion.AngleAxis(angle, Vector3.forward) * lastDir;
            }
    
            // 每个分裂方向生成若干个污渍
            foreach (var dir in splitDirs)
            {
                int stainNum = Random.Range(3, 6);
                float stainScale;    // 污渍
                float radiusDelta = radius / 6f;    // 每个污渍间距
                Vector3 stainPos = position;    // 污渍位置
                for (int i = 0; i < stainNum; i++)
                {
                    stainScale = scale - (i * scale / stainNum);    // 缩放随距离衰减
                    stainPos += dir * RandomNum(radiusDelta, .4f);
                    stainPos += (Vector3) Random.insideUnitCircle * RandomNum(radiusDelta * .2f, radiusDelta * .1f); // 位置随机
                    var go = Object.Instantiate(prefab);
                    go.transform.position = stainPos;
                    go.transform.right = dir;
                    go.transform.localScale = new Vector3(stainScale, stainScale, 1f);
                    go.GetComponent<SpriteRenderer>().color = color;
                }
            }
        }
    
        public static float RandomNum(float num, float randomness)
        {
            return num + Random.Range(-num * randomness, num * randomness);
        }
    }
    

    子弹与地面发生碰撞时调用,传入污渍预制体、颜色、冲击位置、冲击方向、自定义的污渍缩放与喷溅半径:

    private void OnCollisionEnter2D(Collision2D other) 
    {
        ...
        StainGenerator.Generate(stainPrefab, color, transform.position, transform.right, stainScale, stainRadius);
    }
    

    比较简单的实现就完成了,但这种不断生成Sprite的方法将导致场景里分分钟就会多出上千个游戏对象。

    优化

    由于污渍的材质都一样,Unity对它们做了动态合批,这里是否还可以通过自定义mesh来优化,暂时没有什么头猪。

    好在对游戏对象的数量优化还是比较简单的,打在同一个位置的颜料污渍将会发生重叠,被遮挡住的污渍是不再需要的。定义一个同一位置最大可叠加层数,在创建新的污渍时,先判断当前位置共叠加了几层,如果超过允许的最大层数,则将最底层的污渍对象回收,再从对象池中取出已回收的污渍对象重复利用。

    这种做法的缺点是只判断重叠,而不是判断污渍是否被完全覆盖,显示效果上不太好,最大层数设置较低时会出现有些污渍还未被完全覆盖,却依然被回收了的情况。

    检测重叠可以使用SpriteRenderer中Bounds的Intersects方法,但每次生成都要遍历当前所有污渍对象做判断,感觉过于繁琐。最终还是选择用了Physics2D.OverlapBoxAll,这样要先在污渍预制体中添加BoxCollider2D。

    修改StainGenerator,令它继承MonoBehaviour。叠加层数利用SpriteRenderer的Order in Layer属性实现,假设三个污渍重叠,且它们的Order in Layer分别是2、3、4,如果此时已达到了最大叠加层数,同时有新的污渍即将覆盖它们,则回收Order为2的,其余的Order减1,新的污渍Order设为4,这样依然能保持2、3、4的重叠顺序。

    public class StainGenerator : MonoBehaviour
    {
        
        public static StainGenerator Instance { get; private set;}
    
        [Tooltip("污渍Prefab")]
        public GameObject prefab;
        [Tooltip("最小Order in Layer")]
        public int minOrderInLayer;
        [Tooltip("最大Order in Layer")]
        public int maxOrderInLayer;
        [Tooltip("污渍大小")]
        public Vector2 stainSize;
    
        // 污渍对象池
        [SerializeField] List<GameObject> stainPool;
        // 临时污渍对象列表,用来记录本次生成已处理过的对象,避免重复处理
        List<GameObject> tempStains;
    
        void Awake() 
        {
            // 初始化单例和列表
            ...
        }  
        ...
    }
    

    修改Generate方法,去除多余的参数,仅改动污渍对象生成部分,子弹碰撞中相应修改对其的调用。

    public void Generate(Color color, Vector3 position, Vector3 direction, float scale, float radius)
    {
        // 以position为中心分裂到若干个方向,每个分裂的角度随机
        ...
        // 每个分裂方向生成若干个污渍
        tempStains.Clear(); // 每次生成时清空临时列表
        foreach (var dir in splitDirs)
        {
            ...
            for (int i = 0; i < stainNum; i++)
            {
                ...
                // var go = Object.Instantiate(prefab);
                var go = GetStain(stainPos, stainScale, dir);   // 替换为GetStain方法
                ...
            }
        }
    }
    

    编写GetStain方法。

    /// <summary>
    /// 获取当前污渍对象,若当前位置发生重叠则调整回收
    /// </summary>
    /// <param name="pos">位置</param>
    /// <param name="scale">缩放</param>
    /// <param name="dir">朝向</param>
    /// <returns></returns>
    GameObject GetStain(Vector3 pos, float scale, Vector3 dir)
    {
        int order = minOrderInLayer;   // 当前污渍需要设置的sortingOrder
        var angle = Vector2.SignedAngle(Vector2.right, dir);
        var size = stainSize * scale;   // 实际大小
        var cols = Physics2D.OverlapBoxAll(pos, size, angle, LayerMask.GetMask("Stain"));
        if (cols.Length != 0)
        {
            // 若检测到污渍重叠,获取当前最顶层污渍的sortingOrder
            SpriteRenderer spriteRenderer;
            foreach (var item in cols)
            {
                spriteRenderer = item.GetComponent<SpriteRenderer>();
                if (spriteRenderer.sortingOrder > order)
                {
                    order = spriteRenderer.sortingOrder;
                    // 如果即将超出最大叠加层数则直接快进到处理重叠的污渍
                    if (order + 1 > maxOrderInLayer)
                        break;
                }
            }
            if (order + 1 > maxOrderInLayer)
            {
                // 回收最底层的污渍,其余层sortingOrder减1,并标记为已处理,避免被重复处理
                foreach (var item in cols)
                {
                    spriteRenderer = item.GetComponent<SpriteRenderer>();
                    if (spriteRenderer.sortingOrder == minOrderInLayer)
                        item.gameObject.SetActive(false);
                    else if (!tempStains.Contains(item.gameObject))
                    {
                        spriteRenderer.sortingOrder--;
                        tempStains.Add(item.gameObject);
                    }
                }
            }
            order = Mathf.Clamp(order + 1, minOrderInLayer, maxOrderInLayer);
        }
    
        // 简易对象池
        GameObject go = null; 
        foreach (var item in stainPool)
        {
            if (!item.activeInHierarchy)
            {
                go = item;
                go.SetActive(true);
                break;
            }
        }
        if (go == null)
        {
            go = Instantiate(prefab, transform);
            stainPool.Add(go);
        }
        go.GetComponent<SpriteRenderer>().sortingOrder = order;
        
        return go;
    }
    

    脚本挂到场景中,设置相应值:

    调整之后:

    调整后

    和调整前对比:

    调整前

    可以发现不再生成那么多对象了,帧数也相对稳定,但出现了上面提到的显示问题,将最大叠加层数调高基本可以解决。

    用到的素材:Cavernas by Adam Saltsman8 Guns + Projectiles by KingKelpo

    相关文章

      网友评论

          本文标题:[Unity]2D颜料泼溅效果

          本文链接:https://www.haomeiwen.com/subject/ztjwxltx.html