Lua绑定进阶篇

之前已经写过两篇文章
《Cocos2dx Lua 绑定》详细介绍了,如何在lua中调用c++;
《Cocos2dx 插入广告》详细介绍了,对于打包成apk需要注意的事项;
而对于今天这篇文章,是在对lua绑定有了进一步的理解之后做的一篇详细记录
准备工作请参考第一篇文章,里面有详细的介绍。

还是用一个简单的例子,来开始这篇文章
本文是基于cocos3.6的版本,vs2012
首先建立一个cocoslua工程,然后在

工程\frameworks\runtime-src\Classes

下建立一个Custom文件夹放我们自定义的c++代码文件,并且在Custom下再建立一个Lua文件夹,用来放lua和c++的桥接文件,接着回到vs2012中,建立对应的筛选器(筛选器和实际的物理路径不是一个意思,筛选器只是方便我们管理代码而已,筛选器A完全可以把B文件夹里的文件放进来,这个设定我倒是不太明白为什么)
如此之后,vs2012是这个结构了

工程目录.png

我们这里写两个自定义类,Test和Student,你为我为什么是Student,我只好告诉你,我英语不好。。只会这个单词!

Test.h

class Test
{
private:
    int _handler;
public:
    int test();
    void run();
    void registerScript(int handler);
    static Test* create();
};

Test.cpp

#include "Test.h"
#include "cocos2d.h"
#include "CCLuaEngine.h"
#include "Student.h"

int Test::test()
{
    return 100;
}

Test* Test::create()
{
    Test * p = new Test();
    return p;
}

void Test::registerScript(int handler)
{
    _handler = handler;
}

void Test::run()
{
    auto engine = cocos2d::LuaEngine::getInstance();
    //_handler的三个参数,第一个是一个指针,用来传递对象,第二个是bool类型,第三个是int类型
    auto st = Student::create(10,"nevermore");
    engine->getLuaStack()->pushObject((cocos2d::Ref*)st,"Ref");
    engine->getLuaStack()->pushBoolean(true);
    engine->getLuaStack()->pushInt(32);

    //执行_handler函数,并且说明该函数需要的参数个数
    engine->getLuaStack()->executeFunctionByHandler(_handler,3);
}
函数 解释
static Test* create() 在lua中对于对象的操作实际都是用指针来完成的,所以需要一个create函数来创建一个对象
int test() 简单的返回一个数字,没有特殊的意义
void registerScript(int handler) 将一个lua函数注册到这里来,将在run中执行这个函数
void run() 传递参数给_handler并执行_handler

在run函数中需要传递一个student对象,这里贴上student的代码

Student.h

#include "cocos2d.h"
class Student:public cocos2d::Ref
{
private:
    int _age;
    std::string _name;
public:
    static Student * create(int age,std::string name);
    std::string getName();
    void setName(std::string);
    int getAge();
    void setAge(int);
};

Student.cpp

#include "Student.h"

Student* Student::create(int age,std::string name)
{
    Student * st = new Student();
    st->setAge(age);
    st->setName(name);
    return st;
}

void Student::setAge(int age)
{
    _age = age;
}

void Student::setName(std::string name)
{
    _name = name;
}

int Student::getAge()
{
    return _age;
}

std::string Student::getName()
{
    return _name;
}

写好了c++代码之后,我们需要做的,便是生成桥接文件,对于桥接文件的生成,使用的tolua的工具
我们首先找到

E:\LuaGame_3\frameworks\cocos2d-x\tools\tolua
tolua.png

这个文件夹下有很多ini文件,以及有一个叫做 genbindings.py 的文件,这个文件的作用,就是生成桥接文件的关键,这是一个python文件

