【乐府】突破Label的缓存模式之(1) BITMAP

“ 如果你还在困扰在应该选择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来判断图集是否发生变化:

  1. 对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);
  1. 对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数组中对应的对象,删除即可。这里代码就不贴了。

  1. manager.js中insertSpriteFrame函数中增加fetchSpriteFrame的调用:
// 如果能从当前图集中找到相同_uuid的图集块,则重用
frame = atlas.fetchSpriteFrame(spriteFrame)

至此,重用动态图集的功能就完成了,看下效果:

这里动态图集中只有白色和红色两个label的图集了。

另外:这个办法同时还解决了非文本贴图的重用问题,当然这不是本文章的重点,后续 我们会在突破系列中有一篇关于突破动态合批的低效问题的文章,请静候。

下篇文章我们继续来突破CHAR模式的贴图限制,释放无限可能,敬请期待!

如果您想获得更及时推送,请扫文末二维码关注“乐府札记”,谢谢。
如果您想获得更及时推送,请扫文末二维码关注“乐府札记”,谢谢。
如果您想获得更及时推送,请扫文末二维码关注“乐府札记”,谢谢。


关于乐府互娱

乐府互娱成立于2019年,是一家专注于精品移动游戏研发和运营的明星初创企业。

公司核心团队是《少年三国志》《少年西游记》系列作品的原班人马,其中制作人、策划、技术、美术、UI、发行等模块核心成员已共事多年,拥有成熟的研发产品体系和管理体系。

团队长期深耕卡牌手游等品类,具备敏锐的嗅觉和高效的研运能力,以长线游戏研发运营著称,打造过数款月流水过亿的产品,包括《少年三国志》《少年西游记》等,游戏累计流水近100亿元。

公司在成立之初即获得资本市场高度认可,目前已完成天使轮和A轮融资。短期内,乐府将结合自身优势,继续致力于中轻度卡牌手游的深耕细作,保持在该领域的头部地位。

在长期,乐府会重点聚焦于“游戏工业化”之路,在自研自发的前提下,继续打造经典游戏IP。

16赞

先插个眼:grinning:

这是要做品牌啊

铁子 很专业

这里的fitRect是不是写错了,应该是rect。

可以看下demo,或者那三个文件的修改记录么,按照文章的方法试了,重新编译引擎了,没起到效果。感激!!!

实战之王,给力

少年三国志是用ccc做的?!

大佬给力!

我都是直接花钱买一个 个 Glyph Designer

测试过,很遗憾然并卵

mark 思路不错

这里有个问题请教下
为什么只从当前图集查找呢,如果纹理被打在前面的图集里面就会重复插入了。