UI批量渲染优化

众所周知,游戏中的背包是drawcall的灾难地区,主要原因是背包物品数值和不同状态的图片打断了合批渲染。已知cocos creator的最新版能让不同颜色和不同Type的Sprite也合批渲染。对于我们项目已经不好再升级引擎,BMPfont的使用也感觉不是很好。我们游戏的背包一打开就是200以上的drawcall,这么高的drawcall在微信小游戏上虽然不会导致卡顿,但是长时间打开背包游戏发烫会比较严重,一时也不知道如何优化。
某次参加cocos creator的沙龙,panda老师提了一个解决方案:将不同渲染的阶段的节点放在不同的content上,也就是说一个物品的详细信息节点比如背景和和物品数量就分别放在2个content上,这2个content随着ScrollView的滑动同步滑动。
这种方案实现起来比较复杂。
最近看到cocos creator公众号的一篇文章:SLG《乱世王者》深度优化方案里面提到了ui的batch渲染,就是将同层级的ui放在一起渲染,详情见文章。

具体到cocos creator如何实现:
主要是修改引擎的render-flow.js

1、增加新的渲染阶段



2、实现_childrenBatchRender

let childrens = [];
let cqueue = [];
_proto._childrenBatchRender = function(node) {
    // 排序逻辑
    node._renderFlag &= ~CHILDREN;
    node._renderFlag |= CHILDREN_BATCH_RENDER;
    let cullingMask = _cullingMask;

    let parentOpacity = _walker.parentOpacity;
    _walker.parentOpacity *= (node._opacity / 255);

    let worldTransformFlag = _walker.worldMatDirty ? WORLD_TRANSFORM : 0;
    let worldOpacityFlag = _walker.parentOpacityDirty ? COLOR : 0;

    let config = node.config;
    childrens.length = 0;
    cqueue.length = 0;
    Array.prototype.push.apply(cqueue, node._children);
    let ch = null;
    while(cqueue.length > 0) {
        for(let i = 0, len = cqueue.length; i< len; i++) {
            ch = cqueue.shift();
            if(ch.active) {
                childrens.push(ch);
                Array.prototype.push.apply(cqueue, ch._children);
            }
        }
    }
    childrens.sort((ch1, ch2) => {
        return config[ch1._name] - config[ch2._name];
    });
    let children = childrens;
   // 下面是_children里面拷贝过来的,除了标识的代码块要加外,尽量不要改。
    for (let i = 0, l = children.length; i < l; i++) {
        let c = children[i];
        //>>>>>下面这个一定要有
        if(c.childrenCount > 0) {
            c._renderFlag &= ~CHILDREN;
        }
        //<<<<<
        if (!c._activeInHierarchy) continue;
        _cullingMask = c._cullingMask = c.groupIndex === 0 ? cullingMask : 1 << c.groupIndex;
        c._renderFlag |= worldTransformFlag | worldOpacityFlag;

        // TODO: Maybe has better way to implement cascade opacity
        c._color.a = c._opacity * _walker.parentOpacity;
        flows[c._renderFlag]._func(c);
        //>>>>>下面这个一定要有
        if(c.childrenCount > 0) {
            c._renderFlag |= CHILDREN;
        }
        //<<<<<
        c._color.a = 255;
    }
    _walker.parentOpacity = parentOpacity;
    this._next._func(node);
    _cullingMask = cullingMask;
}

3、使用方法
创建config配置每个节点对应的层级,这个config是我们自己项目背包物品item的节点层级,key就是节点名称,value就是对应的节点层级。

let config = {
    everydaySignItem : 0,
    iconItem : 1,
    item_bg : 2,
    item_check : 3,
    equip_base_0 : 4,
    icon : 5,
    iconColor : 6,
    special_effect : 7,
    image01_1 : 8,
    num_bg : 12,
    lable_num : 13,
    starLayout : 9,
    X5 : 10,
    X4 : 10,
    X3 : 10,
    X2 : 10,
    X1 : 10,
    levellayout : 9,
    _level$0 : 10,
    bangding_icon : 10,
    vipNode : 11,
    EverydaySinIconMask : 20,
    _sinedMask : 21,
    k2 : 23,
    k1 : 23,
    _bq : 22,
    bqLbl : 23,
    _sinDay$0 : 25
}

设置渲染状态标识

let content = this._listView.$ScrollView.content;
content._renderFlag |= cc.RenderFlow.FLAG_CHILDREN_BATCH_RENDER;
content.config = config;

这样就能将同层级的Sprite放在一起进行渲染。

自动计算节点的层级代码

function deapSearchConfig(node, h, config) {
    config[node.name] = h;
    if(node.childrenCount > 0){
        for(let i = 0; i < node.childrenCount; i++) {
            deapSearchConfig(node.children[i], h + 1, config);
        }
    }
}