def main():

    cur_platform= '??'
    llvm_path = '??'
    ndk_root = _check_ndk_root_env()
    # del the " in the path
    ndk_root = re.sub(r"\"", "", ndk_root)
    python_bin = _check_python_bin_env()

    platform = sys.platform
    if platform == 'win32':
        cur_platform = 'windows'
    elif platform == 'darwin':
        cur_platform = platform
    elif 'linux' in platform:
        cur_platform = 'linux'
    else:
        print 'Your platform is not supported!'
        sys.exit(1)

    if platform == 'win32':
        x86_llvm_path = os.path.abspath(os.path.join(ndk_root, 'toolchains/llvm-3.3/prebuilt', '%s' % cur_platform))
        if not os.path.exists(x86_llvm_path):
            x86_llvm_path = os.path.abspath(os.path.join(ndk_root, 'toolchains/llvm-3.4/prebuilt', '%s' % cur_platform))
    else:
        x86_llvm_path = os.path.abspath(os.path.join(ndk_root, 'toolchains/llvm-3.3/prebuilt', '%s-%s' % (cur_platform, 'x86')))
        if not os.path.exists(x86_llvm_path):
            x86_llvm_path = os.path.abspath(os.path.join(ndk_root, 'toolchains/llvm-3.4/prebuilt', '%s-%s' % (cur_platform, 'x86')))

    x64_llvm_path = os.path.abspath(os.path.join(ndk_root, 'toolchains/llvm-3.3/prebuilt', '%s-%s' % (cur_platform, 'x86_64')))
    if not os.path.exists(x64_llvm_path):
        x64_llvm_path = os.path.abspath(os.path.join(ndk_root, 'toolchains/llvm-3.4/prebuilt', '%s-%s' % (cur_platform, 'x86_64')))

    if os.path.isdir(x86_llvm_path):
        llvm_path = x86_llvm_path
    elif os.path.isdir(x64_llvm_path):
        llvm_path = x64_llvm_path
    else:
        print 'llvm toolchain not found!'
        print 'path: %s or path: %s are not valid! ' % (x86_llvm_path, x64_llvm_path)
        sys.exit(1)

    project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
    cocos_root = os.path.abspath(os.path.join(project_root, ''))
    cxx_generator_root = os.path.abspath(os.path.join(project_root, 'tools/bindings-generator'))

    # save config to file
    config = ConfigParser.ConfigParser()
    config.set('DEFAULT', 'androidndkdir', ndk_root)
    config.set('DEFAULT', 'clangllvmdir', llvm_path)
    config.set('DEFAULT', 'cocosdir', cocos_root)
    config.set('DEFAULT', 'cxxgeneratordir', cxx_generator_root)
    config.set('DEFAULT', 'extra_flags', '')

    # To fix parse error on windows, we must difine __WCHAR_MAX__ and undefine __MINGW32__ .
    if platform == 'win32':
        config.set('DEFAULT', 'extra_flags', '-D__WCHAR_MAX__=0x7fffffff -U__MINGW32__')

    conf_ini_file = os.path.abspath(os.path.join(os.path.dirname(__file__), 'userconf.ini'))

    print 'generating userconf.ini...'
    with open(conf_ini_file, 'w') as configfile:
      config.write(configfile)


    # set proper environment variables
    if 'linux' in platform or platform == 'darwin':
        os.putenv('LD_LIBRARY_PATH', '%s/libclang' % cxx_generator_root)
    if platform == 'win32':
        path_env = os.environ['PATH']
        os.putenv('PATH', r'%s;%s\libclang;%s\tools\win32;' % (path_env, cxx_generator_root, cxx_generator_root))


    try:

        tolua_root = '%s/tools/tolua' % project_root
        output_dir = '%s/../runtime-src/Classes/Custom/Lua' % project_root

        cmd_args = {#'cocos2dx.ini' : ('cocos2d-x', 'lua_cocos2dx_auto'), \
                    # 'cocos2dx_extension.ini' : ('cocos2dx_extension', 'lua_cocos2dx_extension_auto'), \
                    # 'cocos2dx_ui.ini' : ('cocos2dx_ui', 'lua_cocos2dx_ui_auto'), \
                    # 'cocos2dx_studio.ini' : ('cocos2dx_studio', 'lua_cocos2dx_studio_auto'), \
                    # 'cocos2dx_spine.ini' : ('cocos2dx_spine', 'lua_cocos2dx_spine_auto'), \
                    # 'cocos2dx_physics.ini' : ('cocos2dx_physics', 'lua_cocos2dx_physics_auto'), \
                    # 'cocos2dx_experimental_video.ini' : ('cocos2dx_experimental_video', 'lua_cocos2dx_experimental_video_auto'), \
                    # 'cocos2dx_experimental.ini' : ('cocos2dx_experimental', 'lua_cocos2dx_experimental_auto'), \
                    # 'cocos2dx_controller.ini' : ('cocos2dx_controller', 'lua_cocos2dx_controller_auto'), \
                    # 'cocos2dx_cocosbuilder.ini': ('cocos2dx_cocosbuilder', 'lua_cocos2dx_cocosbuilder_auto'), \
                    # 'cocos2dx_cocosdenshion.ini': ('cocos2dx_cocosdenshion', 'lua_cocos2dx_cocosdenshion_auto'), \
                    # 'cocos2dx_3d.ini': ('cocos2dx_3d', 'lua_cocos2dx_3d_auto'), \
                    # 'cocos2dx_audioengine.ini': ('cocos2dx_audioengine', 'lua_cocos2dx_audioengine_auto'), \
                    # 'cocos2dx_csloader.ini' : ('cocos2dx_csloader', 'lua_cocos2dx_csloader_auto'), \
                    # 'cocos2dx_experimental_webview.ini' : ('cocos2dx_experimental_webview', 'lua_cocos2dx_experimental_webview_auto'), \
                    'custom_test.ini' : ('custom_test', 'custom_test'), \
                    'custom_student.ini' : ('custom_student', 'custom_student'), \
                    }
        target = 'lua'
        generator_py = '%s/generator.py' % cxx_generator_root
        for key in cmd_args.keys():
            args = cmd_args[key]
            cfg = '%s/%s' % (tolua_root, key)
            print 'Generating bindings for %s...' % (key[:-4])
            command = '%s %s %s -s %s -t %s -o %s -n %s' % (python_bin, generator_py, cfg, args[0], target, output_dir, args[1])
            _run_cmd(command)

        print '---------------------------------'
        print 'Generating lua bindings succeeds.'
        print '---------------------------------'

    except Exception as e:
        if e.__class__.__name__ == 'CmdError':
            print '---------------------------------'
            print 'Generating lua bindings fails.'
            print '---------------------------------'
            sys.exit(1)
        else:
            raise


