如何一行代码减少合批

众所周知,做游戏主要敌人之一就是渲染批次,批次越高,每帧销毁的时间可能就会越多,机器也就发热的厉害。一般的优化合批方案相信大家已经了解不少了,但是遇到类似于list,group之类的,里面有大量重复组件,但是里面又有文字、特效或者spine动画分层叠加,这时就会出现各种阻断合批,但是我们又不想改变原有逻辑和层级,或者改动起来非常麻烦。我们又该怎么做呢,这里就讨论下这种情况该怎么优化。

先看看效果:
优化前:
QQ截图20220707093447
优化后:
428c3bfdc36f1e8a37a7b4f38d17afbfa34d6faf

这只是其中一个主要模块的优化,优化后,drawcall减少了20%,之后再加上其他模块优化,保守估计总共能减少30%左右。

核心就在于需要重新排列渲染顺序,需要将组件中的重复类型一次绘制,这样就能让引擎将重复类型合批了。因此,我们需要修改ui的walk函数,这里不对引擎进行修改,直接对函数进行重写。

UI.prototype.walk = function(node: Node, level = 0) {
...
    // 判定是否需要对节点进行优化
    let data = node["__children_pref__"];
    if(data) {
        if (children.length > 0 && !node._static) {      
            // 将深度优先转换为广度优先      
            let caches: Node[][] = [];
            for (let i = 0; i < children.length; ++i) {
                let child = children[i];
                if(child["__ignore_pref__"]) {
                    this.walk(child, level);
                }else{
                    if(!child._static) {
                        if(data.level == 2) {
                            child = child.children[0];
                        }
                        for(let k=0;k<child.children.length;k++) {
                            if(!caches[k]) {
                                caches[k] = [];
                            }
                            caches[k].push(child.children[k]);
                        }
                    }
                }
            }
            
            // 这里才真正的决定渲染顺序,将先渲染同类型节点
            for(let i=0;i<caches.length;i++) {
                let arr = caches[i];
                for(let j=0;j<arr.length;j++) {
                    let oldLv = level;
                    this.walk(arr[j], level);
                    level = oldLv;
                }
            }
        }
    }else{
        // 原递归函数,不需要优化的节点继续走此分支
        if (children.length > 0 && !node._static) {
            for (let i = 0; i < children.length; ++i) {
                const child = children[i];
                this.walk(child, level);
            }
        }
    }
...
}

至此核心逻辑已经写完,接下来我们只需要暴露两个函数,就能做到一句代码进行渲染优化。

// 标记下此节点需要进行优化,
 //childLevel 表示需要到达此节点下第几级才会渲染(针对fairygui的处理,而且只处理了等于2的情况,不要再walk中进行太多的逻辑操作,
// 注意:被跳过的层级节点将不会被渲染,如有需要自行处理)
export function preformanceNodeRenderer(node: Node, childLevel = 2) {
    node["__children_pref__"] = {
        level: childLevel,
    };
}

// 标记那些节点不属于优化节点
export function ignorePreformance(node: Node) {
    node["__ignore_pref__"] = true;
}

最终项目中,只需要一句代码就能优化列表类型的批次啦。

let root = this.bg_merge.node.children[0];
preformanceNodeRenderer(root);
ignorePreformance(root.children[0]);

下图展示我们项目中的列表层级:


标有需要的层级将会在同一批次中渲染。

注意:

  1. 列表中的元素需要保持数量一直
  2. 不能改变列表中元素的顺序
  3. 同一层级的元素,需要相同图集相同材质球才能合批
  4. 列表元素间不要有重叠,否则造成层级错误

希望官方能为UI添加sortingOrder属性,这样就能通过改变值来改变渲染排序,实在不行将walk中的排序暴露出来,避免为了添加这个功能去覆盖引擎代码,造成代码浪费。

这里感谢下@ Bool Chen 在这篇帖子 性能优化1-列表渲染优化|社区征文 提供思路

45赞

战术Mark

Mark一下

mark一下

我觉得官方应该在ui上出个勾选框,勾上表示对该ui及其子元素进行合批优化

1赞

mark一下

mark一下,

:cow:

还有这操作,直接改walk…其他组件的渲染会受到影响不?

不会,没标记的节点还是走原来的代码

Mark一下

是的,仔细看了下你的代码
就像核酸检测,你把红码 黄码 绿码的列出来再排队。这样的做法挺好
就是需要给不同的人编一个码。

MultiTexture那种做法,就是十个人一批十个人一批不管红黄绿码,多纹理。
然而目前我编写了3.4版本的MultiTexture 但是在合批检测的时候遇到问题了。
就是一个材质用了十个纹理之后,第十一张纹理得重新创建一个新材质。
大佬是否有兴趣看下我的帖子,那个label的问题。
Label出现问题后,RichText也出现问题。

我都是工作的东西拿出来分享,其他的暂时没时间弄哦

哈哈哈,好的

思路不错。还是要看性能消耗。之前用的 render 属性做的。但是性能消耗要比不合批还要高。

是的。看具体需求把。在需要的地方加就行了

1赞

render属性指的是?

mark一下

这种方法的缺点就是,每帧刷新,都会触发排序,而且目标节点越复杂,越多,排序次树越多

并没有排序,顺序的,所以还好