【优化列表drawcall】在项目中的实际应用及再优化

一:前言
前段时间在论坛看文档,偶然看到有个列表优化相关的文档 【muzzik教程】50行代码,教你优化列表draw call!【网格、横、纵 全包揽】,感觉实现思路不错,而且还很简单,虽然我们项目的列表优化已经用了【节点复用】+【分层渲染】的方式去优化,但是秉承着能优则优的原则,还是决定去实现一下。

二:实现原理解析
1:监听列表的滚动,监听列表子节点的变化
2:进行碰撞检测,对未与容器节点发生碰撞的节点,透明度设置为0,碰撞则设置为1

三:扩展
1:因为原有的实现使用的是子节点,没有使用包围盒,可能会存在当创建子节点使用不规范的时候(如子节点的大小为0,但是子子节点却有大小,导致的计算错误的问题),所以增加一种包围盒模式

四:优化
1:原有的列表优化实现虽然简单,但是还是有几处思路是可以继续优化的,下面就让我们来探讨这几处可以优化的点
(1):将主矩阵的包围盒计算放到整个循环之前,将计算结果当参数传递进去,这样可以有效避免循环体内部的计算量,避免重复无意义工作


(2)至于原文下面这段话,为了防止滑动过快导致渲染跟不上,从而导致提前激活的实现其实也是可以进行优化的。
优化思路大致是:增加对列表【滑动开始/滑动结束】的监听,从而判断列表是否处于滑动状态,如果处于滑动状态,则扩充检测范围,如果不处于滑动状态,则不扩充检测范围

五:附带开箱即用源码

/***

 * 列表的drawcall减少方案

 * 1:对滑动进行监听

 * 2:对超出可视区域的节点透明度设置为0

 */

export default class ScrollOpacityList

{

    //滚动列表的持有对象

    private m_stScrollView: cc.ScrollView;

    //是否使用安全模式

    private m_bSafe: boolean;

    //列表是否处于滚动状态

    private m_bIsScrolling: boolean;

    //检测的子节点的容器节点(当前节点必须在scrollview所在节点下,child所在节点上,默认为scrollview的content节点)

    private m_stChildParent: cc.Node;

    //当前列表的检测模式

    private m_stType: eScrollOpacityType;

    /**

     * 初始化节点

     * @param scrollView 滚动列表组件

     * @param childParent 如果不选择滚动列表的content,则可自定义滚动列表内的某一节点为content

     * @param eType 子节点模式

     */

    public Init(scrollView: cc.ScrollView,childParent?: cc.Node,eType?: eScrollOpacityType)

    {

        this.m_stType = eType ? eType : eScrollOpacityType.ChildSize;

        this.m_stChildParent = childParent;

        this.m_stScrollView = scrollView;

        this.UpdateOpacity();

        scrollView.node.on("scrolling",this.OnScrolling,this);

        scrollView.node.on("scroll-began",this.OnScrollBegan,this);

        scrollView.node.on("scroll-ended",this.OnScrollEnded,this);

        scrollView.content.on(cc.Node.EventType.CHILD_REMOVED,this.OnChildRemove,this);

        scrollView.content.on(cc.Node.EventType.CHILD_REORDER,this.OnChildReOrder,this);

    }

    private OnScrolling(): void

    {

        this.m_bSafe = true;

        this.m_bIsScrolling = true;

        this.UpdateOpacity();

    }

    private OnScrollBegan(): void

    {

        this.m_bSafe = true;

        this.m_bIsScrolling = true;

        this.UpdateOpacity();

    }

    private OnScrollEnded(): void

    {

        this.m_bSafe = false;

        this.m_bIsScrolling = false;

        this.UpdateOpacity();

    }

    private OnChildRemove(): void

    {

        this.m_bSafe = false;

        this.UpdateOpacity();

    }

    private OnChildReOrder(): void

    {

        this.m_bSafe = false;

        this.UpdateOpacity();

    }

    //滑动的时候非精准隐藏

    //非滑动状态下精准隐藏

    /* ***************自定义事件*************** */

    private UpdateOpacity(): void

