Cocos2Dx之處理鍵盤輸入

jopen 10年前發布 | 46K 次閱讀 Cocos2Dx 游戲開發

鍵盤輸入在Cocos2Dx分為兩個部分。第一部分是一些功能鍵的處理:后退鍵和菜單鍵。第二部分是處理字符輸入。功能鍵相關的代碼位于cocos2dx\keypad_dispatcher。字符輸入的代碼位于\cocos2dx\text_input_node。

我們先看功能鍵的處理部分。功能鍵的處理比較簡單,只是支持后退鍵和菜單鍵。而且功能鍵的支持只是對WP和Android有效。功能鍵部分的類關系如下圖所示。結構類似于觸控處理。只是少了一個Delegate接口。

Cocos2Dx之處理鍵盤輸入

CCKeypadDelegate定義了兩個功能鍵的處理接口。如果我們對處理功能鍵感興趣,我們只需要繼承CCKeypadDelegate,然后實現這兩個接口即可。CCLayer已經繼承了CCKeypadDelegate。我們在定義自己的層的時候,可以重載它即可。

我們還是從WIN32的窗口過程開始,來看功能鍵處理接口怎么被調用到。CCEGLView::WindowProc:

    case WM_KEYDOWN:
        if (wParam == VK_F1 || wParam == VK_F2)
        {
            CCDirector* pDirector = CCDirector::sharedDirector();
            if (GetKeyState(VK_LSHIFT) < 0 || GetKeyState(VK_RSHIFT) < 0 || GetKeyState(VK_SHIFT) < 0)
                pDirector->getKeypadDispatcher()->dispatchKeypadMSG(wParam == VK_F1 ? kTypeBackClicked : kTypeMenuClicked);
        }
        else if (wParam == VK_ESCAPE)
        {
            CCDirector::sharedDirector()->getKeypadDispatcher()->dispatchKeypadMSG(kTypeBackClicked);
        }
        break;

CCDirector內部有一個CCKeypadDispatcher類型的成員。它就是Cocos2Dx里面負責功能鍵處理的唯一對象。CCDirector有接口setKeypadDispatcher來替換它默認的功能鍵處理對象。但現在還沒有被使用到。系統將收到的功能鍵消息(退出鍵、菜單鍵)交給CCKeypadDispatcher的dispatchKeypadMSG處理。

bool CCKeypadDispatcher::dispatchKeypadMSG(ccKeypadMSGType nMsgType)
{
    CCKeypadHandler* pHandler = NULL;
    CCKeypadDelegate* pDelegate = NULL;
    m_bLocked = true;
    if (m_pDelegates->count() > 0)
    {
        CCObject* pObj = NULL;
        CCARRAY_FOREACH(m_pDelegates, pObj)
        {
            CC_BREAK_IF(!pObj);
            pHandler = (CCKeypadHandler*)pObj;
            pDelegate = pHandler->getDelegate();
            switch (nMsgType)
            {
            case kTypeBackClicked:
                pDelegate->keyBackClicked();
                break;
            case kTypeMenuClicked:
                pDelegate->keyMenuClicked();
                break;
            default:
                break;
            }
        }
    }
    m_bLocked = false;
    return true;
}

CCKeypadDispatcher::dispatchKeypadMSG內部遍歷所有的Handler,然后取出Handler包裹的Delegate,再根據功能鍵的類型,分別調用Delegate的keyBackClicked或keyMenuClicked。m_bLocked標記是為了分發功能鍵消息的過程中,又有Handler被添加進來,或者刪除掉。CCKeypadDispatcher::removeDelegate和CCKeypadDispatcher::addDelegate在刪除和添加時,會檢查m_bLocked標記。如果現在正在分發消息,添加和刪除的Delegate都先暫存起來,分別放到m_pHandlersToRemove和m_pHandlersToAdd當中。當CCKeypadDispatcher::dispatchKeypadMSG完成消息分發以后,才真正從m_pDelegates中進行刪除或添加。相應的代碼由于篇幅的關系,沒有貼在這。

