记一次Unity性能优化

Before you make any changes, you must profile your application to identify the cause of the problem. If you attempt to solve a performance problem before you understand its cause, you might waste your time or make the problem worse. ——unity文档
在改代码之前,你需要先分析你的应用来定位问题的根源。如果你没有理解问题根源所在就去解决它,你可能会浪费时间甚至让问题变得更糟糕。

问题简介

开发过程中加入了一个模块后,可以感知到游戏渲染变得卡顿。于是开始分析导致卡顿的原因。首先打开The Rendering Statistics window简单分析一下问题。

可以看到本次问题导致了FPS缩水为原来的1/3。 CPU主线程一帧的时间从5ms变为15ms。渲染线程因为需要等待主线程的指令测得的耗时也变长。
而渲染相关的数据没有太多改变,可以初步确定问题位于主线程。

问题分析与改进

接着我们通过Window > Analysis > Profiler.打开分析窗口,勾选CPU Usage

可以看到SpriteMerge.CreateSprite函数是引起性能变慢的主要原因。它消耗了13.87ms,分配了136.4KB的内存。

点击显示SpriteMerge.Create代码


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

public class SpriteMerge : MonoBehaviour
{
    public SpriteRenderer spriteRenderer;// assumes you've dragged a reference into this
    public Action OnMainCharacterSpriteUpdate = delegate{};

    // Use this for initialization
    void Start()
    {
        spriteRenderer = GetComponent();
        if(spriteRenderer == null)
            spriteRenderer = this.gameObject.AddComponent();
    }

    public void Update()
    {
        spriteRenderer.sprite = Create(this.transform);
        OnMainCharacterSpriteUpdate(spriteRenderer.sprite);
    }

    /* Takes a transform holding many sprites as input and creates one flattened sprite out of them */
    public Sprite Create( Transform input)
    {
        var spriteRendererList = input.GetComponentsInChildren().ToList();
        if (spriteRendererList.Count == 0)
        {
            Debug.Log("No SpriteRenderers found in " + input.name + " for SpriteMerge");
            return null;
        }
        spriteRendererList.Sort((sr1,sr2)=>sr1.sortingOrder.CompareTo(sr2.sortingOrder));
        var spriteList = new List();
        foreach(var spriteR in spriteRendererList)
        {
            var sprite = spriteR.sprite;
            if (sprite == null)
                continue;
            if (spriteR.gameObject == this.gameObject)
                continue;

            spriteList.Add(sprite);
        }

        var size = CalculateSize(spriteList,out Vector2Int pivot);
        if(Input.GetKeyDown(KeyCode.F1))
            Debug.Log(size);

        return CreateSprite(spriteList, size, pivot);
   }

    private Vector2Int CalculateSize(List spriteList, out Vector2Int pivot)
    {
        UnityEngine.Profiling.Profiler.BeginSample("SpriteMerge CalculateSize");

        if(spriteList.Count == 0)
        {
            pivot = Vector2Int.zero;
            return Vector2Int.zero;
        }

        int minX = int.MaxValue;
        int maxX = int.MinValue;
        int minY = int.MaxValue;
        int maxY = int.MinValue;

        int pivotX = 0;
        int pivotY=0;
        foreach(var sprite in spriteList)
        {
            var minVec =  -sprite.pivot;
            if(minVec.x < minX)
            {
                pivotX = (int)sprite.pivot.x;
                minX = (int)minVec.x;
            }
            if(minVec.y < minY)
            {
                pivotY = (int)sprite.pivot.y;
                minY = (int)minVec.y;
            }

            var maxVec = sprite.rect.size - sprite.pivot;
            maxX = (int)Mathf.Max(maxX, maxVec.x);
            maxY = (int)Mathf.Max(maxY, maxVec.y);
        }

        var result = new Vector2Int(maxX-minX,maxY-minY);
        pivot = new Vector2Int(pivotX, pivotY);

        UnityEngine.Profiling.Profiler.EndSample();
        return result;
    }

