【Cocos2d-x】使用 tolua++ 导出 C++ 类(2.x版本)

Cocos2dx 2.x 版本使用 tolua++ 进行 Lua 绑定,需要三个文件,包描述文件 *.tolua,定制脚本 basic.lua 和可执行脚本 build.bat

每天进步一点点,能不退步太多;再进步一点,能不退步;再进步一点点,能有所进步;再进步一点点,方能成功。

Cocos2dx 版本非常乱,主要有 2.x 和 3.x 两个版本,另外还是 lua 版本还有 quick 分支。本文主要介绍低版本(2.x)cocos2d-x 对应的 quick 版本,如何使用 tolua++ 来导出自定义的 C++ 类,因为没找到 cocos2d-x 的 2.x 版本,所以不知道 2dx 和 quick 是不是一样。

工程结构

先来看一下 quick-cocos2d-x-2.x 的工程结构,

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
|-quick-cocos2d-x
|-bin
|-win32
|-lua51.dll
|-luac.exe
|-luajit.exe
|-php.exe
|-tolua++.exe
|-framework
|-display.lua
|-lib
|-cocos2d-x
|-scripting
|-lua
|-cocosdx_support
|-LuaCocos2d.h
|-LuaCocos2d.cpp
|-framework_precompiled
|-luabinding
|-cocos2dx
|-Cocos2d.tolua
|-Cocos2d.tolua
|-build.bat
|-build.sh
|-proj.win32
|-proj.android
|-player
|-sources
|-proj.win32
|-proj.mac

最重要的就是上面列出的 bin, framework, lib, player 四个文件夹,另外还有几个不那么重要的文件夹,docs 是帮助文档,samples 是官方例子,template 是创建工程的默认模板,tool 貌似没什么用。

  • bin 保存了引擎用到的一些工具和可执行脚本,其中 bin/win32 下有跟 Lua 相关的工具,包括 Lua 编译器 luacluajit,还有我们马上要用到的 tolua++
  • framework 为引擎的基础框架,即封装好的一些代码库集合,里面全是 Lua 文件,因为 quick 是纯 Lua 版本,引擎源码已经被编成一个“播放器”了,对项目而言,这些上层封装的 Lua 代码才是 framework;
  • lib 是引擎源码存放的地方,包括 cocos2d-x 目录下的源码和 proj.win32, proj.android 等源码工程,以及用于将 C++ 导出到 Lua 的相关文件 luabinding
  • player 则是一个播放器工程,用于生成一个可直接运行的播放器,之后使用 quick 创建项目就只需要把这个编好的播放器拷过去就行,然后直接开撸 Lua 代码,不需要引擎的 C++ 源码;在 player/proj.win32 下有个解决方案 player.sln,它包含自身的播放器项目 player.vcxproj 和源码项目 lib/proj.win32/cocos2dx.vcxproj

Lua 绑定

引擎的目录结构已经很清楚了,lib 是引擎源码,framework 是 quick 封装的上层 Lua 库,player 是播放器工程,bin 则是常用工具。进行 Lua 绑定需要用到 binlib 这两个文件夹,

  • bin 目录下有 Lua 绑定工具 tolua++.exe
  • lib/luabinding 为 Lua 绑定的工作目录,包括每个类的包描述文件 *.tolua、定制脚本 basic.lua 和可执行脚本 build.bat/build.sh
  • lib/framework_precompiled 目录下 Lua framework 的预编译文件,下面只有 framework_precompiled.zip 一个文件;
  • lib/cocos2d-x 则为引擎的源码目录,tolua 导出的文件自然也是源码的一部分,所以也在这个目录下,其完整路径为 lib/cocos2d-x/scripting/lua/cocos2dx_support

进行 Lua 绑定只需要一步,执行 lib/luabinding/build.bat/build.sh 即可,这个脚本的内容如下,

1
2
3
4
5
6
@echo off
set DIR=%~dp0
set TOLUA=%QUICK_COCOS2DX_ROOT%\bin\win32\tolua++.exe

cd /d "%DIR%"
%TOLUA% -L "%DIR%basic.lua" -o "%QUICK_COCOS2DX_ROOT%\lib\cocos2d-x\scripting\lua\cocos2dx_support\LuaCocos2d.cpp" Cocos2d.tolua

