【UGUI】利用CanvasRenderer.cull来实现低消耗界面显隐

用SetActive或者设置enabled隐藏UI界面的缺陷很多人都讲过了,无非是"无谓的操作"和GC Alloc的问题。我这里多说一句,设置enabled最要命的地方其实是,UI基类Graphic会在OnEnabled,OnDisable,还有OnTransformParentChanged时执行SetAllDirty(),这个操作几乎等同于全部重建。我并不理解这样做的理由,虽然在禁用状态下无法接受Mono事件,无法根据属性变化更新界面,重新激活的时候确实需要一次ReBuild,但并不需要直接设置脏标记啊,这东西不是在具体属性变化的时候才设置的吗?而在没有脏标记的时候ReBuild只是一次空循环,虽然Canvas.BuildBatch的消耗跑不掉,Canvas.sendWillrenderCanvas起码不会多,当大量界面同时激活时,后者的耗时才是大头。
(Canvas.sendWillrenderCanvas是一个事件,它上面挂载了CanvasUpdateRegistry.PerformUpdate,会根据属性变化重新处理Image的网格并更新CanvasRenderer,而Canvas.BuildBatch指的是Canvas根据全部CanvasRenderer进行网格合并的C++部分。前者只更新变化,但效率低,后者必须处理全部,但效率高。之后我说的“重建”都指的前者。)

这里要专门提一句,不仅仅是OnEnabled,OnTransformParentChanged也会执行几乎相同的内容,主要的优点也就是少了一次GetCompent<Mask>()的GC Alloc,界面还是会重建,将界面移出RootCanvas这种方案优化幅度并不大。有差距大的说法,有可能是在编辑器里拖动层次的时Profiler显示的假象。不要拖拽,要用代码来修改transform的parent,至少我这没看出两者明显的区别,而且从源码分析两者有差异是不可思议的。
对于用Canvas包裹的界面而言,直接禁用Canvas组件反而是一个办法,它虽然会ReBuild,但不会让子级设置脏标记,就还OK,虽然是有一点消耗,但比前面的方法强。最彻底的做法是设置UI摄像机,并设置要隐藏Canvas的Layer让摄像机Cull掉(注意UGUI是以Canvas为渲染单位的,设置单独UI组件的Layer无效),这才是真正的零消耗。再把GraphicsRaycaster一并禁用就好了。
注意这种做法对Canvas内的Canvas是同样有效的。

 

但这都是针对“一个窗体”这样的界面而言的,如果我只是想暂时隐藏某个UI上的图片呢?

其实只要设置image.canvasRenderer.cull = true就可以了

 

当然仅仅是这样我也不会单独拿一篇文章来讲这个事。

 

CanvasRenderer.cull其实是用来配合ugui自己的RectMask2D(注意不是Mask)的功能,本身是没打算让我们来操作的,只是碰巧写成了public而已。但只要你这个Image不在RectMask2D内,完全可以把这个属性当作visible随便使用。

然而如果你就是想用RectMask2D,想利用它的区域裁剪功能(Mask不会裁剪区域外的UI对象,有多少就会渲染多少,而RectMask2D则只会渲染内部的元素),不愿意用Mask,那就……

不好办了,因为RectMask2D也会修改这个cull的值,会覆盖你Image的设置。

 

怎么办呢,只能扩展了。

写这个东西遇到了很多private和非虚方法的问题,只能算勉强完成。还复制了一部分原始代码,搞不好UGUI更新后就会完蛋。

然而也仅仅实现了在RectMask2D下正常设置cull这一件事……个人觉得挺不值的。RectMask2D完全可以不用啊。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

namespace UnityEngine.UI
{
    [AddComponentMenu("UI/AdvancedImage", 11)]
    public class AdvancedImage : Image
    {
        [SerializeField] private bool m_Visible = true;
        public bool Visible
        {
            get
            {
                return m_Visible;
            }
            set
            {
                m_Visible = value;
                UpdateVisible();
            }
        }

        protected override void OnEnable()
        {
            base.OnEnable();
            UpdateVisible();
        }