m_pDelegates中的Delegate是CCKeypadDispatcher::forceAddDelegate添加進來的。但它并不直接暴露給外部。暴露給外部的添加功能鍵處理Handler的接口是CCKeypadDispatcher::addDelegate。CCLayer在自己的成員函數CCLayer::setKeypadEnabled中,將自己注冊到CCKeypadDispatcher:

void CCLayer::setKeypadEnabled(bool enabled)
{
    if (enabled != m_bKeypadEnabled)
    {
        m_bKeypadEnabled = enabled;
        if (m_bRunning)
        {
            CCDirector* pDirector = CCDirector::sharedDirector();
            if (enabled)
            {
                pDirector->getKeypadDispatcher()->addDelegate(this);
            }
            else
            {
                pDirector->getKeypadDispatcher()->removeDelegate(this);
            }
        }
    }
}

跟觸控處理一樣,CCLayer默認也不是不開啟功能鍵功能的。

現在我們總結一下怎么在游戲中使用功能鍵。

方式一:使用自己的Layer。然后重寫keyBackClicked和keyMenuClicked函數,并且setKeypadEnabled(true)。

方式二:自己定義了一個處理類,讓它繼承自CCKeypadDelegate,并實現keyBackClicked和keyMenuClicked函數。然后調用CCDirector::sharedDirector()->getKeypadDispatcher()->addDelegate(pYourOwnHandler)。

Android上面,使用的本地方法是Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeKeyDown。有興趣可以自己研究。

對于字符串的輸入是一個比較復雜的話題。Cocos2Dx提供的功能不包括輸入法,雖然名字叫做IME。Cocos2Dx提供的處理字符輸入的功能包括:從系統接收輸入的字符串,然后進行分發;一些常見的輸入控件:CCTextFieldTTF和CCEditBox。Cocos2Dx的GUI部分還是比較弱,很多輸入控件的支持都需要自己去做。我們先看下用戶輸入的流程:

第一步:使得輸入控件得到焦點。在手機上一般就會彈出虛擬鍵盤。

第二步:用戶輸入內容。內容在控件上面得到及時地反應。

第三部:用戶輸入完畢。可以以回車結束,也可能是通過控件失去焦點。

第一步和第三步是用戶操作的主動過程。分別對應到CCIMEDelegate::attachWithIME和CCIMEDelegate::detachWithIME。第二步,每輸入一個字或者詞,操作系統就會將輸入的字符通過系統消息的方式告知應用。對應的接口是CCIMEDelegate::insertText和CCIMEDelegate::deleteBackward。

我們先看attachWithIME和detachWithIME。attachWithIME和detachWithIME在CCIMEDelegate中有默認實現。就是調用CCIMEDispatcher::attachDelegateWithIME和CCIMEDispatcher::detachDelegateWithIME。主要功能是在何時的時機調用CCIMEDelegate的四個函數:canAttachWithIME、didAttachWithIME、canDetachWithIME和didDetachWithIME。帶有can前綴的用來測試現在是否可以attach或者detach IME。did前綴的函數用來通知Delegate現在已經做了attach或detach IME。這些回調函數給Delegate一些處理其它的事務的機會。

但什么怎么觸發attachWithIME或者detachWithIME呢?答案是我們自己控制。舉一個常見例子:用戶點擊一個控件。控件在Cocos2Dx可以做成一個CCLayer。因為CCLayer帶有處理觸控事件的能力。我們重載CCLayer::ccTouchEnd的函數,讓它收到控件所在區域的觸控消息后,就讓操作系統彈出鍵盤允許進行輸入,如果觸控區域不在控件所在區域,隱藏鍵盤。

void TextFieldTTFDefaultTest::onClickTrackNode(bool bClicked)
{
    CCTextFieldTTF * pTextField = (CCTextFieldTTF*)m_pTrackNode;
    if (bClicked)
    {
        pTextField->attachWithIME();
    }
    else
    {
        pTextField->detachWithIME();
    }
}

這里用到了CCTextFieldTTF。他是Cocos2Dx提供的一個完整輸入控件。雖然還有CCEditBox,但它是以擴展插件的身份存在的,并且實現方式也不一樣。CCTextFieldTTF本身我們后面還會提到。