其实就只做一件事,调用 bin/win32/tolua++.exe,根据包描述文件 lib/luabinding/Cocos2d.tolua 和定制脚本 lib/luabinding/basic.lua,生成文件 lib/cocos2d-x/scripting/lua/cocos2dx_support/LuaCocos2d.cpp

描述文件以 .tolua 为后缀(本来的后缀名应该是 *.pkg,不过这不重要),语法和 C++ 头文件差不多,其作用是描述要导出的函数,每个 C++ 类都需要一个描述文件,但我们执行脚本的时候只指定了一个描述文件,这是因为描述文件和头文件一样,可以包含其它文件,下面是 Cocos2d.tolua 的内容,

1
2
3
4
5
$#include "LuaCocos2d.h"
$pfile "cocos2dx/include/ccTypes.tolua"
$pfile "cocos2dx/cocoa/CCObject.tolua"
$pfile "cocos2dx/cocoa/CCString.tolua"
//...

在运行标准的 tolua++ 之前,还加载了额外的脚本 basic.lua,这是由于 cocos2dx 在 Lua 绑定方面并未完全遵照 tolua++ 的默认做法,因此需要对其进行定制,basic.lua 主要做的事情是:

  1. 将所有导出的 CCXXXX 类的 push 函数(也就是将 cpp obj h传进 lua 时调用的函数)修改为自己的 toluafix_pushusertype_ccobject
  2. functiontable 两种类型的 tois 函数(分别是指将 lua obj 传进 cpp 时调的函数、判断一个变量是否为本类型时调的函数)修改为 toluafix_is/to_funtion/table

自定义类导出

  • 第一步,编写自己的 C++ 类 MyClass,放在源码目录 lib/cocos2d-x 下即可,无具体要求;
  • 第二步,编写包描述文件 MyClass.tolua,放在 Lua 绑定工作目录 lib/luabinding 下即可,无具体要求;
  • 第三步,在 Cocos2d.tolua 中包含 MyClass.tolua
  • 第四步,在 cocos2d.h 中包含 MyClass.h
  • 第五步,重新执行 lib/luabinding/build.bat,这样就会将自定义类 MyClass 也导出到 LuaCocos2d 库中去,所以使用的时候不需要自己去导入库。

basic.lua 中定义了一个 table CCObjectTypes,放在这个 table 里的类表示是 cocos2d-x 对象类,需要对其进行定制;如果我们的自定义继承自 cocos2d-x 的对象,则需要把我们的自定义类也加入到 CCObjectTypes 中去,否则不能加进去,cocos2d-x 的对象类会修改 toluafix_pushusertype_ccobject,会使用到 m_uIDm_nLuaID,而我们自定义的类没有这两个字段,将其当作 cocos2d-x 对象处理是编译不过去的。

第二步和第三步是编写包描述文件,仿照着已有文件写就行,这两步做好之后通常就可以进行导出了,通常容易忽略的是第四步。这里要讲一下 Lua 绑定的原理,就是在原来 C++ 类的基础上封装一层,为每个需要导出的方法都封装一个静态函数,然后 C++ 程序打开 Lua 环境的时候将这些静态函数全部注册到 Lua 环境中去,生成一个全局的 Lua 变量,Lua 代码通过这个全局变量访问到 C++ 静态函数,而这个静态函数内部才调用真正的类方法。所以,Lua 绑定的结果是根据原来的 C++ 文件,再生成一个 C++ 文件,新生成的文件需要包含原来的头文件,因为新文件定义的静态方法做的事情就是访问原来的类方法。因此才有了第四步,引用头文件 MyClass.h,因为生成的文件 LuaCocos2d.cpp 默认会包含 cocos2d.h,所以这里才在 cocos2d.h 中包含 MyClass.h,当然也可以在其它地方包含,比如直接在 LuaCocos2d.cpp 中直接包含 MyClass.h 也行,或者在定制脚本 basic.lua 中写也行。

下面是一个导出方法的示例,是为 MyClass 的构造函数生成的一个静态函数,这个函数使用 C API 创建一个 MyClass 对象,注册到 Lua 环境中去。

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef TOLUA_DISABLE_tolua_Cocos2d_MyClass_new00
static int tolua_Cocos2d_MyClass_new00(lua_State* tolua_S)
{
//...
int a = ((int) tolua_tonumber(tolua_S,2,0));
int b = ((int) tolua_tonumber(tolua_S,3,0));
{
MyClass* tolua_ret = (MyClass*) Mtolua_new((MyClass)(a,b));
tolua_pushusertype(tolua_S,(void*)tolua_ret,"MyClass");
}
//...
}
#endif //#ifndef TOLUA_DISABLE