    private Sprite CreateSprite(List spriteList, Vector2Int size, Vector2Int pivotPixel)
    {
        UnityEngine.Profiling.Profiler.BeginSample($"SpriteMerge CreateSprite");
        var pivoteFloat = ((Vector2)pivotPixel) / size;
        var targetTexture = new Texture2D(size.x, size.y, TextureFormat.RGBA32, false, false);
        targetTexture.filterMode = FilterMode.Point;

        var targetPixels = targetTexture.GetPixels();

        var fillColor = new Color(0, 0, 0, 0);
        for(int i = 0; i < targetPixels.Count();++i)
        {
            targetPixels[i] = fillColor;
        }
        targetTexture.SetPixels(targetPixels);

        foreach(var sprite in spriteList)
        {
            UnityEngine.Profiling.Profiler.BeginSample("SpriteMerge CreateSprite 1 sprite");
            var offsetPixel = pivotPixel - new Vector2Int((int)sprite.pivot.x, (int)sprite.pivot.y);

            var spriteSize = sprite.rect.size;

            for(int i = 0;i< (int)spriteSize.x;i++)
            {
                for(int j = 0; j < (int)spriteSize.y;j++)
                {

                    int x = (int)sprite.rect.x + i;
                    int y = (int)sprite.rect.y + j;

                    UnityEngine.Profiling.Profiler.BeginSample("SpriteMerge CreateSprite GetPixel");
                    var color = sprite.texture.GetPixel(x, y);
                    UnityEngine.Profiling.Profiler.EndSample();

                    //避免透明的像素覆盖之前的颜色
                    if (color.a == 0)
                        continue;

                    UnityEngine.Profiling.Profiler.BeginSample("SpriteMerge CreateSprite SetPixel");
                    targetTexture.SetPixel(i + offsetPixel.x, j + offsetPixel.y, color);
                    UnityEngine.Profiling.Profiler.EndSample();
                }
            }
            UnityEngine.Profiling.Profiler.EndSample();
        }

        targetTexture.Apply(false, true);// read/write is disabled in 2nd param to free up memory
        var result =  Sprite.Create(targetTexture, new Rect(new Vector2(), size), pivoteFloat, 100, 0, SpriteMeshType.FullRect);

        UnityEngine.Profiling.Profiler.EndSample();
        return result;
    }
}

其中Self ms占用最多的标记分别是GetPixel 6.22ms,1 Sprite 5.00ms, SetPixel 2.06ms。

这里我们分别观察对应的代码。

  foreach(var sprite in spriteList)
  {
      //self cost 5.00ms
      UnityEngine.Profiling.Profiler.BeginSample("SpriteMerge CreateSprite 1 sprite");
      var offsetPixel = pivotPixel - new Vector2Int((int)sprite.pivot.x, (int)sprite.pivot.y);

      var spriteSize = sprite.rect.size;

      for(int i = 0;i< (int)spriteSize.x;i++)
      {
          for(int j = 0; j < (int)spriteSize.y;j++)
          {
              int x = (int)sprite.rect.x + i;
              int y = (int)sprite.rect.y + j;
              
              //self cost 6.22ms
              UnityEngine.Profiling.Profiler.BeginSample("SpriteMerge CreateSprite GetPixel");
              var color = sprite.texture.GetPixel(x, y);
              UnityEngine.Profiling.Profiler.EndSample();

              //避免透明的像素覆盖之前的颜色
              if (color.a == 0)
                  continue;

              //self cost 2.06ms
              UnityEngine.Profiling.Profiler.BeginSample("SpriteMerge CreateSprite SetPixel");
              targetTexture.SetPixel(i + offsetPixel.x, j + offsetPixel.y, color);
              UnityEngine.Profiling.Profiler.EndSample();
          }
      }
      UnityEngine.Profiling.Profiler.EndSample();
  }

1,3两个标记的代码比较简单,就是一行函数调用。 第2个标记,有着两层循环,很可能是循环次数太多,起到了放大的作用。因此我们这里首先对循环进行优化。

优化1: float到int的强转放到循环外。时间开销占比从22%降低到14.4%。

这里我们就会继而想去优化GetPixel的开销。
优化2: 通过使用另一个GetPixels函数,一次性获得所有的像素。

        foreach(var sprite in spriteList)
        {
            UnityEngine.Profiling.Profiler.BeginSample("SpriteMerge CreateSprite 1 sprite");
            var offsetPixel = pivotPixel - new Vector2Int((int)sprite.pivot.x, (int)sprite.pivot.y);

            var spriteSize = sprite.rect.size;

            //优化1
            int spriteSizeX = (int)spriteSize.x;
            int spriteSizeY = (int)spriteSize.y;

            int spriteRectX = (int)sprite.rect.x;
            int spriteRectY = (int)sprite.rect.y;

            //优化2
            var pixels = sprite.texture.GetPixels(spriteRectX, spriteRectY, spriteSizeX, spriteSizeY);
            for(int i = 0;i< spriteSizeX; i++)
            {
                for(int j = 0; j < spriteSizeY; j++)
                {
                    UnityEngine.Profiling.Profiler.BeginSample("SpriteMerge CreateSprite GetPixel");
                    var index = i + j * spriteSizeX;
                    var color = pixels[index];
                    UnityEngine.Profiling.Profiler.EndSample();

                    //避免透明的像素覆盖之前的颜色
                    if (color.a == 0)
                        continue;

                    UnityEngine.Profiling.Profiler.BeginSample("SpriteMerge CreateSprite SetPixel");
                    targetTexture.SetPixel(i + offsetPixel.x, j + offsetPixel.y, color);
                    UnityEngine.Profiling.Profiler.EndSample();
                }
            }
            UnityEngine.Profiling.Profiler.EndSample();
        }

总结与展望

通过分析优化,我们将帧率从66帧提高到了100帧左右。

除了本篇文章的优化方式外,我们还可以通过缓存处理结果,以及将处理过程放在另一个线程中完成来提高帧率。

posted @ 2023-11-03 21:18  dewxin  阅读(92)  评论(0编辑  收藏  举报