【cocos2d-x從c++到js】12:回調函數1——按鍵回調

回調函數是界面交互和接入各種第三方SDK的關鍵所在,因爲回調函數的C++代碼是不能自動生成的,一切的一切,都需要手寫完成。


比較不錯的是,Cocos2d-x引擎對於回調函數提供了完整的包裝機制。我們所需要做的就是了解這個機制,並使用他。學習引擎自己的代碼例子,可以比較快速準確的上手這一機制。


首先,我們在Cocos2d-x 3.0 beta版中,使用他自帶的工程創建工具,新建一個跨平臺的JS項目。按照慣例,這是一個helloworld項目。在XCode運行時,我們可以看到:

wKiom1LjP67wo2etAAGX7-JVxuY628.jpg


可以看到右下角的回調按鈕。我們來看看他是怎麼實現的。分成兩個過程來做:



一、綁定回調函數過程


首先,我們要去找回調函數JS的綁定代碼,在myApp.js中,init函數裏面,可以看到如下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
// add a "close" icon to exit the progress. it's an autorelease object
var closeItem = cc.MenuItemImage.create(
    "res/CloseNormal.png",
    "res/CloseSelected.png",
    function () {
        cc.log("close button was clicked.");
    },this);
closeItem.setAnchorPoint(cc.p(0.5, 0.5));
var menu = cc.Menu.create(closeItem);
menu.setPosition(cc.p(0, 0));
this.addChild(menu, 1);
closeItem.setPosition(cc.p(size.width - 20, 20));

cc.MenuItemImage.create函數的第三個參數,綁定了匿名回調函數。第四個參數,傳入的是回調函數調用時的this(如果不理解JS的this機制,請先閱讀一些JS的資料)。這些都是意圖和作用很明顯的JS代碼,不用細說。


然後,我們去看底層對應執行的C++代碼。在cocos2d_specifics.cpp文件中,找到js_cocos2dx_CCMenuItemImage_create函數。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// "create" in JS
// cc.MenuItemImage.create( normalImage, selectedImage, [disabledImage], callback_fn, [this]
JSBool js_cocos2dx_CCMenuItemImage_create(JSContext *cx, uint32_t argc, jsval *vp)
{
    if (argc >= 2 && argc <= 5) {
        jsval *argv = JS_ARGV(cx, vp);
        JSStringWrapper arg0(argv[0]);
        JSStringWrapper arg1(argv[1]);
        JSStringWrapper arg2;
        bool thirdArgIsString = true;
        jsval jsCallback = JSVAL_VOID;
        jsval jsThis = JSVAL_VOID;
        int last = 2;
        if (argc >= 3) {
            thirdArgIsString = argv[2].isString();
            if (thirdArgIsString) {
                arg2.set(argv[2], cx);
                last = 3;
            }
        }
        cocos2d::MenuItemImage* ret = cocos2d::MenuItemImage::create(arg0.get(), arg1.get(), std::string(arg2.get()));
        if (argc >= 3) {
            if (!thirdArgIsString) {
                //cc.MenuItemImage.create( normalImage, selectedImage, callback_fn, [this] )
                jsCallback = argv[last++];
                if (argc == 4) {
                    jsThis = argv[last];
                }
            }
            else {
                //cc.MenuItemImage.create( normalImage, selectedImage, disabledImage, callback_fn, [this] )
                if (argc >= 4) {
                    jsCallback = argv[last++];
                    if (argc == 5) {
                        jsThis = argv[last];
                    }
                }
            }
        }
        JSObject *obj = bind_menu_item<cocos2d::MenuItemImage>(cx, ret, jsCallback, jsThis);
        JS_SET_RVAL(cx, vp, OBJECT_TO_JSVAL(obj));
        return JS_TRUE;
    }
    JS_ReportError(cx, "Invalid number of arguments. Expecting: 2 <= args <= 5");
    return JS_FALSE;
}

因爲在C++層,這是一個重載過的函數,所以他的實現裏面有很多參數個數的判斷(關於重載問題請參考之前的章節)。過濾掉很多代碼,我們直接看關鍵部分:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (argc >= 3) {
            if (!thirdArgIsString) {
                //cc.MenuItemImage.create( normalImage, selectedImage, callback_fn, [this] )
                jsCallback = argv[last++];
                if (argc == 4) {
                    jsThis = argv[last];
                }
            }
            else {
                //cc.MenuItemImage.create( normalImage, selectedImage, disabledImage, callback_fn, [this] )
                if (argc >= 4) {
                    jsCallback = argv[last++];
                    if (argc == 5) {
                        jsThis = argv[last];
                    }
                }
            }
        }

在這裏我們從參數中取出回調函數和this,分別賦值給jsCallback和jsThis。


1
JSObject *obj = bind_menu_item<cocos2d::MenuItemImage>(cx, ret, jsCallback, jsThis);

