【Cocos2d-x】RichText打字机效果思路分享

前情提要

今天在开发游戏引导框架时,遇到这样的需求:人物对话文本支持打字机效果,且需要个别文字高亮。如果仅仅是前者的需求,是挺好实现的,创建一个Label,通过getLetter(index)获取每个字,调用setVisible(isVisible)即可;但是个别文字高亮是RichText才有的功能,于是难点变成了如何获取RichText里的每个字

代码分析

话不多说,看源代码,从UIRichText.cpp文件的formatText方法中,我们发现RichText的本质就是多个Label的拼接:

void RichText::formatText(bool isForce)
{
  ...
        _elementRenders.clear();
            ...
            for (ssize_t i=0; i<_richElements.size(); i++)
            {
                RichElement* element = _richElements.at(i);
                Node* elementRenderer = nullptr;
                switch (element->_type)
                {
                    case RichElement::Type::TEXT:
                    {
                        RichElementText* elmtText = static_cast<RichElementText*>(element);
                        Label* label;
                        if (FileUtils::getInstance()->isFileExist(elmtText->_fontName))
                        {
                             label = Label::createWithTTF(elmtText->_text, elmtText->_fontName, elmtText->_fontSize);
                        }
                        else
                        {
                            label = Label::createWithSystemFont(elmtText->_text, elmtText->_fontName, elmtText->_fontSize);
                        }
                        ...
                        elementRenderer = label;
                        break;
                    }
                    case RichElement::Type::IMAGE:
                    {
                      ...
                    }
                    case RichElement::Type::CUSTOM:
                    {
                      ...
                    }
                    case RichElement::Type::NEWLINE:
                    {
                      ...
                    }
                    default:
                        break;
                }

                if (elementRenderer)
                {
                    Label * pLabel = dynamic_cast<Label *>(elementRenderer);
                    if (pLabel)
                    {
                        pLabel->setTextColor(Color4B(element->_color, element->_opacity));
                    }
                    else
                    {
                        elementRenderer->setColor(element->_color);
                        elementRenderer->setOpacity(element->_opacity);
                    }

                    pushToContainer(elementRenderer);
                }
            }
        }
        else
        {
          // 与前一段if结构基本一致
          ...
        }
        formarRenderers();
        _formatTextDirty = false;
    }
}

这段代码的逻辑是通过读取insertElement()方法传入的RichElement,根据RichElement类别的不同,创建LabelSpriteNode等,放入RichText这个容器中,因为在当前情境下,只有Label被创建,所以其他不在考虑范围。

有了Label就可以拿到每个文字了,那么Label从哪里获取呢?我们把上面代码再精简下:

void RichText::formatText(bool isForce)
{
    _elementRenders.clear();
    Node* elementRender = Label::create...
    pushToContainer(elementRender);
    formarRenderers();
}

发现Label被传进了pushToContainer(render)方法中,这个方法的代码很简单:

void RichText::pushToContainer(cocos2d::Node *renderer)
{
    if (_elementRenders.size() <= 0)
    {
        return;
    }
    _elementRenders[_elementRenders.size()-1]->pushBack(renderer);
}

所以思路就变成了如何从_elementRenders中获取所有的Label

逻辑编写

基于上述梳理,编写获取RichText中所有文字的逻辑如下:

void RichText::getAllLetters()
{
    Vector<Sprite*> letters;

    for (auto& element : _elementRenders)
    {
        Vector<Node*>* row = element;
        for (ssize_t i = 0; i<row->size(); i++)
        {
            Node* pNode = row->at(i);
            Label * pLabel = dynamic_cast<Label*>(pNode);
            if (pLabel)
            {
                int len = pLabel->getStringLength();
                for (int j = 0; j < len; j++)
                {
                    Sprite* letter = pLabel->getLetter(j);
                    if (letter)
                        letters.pushBack(letter);
                }
            }
        }
    }

    return letters;
}

编写完成,将CPP转为Lua,看一下效果!

代码:
local ret = richText:getAllLetters()

输出:
ret: {}

为什么会出现这样的情况呢?

打断点,进入getAllLetters(),傻眼了,_elementRenders是个空数组。原因是formatText()方法的最后,RichText调用了formarRenderers()方法,我们来简单地看一下formarRenderers的逻辑:

void RichText::formarRenderers()
{
    // 此处省略一大坨代码...
    
    size_t length = _elementRenders.size();
    for (size_t i = 0; i<length; i++)
      {
        Vector<Node*>* l = _elementRenders[i];
        l->clear();
        delete l;
      }    
    _elementRenders.clear();
    
    updateContentSizeWithTextureSize(_contentSize);
}

前面做了什么逻辑我们不关心,我们关心的是方法的最后调用了_elementRenders.clear(),也就是说:每次执行formatText()后,_elementRenders都会被清空,它只是一个临时变量,所以接下来要做的就是_elementRenders被清掉前,遍历获取每一个Letter并存下来

于是代码变成了这样:

UIRichText.h

class CC_GUI_DLL RichText : public Widget
{
public:
    Vector<Sprite*>& getAllLetters();

protected:
    Vector<Sprite*> _letters;
};

