【实用系列】3.x循环列表+分层渲染实现,解决item多drawcall高的问题

之前搞了个2.x版本的层级渲染,然后感觉早晚都要用3.x版本的了。所以再搞个3.x的层级渲染,后面新项目就直接用3.x的了

3.x的改起来轻松了好多,2.x的还要处理父节点进行变换时,子节点也跟着变换。

使用方式

  1. 项目启动时调用CCCExtend.ts的init方法,进行引擎代码扩展。
  2. 需要层级渲染的节点,挂上LevelRender组件即可

挂上LevelRender的节点下所有孩子节点将进行层级渲染

原理

3.x和2.x版本的渲染代码并不一样,3.x的2d渲染代码是在batcher-2d.ts里。虽说渲染代码不一样,但分层处理手法是一样的,甚至直接拷贝2.x那写好的分层方法过来用,都不需要修改。
下面看看batcher-2d.ts的部分渲染代码, 因为要子节点分层,所以,看遍历子节点的方法,也就是batcher-2d.ts里的walk方法。

    /** 遍历子节点渲染的方法 */
    public walk (node: Node, level = 0) {
        if (!node.activeInHierarchy) { //如果节点在节点树中没有被激活,则不用渲染
            return;
        }
        const children = node.children;
        const uiProps = node._uiProps;//ui的一些属性放在这里,如透明度
        const render = uiProps.uiComp as UIRenderer;//节点的渲染组件

        // Save opacity
        const parentOpacity = this._pOpacity;//父节点透明度(假设父节不透明度50%,就算子节点不透明度100%,最终显示也只有50%的不透明度)
        let opacity = parentOpacity;
        // TODO Always cascade ui property's local opacity before remove it
        const selfOpacity = render && render.color ? render.color.a / 255 : 1;
        this._pOpacity = opacity *= selfOpacity * uiProps.localOpacity;
        // TODO Set opacity to ui property's opacity before remove it
        uiProps.setOpacity(opacity);
        if (!approx(opacity, 0, EPSILON)) {
            if (uiProps.colorDirty) {
            // Cascade color dirty state
                this._opacityDirty++;
            }

            // Render assembler update logic
            if (render && render.enabledInHierarchy) { //开始填充渲染数据
                render.fillBuffers(this);// for rendering
            }

            // Update cascaded opacity to vertex buffer
            if (this._opacityDirty && render && !render.useVertexOpacity && render.renderData && render.renderData.vertexCount > 0) {
            // HARD COUPLING
                updateOpacity(render.renderData, opacity);
                const buffer = render.renderData.getMeshBuffer();
                if (buffer) {
                    buffer.setDirty();
                }
            }

            //开始遍历子节点,进行递归渲染,这部分就是重点修改部分
            if (children.length > 0 && !node._static) {
                for (let i = 0; i < children.length; ++i) {
                    const child = children[i];
                    this.walk(child, level);
                }
            }

            if (uiProps.colorDirty) {
            // Reduce cascaded color dirty state
                this._opacityDirty--;
                // Reset color dirty
                uiProps.colorDirty = false;
            }
        }
        // Restore opacity
        this._pOpacity = parentOpacity;

        // Post render assembler update logic
        // ATTENTION: Will also reset colorDirty inside postUpdateAssembler
        if (render && render.enabledInHierarchy) { //后置渲染,像mask组件,需要等子节点渲染完毕,然后mask才能根据子节点内容,遮罩出来。
            render.postUpdateAssembler(this);
            if ((render.stencilStage === Stage.ENTER_LEVEL || render.stencilStage === Stage.ENTER_LEVEL_INVERTED)
            && (StencilManager.sharedManager!.getMaskStackSize() > 0)) {
                this.autoMergeBatches(this._currComponent!);
                this.resetRenderStates();
                StencilManager.sharedManager!.exitMask();
            }
        }

        level += 1;
    }

上述代码重点关注的部分如下

    if (children.length > 0 && !node._static) {
        for (let i = 0; i < children.length; ++i) {
            const child = children[i];
            this.walk(child, level); //遍历孩子节点,然后递归调用walk进行渲染数据处理。
        }
    }

按照层级渲染思路,遇到孩子先别急着进行渲染,先分层放进一个渲染队列(renderQueue)。然后逐个出队渲染,即可,具体可自行查看代码

注意:

  1. 写这个组件是在3.8.0版本引擎上写的,其他3.x版本不一定有用,还没进行测试。
  2. 层级渲染的子节点,mask组件只能遮罩一层,多层失效(最好不要在层级渲染下用mask组件)。
  3. 分层渲染合批需要符合合批条件。
  4. 如有bug,欢迎反馈,我这边空了会修复 :yum:

运行时截图

WX20230912-200855@2x

共7个drawcall,左下角性能分析面板1个,空scroll2个,滑动item2个,角色按钮等2个。

demo.zip (20.9 KB)

18赞

哈喽~大佬麻烦问下当我在item的预设里将label增加四个后drawcall会突然提升10个,还有是CCCExtend.ts中3.8 有部分接口已经在3.5和3.7中被废弃掉,有什么好的替换方案嘛

(帖子被作者删除,如无标记将在 24 小时后自动删除)

新增的label符合合批要求吗

找到问题了,是cacheMode使用CHAR的问题,谢谢大佬提醒

是使用char导致断批,还是说使用char解决了问题 :rofl:,而且char的使用场景是比较特殊的,例如字数使用不多,或者只有英文和数字,用这个模式就挺好。

还有就是接口废除问题,引擎3.8使用的2d渲染代码在cclegacy对象里,这个是标记废弃了,但引擎自身也还在用。如果看那个标记不爽,可以声明为any类型 :sweat_smile:

请问这个支持原生平台吗

支持的,因为这个只改了渲染顺序,是和平台无关的,只和合批条件有关。

3.7.4 测试,这里循环报错
image image

感谢反馈,确实有这个问题,已经修复,可以重新下载,或者手动修改
WX20231009-210713@2x

3.7.4 测试安卓原生合批没有生效

3.7开始,batcher原生化了,你要改c++代码才行

1赞

大佬66666mark

我之前跑过安卓原生,Label的char模式并无法合批,这个是Label原生合批的问题,即使是两个Label并排一起然后使用char模式也无法合,用bmfont倒是可以 :rofl:

(帖子被作者删除,如无标记将在 24 小时后自动删除)

@853517173 我使用3.6.3版本进行了安卓端测试,以上的修改,是无法实现分层渲染的。跟前面的一位同学说的一样,原生端要实现分层渲染,得改C++层的代码。想问下楼主,是不是有这样的尝试。

我这记错了,是2.x版本安卓可以bmfont合批,3.x的我跑了,确实没有合批,改c++目前没尝试,因为现在写的都是能外部覆盖引擎的,并不需要编译引擎啥的,这样多人协作起来方便。至于这个合批,感觉让引擎组去处理更好,处理好了,自然就能合批了 :rofl:

感觉我们制作游戏的人,特别关心drawcall的优化,想了各种方法。不知道引擎组太忙呢,还是有别的限制,就是不官方将比较好的方案集成到引擎中。

3.x版本, 使用相同bmfont的lable也可以合批啊。官方全平台支持的。

大佬遇到bug了,当滑动的时候数据由多变少,会出现位置偏移或者所有item全部消失的情况image image 还是说我这边切换的时候使用方法不对呢?测试的四个按钮分别对应以下的4个方法