Unity UGUI优化:解决EventSystem耗时过长的问题 第一部分

前言:
最近项目一直在做优化,然后发现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

发布了42 篇原创文章 · 获赞 23 · 访问量 20万+

猜你喜欢

转载自blog.csdn.net/gaojinjingg/article/details/103563409