前言:
最近项目一直在做优化,然后发现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
{
/// <summary>
/// Base class for all UI components that should be derived from when creating new Graphic types.
/// </summary>
[DisallowMultipleComponent]
[RequireComponent(typeof(CanvasRenderer))]
[RequireComponent(typeof(RectTransform))]
[ExecuteInEditMode]
public abstract class Graphic
: UIBehaviour,
ICanvasElement
{
//……
[NonSerialized] private CanvasRenderer m_CanvasRender;
//……
/// <summary>
/// UI Renderer component.
/// </summary>
public CanvasRenderer canvasRenderer
{
get
{
if (m_CanvasRender == null)
m_CanvasRender = GetComponent<CanvasRenderer>();
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<Canvas>();
return m_Canvas;
}
}
[NonSerialized] private List<Graphic> m_RaycastResults = new List<Graphic>();
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
if (canvas == null)
return;
var canvasGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
if (canvasGraphics == null || canvasGraphics.Count == 0)
return;
int displayIndex;
var currentEventCamera = eventCamera; // Propery can call Camera.main, so cache the reference
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay || currentEventCamera == null)
displayIndex = canvas.targetDisplay;
else
displayIndex = currentEventCamera.targetDisplay;
var eventPosition = Display.RelativeMouseAt(eventData.position);
if (eventPosition != Vector3.zero)
{
int eventDisplayIndex = (int)eventPosition.z;
if (eventDisplayIndex != displayIndex)
return;
}
else
{
eventPosition = eventData.position;
}
// Convert to view space
Vector2 pos;
if (currentEventCamera == null)
{
float w = Screen.width;
float h = Screen.height;
if (displayIndex > 0 && displayIndex < Display.displays.Length)
{
w = Display.displays[displayIndex].systemWidth;
h = Display.displays[displayIndex].systemHeight;
}
pos = new Vector2(eventPosition.x / w, eventPosition.y / h);
}
else
pos = currentEventCamera.ScreenToViewportPoint(eventPosition);
if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f)
return;
float hitDistance = float.MaxValue;
Ray ray = new Ray();
if (currentEventCamera != null)
ray = currentEventCamera.ScreenPointToRay(eventPosition);
if (canvas.renderMode != RenderMode.ScreenSpaceOverlay && blockingObjects != BlockingObjects.None)
{
float distanceToClipPlane = 100.0f;
if (currentEventCamera != null)
{
float projectionDirection = ray.direction.z;
distanceToClipPlane = Mathf.Approximately(0.0f, projectionDirection)
? Mathf.Infinity
: Mathf.Abs((currentEventCamera.farClipPlane - currentEventCamera.nearClipPlane) / projectionDirection);
}
}
m_RaycastResults.Clear();
Raycast(canvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults);
int totalCount = m_RaycastResults.Count;
for (var index = 0; index < totalCount; index++)
{
var go = m_RaycastResults[index].gameObject;
bool appendGraphic = true;
if (ignoreReversedGraphics)
{
if (currentEventCamera == null)
{
var dir = go.transform.rotation * Vector3.forward;
appendGraphic = Vector3.Dot(Vector3.forward, dir) > 0;
}
else
{
var cameraFoward = currentEventCamera.transform.rotation * Vector3.forward;
var dir = go.transform.rotation * Vector3.forward;
appendGraphic = Vector3.Dot(cameraFoward, dir) > 0;
}
}
if (appendGraphic)
{
float distance = 0;
if (currentEventCamera == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay)
distance = 0;
else
{
Transform trans = go.transform;
Vector3 transForward = trans.forward;
distance = (Vector3.Dot(transForward, trans.position - currentEventCamera.transform.position) / Vector3.Dot(transForward, ray.direction));
if (distance < 0)
continue;
}
if (distance >= hitDistance)
continue;
var castResult = new RaycastResult
{
gameObject = go,
module = this,
distance = distance,
screenPosition = eventPosition,
index = resultAppendList.Count,
depth = m_RaycastResults[index].depth,
sortingLayer = canvas.sortingLayerID,
sortingOrder = canvas.sortingOrder
};
resultAppendList.Add(castResult);
}
}
}
/// <summary>
/// Perform a raycast into the screen and collect all graphics underneath it.
/// </summary>
[NonSerialized] static readonly List<Graphic> s_SortedGraphics = new List<Graphic>();
private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, IList<Graphic> foundGraphics, List<Graphic> results)
{
int totalCount = foundGraphics.Count;
Graphic upGraphic = null;
int upIndex = -1;
for (int i = 0; i < totalCount; ++i)
{
Graphic graphic = foundGraphics[i];
int depth = graphic.depth;
if (depth == -1 || !graphic.raycastTarget || graphic.canvasRenderer.cull)
continue;
if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera))
continue;
if (eventCamera != null && eventCamera.WorldToScreenPoint(graphic.rectTransform.position).z > eventCamera.farClipPlane)
continue;
if (graphic.Raycast(pointerPosition, eventCamera))
{
s_SortedGraphics.Add(graphic);
if (depth > upIndex)
{
upIndex = depth;
upGraphic = graphic;
}
}
}
if (upGraphic != null)
results.Add(upGraphic);
}
#endregion
经过此优化之后,应该能减少depth和canvasRenderer的调用。效果如下:
可见将其调用次数有较多的下降(477+477)--->(419+151),从而使耗时降低了不少。
3、get_canvas的优化
后面发现,在获取Canvas居然也这么耗时……真是郁闷。
仔细看了下性能,是因为在做缓存的Canvas==null的时候耗时1.28ms。我寻思着,这玩意在我们项目中从来没改过,何必一直判空呢?
所以干脆就在Awake里面给他赋值,干脆就不判空了算了。
代码修改如下:
protected override void Awake()
{
canvas= GetComponent<Canvas>();
}
private Canvas canvas;
优化效果:
可见其中给Canvas判空的消耗已经没有了。
后面的工作还是主要针对YHGraphicRaycaster.Raycast进行优化。毕竟他真用了最多的时间,将近一半了。
待续:
还有一部分对EventSystem的优化,在 这里 。
因为写在一篇里太长了,所以新开了一篇文章。经过第二次优化之后cpu耗时能下降到2ms左右,可喜可贺~~
后来又新增了优化的第三部分:https://blog.csdn.net/cyf649669121/article/details/86484168
————————————————
版权声明:本文为CSDN博主「魔术师Dix」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cyf649669121/article/details/83661023