最近项目一直在做优化,然后发现Unity的其中一个大坑,关于EventSystem的。当玩家在连续操作屏幕的时候,就会触发EventSystem.Update() -->....-->EventSystem.RaycastAll();这个RaycastAll非常耗时,每帧七八毫秒、甚至十几毫秒的情况都有。
虽然是Unity 的底层,但是还是要想办法来优化一下。
1、现状分析:
由上图我们可以看到,耗时的大头分别是:
GraphicRaycaster.get_eventCamera(); --------1.84ms
Graphic.get_canvasRenderer(); --------1.14ms+1.17ms=2.31ms
其中Graphic.get_canvasRenderer()总计调用了2次。这两个方法总计造成耗时4.15ms,占总共8.32ms的49.88%。如果能解决这两个的耗时问题,就能将这个函数的耗时减少一半,还是非常可观的。
2、优化GraphicRaycaster.get_eventCamera()
这个函数使用来获取当前UI的摄像机的,但是实际上相机都是与各个Canvas绑定的,按理来说一旦初始化之后,只要游戏内没有更改过相机,那么就应该一直是原来的相机。其实他的动态获取在兼容性上会好些,但是在性能上面是没有必要的。
优化这一个需要重写GraphicRaycaster,一般这个类会挂载在Canvas上面:
public class YHGraphicRaycaster : GraphicRaycaster
{
public Camera TargetCamera;
public override Camera eventCamera
{
get
{
if (TargetCamera == null)
{
TargetCamera = base.eventCamera;
}
return TargetCamera;
}
}
}
代码其实非常简单,然后只需要将这个YHGraphicRaycaster 挂载在Canvas下面,替换掉原来的Canvas可以了。
经过这么一堆操作,目前性能状况如下:
优化之后(YHGraphicRaycaster.get_eventCamera())剩余0.16ms,基本减少了2ms。其实可以看到其实还是有调用GraphicRaycaster.get_eventCamera(),说明还有哪里的GraphicRaycaster没有替换干净,只有再去项目里找找看在哪里。不过比较而言,原来的方法10次调用0.2ms,新方法76次调用0.16ms,区别还是很大的。
经过优化之后点击事件没有任何问题,亲测无Bug。
另:翻阅其源码:
namespace UnityEngine.UI
{
[AddComponentMenu("Event/Graphic Raycaster")]
[RequireComponent(typeof(Canvas))]
public class GraphicRaycaster : BaseRaycaster
{
//……
public override Camera eventCamera
{
get
{
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay || (canvas.renderMode == RenderMode.ScreenSpaceCamera && canvas.worldCamera == null))
return null;
return canvas.worldCamera != null ? canvas.worldCamera : Camera.main;
}
}
//……
}
}
可见其原本并没有缓存,而是每次都是去获取相机,所以是有优化空间的。
3、 优化Graphic.get_canvasRenderer()
与EventCamera不同的是,canvasRenderer在Unity的内部是有缓存的:
namespace UnityEngine.UI
{
///
/// Base class for all UI components that should be derived from when creating new Graphic types.
///
[DisallowMultipleComponent]
[RequireComponent(typeof(CanvasRenderer))]
[RequireComponent(typeof(RectTransform))]
[ExecuteInEditMode]
public abstract class Graphic
: UIBehaviour,
ICanvasElement
{
//……
[NonSerialized] private CanvasRenderer m_CanvasRender;
//……
///
/// UI Renderer component.
///
public CanvasRenderer canvasRenderer
{
get
{
if (m_CanvasRender == null)
m_CanvasRender = GetComponent();
return m_CanvasRender;
}
}
//……
}
}
所以其耗时多就是因为调用此时太多了,其实可以看到其477+477次的调用,单次0.002ms,其实和优化后的eventCamera单次调用是在一个水平上的。
所以从canvasRenderer的获取上来看,没有什么值得优化的点,值得优化的部分在于减少对此函数的调用。从源码分析来看,其实UGUI在获取到很多点击控件的时候会对其进行排序。但是我们项目的实际情况是根本不需要排序,因为只响应最顶层的UI就可以了。那些被遮住的UI干嘛还要排序呢?就算排序完了,发现不在顶层也不会响应啊。
那么如果需要UI穿透,那排序有用吗?当然也是没用的。因为这里只是每个Canvas下面的UI元素排序,在收集完各个Canvas的点击元素之后之后Unity会再一次进行排序,最后选取最上层的。所以这里的排序我认为是没有什么实际意义的,所以就取消了这里的排序,而只返回最顶层的元素。
后来想,这个会不会和手机上的多点触控有关系,不排序导致其不能多点触控了。不过后来打了安卓包试了试发现没这个问题,那这个就基本告别排序了。
将YHGraphicRaycaster增加如下部分:
#region 事件点击部分;
private Canvas m_Canvas;
private Canvas canvas
{
get
{
if (m_Canvas == null)
m_Canvas = GetComponent
经过此优化之后,应该能减少depth和canvasRenderer的调用。效果如下:
可见将其调用次数有较多的下降(477+477)--->(419+151),从而使耗时降低了不少。
3、get_canvas的优化
后面发现,在获取Canvas居然也这么耗时……真是郁闷。
仔细看了下性能,是因为在做缓存的Canvas==null的时候耗时1.28ms。我寻思着,这玩意在我们项目中从来没改过,何必一直判空呢?
所以干脆就在Awake里面给他赋值,干脆就不判空了算了。
代码修改如下:
protected override void Awake()
{
canvas= GetComponent
优化效果:
可见其中给Canvas判空的消耗已经没有了。
后面的工作还是主要针对YHGraphicRaycaster.Raycast进行优化。毕竟他真用了最多的时间,将近一半了。
还有一部分对EventSystem的优化,在 这里 。
因为写在一篇里太长了,所以新开了一篇文章。经过第二次优化之后cpu耗时能下降到2ms左右,可喜可贺~~
后来又新增了优化的第三部分:https://blog.csdn.net/cyf649669121/article/details/86484168