“ 如果你还在困扰在应该选择LABEL的哪个模式,那么你应该看看这篇文章!”
一、引擎中关于Label的缓存模式的描述
Label组件目前提供三种 Cache Mode:NONE、BITMAP、CHAR。
NONE:即 Label 的整个文本内容会进行一次绘制,并进行提交,但是并不参与动态合图。
BITMAP:即 Label 的整个文本内容会进行一次绘制,并加入到动态图集中,以便进行批次合并。
CHAR:即 Label 会将文本内容进行拆分,然后对单个字符进行绘制,并将字符缓存到一张单独的字符图集中。下次遇到相同字符不再重新绘制。
三者的主要区别如下所示:
二、内存中的状态
示例说明:每种缓存模式下分别创建三个label, 两个白色一个红色。然后查看在内存中的贴图。
DEMO显示示例(none-cache, dynamic-cache, char-cache三个按钮是none模式,用于打开对应的缓存贴图的查看界面):
然后分别查看各种不同模式的Label贴图在内存中的状态:
NONE模式(三个按钮都是用的None模式):一个label占一个贴图,即使文本相同也不会重用。
BITMAP模式:一个Label在动态图集中画了两次(是不是有点奇怪,稍后解答)。
CHAR模式:比较正常,相同颜色的文本是重用的。
三、问题分析
###3.1 三种模式的表现对比
###3.2 原理分析
####3.2.1 NONE模式能不能用?
只能用于用完即删且可能会频繁更新大批量文本的需求。可以从反面来印证这个结果:
如果使用BITMAP会导致频繁合批,导致动态图集加快用完,导致重建浪费效率。
如果使用CHAR模式,也会加速唯一的文字图集的消耗。尤其是在引擎中的文字图集无法扩展的情况下。
####3.2.2 BITMAP模式为什么效率低下?
一个静态文本会发生两次合批,一次是在onEnable的回调中触发,另一次是在第一次渲染时合进去的。
第一次合图:
第二次合图:
问题出现的根本原因是:动态合图只能往图集上加贴图,而不能继续重用上次的,更不会删除已经作废的子贴图(因为引擎本身的机制无法判断,其实也是可以判断出来的)。
####3.2.3 CHAR模式是否推荐?
从以上的结果看来,比较推荐使用CHAR模式。但从文字贴图的上限角度考虑,这个模式也不保险:如果游戏中单场景模式,不切换场景,那么文字图集有消耗完的可能。另外,即使在多场景下,使用CHAR模式过多,也会加快图集消耗,毕竟一个字符会因为字号,字体,颜色和描边的不同而不能重用导致反复创建。
四. 解决办法-BITMAP
针对以上的分析,我们得到的结果看起来只有NONE能在部分特殊需求下能用,其它两种模式都有无法避免的问题的风险。因此我们需要改造引擎的后两种模式来解决相应的效率和风险问题。由于不可描述的特殊原因,本篇文章主要提供BITMAP的改造思路和部分代码。
上面分析得出BITMAP的问题主要是重画次数太多,导致动态图集的快速消耗问题。因此如果能减少重画次数,或者能重用图集上的文字贴图,那么这个BITMAP的效率问题就不存在了。这里主要从减少合图的角度来找解决方案。
仔细分析引擎中动态合批的机制,可以知道,贴图是否已经在动态图集中画过,主要是通过Atlas._innerSpriteFrames数组是否包含对应的文字贴图spriteFrame(Label._ttfTexture)来判断的。但是当第二次合图集时,spriteFrame已经发生变化,但不能直接判断文本是否发生变化,如果变化,那么的确需要重新合图集,如果没变,则可以直接取上次文本贴图的位置使用,然后重新计算uv值即可重用上次的合图结果。那么如何实现?
办法是有的,思路是通过spriteFrame中_texture的_uuid来判断图集是否发生变化:
- 对ttf.js(core/renderer/util/ttf.js)的_calDynamicAtlas函数做如下修改:
// 给frame的texture计算一个新的_uuid值
frame._texture._uuid = comp.string + "_" + comp.node.color + "_" + comp.fontSize + comp.fontFamily;
this.packToDynamicAtlas(comp, frame);
- 对atlas.js的修改:
函数insertSpriteFrame做如下修改:
let rect = spriteFrame._rect
let texture = spriteFrame._texture
// 在这里通过_uuid来判断重用性
// this._dynamicTextureRect 增加的一个字典用来记录uuid与texture的映射关系
let info = this._dynamicTextureRect[texture._uuid];
this._dynamicTextureRect[texture._uuid] = {
x: fitRect.x,
y: fitRect.y,
};
增加函数:fetchSpriteFrame用于从图集中查找相同_uuid的图集块,然后直接重用:
fetchSpriteFrame(spriteFrame) {
let texture = spriteFrame._texture
let info = this._dynamicTextureRect[texture._uuid];
if (!info) {
return null;
}
let rect = spriteFrame._rect
let sx = rect.x + info.x, sy = rect.y + info.y;
let frame = {
x: sx,
y: sy,
texture: this._texture
}
if (!this._innerSpriteFrames.includes(spriteFrame)) {
this._innerSpriteFrames.push(spriteFrame);
}
return frame;
},
最后在deleteInnerTexture中删除spriteFrame时,也要通过spriteFrame._original._texture._uuid来查找this._innerSpriteFrames数组中对应的对象,删除即可。这里代码就不贴了。
- manager.js中insertSpriteFrame函数中增加fetchSpriteFrame的调用:
// 如果能从当前图集中找到相同_uuid的图集块,则重用
frame = atlas.fetchSpriteFrame(spriteFrame)
至此,重用动态图集的功能就完成了,看下效果:
这里动态图集中只有白色和红色两个label的图集了。
另外:这个办法同时还解决了非文本贴图的重用问题,当然这不是本文章的重点,后续 我们会在突破系列中有一篇关于突破动态合批的低效问题的文章,请静候。
下篇文章我们继续来突破CHAR模式的贴图限制,释放无限可能,敬请期待!
如果您想获得更及时推送,请扫文末二维码关注“乐府札记”,谢谢。
如果您想获得更及时推送,请扫文末二维码关注“乐府札记”,谢谢。
如果您想获得更及时推送,请扫文末二维码关注“乐府札记”,谢谢。
关于乐府互娱
乐府互娱成立于2019年,是一家专注于精品移动游戏研发和运营的明星初创企业。
公司核心团队是《少年三国志》《少年西游记》系列作品的原班人马,其中制作人、策划、技术、美术、UI、发行等模块核心成员已共事多年,拥有成熟的研发产品体系和管理体系。
团队长期深耕卡牌手游等品类,具备敏锐的嗅觉和高效的研运能力,以长线游戏研发运营著称,打造过数款月流水过亿的产品,包括《少年三国志》《少年西游记》等,游戏累计流水近100亿元。
公司在成立之初即获得资本市场高度认可,目前已完成天使轮和A轮融资。短期内,乐府将结合自身优势,继续致力于中轻度卡牌手游的深耕细作,保持在该领域的头部地位。
在长期,乐府会重点聚焦于“游戏工业化”之路,在自研自发的前提下,继续打造经典游戏IP。