cocos2dx-详细剖析lua(如何与lua集成,如何导出lua api,如何与lua交互)

本文详解lua是怎么跟c/c++交互的;cocos怎么利用luac/c++交互的技术,导出lua api的供脚本使用;cocos如何进行c++与lua混合编程,cocos有哪些重要lua接口。

一、lua与c、c++的交互

1、lua简介

lua是由c语言编写,c/c++程序可以包含lua库,利用提供的api进行lua脚本开发,lua提供了c与lua相互调用的接口。下面是lua的全部库文件:

lapi.c ldo.h lmathlib.c lstate.c lua.h
lapi.h ldump.c lmem.c lstate.h luaconf.h
lauxlib.c lfunc.c lmem.hlstring.clualib.h
lauxlib.h lfunc.h loadlib.clstring.hlundump.c
lbaselib.c lgc.c lobject.clstrlib.clundump.h
lcode.c lgc.h lobject.h ltable.c lvm.c
lcode.h linit.c lopcodes.c ltable.h lvm.h
ldblib.c liolib.c lopcodes.hltablib.clzio.c
ldebug.c llex.c loslib.cltm.clzio.h
ldebug.h llex.h lparser.cltm.hprint.c
ldo.c llimits.h lparser.h lua.c

这些文件中lua_开头的api是c api,luaL_开头的是auxiliary library,其它的是Lua functions

2、c、c++调用lua

可以利用lua编写一些代码,然后供c++调用,可以用lua编写配置文件,从中提取信息。下面是lua代码,描述了app的配置信息以及一个测试函数:

-- app config 
config = {
	version = "1.0",
	name = "myApp",
	date = "2016/6/25"
	author = "lewis" 
}

-- for function test
function add(x, y)
	return x + y
end
下面是c++访问lua的内容:

void LuaTest::btnClick_c_call_lua(CCObject* pSender, CCControlEvent event){
    //创建Lua状态机
    lua_State *L = luaL_newstate();
    
    //加载并执行Lua文件
    string path = CCFileUtils::sharedFileUtils()->fullPathForFilename("config.lua");
    if(luaL_dofile(L, path.c_str()))
    {
        cout<<"luaL_dofile error"<
输出:

version=1.0
name=myApp
date=2016/6/25
author=lewis
10 + 20 = 30.000000
lua执行时,要先创建一个lua状态,lua状态包含了lua栈,lua栈用来让c与lua通信,c通过lua提供的c api,把lua中的变量放到栈中,调用lua函数,获得lua变量值,设置lua变量值等操作。进行所有的c操作都是针对栈顶的,例如lua_getglobal、lua_getfield、lua_pushnumber都是往栈顶写元素。lua_tostring这种lua_toTYPE函数是获得lua中变量对应的c类型变量值。lua提供的api执行后的返回值不为0就是出错了,可以用来检查是否程序出错。luaL_dofile是个宏分两步,一步加载lua代码生成字节码,一步lua_pcall执行代码,就是把代码从头到尾跑一边,对于定义就是分配内存,对于函数调用就是执行函数。lua的详细api可以去官网看看:https://www.lua.org/manual/5.1/

3、lua调用c、c++

lua调用c,所提供的c函数签名必需是typedef int (*lua_CFunction) (lua_State *L);这种类型。编写好函数后,只需向lua的lua_State L注册一下就OK了,之后使用L执行的lua代码都可以访问在L注册的c函数。看上面定义,好像lua函数不好传参数给c函数啊!其实你可以在lua函数传任意参数,它会写到L的栈中,前面曾说过c与lua是通过一个栈通信的。

当lua函数带调用提供了N个参数时,lua最终调用的c函数可以通过上面讲的c/c++调用lua里面提供的接口提取出参数,进行计算,最后再把结果push到栈中返回给lua,详细图解如下:

cocos2dx-详细剖析lua(如何与lua集成,如何导出lua api,如何与lua交互)_第1张图片

图片地址:http://photo.yupoo.com/lewislufie/FEuF115o/medish.jpg

下面是代码,在lua中创建一个CCLabelTTF加到当前场景中。

int cAPI_addLabel(lua_State *L){
    printf("stack size:%d\n", lua_gettop(L));
    
    //获得位置x、y
    double y = lua_tonumber(L, -1);
    double x = lua_tonumber(L, -2);
    
    //create label
    CCString *str = CCString::createWithFormat("hello world\n x:%f y%f", x, y);
    Label *label = Label::create(str->getCString(), BIG);
    label->setPosition(ccp(x, y));
    CCDirector::sharedDirector()->getRunningScene()->addChild(label);
    
    lua_pushlightuserdata(L, label);//把指针传给lua
    
    return 1;
}

void LuaTest::btnClick_lua_call_c_2(CCObject* pSender, CCControlEvent event){
    //新建状态
    lua_State *L = luaL_newstate();
    
    luaL_openlibs(L);//we will use lua io api
    
    //向lua注册c函数
    lua_register(L, "cAPI_addLabel", cAPI_addLabel);
    
    //加载并执行脚本
    string path = CCFileUtils::sharedFileUtils()->fullPathForFilename("luacallc.lua");
    luaL_dofile(L, path.c_str());
    
    //关闭状态
    lua_close(L);
}
点击下面按钮出现最下面的标签并显示它的x、y值:

cocos2dx-详细剖析lua(如何与lua集成,如何导出lua api,如何与lua交互)_第2张图片

图片地址:http://photo.yupoo.com/lewislufie/FEv1Q9r7/medish.jpg
 
  

输出:

stack size:2
result:	userdata: 0x7a37c400
这里点击按钮后调用 LuaTest ::btnClick_lua_call_c_2( CCObject * pSender, CCControlEvent event),它会使用lua_register(L,"cAPI_addLabel", cAPI_addLabel);注册函数cAPI_addLabel,然后加载执行luacallc.lua这个文件,内容如下:

local x = cAPI_addLabel(320, 200)

print("result:", x)
上面代码创建标签,并把它加到当前场景,并输出它的返回值,返回值类型是userdata,就是一个包装了指针的类型,输出的值跟指针值一样。

函数注册后执行lua文件,lua里调用了cAPI_addLabel这个函数,lua首先会把L中的栈清空,然后把参数从左往右压栈,这也是上面x、y的获取是

 先double y =lua_tonumber(L, -1);再double x =lua_tonumber(L, -2);因为y在栈顶-1位置,x在下面-2的位置,c函数cAPI_addLabel取出栈中2个元素来设置标签位置。设置完后把标签对象的指针压栈,然后返回1,告诉lua引擎我这个函数返回1个返回值,然后返回到lua代码的执行,它从栈顶往下取1个数给result,最后输出result这个userdata元素的值。这里看出lua个c是如何通过栈交换数据的,c从栈中取数据是我们手动编码,lua从栈中取数据是lua引擎执行的。每次lua调c函数L的栈会清空,然后压入参数这个要注意,获取的返回值个数是根据c函数返回值确定的,这个也要注意下。

二、利器tolua++

1、cocos怎么把tolua++导出的c api给lua调用的

上面的lua调用c/c++已经展示了lua调用c函数并且传参数给它设置cocos对象位置,但是如果我们都这样编码,对于一个cocos当中的类,比如CCSprite有好多个函数,我们要编写符合lua调用的c接口,那得先从栈中取出参数,然后把参数传给对应的c++函数,执行后再把c++的函数结果压栈,这些操作在的cAPI_addLabel函数中可以找到,此外还要进行检查,万一lua那边传来的参数类型不对呢,就得用lua_isTYPE来判断类型,那工程量有点大啊。好在我们有工具tolua++,利用它可以轻松导出c api供lua调用。

下面是CCLabelTTF向lua注册的代码在LuaCocos2d.cpp里面可以找到

  tolua_beginmodule(tolua_S,"CCLabelTTF");
   tolua_function(tolua_S,"new",tolua_Cocos2d_CCLabelTTF_new00);
   tolua_function(tolua_S,"new_local",tolua_Cocos2d_CCLabelTTF_new00_local);
   tolua_function(tolua_S,".call",tolua_Cocos2d_CCLabelTTF_new00_local);
   tolua_function(tolua_S,"delete",tolua_Cocos2d_CCLabelTTF_delete00);
   tolua_function(tolua_S,"init",tolua_Cocos2d_CCLabelTTF_init00);
   tolua_function(tolua_S,"setString",tolua_Cocos2d_CCLabelTTF_setString00);
   tolua_function(tolua_S,"getString",tolua_Cocos2d_CCLabelTTF_getString00);
   tolua_function(tolua_S,"getHorizontalAlignment",tolua_Cocos2d_CCLabelTTF_getHorizontalAlignment00);
   tolua_function(tolua_S,"setHorizontalAlignment",tolua_Cocos2d_CCLabelTTF_setHorizontalAlignment00);
   tolua_function(tolua_S,"getVerticalAlignment",tolua_Cocos2d_CCLabelTTF_getVerticalAlignment00);
   tolua_function(tolua_S,"setVerticalAlignment",tolua_Cocos2d_CCLabelTTF_setVerticalAlignment00);
   tolua_function(tolua_S,"getDimensions",tolua_Cocos2d_CCLabelTTF_getDimensions00);
   tolua_function(tolua_S,"setDimensions",tolua_Cocos2d_CCLabelTTF_setDimensions00);
   tolua_function(tolua_S,"getFontSize",tolua_Cocos2d_CCLabelTTF_getFontSize00);
   tolua_function(tolua_S,"setFontSize",tolua_Cocos2d_CCLabelTTF_setFontSize00);
   tolua_function(tolua_S,"getFontName",tolua_Cocos2d_CCLabelTTF_getFontName00);
   tolua_function(tolua_S,"setFontName",tolua_Cocos2d_CCLabelTTF_setFontName00);
   tolua_function(tolua_S,"create",tolua_Cocos2d_CCLabelTTF_create00);
   tolua_function(tolua_S,"create",tolua_Cocos2d_CCLabelTTF_create01);
   tolua_function(tolua_S,"create",tolua_Cocos2d_CCLabelTTF_create02);
   tolua_function(tolua_S,"create",tolua_Cocos2d_CCLabelTTF_create03);
  tolua_endmodule(tolua_S);
这些代码以及后面贴的代码都是tolua++生成的,对上面最后注册的4个函数进行分析,lua中的函数名都是create,对应不同的c函数,其实lua只会调用里面的一个,至于到底调用哪个c函数,还得在c函数中分析参数决定。还有好多其它函数名字也是create,那都注册为名为create的函数不就出问题了,要指定lua里函数也是变量,它可以存在一个全局的表中,后面有时间把上面的lua调c/c++补全,少了个调c++。lua里面CCLabelTTF:create(arg1, arg2, arg3)这种调用可以改写成CCLabelTTF.create(CCLabelTTF, arg1, arg2, arg3),调用的是表CCLabelTTF的函数,然后传入CCLabelTTF, arg1, arg2, arg3四个参数。下面是cocos生成CCLabelTTF表的代码:

tolua_beginmodule(tolua_S,"CCLabelTTF");
TOLUA_API void tolua_beginmodule (lua_State* L, const char* name)
{
    if (name)
    {
        lua_pushstring(L,name);
        lua_rawget(L,-2);
    }
    else
        lua_pushvalue(L,LUA_GLOBALSINDEX);
}

lua_pushstring(L,name);压栈,lua_rawget(L,-2)等价于lua_gettable(L, -2)【Similar to lua_settable, but does a raw assignment (i.e., without metamethods).】

这里lua_pushstring后栈中应该一个元素呀,怎么还有-2呢?查看下面代码:

/* Open function */
TOLUA_API int tolua_Cocos2d_open (lua_State* tolua_S)
{
 tolua_open(tolua_S);
 tolua_reg_types(tolua_S);
/* method: create of class  CCLabelTTF */
#ifndef TOLUA_DISABLE_tolua_Cocos2d_CCLabelTTF_create03
static int tolua_Cocos2d_CCLabelTTF_create03(lua_State* tolua_S)
{
 tolua_Error tolua_err;
 if (
     !tolua_isusertable(tolua_S,1,"CCLabelTTF",0,&tolua_err) ||
     !tolua_isnoobj(tolua_S,2,&tolua_err)
 )
  goto tolua_lerror;
 else
 {
  {
   CCLabelTTF* tolua_ret = (CCLabelTTF*)  CCLabelTTF::create();
    int nID = (tolua_ret) ? (int)tolua_ret->m_uID : -1;
    int* pLuaID = (tolua_ret) ? &tolua_ret->m_nLuaID : NULL;
    toluafix_pushusertype_ccobject(tolua_S, nID, pLuaID, (void*)tolua_ret,"CCLabelTTF");
  }
 }
 return 1;
tolua_lerror:
 return tolua_Cocos2d_CCLabelTTF_create02(tolua_S);
}
#endif //#ifndef TOLUA_DISABLE

tolua_module(tolua_S,NULL,0); tolua_beginmodule(tolua_S,NULL); tolua_cclass(tolua_S,"kmMat4","kmMat4","",NULL); tolua_beginmodule(tolua_S,"kmMat4"); tolua_array(tolua_S,"mat",tolua_get_Cocos2d_kmMat4_mat,tolua_set_Cocos2d_kmMat4_mat); tolua_endmodule(tolua_S); tolua_function(tolua_S,"kmGLFreeAll",tolua_Cocos2d_kmGLFreeAll00); tolua_function(tolua_S,"kmGLPushMatrix",tolua_Cocos2d_kmGLPushMatrix00); tolua_function(tolua_S,"kmGLPopMatrix",tolua_Cocos2d_kmGLPopMatrix00); tolua_function(tolua_S,"kmGLMatrixMode",tolua_Cocos2d_kmGLMatrixMode00); tolua_function(tolua_S,"kmGLLoadIdentity",tolua_Cocos2d_kmGLLoadIdentity00);

 上面是注册cocos c代码的函数 
  tolua_Cocos2d_open,里面有一句 
  tolua_beginmodule 
  (tolua_S, 
  NULL 
  );代码如下 
  

TOLUA_API void tolua_beginmodule (lua_State* L, const char* name)
{
    if (name)
    {
        lua_pushstring(L,name);
        lua_rawget(L,-2);
    }
    else
        lua_pushvalue(L,LUA_GLOBALSINDEX);
}
调用else部分,此时栈顶是 LUA_GLOBALSINDEX一个全局表__G,我们可以在lua里可以通过__G["变量名"]访问全局变量,这里把__G压入栈中了,现在在栈顶位置-1.然后
tolua_beginmodule(tolua_S,"CCLabelTTF");会调用if部分代码,
  lua_pushstring (L,name);把字符串CCLabelTTF压栈,lua_rawget(L,-2);获得__G中字段为CCLabelTTF的表,现在__G["CCLabelTTF"] = {},栈中1个元素__G["CCLabelTTF"]。再看

tolua_function(tolua_S,"create",tolua_Cocos2d_CCLabelTTF_create00);tolua_function的代码如下:

TOLUA_API void tolua_function (lua_State* L, const char* name, lua_CFunction func)
{
    lua_pushstring(L,name);
    lua_pushcfunction(L,func);
    lua_rawset(L,-3);
}
这里lua_pushstring(L,name);把字符串 create压入栈,__G["CCLabelTTF"]在-2,"create"在-1,lua_pushcfunction(L,func);把函数tolua_Cocos2d_CCLabelTTF_create00压栈,现在__G["CCLabelTTF"]在-3,"create"在-2,tolua_Cocos2d_CCLabelTTF_create00在-1.最后lua_rawset(L,-3);设置__G["CCLabelTTF"].create=tolua_Cocos2d_CCLabelTTF_create00,栈中一个元素__G["CCLabelTTF"],所以现在lua可以通过CCLabelTTF:create(a,b,c...)来调用tolua_Cocos2d_CCLabelTTF_create00这个函数了。tolua_Cocos2d_CCLabelTTF_create01、tolua_Cocos2d_CCLabelTTF_create02、tolua_Cocos2d_CCLabelTTF_create03通过相同的方法注册,它们都是通过CCLabelTTF:create(a,b,c...)调用,其实__G["CCLabelTTF"].create的值被最后一个注册覆盖了,即被tolua_Cocos2d_CCLabelTTF_create03给覆盖了,所以最好__G["CCLabelTTF"].create=tolua_Cocos2d_CCLabelTTF_create03,lua里调用的是这个函数,代码如下:

/* method: create of class  CCLabelTTF */
#ifndef TOLUA_DISABLE_tolua_Cocos2d_CCLabelTTF_create03
static int tolua_Cocos2d_CCLabelTTF_create03(lua_State* tolua_S)
{
 tolua_Error tolua_err;
 if (
     !tolua_isusertable(tolua_S,1,"CCLabelTTF",0,&tolua_err) ||
     !tolua_isnoobj(tolua_S,2,&tolua_err)
 )
  goto tolua_lerror;
 else
 {
  {
   CCLabelTTF* tolua_ret = (CCLabelTTF*)  CCLabelTTF::create();
    int nID = (tolua_ret) ? (int)tolua_ret->m_uID : -1;
    int* pLuaID = (tolua_ret) ? &tolua_ret->m_nLuaID : NULL;
    toluafix_pushusertype_ccobject(tolua_S, nID, pLuaID, (void*)tolua_ret,"CCLabelTTF");
  }
 }
 return 1;
tolua_lerror:
 return tolua_Cocos2d_CCLabelTTF_create02(tolua_S);
}
#endif //#ifndef TOLUA_DISABLE
前面if()里面代码是进行类型检查,如果不是CCLabelTTF类型,就跳到tolua__lerror.tolua_isnoobj(tolua_S,2,&tolua_err)代码如下:

TOLUA_API int tolua_isnoobj (lua_State* L, int lo, tolua_Error* err)
{
    if (lua_gettop(L)index = lo;
    err->array = 0;
    err->type = "[no object]";
    return 0;
}
if (lua_gettop(L)tolua_isnoobj的第二个参数,这里是小于2,也就是一个参数,它用于检查参数是否匹配。else部分就是调用cocos中CCLabelTTF的相关函数了,第一个参数是lua中的CCLabelTTF表,调用CCLabelTTF:create(a,b,c...)时会把CCLabelTTF压栈,它是第一个压栈的,它是一个lua表。 tolua_isusertable(tolua_S,1,"CCLabelTTF",0,&tolua_err)代码如下:

TOLUA_API int tolua_isusertable (lua_State* L, int lo, const char* type, int def, tolua_Error* err)
{
    if (def && lua_gettop(L)index = lo;
    err->array = 0;
    err->type = type;
    return 0;
}
上面重点是lua_isusertable(L,lo,type)代码如下:

static  int lua_isusertable (lua_State* L, int lo, const char* type)
{
    int r = 0;
    if (lo < 0) lo = lua_gettop(L)+lo+1;
    lua_pushvalue(L,lo);
    lua_rawget(L,LUA_REGISTRYINDEX);  /* get registry[t] */
    if (lua_isstring(L,-1))
    {
        r = strcmp(lua_tostring(L,-1),type)==0;
        if (!r)
        {
            /* try const */
            lua_pushstring(L,"const ");
            lua_insert(L,-2);
            lua_concat(L,2);
            r = lua_isstring(L,-1) && strcmp(lua_tostring(L,-1),type)==0;
        }
    }
    lua_pop(L, 1);
    return r;
}
上面用if (lua_isstring(L,-1))检查栈顶是否是字符串,然后用strcmp(lua_tostring(L,-1),type)跟type这个类型名进行比较,就是跟字符串 CCLabelTTF比较。lua_rawget(L,LUA_REGISTRYINDEX);是获得注册表中的名字,也就是函数CCLabelTTF这个表名字。tolua_lerror:部分是调用tolua_Cocos2d_CCLabelTTF_create02,因为可能参数不对,客户调用的是其它重载函数。查看代码tolua_Cocos2d_CCLabelTTF_create02不满足会继续调用
tolua_Cocos2d_CCLabelTTF_create1-》tolua_Cocos2d_CCLabelTTF_create00, tolua_Cocos2d_CCLabelTTF_create00的tolua_lerror:部分就是真正出错处理了,这里不追踪错误处理,继续看下非静态函数setFontName对应的c函数的代码:

/* method: setFontName of class  CCLabelTTF */
#ifndef TOLUA_DISABLE_tolua_Cocos2d_CCLabelTTF_setFontName00
static int tolua_Cocos2d_CCLabelTTF_setFontName00(lua_State* tolua_S)
{
#ifndef TOLUA_RELEASE
 tolua_Error tolua_err;
 if (
     !tolua_isusertype(tolua_S,1,"CCLabelTTF",0,&tolua_err) ||
     !tolua_isstring(tolua_S,2,0,&tolua_err) ||
     !tolua_isnoobj(tolua_S,3,&tolua_err)
 )
  goto tolua_lerror;
 else
#endif
 {
  CCLabelTTF* self = (CCLabelTTF*)  tolua_tousertype(tolua_S,1,0);
  const char* fontName = ((const char*)  tolua_tostring(tolua_S,2,0));
#ifndef TOLUA_RELEASE
  if (!self) tolua_error(tolua_S,"invalid 'self' in function 'setFontName'", NULL);
#endif
  {
   self->setFontName(fontName);
  }
 }
 return 0;
#ifndef TOLUA_RELEASE
 tolua_lerror:
 tolua_error(tolua_S,"#ferror in function 'setFontName'.",&tolua_err);
 return 0;
#endif
}
#endif //#ifndef TOLUA_DISABLE
上面代码跟create的c函数代码区别有两点:一点是没有重载不会由于参数个数不对去调用其它重载函数,一个是tolua_isusertype(tolua_S,1,"CCLabelTTF",0,&tolua_err),它是判断是否是userdata类型,而不是usertable这个lua表类型。 tolua_isusertype代码如下:

TOLUA_API int tolua_isusertype (lua_State* L, int lo, const char* type, int def, tolua_Error* err)
{
    if (def && lua_gettop(L)index = lo;
    err->array = 0;
    err->type = type;
    return 0;
}
上面lua_isusertype(L,lo,type)代码如下:

static int lua_isusertype (lua_State* L, int lo, const char* type)
{
    if (!lua_isuserdata(L,lo)) {
        if (!push_table_instance(L, lo)) {
            return 0;
        };
    };
    {
        /* check if it is of the same type */
        int r;
        const char *tn;
        if (lua_getmetatable(L,lo))        /* if metatable? */
        {
            lua_rawget(L,LUA_REGISTRYINDEX);  /* get registry[mt] */
            tn = lua_tostring(L,-1);
            r = tn && (strcmp(tn,type) == 0);
            lua_pop(L, 1);
            if (r)
                return 1;
            else
            {
                /* check if it is a specialized class */
                lua_pushstring(L,"tolua_super");
                lua_rawget(L,LUA_REGISTRYINDEX); /* get super */
                lua_getmetatable(L,lo);
                lua_rawget(L,-2);                /* get super[mt] */
                if (lua_istable(L,-1))
                {
                    int b;
                    lua_pushstring(L,type);
                    lua_rawget(L,-2);                /* get super[mt][type] */
                    b = lua_toboolean(L,-1);
                    lua_pop(L,3);
                    if (b)
                        return 1;
                }
            }
        }
    }
    return 0;
}
lua_isuserdata(L,lo)是lua提供的判断是否是userdata类型,上面一开始的if (!lua_isuserdata(L,lo))进行了类型判断。后面部分获得注册的类型名字跟字符串比较,需要一样,这种情况就会被拒绝CCLabelTTF.create(其它对象的userdata, a, b, c...),它把其它对象的userdata(包含指针)传给了 create函数。

上面研究了cocos怎么把tolua++导出的c函数注册为lua中相应的表的函数变量,以及lua调用这些函数,对应的c函数是怎么最终调用cocos的api的。需要区别类的静态函数与非静态有所区别,一个检查是否是表,一个检查是否是userdata.还有重载的函数它们只有一个注册,需要根据参数判断最终调用哪个。

2、tolua++怎么把cocos的api导出为c函数

下面进行下分析,主要就是怎么用tolua++这个工具。这个工具可以在cocos工程下找到,也可以到网上下载,这个地址是tolua++的参考手册:https://www8.cs.umu.se/kurser/TDBD12/VT04/lab/lua/tolua%2B%2B.html ,里面介绍的很详细。cocos的tolua++里面的readme内容如下:

1. Generating the lua<-->C bindings with tolua++

    Build scripts for windows (build.bat) and unix (build.sh) are provided
    to generate the relevant files after modifying the .pkg files.  These
    scripts basically run the following command:

        tolua++.exe -L basic.lua -o LuaCocos2d.cpp Cocos2d.pkg

    This will generate the bindings file and patch it with come cocos2dx
    specific modifications.

    On POSIX systems you can also just run "make" to build the bindings
    if/when you change .pkg files.

2. Writing .pkg files

    1) enum keeps the same
    2) remove CC_DLL for the class defines, pay attention to multi inherites
    3) remove inline keyword for declaration and implementation
    4) remove public protect and private
    5) remove the decalration of class member variable
    6) keep static keyword
    7) remove memeber functions that declared as private or protected 
