<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[[Unity]2D颜料泼溅效果]]></title><description><![CDATA[<p dir="auto"><img src="https://s2.loli.net/2023/03/03/FSQgmWDJrBaCcy6.png" alt class=" img-responsive img-markdown" /></p>
<p dir="auto">做一个类似于《INK》的2D颜料泼溅效果</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/oDi7mKQVRrY1v9S.jpg" alt="INK" class=" img-responsive img-markdown" /></p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/Xq9JdOYzWkxi5ct.gif" alt="实现效果" class=" img-responsive img-markdown" /></p>
<h2>表面与颜料</h2>
<p dir="auto">利用模板测试，让颜料污渍能在物体表面上重叠显示且不超出物体轮廓。在物体表面的Shader中，总是通过模板测试并替换参考值，同时裁剪掉透明度为0的像素；颜料污渍的Shader中，如果与参考值相等则通过测试。</p>
<p dir="auto">项目用了URP，这里直接将Sprite-Lit-Default.shader拷贝两份出来修改。</p>
<p dir="auto"><strong>被染色表面Surface-Lit.shader</strong></p>
<p dir="auto">SubShader中加入Stencil配置</p>
<pre><code class="language-C">SubShader
{
    Tags {"Queue" = "Transparent" "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" }

    Stencil
    {
        Ref 2
        Comp Always
        Pass Replace
    }
    ...
</code></pre>
<p dir="auto">透明度裁剪在原来的Sprite-Lit-Default.shader中已经做好了所以不用再写。</p>
<p dir="auto"><strong>颜料污渍Stain-Lit.shader</strong></p>
<pre><code class="language-C">SubShader
{
    Tags {"Queue" = "Transparent" "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" }

    Stencil
    {
        Ref 2
        Comp Equal
    }
    ...
</code></pre>
<p dir="auto">建一个场景测试效果，Tilemap使用Surface-Lit材质，污渍Sprite使用Stain材质，二者在同一个Sorting Layer，Tilemap的Order in Layer需要比污渍Sprite小。</p>
<p dir="auto">Tilemap:</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/f7EReqQKLY6mtF2.png" alt="Tilemap" class=" img-responsive img-markdown" /></p>
<p dir="auto">颜料污渍:</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/fNCEDSUtOW3MQYh.png" alt="颜料污渍" class=" img-responsive img-markdown" /></p>
<p dir="auto">颜料污渍已经可以显示在Tilemap上并且不会超出其轮廓。</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/qXpfbn3GNMkcCFH.gif" alt="初步效果" class=" img-responsive img-markdown" /></p>
<h2>喷溅</h2>
<p dir="auto">添加一把玩具水枪，简单写一个发射颜料子弹的逻辑，子弹爆开后围绕当前位置随机生成多个颜料污渍预制体。颜料污渍预制体用一张1像素的白色图片，通过缩放来显示出不同大小的污渍。子弹中设置多种颜色随机应用到子弹与污渍的SpriteRenderer。</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/zNTS7keiQ8vLIOA.png" alt="子弹" class=" img-responsive img-markdown" /></p>
<p dir="auto"><code>Stain Radius</code>为喷溅半径，<code>Stain Scale</code>为最大污渍缩放。</p>
<p dir="auto">编写StainGenerator类用来生成颜料污渍：</p>
<pre><code class="language-C#">public class StainGeneratorOld
{
    /// &lt;summary&gt;
    /// 在中心点生成向四周发散的污渍
    /// &lt;/summary&gt;
    /// &lt;param name="prefab"&gt;污渍预制体&lt;/param&gt;
    /// &lt;param name="color"&gt;颜色&lt;/param&gt;
    /// &lt;param name="position"&gt;中心点位置&lt;/param&gt;
    /// &lt;param name="direction"&gt;冲击方向&lt;/param&gt;
    /// &lt;param name="scale"&gt;污渍缩放&lt;/param&gt;
    /// &lt;param name="radius"&gt;污渍分布半径&lt;/param&gt;
    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 &lt; 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 &lt; 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&lt;SpriteRenderer&gt;().color = color;
            }
        }
    }

    public static float RandomNum(float num, float randomness)
    {
        return num + Random.Range(-num * randomness, num * randomness);
    }
}
</code></pre>
<p dir="auto">子弹与地面发生碰撞时调用，传入污渍预制体、颜色、冲击位置、冲击方向、自定义的污渍缩放与喷溅半径：</p>
<pre><code class="language-C#">private void OnCollisionEnter2D(Collision2D other) 
{
    ...
    StainGenerator.Generate(stainPrefab, color, transform.position, transform.right, stainScale, stainRadius);
}
</code></pre>
<p dir="auto">比较简单的实现就完成了，但这种不断生成Sprite的方法将导致场景里分分钟就会多出上千个游戏对象。</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/sykMWFG5CuJnvhB.gif" alt class=" img-responsive img-markdown" /></p>
<h2>优化</h2>
<p dir="auto">由于污渍的材质都一样，Unity对它们做了动态合批，但要处理的顶点数量依然没变，这里是否可以通过自定义mesh来优化，暂时没有什么头猪。</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/4HLD1fmd9GpgJa7.png" alt class=" img-responsive img-markdown" /></p>
<p dir="auto">好在对游戏对象的数量优化还是比较简单的，打在同一个位置的颜料污渍将会发生重叠，被遮挡住的污渍是不再需要的。定义一个同一位置最大可叠加层数，在创建新的污渍时，先判断当前位置共叠加了几层，如果超过允许的最大层数，则将最底层的污渍对象回收，再从对象池中取出已回收的污渍对象重复利用。</p>
<p dir="auto">这种做法的缺点是只判断重叠，而不是判断污渍是否被完全覆盖，显示效果上不太好，最大层数设置较低时会出现有些污渍还未被完全覆盖，却依然被回收了的情况。</p>
<p dir="auto">检测重叠可以使用SpriteRenderer中Bounds的Intersects方法，但每次生成都要遍历当前所有污渍对象做判断，感觉过于繁琐。最终还是选择用了Physics2D.OverlapBoxAll，这样要先在污渍预制体中添加BoxCollider2D。</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/x3OvWeru59UImAP.png" alt class=" img-responsive img-markdown" /></p>
<p dir="auto">修改StainGenerator，令它继承MonoBehaviour。叠加层数利用SpriteRenderer的Order in Layer属性实现，假设三个污渍重叠，且它们的Order in Layer分别是2、3、4，如果此时已达到了最大叠加层数，同时有新的污渍即将覆盖它们，则回收Order为2的，其余的Order减1，新的污渍Order设为4，这样依然能保持2、3、4的重叠顺序。</p>
<pre><code class="language-C#">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&lt;GameObject&gt; stainPool;
    // 临时污渍对象列表，用来记录本次生成已处理过的对象，避免重复处理
    List&lt;GameObject&gt; tempStains;

    void Awake() 
    {
        // 初始化单例和列表
        ...
    }  
    ...
}
</code></pre>
<p dir="auto">修改Generate方法，去除多余的参数，仅改动污渍对象生成部分，子弹碰撞中相应修改对其的调用。</p>
<pre><code class="language-C#">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 &lt; stainNum; i++)
        {
            ...
            // var go = Object.Instantiate(prefab);
            var go = GetStain(stainPos, stainScale, dir);   // 替换为GetStain方法
            ...
        }
    }
}
</code></pre>
<p dir="auto">编写GetStain方法。</p>
<pre><code class="language-C#">/// &lt;summary&gt;
/// 获取当前污渍对象，若当前位置发生重叠则调整回收
/// &lt;/summary&gt;
/// &lt;param name="pos"&gt;位置&lt;/param&gt;
/// &lt;param name="scale"&gt;缩放&lt;/param&gt;
/// &lt;param name="dir"&gt;朝向&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
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&lt;SpriteRenderer&gt;();
            if (spriteRenderer.sortingOrder &gt; order)
            {
                order = spriteRenderer.sortingOrder;
                // 如果即将超出最大叠加层数则直接快进到处理重叠的污渍
                if (order + 1 &gt; maxOrderInLayer)
                    break;
            }
        }
        if (order + 1 &gt; maxOrderInLayer)
        {
            // 回收最底层的污渍，其余层sortingOrder减1，并标记为已处理，避免被重复处理
            foreach (var item in cols)
            {
                spriteRenderer = item.GetComponent&lt;SpriteRenderer&gt;();
                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&lt;SpriteRenderer&gt;().sortingOrder = order;
    
    return go;
}
</code></pre>
<p dir="auto">脚本挂到场景中，设置相应值：</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/r3ZQm8yJVv4R5Lc.png" alt class=" img-responsive img-markdown" /></p>
<p dir="auto">调整之后：</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/zH1xYUA5GBKngbo.gif" alt="调整后" class=" img-responsive img-markdown" /></p>
<p dir="auto">和调整前对比：</p>
<p dir="auto"><img src="https://s2.loli.net/2023/03/03/4qm3MTsNyZAX9tp.gif" alt="调整前" class=" img-responsive img-markdown" /></p>
<p dir="auto">可以发现不再生成那么多对象了，帧数也相对稳定，但出现了上面提到的显示问题，将最大叠加层数调高基本可以解决，总之这应该不是最优的做法。</p>
<blockquote>
<p dir="auto">用到的素材：<a href="https://adamatomic.itch.io/cavernas" rel="nofollow ugc">Cavernas by Adam Saltsman</a>、<a href="https://kingkelp.itch.io/8guns" rel="nofollow ugc">8 Guns + Projectiles by KingKelpo</a></p>
</blockquote>
]]></description><link>http://designhub.top/topic/22/unity-2d颜料泼溅效果</link><generator>RSS for Node</generator><lastBuildDate>Tue, 08 Sep 2026 07:11:31 GMT</lastBuildDate><atom:link href="http://designhub.top/topic/22.rss" rel="self" type="application/rss+xml"/><pubDate>Fri, 24 Mar 2023 05:22:51 GMT</pubDate><ttl>60</ttl></channel></rss>