前言
地图在许多游戏中都是一个重要的部分,在我的项目中跑图更是一个重要的玩法。一张超级大的地图在渲染的时候毫无疑问是个大问题,当然,Cocos已经帮我们给出了解决方案:裁剪渲染,只渲染可视范围内的图块。即根据当前屏幕的可视范围进行裁剪,算出需要渲染的格子,避免每次都是全地图渲染,以此提升性能。此功能是默认开启的,相关说明可查看文档。
去年底的时候,由于希望在性能优化方面做一些研究,在论坛找到了江南百景图研发负责人的技术分享文章,其中提到:
2. Share Culling: 《江南百景图》共有三层 TiledMap 地图层, 勾选时 将只对 TiledMap 的第一个地图层进行处理判断可视区域的范围,而其他的地图层将直接照搬第一个地图层的处理结果,这样能够节约不少性能。
本文参照文章中的思路实现了这个优化。
开发环境
浏览器:Chrome
开发语言:JavaScript
引擎版本:CocosCreator 2.4.3
词语缩写对照
分享文章:《江南百景图》的分享文章。
裁剪函数:指TiledLayer中的_updateCulling函数。
Share Culling:不同TiledLayer间,共享可视区域的裁剪计算结果。
研究过程
研究相关源码后,发现其实TiledMap不是渲染组件,TiledMap的渲染其实是通过TiledLayer实现的,对应的渲染器是TmxAssembler。
渲染时,渲染流会逐个调用TmxAssembler的fillBuffers函数进行渲染数据填充,此函数中会调用CCTiledLayer的_updateCulling函数进行可视范围,只有可视范围发生改变时才进行渲染,这也能节约性能。
问题在哪里呢?
每一帧,每一个TiledLayer都需要执行_updateCulling函数来计算可视范围。
一般来说,我们可能会移动地图的位置,但不会单独移动某个图层的位置,此时图层的可视范围是一致的。但实际上,每个图层都需要重新计算,加上裁剪函数每帧都要调用,会产生没有必要的性能浪费,多个图层其实可以只计算一次。
从实现难度上来看,只执行第一个图层的计算是比较简单的。
实现思路
综上,实现Share Culling可拆分为如下步骤:
- 记录首个TiledLayer。
- 修改TiledLayer裁剪函数,增加判断是否为首个TiledLayer。
- 是:进行裁剪计算。
- 否:取首个TiledLayer的计算结果。
实现代码解析
看完方案,肯定是要修改源码了。但由于改动的代码都在cc.TiledMap和cc.TiledLayer中,我们除了修改引擎源码,还可以通过Cocos提供的继承功能实现自己的TiledMap和TiledLayer组件,重写对应的函数实现Share Culling。由于源代码较长,以下代码中只保留了做出修改的部分。
demo中,继承TiledMap和TiledLayer的自定义组件分别命名为ShareCullingTiledMap和ShareCullingTiledLayer。
ShareCullingTiledMap
TiledMap的_buildLayerAndGroup函数用于创建TiledLayer,所以我们需要重写该函数,实现创建自定义的ShareCullingTiledLayer,并记录首个TiledLayer的功能。
_buildLayerAndGroup () {
// 此处修改
let firstTmxLayer = null;
// 修改结束
if (layerInfos && layerInfos.length > 0) {
for (let i = 0, len = layerInfos.length; i < len; i++) {
if (layerInfo instanceof cc.TMXLayerInfo) {
// 此处修改 改为创建ShareCullingTiledLayer
let layer = child.getComponent(ShareCullingTiledLayer);
if (!layer) {
layer = child.addComponent(ShareCullingTiledLayer);
}
// 修改结束
// 此处修改 传递firstTmxLayer 记录firstTmxLayer
layer._init(layerInfo, mapInfo, tilesets, textures, texGrids, firstTmxLayer);
firstTmxLayer = firstTmxLayer || layer;
// 修改结束
// tell the layerinfo to release the ownership of the tiles map.
layerInfo.ownTiles = false;
layers.push(layer);
}
}
}
},
ShareCullingTiledLayer
我们需要重写TiledLayer的_init函数,来保存firstTmxLayer。
// 此处修改 增加firstTmxLayer参数
_init (layerInfo, mapInfo, tilesets, textures, texGrids, firstTmxLayer) {
// 修改结束
this._cullingDirty = true;
this._layerInfo = layerInfo;
this._mapInfo = mapInfo;
// 此处修改 保存firstTmxLayer参数
this._firstTmxLayer = firstTmxLayer;
// 修改结束
}
最后重写TiledLayer的裁剪函数,实现复用裁剪区域的功能。
_updateCulling () {
// 此处修改 若不为首个TiledLayer 直接复用firstLayer的结果
// this._firstTmxLayer不为空时 表示当前layer不是首个layer
let firstTmxLayer = this._firstTmxLayer;
if (!!firstTmxLayer) {
this._cullingRect = firstTmxLayer._cullingRect;
this._cullingDirty = firstTmxLayer._cacheCullingDirty;
return;
}
// 修改结束
this.node._updateWorldMatrix();
cc.Mat4.invert(_mat4_temp, this.node._worldMatrix);
let rect = cc.visibleRect;
let camera = cc.Camera.findCamera(this.node);
if (camera) {
_vec2_temp.x = 0;
_vec2_temp.y = 0;
_vec2_temp2.x = _vec2_temp.x + rect.width;
_vec2_temp2.y = _vec2_temp.y + rect.height;
camera.getScreenToWorldPoint(_vec2_temp, _vec2_temp);
camera.getScreenToWorldPoint(_vec2_temp2, _vec2_temp2);
cc.Vec2.transformMat4(_vec2_temp, _vec2_temp, _mat4_temp);
cc.Vec2.transformMat4(_vec2_temp2, _vec2_temp2, _mat4_temp);
this._updateViewPort(_vec2_temp.x, _vec2_temp.y, _vec2_temp2.x - _vec2_temp.x, _vec2_temp2.y - _vec2_temp.y);
// 此处修改 若为首个TiledLayer 缓存_cullingDirty。
// _cullingDirty会在填充渲染数据后被改为false 所以需要缓存这里的结果
if (!firstTmxLayer) {
this._cacheCullingDirty = this._cullingDirty;
}
// 修改结束
}
}
结束。Share Culling实现起来并不麻烦,但效果是显著的。
属性面板开关实现
在分享文章中,Share Culling功能被做成了面板上的一个开关,可以选择是否开启。由于并不是复杂的样式,实现起来其实非常简单。
首先在ShareCullingTiledMap中声明shareCulling属性。
properties: {
shareCulling: false,
}
之后在_buildLayerAndGroup函数中增加对应的判断即可。
_buildLayerAndGroup () {
if (layerInfos && layerInfos.length > 0) {
if (layerInfo instanceof cc.TMXLayerInfo) {
// 此处修改 传递firstTmxLayer 记录firstTmxLayer
layer._init(layerInfo, mapInfo, tilesets, textures, texGrids, firstTmxLayer);
if (this.shareCulling) {
firstTmxLayer = firstTmxLayer || layer;
}
// 修改结束
}
}
}
Cocos提供了开放的接口,我们可以简单地实现多种面板样式,甚至可以直接自定义属性面板界面,根据自己的需求做出各种特殊功能组件。自定义面板相关内容可以查阅Cocos文档。
优化效果
测试用例
一张有四个图层的地图。2020网格,图块大小6464。
效果对比
输出中,“===”每帧打印一次。截图中是游戏开始的前几帧的输出,不含地图进行拖动时的情况。
每帧中,通过console.timeEnd()依序输出四个图层的_updateCulling函数耗时。
运行耗时并不是准确可信的,但还是可以看到优化后,图层2、3、4的耗时显著减少。结论是可想而知的,因为原本需要运算的代码被直接跳过了。
总结
- Share Culling适用于大部分项目,且在图层数量增加时提升明显 O(n) -> O(1)。
- Share Culling只对图块图层生效,对象图层需要其他优化方式,比如分享文章中提到的场景剔除。
性能优化系列会有多篇文章,目前规划如下(不分先后):
- 地图物件多图渲染合批
- 地图图层Share Culling
- 材质颜色去除
- 分帧寻路
- 列表渲染优化
其中,除了列表渲染优化以外,都源于江南百景图的分享文章。
本文提及的技术优化未投入实际开发环境,使用前请自行斟酌。
相关链接
demo源码地址
如何重绘「江南百景图」?近300页 PPT 免费分享!
TiledMap 组件参考
CCClass 进阶参考
使用 cc.Class 声明类型
属性参数参考
扩展 Inspector