开头
去年底的时候,由于希望在性能优化方面做一些研究,在论坛到处找文章,找到了一些很有启发的思路,其中包括江南百景图的技术分享文章。目前均已实现,正在尝试产出文章。后期也会运用到目前的项目中。
性能优化系列会有多篇文章,目前规划如下(不分先后):
- 地图物件多图渲染合批
- 地图图层Share Culling
- 材质颜色去除
- 分帧寻路
- 列表渲染优化
其中,除了列表渲染优化以外,都源于江南百景图的分享文章。
本文提及的技术优化未投入实际开发环境,使用前请自行斟酌。
词语缩写对照
层级渲染:指本文提出的渲染方式。
层级优先:指本文提出的子节点遍历顺序。
优化简介
问题
关于列表优化的常见方法是循环列表,此方法可以大量减少对象数量以及渲染数量,已经可以达到非常不错的优化效果了。但如果列表项内容比较复杂,图片还是不同图集,一个列表可能要占掉几十个dc,突然就觉得不能忍了。
为什么会这样呢?cocos creator的渲染流程中,会先渲染父节点,然后逐个渲染子节点。简单讲:深度优先。这在渲染列表的时候有个问题,设列表子项的节点为N1、N2,每个列表项有三个子节点,子节点各有一个Sprite组件,设子节点们分别为C1_1、C1_2、C1_3、C2_1、C2_2、C2_3。
按照默认的渲染顺序是这样的:N1->C1_1->C1_2->C1_3->N2->C2_1->C2_2->C2_3。
若Sprite的SpriteFrame分别归属不同图集,则需要6次DC(2 * 3),即若有n个子项,需要3n次DC。但是C1_1和C2_1明明是一个图集,却被其他子节点打断了。
如何处理
如果能够跨列表项,将同层级的子节点一起渲染,不就可以避免这个问题了吗?能不能渲染顺序改成N1->N2->C1_1->C2_1->C1_2->C2_2->C1_3->C2_3,这样就只要3次DC,重要的是,不论有多少列表项,都只需要3次DC!
即按照蓝->棕->浅蓝->绿的顺序渲染。
可以做到吗?(废话不然写屁文章)
优化效果
如图,创建一千个列表项,每个列表项包含三个子节点,每个子节点一个Sprite。
默认渲染下需要1001次dc,多的1次dc是左下角的监视器。优化后只需要4次dc。
当然,实际情况下并不会有一千个列表项。但这里是为了明显地体现出差距。
开发环境
浏览器:Chrome
开发语言:JavaScript
引擎版本:CocosCreator 2.4.3
实现思路
最简单的做法:替换引擎中渲染流相关代码,将深度优先遍历改为层级优先遍历即可。
但这样会影响游戏中所有节点的渲染。所以我们需要一个稍稍复杂一点的方案。
稍稍复杂一点:标记出特殊的列表节点,修改引擎中的相关代码,支持层级优先遍历。
在进行层级遍历时,需要分类每个层级的节点,才能在遍历完之后,进而遍历子节点。这里有两个方案:
- 一开始就记住所有的节点(包括子节点),根据层级分类,渲染时直接读取即可。
- 在渲染时才进行遍历。
各有优缺点,方案1由于缓存在渲染时会更快速,但在列表项进行了增加/删除时,需要同步更新列表,相反地,方案2慢一些但不需要维护列表。
本文采用方案1。
代码
代码分为两部分:
- 创建一个RenderList组件,用于标识层级渲染节点,并缓存子节点数组。
- 修改渲染流函数children,支持层级渲染。
RenderList.js
cc.Class({
extends: cc.Component,
properties: {
},
onEnable () {
this.refreshChildNodeInfo();
},
/**
* 将节点的所有子节点按层级优先展开,并缓存到this.node._renderChildren
*/
refreshChildNodeInfo () {
const levels = [];
let level = 0;
const dfs = function (node) {
levels[level] = levels[level] || [];
levels[level].push(node);
if (node.children.length) {
for (let child of node.children) {
dfs(child, ++level);
}
}
}
for (let child of this.node.children) {
dfs(child);
level = 0;
}
this.node._renderChildren = [].concat(...levels);
},
});
render-flow.js
_proto._children = function (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 ? OPACITY_COLOR : 0;
let worldDirtyFlag = worldTransformFlag | worldOpacityFlag;
// 修改开始
// 子节点列表 是否为层级渲染
let children, isHierarchyRenderList;
if (node._renderChildren) {
isHierarchyRenderList = true;
children = node._renderChildren;
} else {
children = node._children;
}
// 修改结束
for (let i = 0, l = children.length; i < l; i++) {
let c = children[i];
// 修改开始
// 取消子节点的chilren渲染 重新计算透明度值
if (isHierarchyRenderList) {
c._renderFlag &= ~CHILDREN;
opacity = c.parent._opacity / 255;
}
// 修改结束
// 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;
_cullingMask = c._cullingMask = c.groupIndex === 0 ? cullingMask : 1 << c.groupIndex;
// 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;
// 修改开始
// 恢复子节点的children渲染
if (isHierarchyRenderList) {
c._renderFlag |= CHILDREN;
}
// 修改结束
}
batcher.parentOpacity = parentOpacity;
this._next._func(node);
};
当节点是层级渲染节点时,获取缓存的子节点列表,然后遍历就完事了。
需要注意的是,当子节点层级超过1的时候,如:列表项/图片1/图片2,此时图片2的透明度值是不准确的,因为此时透明度计算法是列表项的透明度图片2的透明度,跳过了图片1。将其改为图片2的父节点透明度图片2的透明度就好了。
至于要取消子节点的children渲染的原因,是因为我们会在这里将所有子节点全部遍历一遍,所以也不需要本来的children渲染了。
另外
由于render-flow.js代码位于core文件夹内,所以编译起来有点麻烦,我其实没有做这一步,只修改了cocos2d-js-for-preview.js验证。理论上,你可以找到render-flow.js的位置,并修改对应函数代码如下:
_proto._children = function(node) {
var cullingMask = _cullingMask;
var batcher = _batcher;
var parentOpacity = batcher.parentOpacity;
var opacity = batcher.parentOpacity *= node._opacity / 255;
var worldTransformFlag = batcher.worldMatDirty ? WORLD_TRANSFORM : 0;
var worldOpacityFlag = batcher.parentOpacityDirty ? OPACITY_COLOR : 0;
var worldDirtyFlag = worldTransformFlag | worldOpacityFlag;
var children, isRenderList;
// 修改开始
if (node._renderChildren) {
isRenderList = true;
children = node._renderChildren;
} else {
children = node._children;
}
// 修改结束
for (var i = 0, l = children.length; i < l; i++) {
var c = children[i];
// 修改开始
if (isRenderList) {
c._renderFlag &= ~CHILDREN;
opacity = c.parent._opacity / 255;
}
// 修改结束
c._renderFlag |= worldDirtyFlag;
if (!c._activeInHierarchy || 0 === c._opacity) continue;
_cullingMask = c._cullingMask = 0 === c.groupIndex ? cullingMask : 1 << c.groupIndex;
var colorVal = c._color._val;
c._color._fastSetA(c._opacity * opacity);
flows[c._renderFlag]._func(c);
c._color._val = colorVal;
// 修改开始
if (isRenderList) {
c._renderFlag |= CHILDREN;
}
// 修改结束
}
batcher.parentOpacity = parentOpacity;
this._next._func(node);
};
总结
本文提供的方案仍然有一些缺点,比如:
- 列表项的子节点的数量、节点树位置需要一致。
也有一些进一步优化的空间,比如:
- 方案可以改成:新建一个渲染流函数(如flow._children2),避免在普通节点的children函数中还要一直去判断是否有层级渲染的标记。
注:本文思路源自于论坛中某个帖子,但写文章时没找到= =…该文章中实现了一个新的渲染流函数。没法附链接只好写在这里表示感谢。
本文章也发布于本人语雀文档