# -------------- main --------------
if __name__ == '__main__':
    main()

对于我们来说要修改的地方一个是output_dir ,一个是cmd_args

  • output_dir
    这是桥接文件的输出目录,还记得我们之前说的Lua文件夹么,它的实际路径是
工程目录\frameworks\runtime-src\Classes\Custom\Lua

如何定位到这个目录?代码中是这么写的

output_dir = '%s/../runtime-src/Classes/Custom/Lua' % project_root

%s在python中是用来格式化的,和lua中的string.format是一个意思
print '%s,你好'%'最怕认真'将输出最怕认真,你好
project_root在代码中是这样写的

project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))

这是求路径的代码
os.path.dirname(__file__)得到的是当前文件路径,也就是genbindings.py 的路径

E:\LuaGame_3\frameworks\cocos2d-x\tools\tolua

而..的意思是返回上一层路径,所以最终project_root 指向的是

E:\LuaGame_3\frameworks\cocos2d-x

如此,对于output_dir 要指向我们的lua文件夹,就很简单了

  • cmd_args
    这是一个数组,每一个item由3部分组成
'custom_test.ini' : ('custom_test', 'custom_test'), 
内容 含义
第一部分custom_test.ini 我们配置的关键,是用来配置我们的c++代码到桥接文件的说明文件
第二部分custom_test custom_test.ini的section名称,不明白没关系,等下我们会详细说下ini文件
第三部分custom_test 导出的桥接文件的文件名

