回調(diào)函數(shù)是界面交互和接入各種第三方 SDK 的關(guān)鍵所在,因?yàn)榛卣{(diào)函數(shù)的 C++ 代碼是不能自動(dòng)生成的,一切的一切,都需要手寫完成。
比較不錯(cuò)的是,Cocos2d-x 引擎對(duì)于回調(diào)函數(shù)提供了完整的包裝機(jī)制。我們所需要做的就是了解這個(gè)機(jī)制,并使用他。學(xué)習(xí)引擎自己的代碼例子,可以比較快速準(zhǔn)確的上手這一機(jī)制。
首先,我們?cè)?Cocos2d-x 3.0 beta 版中,使用他自帶的工程創(chuàng)建工具,新建一個(gè)跨平臺(tái)的 JS 項(xiàng)目。按照慣例,這是一個(gè) helloworld 項(xiàng)目。在 XCode 運(yùn)行時(shí),我們可以看到:
http://wiki.jikexueyuan.com/project/cocos2d-x-from-cplusplus-js/images/10.jpg" alt="" />
可以看到右下角的回調(diào)按鈕。我們來看看他是怎么實(shí)現(xiàn)的。分成兩個(gè)過程來做:
一、綁定回調(diào)函數(shù)過程
首先,我們要去找回調(diào)函數(shù) JS 的綁定代碼,在 myApp.js 中,init 函數(shù)里面,可以看到如下代碼:
// 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 函數(shù)的第三個(gè)參數(shù),綁定了匿名回調(diào)函數(shù)。第四個(gè)參數(shù),傳入的是回調(diào)函數(shù)調(diào)用時(shí)的 this(如果不理解 JS 的 this 機(jī)制,請(qǐng)先閱讀一些 JS 的資料)。這些都是意圖和作用很明顯的 JS 代碼,不用細(xì)說。
然后,我們?nèi)タ吹讓訉?duì)應(yīng)執(zhí)行的 C++ 代碼。在 cocos2d_specifics.cpp 文件中,找到 js_cocos2dx_CCMenuItemImage_create 函數(shù)。
// "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;
}
因?yàn)樵?C++ 層,這是一個(gè)重載過的函數(shù),所以他的實(shí)現(xiàn)里面有很多參數(shù)個(gè)數(shù)的判斷(關(guān)于重載問題請(qǐng)參考之前的章節(jié))。過濾掉很多代碼,我們直接看關(guān)鍵部分:
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];
}
}
}
}
在這里我們從參數(shù)中取出回調(diào)函數(shù)和 this,分別賦值給 jsCallback 和 jsThis。
JSObject *obj = bind_menu_item<cocos2d::MenuItemImage>(cx, ret, jsCallback, jsThis);
由這句模板函數(shù)來實(shí)現(xiàn)回調(diào)的綁定,四個(gè)參數(shù)依次是,JS 上下文, cc.MenuItemImage 對(duì)應(yīng)的 C++ 對(duì)象,回調(diào)函數(shù),和回調(diào)函數(shù)調(diào)用時(shí)的 this。
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;
}
}
繼續(xù)看 bind_menu_item 的實(shí)現(xiàn)。簡(jiǎn)單說一下,因?yàn)榻壎ǖ氖且粋€(gè) JS 函數(shù),所以實(shí)際上,需要在 SpiderMonkey 里面做這個(gè)綁定操作。傳進(jìn)來的是一個(gè) C++ 對(duì)象(CCMenuItemImage 類型),首先找到和這個(gè) C++ 對(duì)象對(duì)應(yīng)的 JS 對(duì)象。如果找不到,就新建立一個(gè)。然后通過函數(shù) addCallBackAndThis 執(zhí)行綁定。
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);
}
}
JSBool ScriptingCore::setReservedSpot(uint32_t i, JSObject *obj, jsval value) {
JS_SetReservedSlot(obj, i, value);
return JS_TRUE;
}
最終我們看到,存儲(chǔ)回調(diào)函數(shù)的方法是通過 SpiderMonkey 的 ReservedSlot 機(jī)制。0位存放的是回調(diào)函數(shù),1位存放的是回調(diào)函數(shù)對(duì)應(yīng)的 this。
好,到此為止,回調(diào)函數(shù)的綁定全部結(jié)束。
二、調(diào)用回調(diào)函數(shù)過程
現(xiàn)在我們看從 C++ 層啟動(dòng)JS回調(diào)的過程。我們省略掉事件派發(fā)機(jī)制,直接看按鍵事件發(fā)生時(shí)的調(diào)用代碼。在按鍵事件發(fā)生時(shí),會(huì)調(diào)用 MenuItemImage 的父類 MenuItem 中的 activate 函數(shù)。該函數(shù)在 CCMenuItem.cpp 中。
void MenuItem::activate()
{
if (_enabled)
{
if( _callback )
{
_callback(this);
}
if (kScriptTypeNone != _scriptType)
{
BasicScriptData data(this);
ScriptEvent scriptEvent(kMenuClickedEvent,&data);
ScriptEngineManager::getInstance()->getScriptEngine()->sendEvent(&scriptEvent);
}
}
}
非常簡(jiǎn)單,首先判斷按鍵是否可用。然后如果有 C++ 層回調(diào)就調(diào)用。如果有腳本層(JS 或 lua)回調(diào),就包裝一個(gè) kMenuClickedEvent 事件,然后向?qū)?yīng)的腳本引擎發(fā)送該事件。
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 進(jìn)行事件分發(fā)。 kMenuClickedEvent 事件派發(fā)給 handleMenuClickedEvent 函數(shù)來處理。
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;
}
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 機(jī)制,取回相應(yīng)的參數(shù),最后通過 JS_CallFunctionValue 函數(shù)完成 JS 層回調(diào)函數(shù)的調(diào)用。
下篇繼續(xù)