单独导出

上面的做法是将自定义跟着系统库一起导出,另外一种做法是单独导出我们的自定义类,只需要模仿着系统导出写一套文件即可。最终的文件结构如下:

1
2
3
4
5
6
7
8
9
10
|-quick-cocos2d-x
|-lib
|-custom
|-luabinding
|-basic.lua
|-build.bat
|-MyClass.tolua
|-MyClass.h
|-MyClass.cpp
|-LuaMyClass.hpp

创建一个独立的目录 lib/custom,不干扰引擎原来的文件结构,然后第一步和第二步和原来一样,编写自定义的 C++ 类和包描述文件 MyClass.tolua;之后编写自己的定制脚本 basic.lua 和可执行脚本 build.bat,最终生成文件 LuaMyClass.hpp,默认不会生成头文件,所以这里将生成的文件命名为 *.hpp,这样使用的时候就直接包含就行了,如果生成 *.cpp,则需要自己再手动写一个头文件。

build.bat 基本和原来一样,只是改一下包描述文件和生成文件,代码如下:

1
2
3
4
5
6
@echo off
set DIR=%~dp0
set TOLUA=%QUICK_COCOS2DX_ROOT%\bin\win32\tolua++.exe

cd /d "%DIR%"
%TOLUA% -L "%DIR%basic.lua" -o "%DIR%/../LuaMyClass.cpp" MyClass.tolua

定制脚本 basic.lua 也和原来差不多,直接从 lib/luabinding/basic.lua 拷一份出来,然后做一下删减即可。首先,我们的自定义不是 cocos2d-x 对象,所以 CCObjectTypes 部分直接删掉;然后改一下 replace 部分的代码,简化头文件的引入,不需要引入 cocos2d.h,还有一些不必要的替换也去掉,最终的效果如下:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
-- usage: (use instead of ant)
-- tolua++ "-L" "basic.lua" "-o" "../../scripting/lua/cocos2dx_support/LuaCocos2d.cpp" "Cocos2d.pkg"

_is_functions = _is_functions or {}
_to_functions = _to_functions or {}
_push_functions = _push_functions or {}

-- register LUA_FUNCTION, LUA_TABLE, LUA_HANDLE type
_to_functions["LUA_FUNCTION"] = "toluafix_ref_function"
_is_functions["LUA_FUNCTION"] = "toluafix_isfunction"
_to_functions["LUA_TABLE"] = "toluafix_totable"
_is_functions["LUA_TABLE"] = "toluafix_istable"

local toWrite = {}
local currentString = ''
local out
local WRITE, OUTPUT = write, output

function output(s)
-- ...
end

function write(a)
-- ...
end

function post_output_hook(package)
local result = table.concat(toWrite)
local function replace(pattern, replacement)
-- ...
end

replace([[#ifndef __cplusplus
#include "stdlib.h"
#endif
#include "string.h"

#include "tolua++.h"]],
[[extern "C" {
#include "tolua_fix.h"
}

#include <map>
#include <string>
#include "MyClass.h"
#include "CCLuaEngine.h"]])

replace([[/* Exported function */
TOLUA_API int tolua_Cocos2d_open (lua_State* tolua_S);]], [[]])

replace([[*((LUA_FUNCTION*)]], [[(]])

replace([[tolua_usertype(tolua_S,"LUA_FUNCTION");]], [[]])

replace([[toluafix_pushusertype_ccobject(tolua_S,(void*)tolua_ret]],
[[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]])

replace('\t', ' ')

WRITE(result)
end

因为是单独导出,所以使用的时候要先进行注册,在生成的 LuaMyClass.hpp 中有一个 export function,

1
TOLUA_API int  tolua_MyClass_open (lua_State* tolua_S);

调用这个函数就可以一键注册 MyClass 的所有方法到 Lua 环境中去。

首先,将 MyClass.h, MyClass.cpp, LuaMyClass.hpp 拷贝到项目中去,然后包含 LuaMyClass.hpp,再调用函数 tolua_MyClass_open

1
2
3
4
5
6
7
8
9
10
11
12
#include "cocos2d.h"
#include "LuaMyClass.hpp"
//#include ...

bool AppDelegate::applicationDidFinishLaunching()
{
// ...
lua_State *L = pStack->getLuaState();

tolua_MyClass_open(L);
// ...
}