假设有个item是要显示在ScrollView里面的,获取到这个item的预制体prefab,按照如下方式使用可获得config的内容,避免手动填写config。当然这种方式生成的config并不一定是最优的,可以自己手动再进行调整。

let config = {};
let h = 0;
let itemNode = itemPrefab.data; // 预制体的data属性里面包含了节点的详细信息
deapSearchConfig(itemNode, h, config);
cc.log(config);

注意:

  • 如果ui结构复杂,请大家谨慎使用。最好用WebGL Inspectort调试看看
  • 一定要用gulp build-html5重新生成下引擎
  • 节点中不要有富文本,富文本的节点会生成一个name为null的节点,导致节点排序不是理想情况。
  • 我这里使用的是2.0.5版本的引擎源码,如果是其他版本的引擎最好仿照自己当前引擎render-flow.js_children方法去改写_childrenBatchRender
  • 如果使用的是2.0.10版本的引擎,_childrenBatchRender里面一定要有EventManager._updateRenderOrder(c, ++_renderQueueIndex);,不然按钮事件可能会捕获不到。
  • 节点内部不能有相同名称的节点。因为排序的时候是根据名称获取节点对应的层级,如果有相同名称的节点,可能会出现层级对应不上的问题。

bug修复

  • 修复因为将子节点的CHILDREN渲染阶段清理,导致复用节点的时候渲染不到
20赞

mrak

mark

1赞

大体思路差不多,不过我是新增了一个并行渲染队列来渲染,同时在底层维护这个队列


_proto._children = function (node) {


    if (node._paralleRender) {
        return this._next._func(node);
    }

    let cullingMask = _cullingMask;
    let batcher = _batcher;

    let parentOpacity = batcher.parentOpacity;
    let opacity = (batcher.parentOpacity *= (node._opacity / 255));

    let worldTransformFlag = batcher.worldMatDirty ? WORLD_TRANSFORM : 0;
    let worldOpacityFlag = batcher.parentOpacityDirty ? COLOR : 0;
    let worldDirtyFlag = worldTransformFlag | worldOpacityFlag;

    // change by visow
    if (node._parentParalleRender) {
        let childs = node._paralleRenderNodes;
        let parallRenderFlag = node._paralleRenderFlag || 0;
        for (let i = 0; i < childs.length; i++) {
            let depths = childs[i];
            node._paralleRenderSort && depths.sort(function (a, b) {
                return a._paralleRenderDepth - b._paralleRenderDepth;
            });
            for (let j = 0; j < depths.length; j++) {
                let c = depths[j];
                // Advance the modification of the flag to avoid node attribute modification is invalid when opacity === 0.
                c._renderFlag |= worldDirtyFlag;
                c._renderFlag |= parallRenderFlag;
                if (!c._activeInHierarchy || c._opacity === 0) continue;
                // TODO: Maybe has better way to implement cascade opacity
                let colorVal = c._color._val;
                c._color._fastSetA(c._opacity * opacity);
                flows[c._renderFlag]._func(c);
                c._color._val = colorVal;
            }
        }
        node._paralleRenderSort = false;
        node._paralleRenderFlag = 0;
    }
    else {
        let children = node._children;
        for (let i = 0, l = children.length; i < l; i++) {
            let c = children[i];
            // Advance the modification of the flag to avoid node attribute modification is invalid when opacity === 0.
            c._renderFlag |= worldDirtyFlag;
            if (!c._activeInHierarchy || c._opacity === 0) continue;

            // TODO: Maybe has better way to implement cascade opacity
            let colorVal = c._color._val;
            c._color._fastSetA(c._opacity * opacity);
            flows[c._renderFlag]._func(c);
            c._color._val = colorVal;
        }
        batcher.parentOpacity = parentOpacity;
    }
    this._next._func(node);
};
4赞

mark

你这个文件 在那个位置: render-flow.js

引擎源码里面的

最开始我也是在_children方法上做判断去该,但是这样会多一个if判断,如果是增加一个渲染阶段就能去掉node._parentParalleRender。我现在的做法有个最大的问题就是每次都会排序,很浪费资源,如果能判断不需要排序就好了。我加config配置节点层级是为了最大自由的配置节点的层级。不知道你只是使用_paralleRenderDepth来判断层级,是怎么应用到每个节点的。

我在node上添加了三个属性,添加了一个并行渲染的组件,该组件所在的节点下的所有子节点的渲染顺序我将自己维护,不再使用引擎的渲染。同时将节点下的子节点按照树深度合并到同一层去渲染。同时在setparent的时候重置子节点排序标志。

mark

懂了!

mark

mark

学习了,mark

parallRenderList是自己写的?

mark

是的,就是递归节点像文件夹递归一样,将节点保存到不同的层级上去

能分享哈?

mark

请问调用childrens.sort 方法改变节点的渲染顺序后 会改变节点的父子关系吗? 比如父节点调用了放大或隐藏等方法后, 子节点是否还需要我们单独处理? content 中原来的节点结构是否被打破了?