Cocos2d-x 轻松实现自定义多颜色文本

(本文基于Cocos2d-x 3.x 版本)

一、文本渲染

由于主流引擎并没有对文字渲染做独立支持,所以在游戏中使用到的文字都是通过转存成一一对应的图像,然后再显示到屏幕上的;在 Cocos2d-x 中也是这样,它使用了 FreeType 的库来处理解析字体文件,将文本转换成字形(glyph),并合并生成对应的Texture 纹理图片,最后通过 OpenGL 完成渲染的过程,也就是说文本最终也是通过纹理的形式显示出来的

简单描述一下,Cocos2d-x 需要显示一个纹理,除去纹理本身的信息外,还需要定义纹理对应的顶点信息,基本的顶点信息包括纹理坐标(顶点本身坐标)、颜色(顶点显示的颜色,默认为255)、UV坐标(顶点的纹理UV坐标)这三部分;


(如上图所示,两个三角形,四个顶点可以完整的显示一个纹理)

在引擎中,纹理渲染结构定义如下:

//! a Vec2 with a vertex point, a tex coord point and a color 4B
struct CC_DLL V3F_C4B_T2F
{
    //! vertices (3F)      三个顶点描述一个三角形
    Vec3     vertices;            // 12 bytes
    //! colors (4B)        RGBA四个颜色值
    Color4B      colors;          // 4 bytes
    // tex coords (2F)     UV坐标
    Tex2F        texCoords;       // 8 bytes
};

二、 CCLabel实现

Cocos2d-x 3.x 中通过 CCLabel 支持四种方式显示文字,具体定义分别是:

enum class LabelType {
    TTF,                    // TTF字体
    BMFONT,                 // 图片字体(BMFont)
    CHARMAP,                // 图集字体(CHARMAP)
    STRING_TEXTURE          // 原生字体渲染(系统字)
    };

除了原生渲染(STRING_TEXTURE)类型是直接生成一张文字图外,与其他三种都会根据对应的文字及字体类型生成FontAtlas 来完成渲染。

其中图片字(BMFONT)和图集字体(CHARMAP)每个文字都有现成的纹理可以获取到,只有TTF字体需要实时生成字体纹理;

// 生成TTF文字图集
FontAtlas * FontAtlasCache::getFontAtlasTTF(const TTFConfig & config, bool outlineNewFeature)
{  
    ....
    //根据文件路径及字形信息成生文字图集的名称
    auto atlasName = generateFontName(config.fontFilePath, fontSize, GlyphCollection::DYNAMIC, useDistanceField, outlineNewFeature);
    ....    
    if ( it == _atlasMap.end() )
    {
        // 使用 FreeType 库来生成字形信息
        auto font = FontFreeType::create(config.fontFilePath, fontSize, config.glyphs, 
            config.customGlyphs, useDistanceField, config.outlineSize, outlineNewFeature);
        if (font)
        {
        // 根据字形信息生成文字图集(每个文字对应一个纹理)
            auto tempAtlas = font->createFontAtlas();
            ....
        }
    }
    ....
}

createFontAtlas 方法最终会调用到 FontAtlas::prepareLetterDefinitions 方法最后生成一组文本对应的纹理数据,最终文字渲染被统一成了对对应纹理的渲染操作;

三、 自定义颜色渲染

结合前文,在基于纹理渲染实现文本显示的前提下,可以通过直接修改对应文字的顶点颜色,从而达到显示不同颜色文本内容的效果;

因为数据是现成的,所以只要增加一个顶点颜色改变的方法就可以了,所以我们新增一个 ColorLabel 类实现相应的逻辑:

// ------------- ColorLabel.h 类定义:
class ColorLabel : public cocos2d::Label
{
public:
    ColorLabel();
    virtual ~ColorLabel();
    static ColorLabel * create();
    // 设置单个文字颜色
    void setCharColor(int index, const cocos2d::Color3B color);
protected:
    std::unordered_map<int, cocos2d::Color4B*> _charColors;   // 记录每个字的颜色
protected:
    virtual void updateColor() override;
};

// ------------- ColorLabel.cpp 具体实现:
// 更新指定位置的文本颜色
void ColorLabel::setCharColor(int index, const Color3B color)
{
    if (nullptr == _textureAtlas)
    {
        return;
    }
    // 先存到 _charColor map 里,再刷新顶点颜色
    _charColors[index] = &Color4B(color.r, color.g, color.b, _displayedOpacity);
    updateColor();
}

