Design Hub

    • 注册
    • 登录
    • 搜索
    • 版块
    • 最新

    [Unity]2D颜料泼溅效果

    游戏开发
    1
    1
    229
    正在加载更多帖子
    • 从旧到新
    • 从新到旧
    • 最多赞同
    回复
    • 在新帖中回复
    登录后回复
    此主题已被删除。只有拥有主题管理权限的用户可以查看。
    • Pamisu
      Pamisu 管理员 最后由 编辑

      做一个类似于《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

      颜料污渍:

      颜料污渍

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

      初步效果

      喷溅

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

      子弹

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

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

      public class StainGeneratorOld
      {
          /// <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 Saltsman、8 Guns + Projectiles by KingKelpo

      1 条回复 最后回复 回复 引用
      • 1 / 1
      • First post
        Last post
      版权所有 ©2023 Design Hub 保留所有权利
      鄂ICP备2021004164号-2