由這句模板函數來實現回調的綁定,四個參數依次是,JS上下文,cc.MenuItemImage對應的C++對象,回調函數,和回調函數調用時的this。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<class T>
JSObject* bind_menu_item(JSContext *cx, T* nativeObj, jsval callback, jsval thisObj) {  
    js_proxy_t *p = jsb_get_native_proxy(nativeObj);
    if (p) {
        addCallBackAndThis(p->obj, callback, thisObj);
        return p->obj;
    else {
        js_type_class_t *classType = js_get_type_from_native<T>(nativeObj);
        assert(classType);
        JSObject *tmp = JS_NewObject(cx, classType->jsclass, classType->proto, classType->parentProto);
        // bind nativeObj <-> JSObject
        js_proxy_t *proxy = jsb_new_proxy(nativeObj, tmp);
        JS_AddNamedObjectRoot(cx, &proxy->obj, typeid(*nativeObj).name());      
        addCallBackAndThis(tmp, callback, thisObj);
        return tmp;
    }
}

繼續看bind_menu_item的實現。簡單說一下,因爲綁定的是一個JS函數,所以實際上,需要在SpiderMonkey裏面做這個綁定操作。傳進來的是一個C++對象(CCMenuItemImage類型),首先找到和這個C++對象對應的JS對象。如果找不到,就新建立一個。然後通過函數addCallBackAndThis執行綁定。


1
2
3
4
5
6
7
8
9
static void addCallBackAndThis(JSObject *obj, jsval callback, jsval &thisObj)
{
    if(callback != JSVAL_VOID) {
        ScriptingCore::getInstance()->setReservedSpot(0, obj, callback);
    }
    if(thisObj != JSVAL_VOID) {
        ScriptingCore::getInstance()->setReservedSpot(1, obj, thisObj);
    }
}


1
2
3
4
JSBool ScriptingCore::setReservedSpot(uint32_t i, JSObject *obj, jsval value) {
    JS_SetReservedSlot(obj, i, value);
    return JS_TRUE;
}


最終我們看到,存儲回調函數的方法是通過SpiderMonkey的ReservedSlot機制。0位存放的是回調函數,1位存放的是回調函數對應的this。


好,到此爲止,回調函數的綁定全部結束。


二、調用回調函數過程


現在我們看從C++層啓動JS回調的過程。我們省略掉事件派發機制,直接看按鍵事件發生時的調用代碼。在按鍵事件發生時,會調用MenuItemImage的父類MenuItem中的activate函數。該函數在CCMenuItem.cpp中。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void MenuItem::activate()
{
    if (_enabled)
    {
        if( _callback )
        {
            _callback(this);
        }
                                                                                                                            
        if (kScriptTypeNone != _scriptType)
        {
            BasicScriptData data(this);
            ScriptEvent scriptEvent(kMenuClickedEvent,&data);
            ScriptEngineManager::getInstance()->getScriptEngine()->sendEvent(&scriptEvent);
        }
    }
}


非常簡單,首先判斷按鍵是否可用。然後如果有C++層回調就調用。如果有腳本層(JS或lua)回調,就包裝一個kMenuClickedEvent事件,然後向對應的腳本引擎發送該事件。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
int ScriptingCore::sendEvent(ScriptEvent* evt)
{
    if (NULL == evt)
        return 0;
                                                                 
    JSAutoCompartment ac(_cx, _global);
                                                                    
    switch (evt->type)
    {
        case kNodeEvent:
            {
                return handleNodeEvent(evt->data);
            }
            break;
        case kMenuClickedEvent:
            {
                return handleMenuClickedEvent(evt->data);
            }
            break;
        case kTouchEvent:
            {
                return handleTouchEvent(evt->data);
            }
            break;
        case kTouchesEvent:
            {
                return handleTouchesEvent(evt->data);
            }
            break;
        case kKeypadEvent:
            {
                return handleKeypadEvent(evt->data);
            }
            break;
        case kAccelerometerEvent:
            {
                return handleAccelerometerEvent(evt->data);
            }
            break;
        default:
            break;
    }
                                                                    
    return 0;
}

JS通過ScriptingCore::sendEvent進行事件分發。kMenuClickedEvent事件派發給handleMenuClickedEvent函數來處理。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int ScriptingCore::handleMenuClickedEvent(void* data)
{
    if (NULL == data)
        return 0;
                                                           
    BasicScriptData* basicScriptData = static_cast<BasicScriptData*>(data);
    if (NULL == basicScriptData->nativeObject)
        return 0;
                                                           
    MenuItem* menuItem = static_cast<MenuItem*>(basicScriptData->nativeObject);
                                                           
    js_proxy_t * p = jsb_get_native_proxy(menuItem);
    if (!p) return 0;
    jsval retval;
    jsval dataVal;
    js_proxy_t *proxy = jsb_get_native_proxy(menuItem);
    dataVal = (proxy ? OBJECT_TO_JSVAL(proxy->obj) : JSVAL_NULL);
    executeJSFunctionFromReservedSpot(this->_cx, p->obj, dataVal, retval);
    return 1;
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void executeJSFunctionFromReservedSpot(JSContext *cx, JSObject *obj,
                                              jsval &dataVal, jsval &retval) {
    jsval func = JS_GetReservedSlot(obj, 0);
    if (func == JSVAL_VOID) { return; }
    jsval thisObj = JS_GetReservedSlot(obj, 1);
    JSAutoCompartment ac(cx, obj);
                       
    if (thisObj == JSVAL_VOID) {
        JS_CallFunctionValue(cx, obj, func, 1, &dataVal, &retval);
    else {
        assert(!JSVAL_IS_PRIMITIVE(thisObj));
        JS_CallFunctionValue(cx, JSVAL_TO_OBJECT(thisObj), func, 1, &dataVal, &retval);
    }
}


再次通過SpiderMonkey的ReservedSlot機制,取回相應的參數,最後通過JS_CallFunctionValue函數完成JS層回調函數的調用。




下篇繼續

本文出自 “做遊戲的老G” 博客,請務必保留此出處http://goldlion.blog.51cto.com/4127613/1354756

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章