CCTextFieldTTF::attachWithIME和CCTextFieldTTF::detachWithIME內部會先調用CCIMEDelegate::attachWithIME和CCIMEDelegate::detachWithIME。因為CCIMEDelegate::attachWithIME和CCIMEDelegate::detachWithIME會告知控件當前是否允許做IME的attach和detach操作。如果CCIMEDelegate的兩個函數返回fasle,那么當前控件的IME attach和detach都會被拒絕。

bool CCTextFieldTTF::attachWithIME()
{
    bool bRet = CCIMEDelegate::attachWithIME();
    if (bRet)
    {
        CCEGLView * pGlView = CCDirector::sharedDirector()->getOpenGLView();
        if (pGlView)
        {
            pGlView->setIMEKeyboardState(true);
        }
    }
    return bRet;
}
bool CCTextFieldTTF::detachWithIME()
{
    bool bRet = CCIMEDelegate::detachWithIME();
    if (bRet)
    {
        CCEGLView * pGlView = CCDirector::sharedDirector()->getOpenGLView();
        if (pGlView)
        {
            pGlView->setIMEKeyboardState(false);
        }
    }
    return bRet;
}

CCTextFieldTTF::attachWithIME和CCTextFieldTTF::detachWithIME進一步讓操作系統準備輸入法。但這步依賴于Cocos2Dx所處的平臺,不同的平臺有不同的實現,Win32上面上面都不需要做,但Android上面就需要自己去通過InputMethodManager獲取輸入法服務。平臺的差異都隱藏在CCEGLView::setIMEKeyboardState后面。有興趣可以自己進一步深入跟進。

現在再來看CCIMEDelegate::insertText和CCIMEDelegate::deleteBackward。insertText代表我們現在正在進行輸入;deleteBackward代表我們現在需要刪除一個已經輸入的字符。CCIMEDelegate自身并沒有對這兩個函數提供默認的實現。可用的實現是CCTextFieldTTF。可用在CCTextFieldTTF進行自己的擴展。

Cocos2Dx之處理鍵盤輸入

看到上面的圖,可能會問CCIMEDelegate怎么向CCIMEDispatcher進行注冊的呢?注釋說明了一下:CCIMEDelegate的構造函數會調用CCIMEDispatcher的單例對象的addDelegate函數注冊自己。addDelegate是保護成員,為了能夠訪問它,聲明了CCIMEDelegate為CCIMEDispatcher的友元。析構函數會做類似的取消注冊操作。CCTextFieldTTF繼承CCIMEDelegate,它構造或析構的時候,會分別調用CCIMEDelegate的構造和析構函數,CCTextFieldTTF也就自動向CCIMEDispatcher注冊和取消注冊。

CCTextFieldTTF的輸入字符顯示實際上是靠CCLabelTTFT完成的。CCLabelTTFT還有一個CCTextFieldDelegate成員,它將一些控制功能封裝在一個Delegate當中。CCTextFieldDelegate所有成員都返回bool值,決定相應的操作是否允許被執行。

現在走一些按鍵消息的分發流程。CCEGLView::WindowProc在收到鍵盤消息后,需要進行一下判斷:

  • 刪除鍵(VK_BACK),調用CCIMEDispatcher::dispatchDeleteBackward,進一步調用CCIMEDelegate::deleteBackward(子類實現)。

  • 回車鍵(VK_RETURN),調用CCIMEDispatcher::dispatchInsertText,進一步調用CCIMEDelegate::insertText(子類實現)。但并沒有直接送鍵碼,二是送了'\n'。

  • ASCII字符,同上。

  • 其他可輸入字符。在發送前需要做轉碼,寬字串轉UTF8。

CCIMEDelegate的子類CCTextFieldTTF收到按鍵輸入后,將其存放在一個字符串中。具體的實現,比較易懂,我們就不再累述。

另外,Android上面,字符輸入相關的代碼位于cocos2dx\platform\android\java\src\org\cocos2dx\lib\Cocos2dxTextInputWraper.java。

 

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!