上面是tolua++.exe(mac下tolua++) -L basic.lua -o LuaCocos2d.cpp Cocos2d.pkg,需要先进入cocos的tolua++目录,里面有好多pkg文件,也有basic.lua.这句命令会先执行basic.lua脚本,然后根据 Cocos2d.pkg文件生成 LuaCocos2d.cpp。Cocos2d.pkg部分如下:

$#include "LuaCocos2d.h"

$pfile "matrix.pkg"
$pfile "ccTypes.pkg"
$pfile "CCObject.pkg"
$pfile "CCCommon.pkg"
$pfile "CCGeometry.pkg"
$pfile "CCArray.pkg"
$pfile "CCDictionary.pkg"
$pfile "CCString.pkg"
$pfile "CCPointExtension.pkg"
$pfile "CCEGLViewProtocol.pkg"
$#include包含的头文件,前面要加上$,后面$pfile是包含的其它pkg文件,你可以变下一个pkg文件放到这个目录下,然后在这个文件里加上$pfile " name of your pkg file ",再执行 tolua++ -L basic.lua -o LuaCocos2d.cpp Cocos2d.pkg生成LuaCocos2d.cpp,里面就是生成了一圈的静态c函数,还有一个向lua注册函数的接口。

不过不推荐这种方法,我们可以生成自己的的cpp加到项目中使用,下面是一个例子:

