【muzzik教程】50行代码,教你优化列表draw call!【网格、横、纵 全包揽】

话不多说,上效果:

示例数据

  • 列表item数:1000
  • draw call:43~48

可以看到 draw call 一直在45左右徘徊,(FPS低是因为录屏)。

下面上代码

const {ccclass, property, menu} = cc._decorator;

/**列表draw call优化组件 */
@ccclass
@menu("tool/list_optimize")
export default class list_optimize extends cc.Component {
    /* --------------------------------segmentation-------------------------------- */
    onLoad() {
        if (!this.node.scroll_view) {
            cc.error("不存在ScrollView组件!");
            return;
        }
        // ------------------事件监听
        this.node.on("scrolling", this._event_update_opacity, this);
        this.node.scroll_view.content.on(cc.Node.EventType.CHILD_REMOVED, this._event_update_opacity, this);
        this.node.scroll_view.content.on(cc.Node.EventType.CHILD_REORDER, this._event_update_opacity, this);
    }
    /* ***************功能函数*************** */
    /**获取在世界坐标系下的节点包围盒(不包含自身激活的子节点范围) */
    private _get_bounding_box_to_world(node_o_: any): cc.Rect {
        let w_n: number = node_o_._contentSize.width;
        let h_n: number = node_o_._contentSize.height;
        let 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 _check_collision(node_o_: cc.Node): boolean {
        let rect1_o = this._get_bounding_box_to_world(this.node.scroll_view.content.parent);
        let rect2_o = this._get_bounding_box_to_world(node_o_);
        // ------------------保险范围
        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;
        return rect1_o.intersects(rect2_o);
    }
    /* ***************自定义事件*************** */
    private _event_update_opacity(): void {
        this.node.scroll_view.content.children.forEach(v1_o=> {
            v1_o.opacity = this._check_collision(v1_o) ? 255 : 0;
        });
    }
}

原理:通过检测 item 与列表可视区域的碰撞,如果碰撞到了那么即判断为在可视区域内,将 item 的 opacity 设为255,反之为0.

注:

  • 本代码使用了nodes扩展,例如 this.node.scroll_view 可以替换为 this.node.getComponent(cc.ScrollView),如对nodes感兴趣的朋友请看
    [muzzik分享]: NodeS扩展,优雅的获取节点和组件方式

  • 我该怎么使用它?将上方代码复制到新建脚本内挂载到ScrollView所在节点即可,若你未使用nodes需将部分代码替换,如:this.node.scroll_view 替换为 this.node.getComponent(cc.ScrollView)

  • 此组件将为你做什么?:在不影响其他数据 or 组件的同时为你优化列表的draw call,网格、横、纵列表全搞定,另外此组件还可优化非固定大小item的draw call


代码块讲解:

this.node.on("scrolling", this._event_update_opacity, this);
        this.node.scroll_view.content.on(cc.Node.EventType.CHILD_REMOVED, this._event_update_opacity, this);
        this.node.scroll_view.content.on(cc.Node.EventType.CHILD_REORDER, this._event_update_opacity, this);

这里监听了列表的滑动、子节点移除、子节点层级改变,一旦触发就更新展示

    /**获取在世界坐标系下的节点包围盒(不包含自身激活的子节点范围) */
    private _get_bounding_box_to_world(node_o_: any): cc.Rect {
        let w_n: number = node_o_._contentSize.width;
        let h_n: number = node_o_._contentSize.height;
        let 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;
    }

_get_bounding_box_to_world 函数的作用和注释一样,其实 cc.Node 本来就有个getBoundingBoxToWorld函数,那我为什么没有使用它呢?因为它返回的矩形范围包括了已激活的子节点大小,就是说矩形范围可能会超出当前节点的真实大小,所以我截取了这函数代码的一部分用来单独使用。

    /**检测碰撞 */
    private _check_collision(node_o_: cc.Node): boolean {
        let rect1_o = this._get_bounding_box_to_world(this.node.scroll_view.content.parent);
        let rect2_o = this._get_bounding_box_to_world(node_o_);
        // ------------------保险范围
        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;
        return rect1_o.intersects(rect2_o);
    }

_check_collision函数中上半部分很好理解,对于其中的保险范围可以理解为为了防止列表滑动过快时节点激活展示的速度跟不上,所以需要提前激活,而提前激活的办法就是扩大列表矩形的范围,这里我把宽、高分别扩大了各自的1/2,至于调整x,y是为了让其上下左右对齐,因为矩形的x,y是指其左下角,也就是左下角才是锚点,所以需要调整坐标。

13赞

理论上,这个draw call 优化,要到 4

2赞

没有设置cache mode,并且遮罩也会打断批量绘制,我刚刚实验了一下,设置cache mode后增加单个item时draw call同时增加了3,所以哪里来的七八个item的draw call为4? 你认真思考过吗?

牛逼牛逼,兄弟牛逼,先打一波牛逼,下班我再看看

大佬牛逼啊,用了你的代码一个scrollview的drawcall将近少了一半

666,也可以试试create的自动图集资源个font打包一个图集,drawcall数目3

我先记录下来了,马上优化一下我的代码

不是列表的 怎么优化

把字体弄成图集和图片打包在一起。可以实现整个dc只有4个

为什么你们要纠结这些东西?这不过是一个演示demo而已啊 :joy:,商业项目谁不优化?

demo使用的默认字体,图片是动态下载的,难道为了优化demo还要去找位图字体?去下载图片?

没有纠结,只是提供了一种思路,一种想法

这是一种方案,但实际情况(仅在我经历的项目中)用ttf字体的更多。个人觉得大多数项目用位图字体的都比较少

有这种前置条件就完成不一样了,确实不能按我那样子的来了

嗯嗯。不同情况不同优化方案嘛,很期待其他人有没更好的方法,我觉得还是可以有优化的余地的,我再研究下

还有阿,还有就是肉弹冲击,我知道的就下面这几种

  • list view, 官方示例里面,通过监听滑动动态刷新item,局限比较大,对于网格,横,纵都需要不同实现,对于不同item大小比较难优化,论坛有个这种组件代码两千行…,出了问题谁来解决?
  • 碰撞检测更新,我上面说的
  • 改变渲染顺序,使其合批
  • 使用位图+shader,你上面的方案

而碰撞检测更新是最无脑,最简单,也是最有效的一种方式

1赞

也可以弄个循环列表,创建可视范围内item数量+2行个数,往上滚动时,超出可视范围的item则移动到底部,实现复用,这样无论你1000个item还是10000个,实际上ui实例也就可视范围内数量+2行个数

2赞

6666666可以可以

mark mark