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

Cocos2dx 3.x 使用 Python 进行 Lua 绑定,除了为每个类定制配置文件 *.ini 之外,还需要修改可执行脚本 generator.py

版本之痛,痛中之痛;环境之难,难中之难。

cocos2d-x3.x 与 2.x 很不一样,关于 Lua 绑定也不同。在 2.x 版本,引擎目录下有个 tolua++.exe 文件,通过包描述文件 *.tolua,执行脚本 build.bat 来一键导出。而在 3.x 版本,描述文件换成了 *.ini,然后编写 python 脚本 generator.py,使用 python 来导出。

工程结构

首先来看一下 cocos2d-x3.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
31
32
33
34
35
36
37
38
39
40
|-cocos2d-x-3.x
|-build
|-docs
|-tests
|-templates
|-extensions
|-external
|-tools
|-tolua
|-genbindings.py
|-cocos2dx.ini
|-cocos2dx_ui.ini
|-...
|-README.mdown
|-bindings-generator
|-libclang
|-libclang.dll
|-libclang.so
|-libclang.dylib
|-clang
|-cindex.py
|-generator.py
|-cocos
|-2d
|-...
|scripting
|-js-bindings
|-lua-bindings
|-auto
|-api
|-Action.lua
|-...
|-lua_cocos2dx_auto.hpp
|-lua_cocos2dx_auto.cpp
|-lua_cocos2dx_ui_auto.hpp
|-lua_cocos2dx_ui_auto.cpp
|-...
|-scripting
|-init.lua
|-...

build 为工程目录,docs 帮助文档,tests 官方示例,templates 为创建项目时使用的模板,extensions 为源码的一些扩展,external 为外部库,主要是一些第三方的基础库,比如物理引擎、md5、json 等等。重点要介绍的是 toolscocos 这两个文件夹。

  • cocos 目录是 cocos2d-x 引擎的 C++ 源码,其中 cocos/scripting 为导出的脚本,包括 js 和 lua,分别放在 cocos/scripting/lua-bindingscocos/scripting/js-bindings 目录下;cocos/scripting/lua-bindings/auto 为 tolua 生成文件存放的地方。
  • tools 目录下是 cocos2d-x 非常实用的一些工具,包括创建项目、编译项目、运行项目的控制台命令等,这里我们要看的是 toluabindings-generator 这两个文件夹;tolua 下是包描述文件和可执行脚本 genbindings.pybinding-generator 目录下则是需要用到的一些库或脚本。

环境配置

在开始之前需要做一个准备工作,就是配置环境,主要配置的是 python 环境,在 tools/tolua/README.mdown 中其实已经有关于环境配置的说明:

  • 首先,安装 ndk 并配置环境变量 NDK_ROOT
  • 然后,下载安装 python2.7,注意必须安装 32 位版本,下载地址:python2.7.3
  • 配置 python 环境变量 PYTHON_BIN
  • 下载安装 python 库 pyyaml,下载地址:pyyaml,pyyaml 安装的时候会读取 python 安装路径并将结果安装在 %PYTHON_BIN%\Lib\site-packages 目录下;
  • 下载 python 库 pyCheetah,下载地址:pyCheetah,下载之后解压到 %PYTHON_BIN%\Lib\site-packages 目录下即可。

Lua 绑定

首先,跟写普通 C++ 程序一样,我们先写一个要导出的 C++ 类 MyClass

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
//MyClass.h
class MyClass
{
public:
MyClass(int a, int b);
~MyClass();
int excute();

private:
int a;
int b;
};

//MyClass.cpp
#include "MyClass.h"

MyClass::MyClass(int a, int b)
{
this->a = a;
this->b = b;
}

MyClass::~MyClass()
{
}

int MyClass::excute()
{
return a + b;
}

接下来就可以准备导出这个类了,使用的是 tools/tolua/genbindings.py 以及每个类对应的 ini 文件,这个 python 脚本可以导出 cocos2d-x 中的所有类,我们可以修改这个脚本,添加我们自定义的类,也可以模仿这个脚本另外写一个脚本,然后执行我们自己写的这个脚本。为了防止破坏源代码,我们使用第二种,把 genbindings.py 复制一份,命名为 genbindings_myclass.py,再把 cocos2dx.ini 复制一份,命名为 myclass.ini。在引擎源码下创建一个 custom 目录,然后把 myclass.ini 和 C++ 源文件统一放在这个目录下,而 genbindings_myclass.py 则仍放在 tools/tolua 目录下,这样改动最小。

1
2
3
4
5
6
7
8
9
10
|-cocos2d-x-3.x
|-cocos
|-custom
|-MyClass.h
|-MyClass.cpp
|-myclass.ini
|-auto
|-tools
|-tolua
|-genbindings_myclass.py

修改 genbindings_myclass.py

  • tolua_root 为包描述文件 myclass.ini 的路径,即 project_root/cocos/custom
  • output_dir 为生成文件的路径,我们放在 project_root/cocos/custom/auto 目录下;
  • cmd_args 指定使用的配置文件以及导出的文件名,这是一个 table,可以指定多个 ini 配置文件,一个配置文件可以配置一个类或多个类,格式为 "ini_file" : ("section", "output_file")。这里我们只需要一个 myclass.inisectionmyclass,在 myclass.ini 的第一行定义,导出的文件名为 lua_myclass_auto
