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