// 重载基类颜色刷新方法
void ColorLabel::updateColor()
{
    if (nullptr == _textureAtlas)
    {   // 只处理 _textureAtlas 存在的情况,不存在则表示不是采用纹理方式绘制的文字
        return;
    }
    Color4B color4(_displayedColor.r, _displayedColor.g, _displayedColor.b, _displayedOpacity);
    Color4B* currColor;
    // 特殊的不透明度预乘纹理
    if (_isOpacityModifyRGB)
    {
    color4.r *= _displayedOpacity / 255.0f;
    color4.g *= _displayedOpacity / 255.0f;
    color4.b *= _displayedOpacity / 255.0f;
    }
    cocos2d::TextureAtlas* textureAtlas;
    V3F_C4B_T2F_Quad *quads;
    for (const auto& batchNode : _batchNodes)
    {
    textureAtlas = batchNode->getTextureAtlas();        // 获取文本对应的纹理
    quads = textureAtlas->getQuads();
                   // 获取纹理对应的顶点
        auto count = textureAtlas->getTotalQuads();
        // 循环四个顶点,重置颜色值
        for (int index = 0; index < count; ++index)
        {   // 这里先取 _charColor 里修改的颜色值,如果没有再取原本的颜色
            currColor = (_charColors.count(index)) ? _charColors.at(index) : &color4;
            //if (_currentLabelType == LabelType::STRING_TEXTURE)
            //{ 
                // 这里准备处理系统字的颜色渲染
                // 系统字会生成一整个纹理,无法取到单个文字的,所以暂时不做处理
            //}
            //else
            //{  // 下面与基类方法处理相同
            if (_useGradient) {
                Color4B gradientColor(_gradientColor.r, _gradientColor.g, _gradientColor.b, _displayedOpacity);
                if (_isOpacityModifyRGB)
                {
                    gradientColor.r *= _displayedOpacity / 255.0f;
                    gradientColor.g *= _displayedOpacity / 255.0f;
                    gradientColor.b *= _displayedOpacity / 255.0f;
                }
                quads[index].bl.colors = gradientColor;
                quads[index].br.colors = gradientColor;
            }
            else {
                quads[index].bl.colors = *currColor;
                quads[index].br.colors = *currColor;
            }

            quads[index].tl.colors = *currColor;
            quads[index].tr.colors = *currColor;
            textureAtlas->updateQuad(&quads[index], index);
        }
    }
}

同时为了能在编辑器下应用,需要再封装一个 UIColorText 类,来对接处理 ColorLabel 的显示;

// ------------- UIColorText.h 类定义:
class UIColorText : public cocos2d::ui::Text
{
protected:
    // 重载初始化渲染组件方法,方便用 ColorLabel 代替
    virtual void initRenderer() override;     
public:
    UIColorText();
    virtual ~UIColorText();
    static UIColorText * create(const std::string& textContent, const std::string& fontName, int fontSize);
    // 开放 ColorLabel 的 setCharColor 方法
    void setCharColor(int index, const cocos2d::Color3B color);
};

// ------------- UIColorText.cpp 具体实现:
// 开放 ColorLabel 的 setCharColor 方法
void UIColorText::setCharColor(int index, const cocos2d::Color3B color)
{
    ColorLabel* pRenderer = dynamic_cast<ColorLabel*>(_labelRenderer);
    if (pRenderer) {
        pRenderer->setCharColor(index, color);
    }
}

void UIColorText::initRenderer()
{
    // 重载基类方法,用 ColorLabel 实例代替
    _labelRenderer = ColorLabel::create();
    addProtectedChild(_labelRenderer, -1, -1);
}

四、表现效果

下面给出简单的调用代码示例:

auto visibleSize = Director::getInstance()->getVisibleSize();
auto origin = Director::getInstance()->getVisibleOrigin();
UIColorText* colorText;
std::string textStr;
int nTop = origin.y + visibleSize.height / 2 + 100;
for (int i = 0; i < 3; i++) {
    // 显示的文本(简单处理)
    textStr = (i == 0) ? "normal piece of text" : ((i == 1) ? "color piece of text" : HelloWorld::StrToUTF8("这是一段中文彩色文本"));
    // 初始化 UIColorText 组件
    colorText = UIColorText::create(textStr, "fonts/simhei.ttf", TITLE_FONT_SIZE);        
    colorText->setPosition(Vec2(origin.x + visibleSize.width / 2, nTop));

    // 设置整个文本的颜色(基类原有方法)
    colorText->setColor(ccc3(190, 190, 190));
    // 设置第7个文字的颜色为红色
    colorText->setCharColor(7, ccc3(255, 0, 0));
    this->addChild(colorText, 1);
    _colorTexts.push_back(colorText);
    nTop -= 40;
    }

最终结果呈现如下:

image

在使用 TTF 显示多颜色的应用场合下,UIColorText 可以实现比较理想的优化效果;特别是对比使用 RichText实现方式( UIColorText 不会像 RichText 那样产生很多子节点,这对于比较复杂的界面首次加载的性能影响是很大的),对比效果图如下:

对于系统字来说,按照当前的优化思路继续扩展,还可以实现很多有意思的表现效果,比如下面是彩色流光显示表现效果:

五、示例和参考

欢迎扫码关注我的公众号,获取 自定义多颜色文本组件示例DEMO,以及更多Cocos干货:

2赞

前几年, 用creator2.x的时候做过这个