--------------------------------------------------

UIRichText.CPP

void RichText::updateLetters()
{
    _letters.clear();

    for (auto& element : _elementRenders)
    {
        Vector<Node*>* row = element;
        for (ssize_t i = 0; i<row->size(); i++)
        {
            Node* pNode = row->at(i);
            Label * pLabel = dynamic_cast<Label*>(pNode);
            if (pLabel)
            {
                int len = pLabel->getStringLength();
                for (int j = 0; j < len; j++)
                {
                    Sprite* letter = pLabel->getLetter(j);
                    if (letter)
                        _letters.pushBack(letter);
                }
            }
        }
    }
}

Vector<Sprite*>& RichText::getAllLetters()
{
    return _letters;
}

void RichText::formarRenderers()
{
    // 此处省略一大坨代码...

    // ------------------
    // 添加的代码
    updateLetters();
    // ------------------
    
    size_t length = _elementRenders.size();
    for (size_t i = 0; i<length; i++)
      {
        Vector<Node*>* l = _elementRenders[i];
        l->clear();
        delete l;
      }    
    _elementRenders.clear();
    
    updateContentSizeWithTextureSize(_contentSize);
}

终于,CPP层间的逻辑算是实现了。但这时候,又有新的问题出现了。

解决LabelLetter无法转Lua问题

按照新的逻辑,从Lua层面调用getAllLetters()方法,发现获取的结果依然是空table;但是在CPP层面,却是可以获取到数据的。那么问题就出在toLua的过程。

说明:tolua是cocos2d-x提供的lua-binding工具,位于项目tools/tolua目录下。

继续断点调试,将问题定位在了下面这个方法:

/**
 * Push a table converted from a cocos2d::Vector object into the Lua stack.
 * The format of table as follows: {userdata1, userdata2, ..., userdataVectorSize}
 * The object in the cocos2d::Vector which would be pushed into the table should be Ref type.
 *
 * @param L the current lua_State.
 * @param inValue a cocos2d::Vector object.
 */
template <class T>
void ccvector_to_luaval(lua_State* L,const cocos2d::Vector<T>& inValue)
{
    lua_newtable(L);

    if (nullptr == L)
        return;

    int indexTable = 1;
    for (const auto& obj : inValue)
    {
        if (nullptr == obj)
            continue;


        if (nullptr != dynamic_cast<cocos2d::Ref *>(obj))
        {
            std::string typeName = typeid(*obj).name();
            auto iter = g_luaType.find(typeName);
            if (g_luaType.end() != iter)
            {
                lua_pushnumber(L, (lua_Number)indexTable);
                int ID = (obj) ? (int)obj->_ID : -1;
                int* luaID = (obj) ? &obj->_luaID : NULL;
                toluafix_pushusertype_ccobject(L, ID, luaID, (void*)obj,iter->second.c_str());
                lua_rawset(L, -3);
                ++indexTable;
            }
        }
    }
}

问题的症结出在了g_luaType这里,在这个数组里,找不到LabelLetter类,虽然getAllLetters()返回的是Sprite的数组,但本质上,Label::getLetter()中返回的Sprite是通过LabelLetter创建的,而LabelLetter是在CCLabel.cpp里面定义的,g_luaType根本不知道LabelLetter的存在。

既然如此,就一改到底吧!

template <class T>
void ccvector_to_luaval(lua_State* L,const cocos2d::Vector<T>& inValue)
{
    lua_newtable(L);

    if (nullptr == L)
        return;

    int indexTable = 1;
    for (const auto& obj : inValue)
    {
        if (nullptr == obj)
            continue;


        if (nullptr != dynamic_cast<cocos2d::Ref *>(obj))
        {
            std::string typeName = typeid(*obj).name();

            // --------------------------------------------
            // 添加的代码
            if (typeName == "class cocos2d::LabelLetter")
            {
                typeName = "class cocos2d::Sprite";
            }
            // --------------------------------------------

            auto iter = g_luaType.find(typeName);
            if (g_luaType.end() != iter)
            {
                lua_pushnumber(L, (lua_Number)indexTable);
                int ID = (obj) ? (int)obj->_ID : -1;
                int* luaID = (obj) ? &obj->_luaID : NULL;
                toluafix_pushusertype_ccobject(L, ID, luaID, (void*)obj,iter->second.c_str());
                lua_rawset(L, -3);
                ++indexTable;
            }
        }
    }
}

在获取typeName以后,判断typeName的类别,若是class cocos2d::LabelLetter,则将其强行改为class cocos2d::Sprite

至此,RichText可算可以调用getAllLetters()拿到所有的文字了,本教程也算告一段落。

写在最后

本文的逻辑重在实现思路,其实其中也存在很多待优化的地方,如:富文本存在表情图片时如何支持打字机效果?又如:后面转Lua裸写类别名的判断不够优雅。还有诸如:不需要打字机效果的文本存_letters会造成内存浪费等。感兴趣的读者欢迎在本文的基础上进行优化修改!感谢阅读~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270