#ifndef luaApi_hpp
#define luaApi_hpp

#include 
#include "cocos2D.h"

USING_NS_CC;

enum FontSize{
    SMALL = 10,
    MEDIUM = 20,
    BIG = 30,
};

class Label : public CCLabelTTF{
public:
    Label();
    virtual ~Label();
    
    static Label* create(const char *label, FontSize size);
    
    void setPosition(const CCPoint &point);
};

#endif /* luaApi_hpp */
上面是写的c++的头文件,按照规格enum keeps the same与remove public protect and private修改如下:

$#include "Label.h"

enum FontSize{
    SMALL = 10,
    MEDIUM = 20,
    BIG = 30,
};

class Label : public CCLabelTTF{
    Label();
    virtual ~Label();
    
    static Label* create(const char *label, FontSize size);
    
    void setPosition(const CCPoint &point);
};
$#include "Label.h"是会在生成的cpp加入该头文件,因为生成的cpp会调用 Label.h中声明的方法,所以这里要包含,也可以不包含,生成后手动添加。下面进入pkg所在目录执行下面命令:

/Users/lewis/Desktop/cocos2dx-2.2.2pak/tools/tolua++/tolua++ -o LuaLabel2.cpp Label.pkg 
上面是在mac下Label.pkg所在目录执行的,可以吧文件夹直接拖到command上就可以进入这个目录,然后把tolua++工具拖进去再输入 -o指定生成的文件名字,最后加上pkg文件。这里不再贴出生成的文件代码了,因为跟  LuaCocos2d.cpp里面的代码形式一样。