    {

        if(!this.m_stScrollView)

        {

            return;

        }

        let rect1_o = this.GetBoundBoxToWorld(this.m_stScrollView.content.parent,false);

        //滚动的时候将范围放大有利于展示所有有利信息

        if(this.m_bIsScrolling || this.m_bSafe)

        {

            rect1_o.width += rect1_o.width * 0.5;

            rect1_o.height += rect1_o.height * 0.5;

            rect1_o.x -= rect1_o.width * 0.25;

            rect1_o.y -= rect1_o.height * 0.25;

        }

        let parent: cc.Node = this.m_stChildParent ? this.m_stChildParent : this.m_stScrollView.content;

        parent.children.forEach(v1_o =>

        {

            v1_o.opacity = this.CheckCollision(rect1_o,v1_o) ? 255 : 0;

        });

    }

    /* ***************功能函数*************** */

    /**获取在世界坐标系下的节点包围盒(不包含自身激活的子节点范围) */

    private GetBoundBoxToWorld(node_o_: any,bScrollList: boolean): cc.Rect

    {

        let w_n: number = node_o_._contentSize.width;

        let h_n: number = node_o_._contentSize.height;

        let rect_o: cc.Rect;

        //为什么scrollList固定0.5,创建的时候是以中心点为起始点,大小为原始大小,导致会往左上偏移

        if(bScrollList)

        {

            rect_o = cc.rect(

                -0.5 * w_n,

                -0.5 * h_n,

                w_n,

                h_n

            );

        }

        else

        {

            rect_o = cc.rect(

                -node_o_._anchorPoint.x * w_n,

                -node_o_._anchorPoint.y * h_n,

                w_n,

                h_n

            );

        }

        node_o_._calculWorldMatrix();

        rect_o.transformMat4(rect_o,node_o_._worldMatrix);

        return rect_o;

    }

    /**获取在世界坐标系下的节点包围盒(包含自身激活的子节点范围) */

    private GetBoundBoxToWorld2(node_o_: cc.Node): cc.Rect

    {

        return node_o_.getBoundingBoxToWorld();

    }

    /**检测碰撞 */

    private CheckCollision(rect1_o: cc.Rect,node_o_: cc.Node): boolean

    {

        let rect2_o: cc.Rect;

        switch(this.m_stType)

        {

            case eScrollOpacityType.BoundBox:

                rect2_o = this.GetBoundBoxToWorld2(node_o_);

                break;

            case eScrollOpacityType.ChildSize:

                rect2_o = this.GetBoundBoxToWorld(node_o_,false)

                break;

            case eScrollOpacityType.ScrollList:

                rect2_o = this.GetBoundBoxToWorld(node_o_,true)

                break;

        }

        return rect1_o.intersects(rect2_o);

    }

    public Destory(): void

    {

        let scrollView = this.m_stScrollView;

        if(scrollView)

        {

            scrollView.node.on("scrolling",this.OnScrolling,this);

            scrollView.node.on("scroll-began",this.OnScrollBegan,this);

            scrollView.node.on("scroll-ended",this.OnScrollEnded,this);

            scrollView.content.on(cc.Node.EventType.CHILD_REMOVED,this.OnChildRemove,this);

            scrollView.content.on(cc.Node.EventType.CHILD_REORDER,this.OnChildReOrder,this);

        }

    }

}

export enum eScrollOpacityType

{

    ScrollList = 1,//该列表是虚拟循环列表(项目设计的时候有一点问题,大家可以忽略)

    ChildSize,//正常子节点

    BoundBox//当列表的子节点异常,只能计算包围盒

}

至此,整个列表性能已经得到明显提升主要是在以下几个方面
1:列表中循环体内的计算量明显减少
2:列表真正只做到渲染可视区域内的节点

优化效果:
相对原有方案,理论上:
drawcall可以再优化100%
加载图片数量可再优化100%
计算消耗相对原来减少50%

附录:
1.这个优化思路只是讨论在原文档列表优化思路上的进一步优化,并不掺杂和讨论其它优化思路
2.本身代码设计的并不完美,思路可能还有进一步优化的点,欢迎大家留言讨论,如果觉得不错的话就点点赞吧,谢谢各位了,阿里嘎多

最后,欢迎大家关注【数字媒体与游戏开发】公众号,在这里我们可以一起讨论有意思的优化技巧,相互学习,也感谢@muzzik大佬的优化思路

9赞

自买自夸一波,这么好的方案居然没有人看到

グッド

看到了,+1

看到了,+2

看到了,+3

看到了,+4

看到了,+5

看到了,+6

嗨!老哥,我使用了你的方案,在版本v2.4.10中dc结果非常理想。感谢分享。整个组件代码不足100行…

1赞

mark +1

mark +1

好用,优化效果明显

看到了,+7

看到了。但是代码里最后的destroy里面的事件应该是off吧