1
2
3
4
# genbindings_myclass.py
tolua_root = '%s/tools/tolua' % project_root
output_dir = '%s/tests/custom/auto' % project_root
cmd_args = {'myclass.ini' : ('myclass', 'lua_myclass_auto')}

修改 myclass.ini

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# section,与上面 cmd_args 配置的一致
[myclass]

# 固定前缀,会加在导出函数的前面
prefix = myclass

# 导出的类所在命名空间,在 lua 层使用时需要用到,不写也行,表示在全局空间
target_namespace = cc

# 要导出的 c++ 类头文件路径
headers = %(cocosdir)s/tests/custom/MyClass.h

# 要导出的类名
classes = MyClass

skip =
abstract_classes =

总结一下就是创建了 genbindings_myclass.pymyclass.ini 这两个文件,接下来在 tools/tolua 文件夹下打开命令行,输入 python genbindings_myclass.py。 如果没错误,将在 tests/custom/auto 目录生成相应的文件。

1
2
3
4
5
6
7
8
9
10
11
12
|-cocos2d-x-3.x
|-cocos
|-custom
|-MyClass.h
|-MyClass.cpp
|-myclass.ini
|-auto
|-lua_myclass_auto.hpp
|-lua_myclass_auto.cpp
|-api
|-MyClass.lua
|-lua_myclass_auto.lua

如果报错,检查一下 genbindings_myclass.pymyclass.ini 有没有写错,特别是源文件路径 headers 和 目标文件 output_dir。还有就是 python,pyyaml 和 pyCheetah 是不是全装的 32 位,还有 python 环境变量是否配置对,当然 MyClass.hMyClass.cpp 肯定也不能有错误。

测试

新建一个 lua 项目,把 MyClass.h, MyClass.cpp, lua_myclass_auto.hpp, lua_myclass_auto.cpp 这四个文件拷到项目中,然后打开 AppDelegate.cpp,添加下面内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "lua_myclass_auto.hpp"
//...

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

//register custom function
register_all_myclass(stack->getLuaState());

//...
}

register_all_myclasslua_myclass_auto.hpp 中声明,所以要引入这个头文件,在 lua_myclass_auto.cpp 中实现,我们可以看看这个函数,

1
2
3
4
5
6
7
8
9
10
11
12
TOLUA_API int register_all_myclass(lua_State* tolua_S)
{
tolua_open(tolua_S);

tolua_module(tolua_S,"cc",0);
tolua_beginmodule(tolua_S,"cc");

lua_register_myclass_MyClass(tolua_S);

tolua_endmodule(tolua_S);
return 1;
}

总的来说就是打开 tolua,把 c++ 层的函数注册到 lua 环境中,之后就可以在 lua 中直接使用这些函数了。具体的可以看我这篇文章tolua 用法

接下来打开 src/app/views/MainScene.lua,开始测试我们的例子,

1
2
3
4
5
6
7
8
9
10
11
12
local MainScene = class("MainScene", cc.load("mvc").ViewBase)

function MainScene:onCreate()
local test = cc.MyClass:new(10, 20)
local num = test:excute()
local str = "Hello World" .. num
cc.Label:createWithSystemFont(str, "Arial", 40)
:move(display.cx, display.cy)
:addTo(self)
end

return MainScene

这里我们创建 MyClass 的一个实例,然后调用其 excute 方法;注意我们前面导出的时候定义其命名空间为 cc,所以这里要写 cc.MyClass,否则会报错。接下来就是见证成果的时候了,编译运行项目,看到下面结果就表示大功告成了。

tolua 应用

常见问题

1. NDK 版本不对

1
2
3
4
5
6
====
Errors in parsing headers:
1. <severity = Fatal,
location = <SourceLocation file '/home/xxx/NDK/platforms/android-14/arch-arm/usr/include/android/log.h', line 70, column 10>,
details = "'stdarg.h' file not found">
====

如果出现这个错误,是因为 NDK 工具链 llvm 匹配不上,最简单直接的办法就是换一个版本的 NDK,通常是使用了版本太高的 NDK 才会出现,比如我之前电脑上装了 r10 和 r20 两个版本,不知道什么时候环境变量设成了 NDK_ROOT = D:\Android\android-ndk-r20,把它换成 NDK_ROOT = D:\Android\android-ndk-r10e 就行了。注意要删掉 tools/tolua/userconfi.ini,然后重启终端再执行就可以了。

2. 类名、头文件路径写错

TranslationUnitLoadError: Error parsing translation unit.

如果报这个错,检查一下类名有没有写错,头文件路径是否正确。

<!https://www.jianshu.com/p/48a6fb7123ee>

3. 命名空间解析不了

Exception: The namespace (flash::FlashSpriteFrame) conversion wasn't set in 'ns_map' section of the conversions.yaml

其实这个报错信息很明显了,在 conversions.yamlns_map section 中没找到这个命名空间的映射。也就是说如果定义了新的命名空间,不仅要在 ini 文件中定义 target_namespacecpp_namespace 这两个字段,还要在 conversions.yaml 中定义这两个命名空间的映射关系,格式为 "cpp_namespace::": "target_namespace."

4. 脚本工作目录错误

No option 'cxxgeneratordir' in section: 'DEFAULT',报这个错主要是没有进入到脚本所在的目录去执行脚本,有些配置路径就不对了。

to be continue…