3、怎么使用cocos提供的接口执行lua文件,实现脚本化或者半脚本化编程

在项目中加入导出的cpp文件,然后向lua虚拟机注册这些c函数,就可以在lua中使用了,查看下面代码:

   //新建状态
    lua_State *L = luaL_newstate();
    
    luaL_openlibs(L);//we will use lua io api
    
    //向lua注册c函数
    lua_register(L, "cAPI_addLabel", cAPI_addLabel);
    
    //加载并执行脚本
    string path = CCFileUtils::sharedFileUtils()->fullPathForFilename("luacallc.lua");
    luaL_dofile(L, path.c_str());
    
    //关闭状态
    lua_close(L);
上面代码是用lua接口注册,以及加载执行脚本,其中文件的绝对路径需要通过CCFileUtils获得。假如我们进行半脚本化编程(就是部分逻辑有lua完成,而不是全部逻辑交给lua).我们在要调用lua的函数里都需要写这两句。而且我们还需要加入错误处理代码,代码量还是有点的,所以需要包装下,真好cocos提供了包装,代码如下

 CCLuaEngine::defaultEngine()->executeScriptFile("luacallc.lua");
int CCLuaEngine::executeScriptFile(const char* filename)
{
    int ret = m_stack->executeScriptFile(filename);
    m_stack->clean();
    return ret;
}
int CCLuaStack::executeScriptFile(const char* filename)
{
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
    std::string code("require \"");
    code.append(filename);
    code.append("\"");
    return executeString(code.c_str());
#else
    std::string fullPath = CCFileUtils::sharedFileUtils()->fullPathForFilename(filename);
    ++m_callFromLua;
    int nRet = luaL_dofile(m_state, fullPath.c_str());
    --m_callFromLua;
    CC_ASSERT(m_callFromLua >= 0);
    // lua_gc(m_state, LUA_GCCOLLECT, 0);
    
    if (nRet != 0)
    {
        CCLOG("[LUA ERROR] %s", lua_tostring(m_state, -1));
        lua_pop(m_state, 1);
        return nRet;
    }
    return 0;
#endif
}
上面m_stack->executeScriptFile(filename)里面有std::string fullPath = CCFileUtils::sharedFileUtils()->fullPathForFilename(filename);与luaL_dofile(m_state, fullPath.c_str());两句代码,这个真是对我们的前面操作包装,m_stack->clean();代码是清空栈。

CCLuaEngine::defaultEngine()->executeString("print('hello world')");
这句代码可以把参数作为lua代码执行,也是对lua的c api的封装。下面代代码是设置脚本引擎与注册函数。

    CCScriptEngineManager::sharedManager()->setScriptEngine(CCLuaEngine::defaultEngine());
    extern int  tolua_Label_open (lua_State* tolua_S);
    tolua_Label_open(CCLuaEngine::defaultEngine()->getLuaStack()->getLuaState());
其中CCScriptEngineManager::sharedManager()->setScriptEngine(CCLuaEngine::defaultEngine());只能调用一次,最好在程序开头调用,如果每次调用lua代码前都调用就会出问题。 setScriptEngine的代码如下:

void CCScriptEngineManager::setScriptEngine(CCScriptEngineProtocol *pScriptEngine)
{
    removeScriptEngine();
    m_pScriptEngine = pScriptEngine;
}
void CCScriptEngineManager::removeScriptEngine(void)
{
    if (m_pScriptEngine)
    {
        delete m_pScriptEngine;
        m_pScriptEngine = NULL;
    }
}
在设置脚本引擎对象前会先删除,CCLuaEngine::defaultEngine()是单件对象,如果把这个单件删了,再赋值就是一个空指针了,会出错。CCScriptEngineManager只是一个脚本引擎的管理类,引擎可以是lua,也可以是javascript。CCLuaEngine::defaultEngine()是获得lua引擎,代码如下:

CCLuaEngine* CCLuaEngine::defaultEngine(void)
{
    if (!m_defaultEngine)
    {
        m_defaultEngine = new CCLuaEngine();
        m_defaultEngine->init();
    }
    return m_defaultEngine;
}
bool CCLuaEngine::init(void)
{
    m_stack = CCLuaStack::create();
    m_stack->retain();
    return true;
}
CCLuaStack *CCLuaStack::create(void)
{
    CCLuaStack *stack = new CCLuaStack();
    stack->init();
    stack->autorelease();
    return stack;
}
bool CCLuaStack::init(void)
{
    m_state = lua_open();
    luaL_openlibs(m_state);
    tolua_Cocos2d_open(m_state);
    toluafix_open(m_state);

    // Register our version of the global "print" function
    const luaL_reg global_functions [] = {
        {"print", lua_print},
        {NULL, NULL}
    };
    luaL_register(m_state, "_G", global_functions);
    tolua_CocoStudio_open(m_state);
#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS || CC_TARGET_PLATFORM == CC_PLATFORM_MAC)
    CCLuaObjcBridge::luaopen_luaoc(m_state);
#endif
    register_all_cocos2dx_manual(m_state);
    register_all_cocos2dx_extension_manual(m_state);
    register_all_cocos2dx_studio_manual(m_state);
    // add cocos2dx loader
    addLuaLoader(cocos2dx_lua_loader);

    return true;
}
上面最后代码是真正的建立lua状态,还启用了lua标准库,以及注册使用tolua_Cocos2d_open(m_state)注册导出的c api。CCLuaObjcBridge::luaopen_luaoc(m_state);注册lua与oc的通信函数,有兴趣的可以深入研究下。后面几个是注册扩展的c api,最后addLuaLoader(cocos2dx_lua_loader);添加引导器。luaL_register(m_state, "_G", global_functions);注册一个全局模块,这个模块就一个函数"print", lua_print,在lua中就是我们用的print,它覆盖了lua自带的print. lua_print代码如下:

int lua_print(lua_State * luastate)
{
    int nargs = lua_gettop(luastate);

    std::string t;
    for (int i=1; i <= nargs; i++)
    {
        if (lua_istable(luastate, i))
            t += "table";
        else if (lua_isnone(luastate, i))
            t += "none";
        else if (lua_isnil(luastate, i))
            t += "nil";
        else if (lua_isboolean(luastate, i))
        {
            if (lua_toboolean(luastate, i) != 0)
                t += "true";
            else
                t += "false";
        }
        else if (lua_isfunction(luastate, i))
            t += "function";
        else if (lua_islightuserdata(luastate, i))
            t += "lightuserdata";
        else if (lua_isthread(luastate, i))
            t += "thread";
        else
        {
            const char * str = lua_tostring(luastate, i);
            if (str)
                t += lua_tostring(luastate, i);
            else
                t += lua_typename(luastate, lua_type(luastate, i));
        }
        if (i!=nargs)
            t += "\t";
    }
    CCLOG("[LUA-print] %s", t.c_str());

    return 0;
}
上面代码就是简单输出print的参数类型,如果字符串或数字就输出为字符串。lua的print被覆盖了,之前print(userdata variable)输出的是指针,现在输出userdata,要想输出指针值,只要显式调用tostring( userdata variable)就可以了。

4、cocos在对返回userdata类型的c api的特殊处理

cocos对返回userdata类型的c api做了特殊处理。查看下面代码:

class CC_DLL CCObject : public CCCopying
{
public:
    // object id, CCScriptSupport need public m_uID
    unsigned int        m_uID;
    // Lua reference id
    int                 m_nLuaID;
CCObject::CCObject(void)
: m_nLuaID(0)
, m_uReference(1) // when the object is created, the reference count of it is 1
, m_uAutoReleaseCount(0)
{
    static unsigned int uObjectCount = 0;

    m_uID = ++uObjectCount;
}


 
  
CCObject::~CCObject(void)
{
    // if the object is managed, we should remove it
    // from pool manager
    if (m_uAutoReleaseCount > 0)
    {
        CCPoolManager::sharedPoolManager()->removeObject(this);
    }

    // if the object is referenced by Lua engine, remove it
    if (m_nLuaID)
    {
        CCScriptEngineManager::sharedManager()->getScriptEngine()->removeScriptObjectByCCObject(this);
    }
    else
    {
        CCScriptEngineProtocol* pEngine = CCScriptEngineManager::sharedManager()->getScriptEngine();
        if (pEngine != NULL && pEngine->getScriptType() == kScriptTypeJavascript)
        {
            pEngine->removeScriptObjectByCCObject(this);
        }
    }
}
m_nLuaID不为0时,执行CCScriptEngineManager::sharedManager()->getScriptEngine()->removeScriptObjectByCCObject(this)删除userdata。 m_nLuaID的值怎么确定的呢?看下面代码:

/* method: autorelease of class  CCObject */
#ifndef TOLUA_DISABLE_tolua_Cocos2d_CCObject_autorelease00
static int tolua_Cocos2d_CCObject_autorelease00(lua_State* tolua_S)
{
#ifndef TOLUA_RELEASE
 tolua_Error tolua_err;
 if (
     !tolua_isusertype(tolua_S,1,"CCObject",0,&tolua_err) ||
     !tolua_isnoobj(tolua_S,2,&tolua_err)
 )
  goto tolua_lerror;
 else
#endif
 {
  CCObject* self = (CCObject*)  tolua_tousertype(tolua_S,1,0);
#ifndef TOLUA_RELEASE
  if (!self) tolua_error(tolua_S,"invalid 'self' in function 'autorelease'", NULL);
#endif
  {
   CCObject* tolua_ret = (CCObject*)  self->autorelease();
    int nID = (tolua_ret) ? (int)tolua_ret->m_uID : -1;
    int* pLuaID = (tolua_ret) ? &tolua_ret->m_nLuaID : NULL;
    toluafix_pushusertype_ccobject(tolua_S, nID, pLuaID, (void*)tolua_ret,"CCObject");
  }
 }
 return 1;
#ifndef TOLUA_RELEASE
 tolua_lerror:
 tolua_error(tolua_S,"#ferror in function 'autorelease'.",&tolua_err);
 return 0;
#endif
}
#endif //#ifndef TOLUA_DISABLE

上面代码是任选的一个导出的c api,int nID = (tolua_ret) ? (int)tolua_ret->m_uID : -1;表示函数有返回值,也就是函数如果返回userdata给lua,那么nID=m_uID,m_uID这个值是CCObject构造函数时得到的,然后有返回值就会把m_nLuaID的地址给pLuaID,接着调用toluafix_pushusertype_ccobject(tolua_S, nID, pLuaID, (void*)tolua_ret,"CCObject"),代码如下:

TOLUA_API int toluafix_pushusertype_ccobject(lua_State* L,
                                             int refid,
                                             int* p_refid,
                                             void* ptr,
                                             const char* type)
{
    if (ptr == NULL || p_refid == NULL)
    {
        lua_pushnil(L);
        return -1;
    }
    
    if (*p_refid == 0)
    {
        *p_refid = refid;
        
        lua_pushstring(L, TOLUA_REFID_PTR_MAPPING);
        lua_rawget(L, LUA_REGISTRYINDEX);                           /* stack: refid_ptr */
        lua_pushinteger(L, refid);                                  /* stack: refid_ptr refid */
        lua_pushlightuserdata(L, ptr);                              /* stack: refid_ptr refid ptr */
        
        lua_rawset(L, -3);                  /* refid_ptr[refid] = ptr, stack: refid_ptr */
        lua_pop(L, 1);                                              /* stack: - */
        
        lua_pushstring(L, TOLUA_REFID_TYPE_MAPPING);
        lua_rawget(L, LUA_REGISTRYINDEX);                           /* stack: refid_type */
        lua_pushinteger(L, refid);                                  /* stack: refid_type refid */
        lua_pushstring(L, type);                                    /* stack: refid_type refid type */
        lua_rawset(L, -3);                /* refid_type[refid] = type, stack: refid_type */
        lua_pop(L, 1);                                              /* stack: - */
        
        //printf("[LUA] push CCObject OK - refid: %d, ptr: %x, type: %s\n", *p_refid, (int)ptr, type);
    }
    
    tolua_pushusertype_and_addtoroot(L, ptr, type);
    return 0;
}
if (*p_refid == 0)*p_refid = refid;就是如果m_nLuaID=0,那么m_nLuaID=m_uID,可以看出所有导出的c api如果有返回值,那么它的m_nLuaID会不等于0,所以在CCObject的析构函数时,可以删除userdata的一些脚本数据。一般返回值(没指针)不会有这样操作,代码如下:

/* get function: y of class  CCPoint */
#ifndef TOLUA_DISABLE_tolua_get_CCPoint_y
static int tolua_get_CCPoint_y(lua_State* tolua_S)
{
  CCPoint* self = (CCPoint*)  tolua_tousertype(tolua_S,1,0);
#ifndef TOLUA_RELEASE
  if (!self) tolua_error(tolua_S,"invalid 'self' in accessing variable 'y'",NULL);
#endif
  tolua_pushnumber(tolua_S,(lua_Number)self->y);
 return 1;
}
#endif //#ifndef TOLUA_DISABLE
上面返回值直接压栈,没有用到m_nLuaID,这种返回值不会记录额外的信息。

5、cocos提供的重要的与lua通信的接口

cocos为我们提供了一些接口直接调用脚本,也就是脚本事件。游戏中的每个节点可以添加脚本处理。CCNode这个类型并没有导出onEnter、onExit、

cleanup、onEnterTransitionDidFinish、OnExitTransitionDidStart这几个函数,因为他们不是供客户直接调用的,而是调用时间写死了,onEnter是在节点加入一个真正running的节点,或者加入一个非running父节点然后父节点running了这两种情况下调用的。可以继承CCNode,然后重写onEnter并且调用CCNode的onEnter,不过不推荐,可以重写init函数完成。而在lua脚本中,我们直接用c api对于的lua api创建的节点,要想给它添加节点,添加动作,我们不想直接创建,然后后面接着写添加节点跟动作的代码,因为脚本被执行时,这些代码都会被执行,假如这个节点根本没用的,代码也被执行了。要想避免这种情况可以向cocos注册脚本事件,等到这个节点真正被使用时,才会调用它的onEnter等方法。注册脚本事件代码如下:

void CCNode::registerScriptHandler(int nHandler)
{
    unregisterScriptHandler();
    m_nScriptHandler = nHandler;
    LUALOG("[LUA] Add CCNode event handler: %d", m_nScriptHandler);
}
对于导出的c api如下:

/* method: registerScriptHandler of class  CCNode */
#ifndef TOLUA_DISABLE_tolua_Cocos2d_CCNode_registerScriptHandler00
static int tolua_Cocos2d_CCNode_registerScriptHandler00(lua_State* tolua_S)
{
#ifndef TOLUA_RELEASE
 tolua_Error tolua_err;
 if (
     !tolua_isusertype(tolua_S,1,"CCNode",0,&tolua_err) ||
     (tolua_isvaluenil(tolua_S,2,&tolua_err) || !toluafix_isfunction(tolua_S,2,"LUA_FUNCTION",0,&tolua_err)) ||
     !tolua_isnoobj(tolua_S,3,&tolua_err)
 )
  goto tolua_lerror;
 else
#endif
 {
  CCNode* self = (CCNode*)  tolua_tousertype(tolua_S,1,0);
  LUA_FUNCTION funcID = (  toluafix_ref_function(tolua_S,2,0));
#ifndef TOLUA_RELEASE
  if (!self) tolua_error(tolua_S,"invalid 'self' in function 'registerScriptHandler'", NULL);
#endif
  {
   self->registerScriptHandler(funcID);
  }
 }
 return 0;
#ifndef TOLUA_RELEASE
 tolua_lerror:
 tolua_error(tolua_S,"#ferror in function 'registerScriptHandler'.",&tolua_err);
 return 0;
#endif
}
#endif //#ifndef TOLUA_DISABLE
onEnter代码如下:

void CCNode::onEnter()
{
    //fix setTouchEnabled not take effect when called the function in onEnter in JSBinding.
    m_bRunning = true;

    if (m_eScriptType != kScriptTypeNone)
    {
        CCScriptEngineManager::sharedManager()->getScriptEngine()->executeNodeEvent(this, kCCNodeOnEnter);
    }

    //Judge the running state for prevent called onEnter method more than once,it's possible that this function called by addChild  
    if (m_pChildren && m_pChildren->count() > 0)
    {
        CCObject* child;
        CCNode* node;
        CCARRAY_FOREACH(m_pChildren, child)
        {
            node = (CCNode*)child;
            if (!node->isRunning())
            {
                node->onEnter();
            }            
        }
    }

    this->resumeSchedulerAndActions();   
}
当m_eScriptType != kScriptTypeNone时会调用CCScriptEngineManager::sharedManager()->getScriptEngine()->executeNodeEvent(this, kCCNodeOnEnter),会调用脚本的方法, executeNodeEvent代码如下:

int CCLuaEngine::executeNodeEvent(CCNode* pNode, int nAction)
{
    int nHandler = pNode->getScriptHandler();
    if (!nHandler) return 0;
    
    switch (nAction)
    {
        case kCCNodeOnEnter:
            m_stack->pushString("enter");
            break;
            
        case kCCNodeOnExit:
            m_stack->pushString("exit");
            break;
            
        case kCCNodeOnEnterTransitionDidFinish:
            m_stack->pushString("enterTransitionFinish");
            break;
            
        case kCCNodeOnExitTransitionDidStart:
            m_stack->pushString("exitTransitionStart");
            break;
            
        case kCCNodeOnCleanup:
            m_stack->pushString("cleanup");
            break;
            
        default:
            return 0;
    }
    int ret = m_stack->executeFunctionByHandler(nHandler, 1);
    m_stack->clean();
    return ret;
}
编写的lua脚本如下:

function eventHandle(event)
	if event = "enter" then
		-- call enter handler
	elseif event = "exit" then
		-- call exit handler
	elseif event = "enterTransitionFinish" then
		-- call enterTransitionFinish handler
	elseif event = "exitTransitionStart" then
		-- call exitTransitionStart handler
	elseif event = "cleanup" then
		-- call cleanup handler
	end
end
此外cocos提供了卸载脚本事件的接口。除了这些脚本事件外,cocos还提供了 CCNode ::scheduleUpdateWithPriorityLua( int  nHandler,  int  priority)供lua使用,代码如下:

void CCNode::scheduleUpdateWithPriorityLua(int nHandler, int priority)
{
    unscheduleUpdate();
    m_nUpdateScriptHandler = nHandler;
    m_pScheduler->scheduleUpdateForTarget(this, priority, !m_bRunning);
}
上面代码,把这个节点加入到调度这个单列对象的调度列表中。m_pScheduler每帧会调用它的update来执行脚本,跟踪代码如下:

// Draw the Scene
void CCDirector::drawScene(void)
{
    // calculate "global" dt
    calculateDeltaTime();

    //tick before glClear: issue #533
    if (! m_bPaused)
    {
        m_pScheduler->update(m_fDeltaTime);
    }
上面是mainloop每帧处理时调用m_pScheduler->update(m_fDeltaTime);m_pScheduler->update(m_fDeltaTime);代码如下:

// main loop
void CCScheduler::update(float dt)
{
    m_bUpdateHashLocked = true;

    if (m_fTimeScale != 1.0f)
    {
        dt *= m_fTimeScale;
    }

    // Iterate over all the Updates' selectors
    tListEntry *pEntry, *pTmp;

    // updates with priority < 0
    DL_FOREACH_SAFE(m_pUpdatesNegList, pEntry, pTmp)
    {
        if ((! pEntry->paused) && (! pEntry->markedForDeletion))
        {
            pEntry->target->update(dt);
        }
    }

    // updates with priority == 0
    DL_FOREACH_SAFE(m_pUpdates0List, pEntry, pTmp)
    {
        if ((! pEntry->paused) && (! pEntry->markedForDeletion))
        {
            pEntry->target->update(dt);
        }
    }

    // updates with priority > 0
    DL_FOREACH_SAFE(m_pUpdatesPosList, pEntry, pTmp)
    {
        if ((! pEntry->paused) && (! pEntry->markedForDeletion))
        {
            pEntry->target->update(dt);
        }
    }
CCScheduler有三个优先级队列,优先级越大越后执行。它会调用每个目标的代理方法update。update代码如下:

// override me
void CCNode::update(float fDelta)
{
    if (m_nUpdateScriptHandler)
    {
        CCScriptEngineManager::sharedManager()->getScriptEngine()->executeSchedule(m_nUpdateScriptHandler, fDelta, this);
    }
    
    if (m_pComponentContainer && !m_pComponentContainer->isEmpty())
    {
        m_pComponentContainer->visit(fDelta);
    }
}
上面代码每帧都会被调用。每个脚本对象要是想要做一些定时器调度任务可以注册这个脚本做点事。依然的是cocos2.2.2没倒出CCNode调度的其它接口,因为那些接口需要函数指针作为参数,lua不能把lua里函数转为c的函数指针。有两个办法可以解决这个问题,一个是导出生成函数指针的lua接口给lua用,二是直接提供直接调用lua函数的c api。还有一种好方法是,不用调度器,使用间隔动作与回调函数动作的组合生成一个伪调度。其实动作都是调度器的调用目标,cocos也就一个调度器,那些需要进行定时调度的都是调度调用的,动作管理器是调度器的一个调度目标。

OK差不多了。本文研究了c/c++与lua的相互调用,cocos如何使用tolua++导出c api供lua使用,如何使用cocos进行lua脚本编程,cocos的CCNode提供了哪些重要的调用lua脚本的接口
本文有的地方没有深入,需要进一步研究lua的c api、辅助库、以及lua functions, 进一步研究tolua++怎么包装lua的,然后再更深入挖掘cocos的lua的使用,比如cocos怎么定制tolua++的,相关的
toluafix前缀的函数具体干了什么可以仔细研究下。还有lua与oc的桥接,lua与java的桥接可以研究等等。有些代码看了部分知道了做了什么,但是不代表知道具体的设计思想。读代码读的更是一种思想,可以启发写出好代码。





你可能感兴趣的:(c++,游戏引擎)