        #region 修正占用canvasRenderer.cull属性和Mask产生的冲突
        readonly Vector3[] m_Corners = new Vector3[4];
        private Rect rootCanvasRect
        {
            get
            {
                rectTransform.GetWorldCorners(m_Corners);

                if (canvas)
                {
                    Canvas rootCanvas = canvas.rootCanvas;
                    for (int i = 0; i < 4; ++i)
                        m_Corners[i] = rootCanvas.transform.InverseTransformPoint(m_Corners[i]);
                }

                return new Rect(m_Corners[0].x, m_Corners[0].y, m_Corners[2].x - m_Corners[0].x, m_Corners[2].y - m_Corners[0].y);
            }
        }

        private bool m_IsCull;
        public override void Cull(Rect clipRect, bool validRect)
        {
            if (!canvasRenderer.hasMoved)
                return;

            var cull = !validRect || !clipRect.Overlaps(rootCanvasRect, true);
            var cullingChanged = m_IsCull != cull;
            m_IsCull = cull;
            UpdateVisible();

            if (cullingChanged)
            {
                onCullStateChanged.Invoke(cull);

                //SetVerticesDirty();
                //重新移回屏幕执行SetVerticesDirty会导致重建。重建确实是必要的,因为在Mask外任何属性修改都不会导致ReBuild,移入后需要执行一次让界面更新。
                //但是,仅仅是移出Mask是不应该标记Vertices变动的,根本没变啊,所以换成下面这句。这样仅仅是进出Mask就不会重建Mesh了。没看出有啥区别。
                if (IsActive()) CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
            }
        }

        public override void SetClipRect(Rect clipRect, bool validRect)
        {
            base.SetClipRect(clipRect, validRect);
            if (!validRect)
            {
                m_IsCull = false;//移出Mask需要手动清除Cull标记
            }
        }

        public override void RecalculateClipping()
        {
            base.RecalculateClipping();
            UpdateVisible();
        }

        protected override void OnTransformParentChanged()
        {
            base.OnTransformParentChanged();
            if (!isActiveAndEnabled)
                return;

            UpdateVisible();
        }
        #endregion

        public void UpdateVisible()
        {
            canvasRenderer.cull = m_IsCull || !m_Visible;
        }

#if UNITY_EDITOR
        protected override void OnValidate()
        {
            base.OnValidate();
            UpdateVisible();
        }
#endif
    }
}

 

嗯……对应编辑器代码。不过编辑器内直接点属性界面会强制SetAllDirty(),所以用属性界面改visible是看不到性能和以前的区别的……你得自己写代码改visible。

using UnityEngine;
using UnityEngine.UI;
using UnityEditor;
using System.Collections;

namespace UnityEditor.UI
{
    [CustomEditor(typeof(AdvancedImage), false)]
    [CanEditMultipleObjects]
    public class AdvancedImageEditor : ImageEditor
    {
        SerializedProperty m_Visible;
        protected override void OnEnable()
        {
            base.OnEnable();
            
            m_Visible = serializedObject.FindProperty("m_Visible");
        }

        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();
            EditorGUILayout.PropertyField(m_Visible);
            serializedObject.ApplyModifiedProperties();
        }
    }
}

 

待会写一个完整的扩展Image好了,有一些更有意义的东西可以做。

 

……然而这个没卵用的东西我都不想留下了,不用RectMask2D不就好了么?现在都是循环列表,哪有什么Mask外的内容。而且这个RectMask2D,即使我已经改了注释那里的代码,避免了Canvas.sendWillrenderCanvas的执行,滚动时还是会导致不必要的Canvas.BuildBatch执行啊。

至于Text和RawImage的版本???

 

 

——————————————————

 

今天再翻了下代码,个人认为UGUI设计者非要在OnEnabled的时候每次重建界面,也就两个理由:

1.最开始的时候CanvasRenderer没有cull属性,必须清空CanvasRenderer才能做到隐藏界面的效果。清空后恢复就只能重建。

2.认为被禁用的CanvasRenderer一直留着不显示的Mesh和Material是在浪费内存。

 

这种做法是否合理,相信大家心里都有数。

然而,Cull方法那里的SetDirty则直接是一个BUG了,因为这次它隐藏的时候并没有清空CanvasRenderer,重建是一丁点意义都没有的行为,用CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this)代替才是正确的写法。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值