【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 51CTO博客,原文链接:http://blog.51cto.com/goldlion/1354756,如需转载请自行联系原作者


相关实践学习
基于Hologres轻松玩转一站式实时仓库
本场景介绍如何利用阿里云MaxCompute、实时计算Flink和交互式分析服务Hologres开发离线、实时数据融合分析的数据大屏应用。
阿里云实时数仓实战 - 项目介绍及架构设计
课程简介 1)学习搭建一个数据仓库的过程,理解数据在整个数仓架构的从采集、存储、计算、输出、展示的整个业务流程。 2)整个数仓体系完全搭建在阿里云架构上,理解并学会运用各个服务组件,了解各个组件之间如何配合联动。 3&nbsp;)前置知识要求 &nbsp; 课程大纲 第一章&nbsp;了解数据仓库概念 初步了解数据仓库是干什么的 第二章&nbsp;按照企业开发的标准去搭建一个数据仓库 数据仓库的需求是什么 架构 怎么选型怎么购买服务器 第三章&nbsp;数据生成模块 用户形成数据的一个准备 按照企业的标准,准备了十一张用户行为表 方便使用 第四章&nbsp;采集模块的搭建 购买阿里云服务器 安装 JDK 安装 Flume 第五章&nbsp;用户行为数据仓库 严格按照企业的标准开发 第六章&nbsp;搭建业务数仓理论基础和对表的分类同步 第七章&nbsp;业务数仓的搭建&nbsp; 业务行为数仓效果图&nbsp;&nbsp;
相关文章
|
2月前
|
存储 安全 数据管理
探索C++中回调函数的数据结构和封装的权衡以及示例
探索C++中回调函数的数据结构和封装的权衡以及示例
74 4
|
2月前
|
存储 JSON 安全
【C++ JSON库 json值的创建手段】深入探究C++中JSON对象定位与操作:从引用到回调函数
【C++ JSON库 json值的创建手段】深入探究C++中JSON对象定位与操作:从引用到回调函数
67 0
|
1月前
|
人工智能 机器人 中间件
【C++】C++回调函数基本用法(详细讲解)
【C++】C++回调函数基本用法(详细讲解)
|
2月前
|
JavaScript
JS回调函数
JS回调函数
9 0
|
2月前
|
传感器 人工智能 算法
掌握C++中的状态-事件回调矩阵:打造强大的有限状态机
掌握C++中的状态-事件回调矩阵:打造强大的有限状态机
38 0
|
2月前
|
JavaScript 前端开发
js开发:请解释什么是ES6的async/await,以及它如何解决回调地狱问题。
ES6的`async/await`是基于Promise的异步编程工具,能以同步风格编写异步代码,提高代码可读性。它缓解了回调地狱问题,通过将异步操作封装为Promise,避免回调嵌套。错误处理更直观,类似同步的try...catch。
|
2月前
|
前端开发 JavaScript
js开发:请解释Promise是什么,以及它如何解决回调地狱(callback hell)问题。
Promise是JavaScript解决异步操作回调地狱的工具,代表未来可能完成的值。传统的回调函数嵌套导致代码难以维护,而Promise通过链式调用`.then()`和`.catch()`使异步流程清晰扁平。每个异步操作封装为Promise,成功时`.then()`传递结果,出错时`.catch()`捕获异常。ES6的`async/await`进一步简化Promise的使用,使异步代码更接近同步风格。
19 1
|
2月前
|
存储 JavaScript 前端开发
js开发:请解释什么是回调函数(callback function),并给出一个示例。
回调函数是JavaScript中处理异步编程的一种常见模式,常用于事件驱动和I/O操作。它们作为参数传递给其他函数,在特定条件满足或任务完成后被调用。例如,`asyncOperation`函数接受回调函数`handleResult`,在模拟的异步操作完成后,调用`handleResult`并传递结果。这使得程序员能在操作完成后执行后续任务。
22 1
|
2月前
|
JavaScript 前端开发 Java
Java Script中的回调函数,可以用来做什么
Java Script中的回调函数,可以用来做什么
10 0
|
2月前
|
JavaScript 前端开发 UED
解释 JavaScript 中的异步编程和回调函数。
解释 JavaScript 中的异步编程和回调函数。
17 0