网上关于Cocos2dx开发过程中Lua的使用以及原理教程已经很多了,结合我的开发经验,我在这里稍微整理下。
可以说Cocos2dx-Lua提供了一种很轻便的开发模式,省去了冗长的编译时间,同时让热更成为了很容易的一件事情,不仅仅是在Android上,iOS上也轻易绕开了官方的审查,毕竟Lua在iOS系统看来都只是资源,就像txt文档。
然而Cocos2d-x是用C++开发的,那么,Lua究竟是怎么和C++通信的呢?在发布iOS平台的时候Lua有时候又需要和Object C交互,又应该怎么处理呢?
C/C++和Lua的通讯是通过维护一个Lua堆栈和Lua全局表进行的。Lua堆栈也是满足堆栈先进后出的特性,同时也支持使用索引进行访问,索引方式可以是正数也可以是负数,索引正数1表示栈底,索引负数-1表示栈顶。Lua启动或调用C语言时,Lua堆栈至少会有20个空闲槽位。Lua堆栈不是一个全局性的结构,每个函数都有自己的局部私有堆栈,当Lua调用C/C++函数的时候第一个参数总是这个局部栈的索引1。当C/C++函数把返回值压入Lua堆栈之后,该栈会被自动清空。
3 栈顶 -1 |
2 -2 |
1 栈底 -3 |
Lua全局表是一个类似于Map哈希表的结构,可以通过Key读取到对应的Value,比如Lua中定义有一个变量:
id = 10086
这就相当于在Lua全局表中存放了一个Key为'id',Value为‘10086’的映射关系,之后我们可以通过Key在Lua全局表中查到对应的Value‘10086’。
那么C\C++和Lua拥有不同的数据类型,要实现两者之间的数据通信该怎么办?Lua虚拟机提供Lua_State这样一种数据结构。任何一种数据从C\C++传入Lua虚拟机中,Lua都会将这类数据转换为一种通用的结构lua_TValue,并且将数据复制一份,将其压入Lua堆栈中。在Lua中,number、boolean、nil、light userdata四种类型的值是直接存在栈上元素里的,和Lua的垃圾回收无关,string、table、closure、userdata、hread存在栈上元素里的只是指针,他们都会在生命周期结束后被Lua垃圾回收。Lua有自己的GC,C\C++由自己申请和释放内存,所以两者之间的内存管理是独立的。从C\C++中传递数据到Lua虚拟机会发生数据拷贝,从Lua虚拟机中传递出来是直接从虚拟栈中取值或者地址,所以数据从虚拟栈中pop之后,是否依然是有效引用需要额外注意。
了解了上面的基础知识之后,接下来了解下Lua提供了哪些C API来对Lua堆栈进行操作,以实现C\C++和Lua的通讯。首先了解下操作Lua堆栈的基本步骤,主要有一下几步:
lua_getglobal(L,“name”) | 把全局变量name压入堆栈 |
lua_setglobal(L,“name”) | 从堆栈上弹出一个值并将其设置到全局变量name中 |
lua_getfield(L,index,“key”) | 把t[key]的值压入栈,t是有效索引 index 指向的值 |
lua_gettable(L,index) | 把value出栈并把t[value]值压入栈,t是有效索引 index 指向的值,value是栈顶存放的值 |
lua_tonumber(L,index) | 把有效索引 index 指向的栈值转换成C类型的lua_Number值 |
lua_tostring(L,index) | 把有效索引 index 指向的栈值转换成C字符串 |
lua_pushnumber(L,num) | 把数字num压栈 |
lua_pushstring(L,str) | 把指针str指向的以零结尾的字符串做一次内存拷贝压栈,因此函数返回后可释放或重用这些字符串 |
lua_pushvalue(L,index) | 把堆栈上给定有效索引index 处的元素做一个拷贝压栈 |
lua_insert(L,index) | 把栈顶元素插入到指定有效索引 index 处,并依次上移这个索引之上的元素 |
lua_replace(L,index) | 把栈顶元素移动到给定的有效索引 index 的位置,并把栈顶值弹出,不移动任何元素 |
lua_remove(L,index) | 从给定有效索引 index 处移除一个元素,把这个元素之上的所有元素下移填补这个空隙 |
5. 销毁Lua实例:lua_close(L);
接下来看下代码:以下是一个hello.lua文件
str = "I am so cool"
tbl = {name = "shun", id = 20114442}
function add(a,b)
return a + b
end
以下是CallLua.cpp的主要代码:
void main()
{
//1.创建Lua状态
lua_State *L = luaL_newstate();
if (L == NULL)
{
return ;
}
//2.加载Lua文件
int bRet = luaL_loadfile(L,"hello.lua");
if(bRet)
{
cout<<"load file error"<
接下来看Lua怎么调用C/C++。Lua调用C/C++有三种实现方法,下面依次介绍怎么调用。
方法一:注册到Lua模块中,可以将函数写lua.c中,然后重新编译Lua文件。注册到Lua模块主要有几个步骤:
- 定义函数:满足注册到Lua模块函数的原型;
- 注册函数:lua_pushcfunction(L,C函数名);
- 出栈函数:lua_setglobal(L,“Lua函数名”);
所有注册到Lua的函数都具有相同的原型,该原型定义在lua.h中的lua_CFunction:
typedef int (*lua_CFunction) (lua_State *L);
该函数只有一个参数:Lua状态,返回值是一个整数,表示压入栈中返回值个数。这个函数无需在压入结果前清空栈,在它返回之后Lua会自动删除Lua栈结果之下的内容。
// This is my function
static int getTwoVar(lua_State *L)
{
// 向函数栈中压入2个值
lua_pushnumber(L, 10);
lua_pushstring(L,"hello");
return 2;
}
// 在pmain函数中,luaL_openlibs函数后加入以下代码:
// 注册函数
lua_pushcfunction(L, getTwoVar); //将函数放入栈中
lua_setglobal(L, "getTwoVar"); //设置lua全局变量getTwoVar
// 上面的注册函数可以这样子写:
// lua_register(L,"getTwoVar",getTwoVar);
方法二:使用静态依赖的方式,大概步骤是:在C/C++中定义一个函数,将函数注册到Lua模块中,然后由C/C++去执行我们的Lua文件,然后在Lua中调用刚刚注册的函数。代码如下:
新建avg.lua文件:
avg, sum = average(10, 20, 30, 40, 50)
print("The average is ", avg)
print("The sum is ", sum)
然后再C/C++中调用Lua文件并执行:
#include
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
/* 指向Lua解释器的指针 */
lua_State* L;
static int average(lua_State *L)
{
/* 得到参数个数 */
int n = lua_gettop(L);
double sum = 0;
int i;
/* 循环求参数之和 */
for (i = 1; i <= n; i++)
{
/* 求和 */
sum += lua_tonumber(L, i);
}
/* 压入平均值 */
lua_pushnumber(L, sum / n);
/* 压入和 */
lua_pushnumber(L, sum);
/* 返回返回值的个数 */
return 2;
}
int main ( int argc, char *argv[] )
{
/* 初始化Lua */
L = lua_open();
/* 载入Lua基本库 */
luaL_openlibs(L);
/* 注册函数 */
lua_register(L, "average", average);
/* 运行脚本 */
luaL_dofile(L, "avg.lua");
/* 清除Lua */
lua_close(L);
/* 暂停 */
printf( "Press enter to exit…" );
getchar();
return 0;
}
方法三:使用动态链接的方式,步骤主要有:创建一个新的工程,命名为mLualib,然后将编写需要注册到Lua中的代码,编译生成动态链接库,将其导出到项目工程中,最后在Lua代码中加载这个动态链接库即可。代码如下:
在新建项目工程中编写需要注册到Lua中的代码,.h头文件:
#pragma once
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
#ifdef LUA_EXPORTS
#define LUA_API __declspec(dllexport)
#else
#define LUA_API __declspec(dllimport)
#endif
extern "C" LUA_API int luaopen_mLualib(lua_State *L);//定义导出函数
.cpp文件:
#include
#include "mLualib.h"
static int averageFunc(lua_State *L)
{
int n = lua_gettop(L);
double sum = 0;
int i;
/* 循环求参数之和 */
for (i = 1; i <= n; i++)
sum += lua_tonumber(L, i);
lua_pushnumber(L, sum / n); //压入平均值
lua_pushnumber(L, sum); //压入和
return 2; //返回两个结果
}
static int sayHelloFunc(lua_State* L)
{
printf("hello world!");
return 0;
}
static const struct luaL_Reg myLib[] =
{
{"average", averageFunc},
{"sayHello", sayHelloFunc},
{NULL, NULL} //数组中最后一对必须是{NULL, NULL},用来表示结束
};
int luaopen_mLualib(lua_State *L)
{
// luaL_register会根据给定名称“ss”创建(或复用)一个table,并用myLib中的信息填充这个table,
// luaL_register函数返回时,会将这个table留在Lua堆栈中
luaL_register(L, "ss", myLib);
return 1; // 把myLib表压入了栈中,所以就需要返回1
}
最后在项目工程的Lua代码中加载这个动态链接库:
require "mLualib"
local ave,sum = ss.average(1,2,3,4,5)//参数对应堆栈中的数据
print(ave,sum) -- 3 15
ss.sayHello() -- hello world!
至此,关于C/C++是如何和Lua的通讯就基本结束了。那么在Cocos2dx开发的过程中,我们其实并不需要自己去手动编写代码把C/C++接口暴露给到Lua,Cocos2dx引擎的开发者已经将大部分的C/C++接口暴露给到了Lua这边,他们在工程中集成了工具tolua++,通过这个工具只需要做一些简单的配置,就可以把C/C++函数暴露给Lua使用。所以,当我们要需求需要把自己编写的接口暴露给Lua调用的时候,也可以模仿这种方式,直接配置一些导出即可。具体关于tolua++工具的环境搭建和参数配置,可以参考这篇文章。
使用tolua导出自定义类的主要步骤有以下几点:
- 首先按照 cocos2d-x-3.0rc0\tools\tolua\README.mdown说的步骤安装必要的库和工具包,配置好相关环境变量,搭建好环境;
- 编写需要导出自定义类的C/C++文件,注意:该类可继承Cocos2d::Ref类,以便使用Cocos2dx的内存回收机制;
- 编写.ini配置文件:在frameworks/cocos2d-x/tools/tolua/目录下可以看到genbindings.py脚本和一大堆.ini文件,复制一个并重命名为新的配置文件,同时修改配置文件中的一些关键配置:
[配置文件名]
prefix = 配置文件名
target_namespace =
headers = %(cocosdir)s/../自定义类头文件目录
classes = 导出类名
4.编写生成脚本:同样在frameworks/cocos2d-x/tools/tolua/目录下,可以看到genbindings.py文件,这个就是生成脚本了,复制并重命名一个文件,并修改里面的一些配置:
output_dir = '%s/cocos/scripting/lua-bindings/auto' % project_root(修改成导出目录)
cmd_args = {'配置文件名.ini' : ('配置文件名', '导出文件名') }
5.编译生成脚本:python 生成脚本.py。
这个时候在导出目录下,你就可以看到两个以导出文件名命名的.hpp文件和.cpp文件,把这两个文件导入到项目工程中。最后把这个自定义类注册到Lua环境中,打开AppDelegate.cpp文件,包含导出文件.hpp头文件,接着找到:
LuaEngine* engine = LuaEngine::getInstance();
在它后面写注册函数,打开导出文件.cpp文件,找到注册函数定义,注册函数名为:register_all_导出类名(lua_State* tolua_s)。编译工程重新生成链接库,这样就可以在Lua中使用自定义类了。
这里提一下在Cocos2dx开发过程中遇到的一个问题,就是将Lua回调函数传递给到C/C++。通过工具tolua++是无法正确生成可以给Lua调用的接口的,需要手动编写绑定方法,在tolua++生成绑定的cpp文件中作出 一点修改就好了:
注释掉的那句是本来生成的代码,改用tolua_fix中的 toluafix_ref_function() 方法即可, 第二个参数用注释掉的那个函数调用的第二个参数。
LUA_FUNCTION arg0;
//ok &= luaval_to_int32(tolua_S, 2,(int *)&arg0, "TcpHelper:test");
arg0 = toluafix_ref_function(tolua_S, 2, 0);
下面是C++中的代码:
void test(LUA_FUNCTION listener){
LuaStack *stack = LuaEngine::getInstance()->getLuaStack();
stack->clean();
LuaValueDict dict;
dict["name"] = LuaValue::stringValue("success");
stack->pushLuaValueDict(dict);
stack->executeFunctionByHandler(listener, 1);
}
Lua中的调用:
local entity = TestClass:new()
entity:test(function(arg)
print("!!!!")
dump(arg)
end)
最后说下Object C和Lua之间的通讯。Cocos2dx提供了一个luaoc.lua类给我们使用,具体的如何使用可以参考这篇文章。这种方法是引擎提供的,但是如果要一些情况这个类无法满足的时候,其实还有另外一种思路可以提供参考。Object C要和Lua进行通讯需要一个中间人,这个就是C/C++。C/C++可以和Object C混编,要实现C/C++和Objcet C混编需要将.m文件的后缀名改为.mm后缀。
参考:
Lua和C/C+的通讯:
https://www.cnblogs.com/sevenyuan/p/4511808.html
https://blog.csdn.net/shun_fzll/article/details/39120965
https://segmentfault.com/a/1190000000631630
https://blog.csdn.net/beautyleaf/article/details/51759742
Lua回调函数传递给到C/C++:
https://www.cnblogs.com/cnxkey/articles/7789231.html
http://www.cnblogs.com/boliu/p/4091274.html
你可能感兴趣的:(游戏开发,Cocos2dx,Lua,Object,C,C,C++)