ini文件要怎么写?
我们首先在外层复制一个cocos2dx.ini文件,更名为custom_test.ini

ini文件.png

对于这个文件有5个地方需要修改

第N部分 解释
1 这个就是上面我们说的section了
2 和1一致
3 包含到哪个模块中,比如写 = cc那么调用test的时候就是cc.Test,如果不写就直接Test来调用
4 需要生成桥接文件的头文件,cocosdier是在genbindings中赋值的,最终跟踪其就是project_root
5 需要生成桥接文件的名称,如果有多个,则用空格隔开,但是不推荐多个文件共用一个配置文件

按照如此我们就配置好了我们的Test文件的ini文件,接着再依葫芦画瓢写好我们的Student的ini文件

Student.png

接着我们回到genbindings的cmd_args中,我们用#注释了很大一部分内容,是因为这些文件都已经生成过了,我们没必要再生成一遍,我们只要添加我们需要生成的两个文件就行了,添加好了这两行,就在genbindings所在的目录下右键执行cmd,然后在cmd中运行genbindings,当出现

---------------------------------
Generating lua bindings succeeds.
---------------------------------

表示生成成功了,失败的话多半是ini那五部分内容写错了,仔细找下都不会太难解决的。
然后在我们的Lua文件下就生成了对应的4个文件,2个cpp2个hpp,hpp和h文件是一个意思

lua.png

然后在vs2012的lua筛选器中
右键-添加现有项-将4个文件包含进来

lua包含.png

对于2个cpp文件,其中

#include "Test.h"和#include "Student.h"修改成正确的路径

然后打开AppDelegate.cpp文件,把我们的c++类注册到lua中去
在函数applicationDidFinishLaunching里调用了函数register_all_packages,在这个register_all_packages里完成我们的注册,首先在AppDelegate.cpp添加我们的2个hpp头文件

#include "Custom\Lua\custom_test.hpp"
#include "Custom\Lua\custom_student.hpp"

register_all_packages改写如下

static int register_all_packages(lua_State* L)
{
    register_all_custom_test(L);
    register_all_custom_student(L);
    return 0; //flag for packages manager
}

同时在函数applicationDidFinishLaunching里传入lua_State
如此,我们所有的准备工作算是完成了,然后编译,如果有错误根据错误提示改正,一般按照文章内容一步步做的话是能编译通过的,接下来就是使用了。
首先测试test函数,我们在一个场景中如下测试

    local t = Test:create()
    local res = t:test()
    local txt = ccui.Text:create()
    txt:setFontSize(70)
    txt:setPosition(568,320)
    self:addChild(txt)
    txt:setString(tostring(res))

用一个文本来显示返回值,运行

test函数.png

可以看到文本成功的显示了100

然后再来测试我们的注册函数到c++的功能

function MainScene:func(student, bool, int)
    print(student:getAge())
    print(student:getName())
    print(bool)
    print(int)
end

添加一个函数,然后在test函数测试后面加两句

t:registerScript(handler(self, self.func))
t:run()

运行,不出意料的话,应该是报错的,这是因为传递函数和int这种基础类型不一样,对于这个错误我在c++如何调用lua函数有详细的说明,按照修正以后,编译运行

函数注册测试.png

可以看到输出的值和我们在c++往里面传递的值是一样的,至此,绑定的所有过程就完成了,掌握了这些我们就可以写底层的通信,复杂的计算,然后在lua中去调用,同时发挥lua和c++各自的优点!

打包android apk的注意事项

以上是完成了在win32平台的所有工作,但是我们打包成apk的时候,会提示找不到文件,我们在上面添加了2个自定义文件,又生成了2个桥接文件(都是指cpp文件),所以需要配置这几个文件的路径
找到目录

E:\LuaGame_3\frameworks\runtime-src\proj.android\jni

这个目录有一个android.mk文件

mk文件.png

需要添加这4个cpp的路径,以及新加的文件夹的路径,如此,打包apk才能顺利通过

推荐阅读更多精彩内容