天天看點

【Cocos2d-x】RichText打字機效果思路分享前情提要代碼分析邏輯編寫解決LabelLetter無法轉Lua問題

前情提要

今天在開發遊戲引導架構時,遇到這樣的需求:人物對話文本支援打字機效果,且需要個别文字高亮。如果僅僅是前者的需求,是挺好實作的,建立一個

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類别的不同,建立

Label

Sprite

Node

等,放入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()

拿到所有的文字了,本教程也算告一段落。