Cocos版本: 3.10
Lua版本: 5.1.4
环境: window Visual Studio 2013
Lua
Lua
作为一种脚本语言, 它的运行需要有宿主的存在,通过Lua虚拟栈进行数据交互。
它的底层实现是
C
语言,C语言封装了很多的API接口,使得C/C++与Lua之间可以很方便的通信交互。
Lua的官网: https://www.lua.org/
在cocos2dx中, Lua与C++的交互是通过**tolua++进行的,tolua++**实质上是对Lua C API的一层封装。
通过tolua++ 设定的接口,使得Lua很方便的调用C++提供的 cocos API接口。
Lua的运行需要有宿主的存在,在cocos2d-x中,C++可以作为Lua的宿主, 而Lua运行的时候需要通过虚拟栈进行数据交互,这个运行的环境,通常被称为
Lua_State
。
Lua虚拟栈
栈的特点是先进后出的, 在Lua的虚拟栈中它有着如下的特点:
- 栈中数据通过索引进行获取数据
- 索引的数值可以为正数,也可为负数; 正数为1的永远表示栈底,负数为-1的永远表示栈顶
如下图所示(图片来源:Lua调用原理展示):
)
假设我们想使用
C++
访问
Lua
文件中的数据
-- test.lua
str ="Get Lua Data Sucess!!!"functionAdd(num1, num2)return num1 + num2
end
以
C++
获取
Lua
文件中的字符串为例,其数据交互的流程是:
- C/C++将参数
str
放入Lua栈的栈顶中 - Lua从栈中获取参数
str
,并将栈顶置为空 - Lua从全局表中查找参数str对应的数据
- 全局表将参数str的数据反馈给Lua
- Lua将参数str的返回值放入堆栈中,此时返回值位于栈顶
- C++从栈中获取数据
C++
调用
Lua
需要进行环境配置:
1. 新建项目,选择Empty Project,在项目的Source Files新增.cpp文件
2. 若有Lua的相关环境,可将Lua/5.1目录下的include,lib文件夹拷贝到与.cpp文件同目录下
若无,则推荐LuaForWindows
其网址为:http://files.luaforge.net/releases/luaforwindows/luaforwindows
它会自动配置lua的环境,并安装SciTE工具相关,以后就可以在控制台,SciTE输入lua相关代码进行调试
属性配置,打开项目属性:
1. C/C++ -> General -> Additional Include Directories 将include目录添加进去
2. Linker -> General -> Additional Library Directories 将lib目录添加进去
3. 再通过Linker -> Input -> Additional Dependencies 添加lua5.1.lib, lua51.lib
示例代码:
#include<iostream>#include<string.h>extern"C"{// 提供了Lua的基本函数,在lua.h中的函数均已"lua_"为前缀#include"lua.h"// 定义lua的标准库函数,比如table, io, math等#include"lualib.h"// 提供了辅助库相关,以"luaL_"为前缀#include"lauxlib.h"}voidmain(){// 创建lua环境,并加载标准库
lua_State* pL =lua_open();luaL_openlibs(pL);// 加载lua文件,返回0表示成功int code =luaL_loadfile(pL,"test.lua");if(code !=0){return;}// 执行lua文件,参数分别为,lua环境,输入参数个数,返回值个数lua_call(pL,0,0);// 重置栈顶索引,设置为0表示栈清空lua_settop(pL,0);// ------------- 读取变量 -------------//lua_getglobal 主要做了这么几件事: 将参数压入栈中,lua获取参数的值后再将返回的结果压入栈中lua_getglobal(pL,"str");// 判定栈顶值类型是否为string,返回1表示成功,0表示失败int isStr =lua_isstring(pL,1);if(isStr ==1){// 获取栈顶值,并将lua值转换为C++类型
std::string str =lua_tostring(pL,1);
std::cout <<"str = "<< str.c_str()<< std::endl;}// ------------- 读取函数 -------------lua_getglobal(pL,"Add");// 将函数所需要的参数入栈lua_pushnumber(pL,1);// 压入第一个参数lua_pushnumber(pL,2);// 压入第二个参数/*
lua_pcall与lua_call类似,均用于执行lua文件,其方法分别为:
void lua_call(lua_State *L, int nargs, int nresults);
int lua_pcall(lua_State *L, int nargs, int nresults, int errfunc);
两者的区别在于:
前者在出现错误,程序会崩溃。后者多了一个errfunc索引,用于准确定位错误处理函数。
函数执行成功返回0,失败后可通过获取栈顶信息获取错误数据
两者的共同之处在于:
会根据nargs将参数按次序入栈,并根据nresults将返回值按次序填入栈中
若返回值结果数目大于nresults时,多余的将被丢弃;若小于nresults时,则按照nil补齐。
*/int result =lua_pcall(pL,2,1,0);if(result !=0){constchar*pErrorMsg =lua_tostring(pL,-1);
std::cout <<"ERROR:"<< pErrorMsg << std::endl;lua_close(pL);return;}/*
此处的栈中情况:
------------- 栈顶 -------------
正索引 负索引 类型 返回值
2 -1 number 3
1 -2 string "Get Lua Data Sucess!!!"
------------- 栈底 -------------
因此如下的索引获取数字索引可以使用-1或者2
*/int isNum =lua_isnumber(pL,-1);if(isNum ==1){double num =lua_tonumber(pL,-1);
std::cout <<"num = "<< num << std::endl;}// 关闭state环境,即销毁Lua_State对象,并释放Lua动态分配的空间lua_close(pL);system("pause");}
注意:
- C++在获取不同文件下的方法时,在通过
include
引用后,然后就直接调用 - Lua通过
luaL_loadfile
进行加载,然后等lua_call/lua_pcall
执行后才能获取数据
Lua这么处理的原因在于: 全局变量表中是不会存储相关数据的
Lua C API
这里简要说明下常用的API相关,有助于对了解后面的**tolua++**有帮助。 主要方法有:
/*
获取栈顶索引即栈中元素的个数,因为栈底为1,所以栈顶索引为多少,就代表有多少个元素
*/intlua_gettop(lua_State *L);/*
将栈顶索引设置为指定的数值
若设置的index比原栈顶高,则以nil补足。若index比原栈顶低,高出的部分舍弃。
比如: 栈中有8个元素,若index为7,则表示删除了一个栈顶的元素。若index为0,表示清空栈
注意: index为正数表示相对于栈底设置的,若为负数则相对于栈顶而设置的
*/voidlua_settop(lua_State *L,int index);/*
将栈中索引元素的副本压入栈顶
比如:从栈底到栈顶,元素状态为10,20,30,40;若索引为3则元素状态为:10,20,30,40,30
类似的还有:
lua_pushnil: 压入一个nil值
lua_pushboolean: 压入一个bool值
lua_pushnumber: 压入一个number值
*/voidlua_pushvalue(lua_State *L,int index);/*
删除指定索引元素,并将该索引之上的元素填补空缺
比如:从栈底到栈顶,元素状态为10,20,30,40;若索引为-3则元素状态为10,30,40
*/voidlua_remove(lua_State *L,int index);/*
将栈顶元素替换索引位置的的元素
比如:从栈底到栈顶,元素状态为10,20,30,40,50;若索引为2则,元素状态为10,50,30,40
即索引为2的元素20被栈顶元素50替换
*/voidlua_replace(lua_State *L,int index);/*
获取栈中指定索引元素的类型,若失败返回类型LUA_TNONE。其它类型有:
LUA_TBOOLEAN, LUA_TNUMBER, LUA_TSTRING, LUA_TTABLE
LUA_TFUNCTION, LUA_USERDATA等
*/intlua_type(lua_State *L,int idx);/*
检测栈中元素是否为某个类型,成功返回1,失败返回0; 类似的还有:
lua_isnumber, lua_isstring, lua_iscfunction, lua_isuserdata
*/intlua_isXXX(lua_State *L,int index);// 将栈中元素转换为C语言指定类型
lua_Number lua_tonumber(lua_State *L,int idx);
lua_Integer lua_tointeger(lua_State *L,int idx);intlua_toboolean(lua_State *L,int idx);constchar*lua_tolstring(lua_State *L,int idx,size_t*len);
lua_CFunction lua_tocfunction(lua_State *L,int idx);void*lua_touserdata(lua_State *L,int idx);
下面我们说下cocos2d-x 对Lua的封装相关。
cocos Lua框架
Lua在cocos引擎封装相关,它主要被放在cocos引擎的libluacocos2d中
- auto: 使用tolua++工具自动生成的C++代码相关
- **manual:**放置了cocos扩展的一些功能,比如LuaEngine, LuaStack, LuaBridge等
- luajit: 高效版的lua库,额外添加了lua没有的cocos库,并在对浮点计算,循环等进行了优化
- luasocket: 网络库相关
- tolua: tolua++库相关,实质是对Lua C库进行的再封装
- xxtea: cocos2d-x 自带的加密相关,目前项目使用的较少,很容易被破解
Lua的在引擎中的封装,主要是:
LuaEngine
封装的对Lua的管理引擎类Lua_Stack
对Lua运行环境Lua_State的封装,LuaEngine主要管理的就是Lua_Stack
项目启动
关于LuaEngine的初始化,主要在项目启动的时候初始化的,它仅仅是项目启动的一个小的环节。
如果想了解引擎的整个启动流程,可参考博客:cocos2dx 的启动和结束流程
而对于LuaEngine的启动,可以简单的分为三个步骤:
- 初始化
LuaEngine
,通过LuaStack
获取LuaState
运行环境 - 通过**tolua++**提供的接口,将C++ 不同的模块进行注册
- 执行Lua脚本
其主要代码实现在:
bool AppDelegate::applicationDidFinishLaunching(){// 初始化LuaEngine// 在getInstance中会初始化LuaStack,LuaStack初始化Lua环境相关auto engine = LuaEngine::getInstance();// 将LuaEngine添加到脚本引擎管理器ScriptEngineManager中
ScriptEngineManager::getInstance()->setScriptEngine(engine);// 获取Lua环境
lua_State* L = engine->getLuaStack()->getLuaState();// 注册额外的=C++提供的API模块相关lua_module_register(L);register_all_packages();// 设置cocos自带的加密相关
LuaStack* stack = engine->getLuaStack();
stack->setXXTEAKeyAndSign("2dxLua",strlen("2dxLua"),"XXTEA",strlen("XXTEA"));// 执行Lua脚本文件if(engine->executeScriptFile("main.lua")){return false;}return true;}
LuaEngine初始化
通过
LuaEngine::getInstance()
,我们了解下**LuaStack::init()**的相关实现
extern"C"{#include"lua.h"#include"tolua++.h"#include"lualib.h"#include"lauxlib.h"}
bool LuaStack::init(void){// 初始化Lua环境并打开标准库
_state =lua_open();luaL_openlibs(_state);toluafix_open(_state);// 注册全局函数print到lua中,它会覆盖lua库中的print方法const luaL_reg global_functions []={{"print", lua_print},{"release_print",lua_release_print},{nullptr, nullptr}};// 注册全局变量luaL_register(_state,"_G", global_functions);// 注册cocos2d-x引擎的API到lua环境中
g_luaType.clear();register_all_cocos2dx(_state);// ...#if(CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)// 导入android下调用java相关API
LuaJavaBridge::luaopen_luaj(_state);#endifaddLuaLoader(cocos2dx_lua_loader);return true;}
针对于
addLuaLoader
,它是Lua的加载器,主要将
cocos2dx_lua_loader
方法添加到Lua全局变量package下的loaders成员中。
当Lua通过
requires
加载脚本时,Lua会借助package下的loaders中的加载器
cocos2dx_lua_loader
加载。
该加载器支持我们自定义设置搜索路径相关,且拓展实现对脚本的加密解密相关。看下它的实现:
extern"C"{intcocos2dx_lua_loader(lua_State *L){// 后缀为luac和luastaticconst std::string BYTECODE_FILE_EXT =".luac";staticconst std::string NOT_BYTECODE_FILE_EXT =".lua";// require传入的要加载的文件名,比如:require "cocos.init" 下的"cocos.init"
std::string filename(luaL_checkstring(L,1));// 去掉后缀名".luac"或“.lua”size_t pos = filename.rfind(BYTECODE_FILE_EXT);if(pos != std::string::npos){
filename = filename.substr(0, pos);}else{
pos = filename.rfind(NOT_BYTECODE_FILE_EXT);if(pos == filename.length()- NOT_BYTECODE_FILE_EXT.length()){
filename = filename.substr(0, pos);}}// 将"."替换为"/"
pos = filename.find_first_of(".");while(pos != std::string::npos){
filename.replace(pos,1,"/");
pos = filename.find_first_of(".");}
Data chunk;
std::string chunkName;
FileUtils* utils = FileUtils::getInstance();// 获取package.path的变量lua_getglobal(L,"package");lua_getfield(L,-1,"path");// 通过package.path获取搜索路径相关,该路径为模版路径,格式类似于:// ?; ?.lua; c:\windows\?; /usr/local/lua/lua/?/?.lua 以“;”作为分割符
std::string searchpath(lua_tostring(L,-1));lua_pop(L,1);size_t begin =0;size_t next = searchpath.find_first_of(";",0);// 遍历package.path中的所有路径,查找文件是否存在,存在则通过getDataFromFile读取数据do{if(next == std::string::npos)
next = searchpath.length();
std::string prefix = searchpath.substr(begin, next);if(prefix[0]=='.'&& prefix[1]=='/'){
prefix = prefix.substr(2);}
pos = prefix.find("?.lua");// 将?替换为文件名,获取搜索路径名,比如:?.lua替换为cocos/init.lua
chunkName = prefix.substr(0, pos)+ filename + BYTECODE_FILE_EXT;if(utils->isFileExist(chunkName)){
chunk = utils->getDataFromFile(chunkName);break;}else{
chunkName = prefix.substr(0, pos)+ filename + NOT_BYTECODE_FILE_EXT;if(utils->isFileExist(chunkName)){
chunk = utils->getDataFromFile(chunkName);break;}}// 指定搜素路径下不存在该文件,则下一个
begin = next +1;
next = searchpath.find_first_of(";", begin);}while(begin <(int)searchpath.length());// 判定文件内容是否获取成功if(chunk.getSize()>0){// 加载文件
LuaStack* stack = LuaEngine::getInstance()->getLuaStack();
stack->luaLoadBuffer(L, reinterpret_cast<constchar*>(chunk.getBytes()),
static_cast<int>(chunk.getSize()), chunkName.c_str());}else{CCLOG("can not get file data of %s", chunkName.c_str());return0;}return1;}}
这个的实现其实就是Lua语言关于
require
实现的本质,比如:
- Cocos2dx 是如何实现搜索指定的Lua文件的
- Lua通过
require
是如何检索引用文件的 - 关于Lua文件查找不到,报错的路径信息是如何获取的
想了解更多内容,参考:lua 之 require
继续代码研究,看下
luaLoadBuffer
的实现:
nt LuaStack::luaLoadBuffer(lua_State *L,constchar*chunk,int chunkSize,constchar*chunkName){int r =0;// 判定是否加密,若lua脚本加密,则解密后在加载脚本文件// luaL_loadbuffer 用于加载并编译Lua代码,并将其压入栈中if(_xxteaEnabled &&strncmp(chunk, _xxteaSign, _xxteaSignLen)==0){// decrypt XXTEA
xxtea_long len =0;unsignedchar* result =xxtea_decrypt((unsignedchar*)chunk + _xxteaSignLen,(xxtea_long)chunkSize - _xxteaSignLen,(unsignedchar*)_xxteaKey,(xxtea_long)_xxteaKeyLen,&len);skipBOM((constchar*&)result,(int&)len);
r =luaL_loadbuffer(L,(char*)result, len, chunkName);free(result);}else{skipBOM(chunk, chunkSize);
r =luaL_loadbuffer(L, chunk, chunkSize, chunkName);}// 判定内容是否存在错误#ifdefined(COCOS2D_DEBUG)&& COCOS2D_DEBUG >0if(r){switch(r){case LUA_ERRSYNTAX:// 语法错误CCLOG("[LUA ERROR] load \"%s\", error: syntax error during pre-compilation.", chunkName);break;case LUA_ERRMEM:// 内存分配错误CCLOG("[LUA ERROR] load \"%s\", error: memory allocation error.", chunkName);break;case LUA_ERRRUN:// 运行错误CCLOG("[LUA ERROR] load \"%s\", error: run error.", chunkName);break;case LUA_ERRFILE:// 文件错误CCLOG("[LUA ERROR] load \"%s\", error: cannot open/read file.", chunkName);break;case LUA_ERRERR:// 运行错误处理函数时发生错误CCLOG("[LUA ERROR] load \"%s\", while running the error handler function.", chunkName);default:// 未知错误CCLOG("[LUA ERROR] load \"%s\", error: unknown.", chunkName);}// 通过lua的堆栈,获取栈顶的错误信息,将错误日志打印出来(-1永远表示栈顶)constchar* error =lua_tostring(L,-1);CCLOG("[LUA ERROR] Error Result: %s", error);lua_pop(L,1);}#endifreturn r;}
这里对Lua做的事情主要是:
- 检测Lua是否加密,如果是,则进行解密; 否则加载并编译Lua代码
- 如果是测试版本,会对Lua的内容进行安全检测。
最后我们梳理下关于
LuaEngine
对Lua的操作流程:
- 获取
LuaEngine
的接口,调用LuaStack
对Lua所需要的环境进行初始化 - 通过
addLuaLoader
将Lua的变量等信息添加到加载器中进行解析 - 解析文件后,通过
loadBuffer
加载Lua文件数据并进行安全检测。
到这里Lua的文件数据相关算是初始化成功了。
tolua++
**tolua++**是cocos官方提供的一个将C++代码相关转换为指定格式文件的工具,用于实现Lua对C++的调用, 简单的看下引擎在项目启动中关于tolua++封装的接口相关:
TOLUA_API intregister_all_cocos2dx(lua_State* tolua_S){tolua_open(tolua_S);tolua_module(tolua_S,"cc",0);tolua_beginmodule(tolua_S,"cc");lua_register_cocos2dx_Ref(tolua_S);lua_register_cocos2dx_Node(tolua_S);// 省略...tolua_endmodule(tolua_S);return1;}intlua_register_cocos2dx_Ref(lua_State* tolua_S){tolua_usertype(tolua_S,"cc.Ref");tolua_cclass(tolua_S,"Ref","cc.Ref","",nullptr);// tolua_function 表示对应的Ref所持有的public接口相关tolua_beginmodule(tolua_S,"Ref");tolua_function(tolua_S,"release",lua_cocos2dx_Ref_release);tolua_function(tolua_S,"retain",lua_cocos2dx_Ref_retain);tolua_function(tolua_S,"getReferenceCount",lua_cocos2dx_Ref_getReferenceCount);tolua_endmodule(tolua_S);
std::string typeName =typeid(cocos2d::Ref).name();
g_luaType[typeName]="cc.Ref";
g_typeCast["Ref"]="cc.Ref";return1;}
**tolua++**的特点就是开头必带前缀:
tolua_
tolua_usertype
用于声明一个自定义的类型,将其注册到Lua中tolua_cclass
声明一个C++类,并将其注册到Lua中。参数中的第一个是Lua中的类名,第二个是C++类名,第三个是父类名,第四个是模板参数(可选),第五个是模板名(可选)tolua_beginmodule/tolua_endmodule
用于定义一个模块,并将接口函数注册到该模块中tolua_function
将一个C++类的成员函数或静态函数注册为Lua中的函数, 方便调用
任意看一段函数的实现:
intlua_cocos2dx_Ref_getReferenceCount(lua_State* tolua_S){int argc =0;
cocos2d::Ref* cobj = nullptr;
bool ok = true;#ifCOCOS2D_DEBUG >=1
tolua_Error tolua_err;#endif// 从Lua栈中获取cocos对象类型,是否为cc.Ref#ifCOCOS2D_DEBUG >=1if(!tolua_isusertype(tolua_S,1,"cc.Ref",0,&tolua_err))goto tolua_lerror;#endif// 将数据转换为Ref对象,若失败则提示:无效的对象
cobj =(cocos2d::Ref*)tolua_tousertype(tolua_S,1,0);#ifCOCOS2D_DEBUG >=1if(!cobj){tolua_error(tolua_S,"invalid 'cobj' in function 'lua_cocos2dx_Ref_getReferenceCount'", nullptr);return0;}#endif// 获取参数数目,-1的原因在于对象类型Ref也在栈中
argc =lua_gettop(tolua_S)-1;if(argc ==0){if(!ok){tolua_error(tolua_S,"invalid arguments in function 'lua_cocos2dx_Ref_getReferenceCount'", nullptr);return0;}unsignedint ret = cobj->getReferenceCount();tolua_pushnumber(tolua_S,(lua_Number)ret);return1;}luaL_error(tolua_S,"%s has wrong number of arguments: %d, was expecting %d \n","cc.Ref:getReferenceCount",argc,0);return0;}
其他的cocos2d-x提供的Lua可调用方法不再赘述,与之类似。
结语
前面的内容将cocos2dx对Lua的封装,以及tolua++的使用,说了很多,主要原因在于:
- 官方提供的一些接口,仅支持在C++中使用,不支持在Lua中使用,比如骨骼动画中的一些复杂逻辑处理
- 项目的拓展需要有底层的支持
- 项目如果将cocosStudio替换为FairyGUI,需要了解这些。
最后祝大家学习生活愉快!
版权归原作者 鹤九日 所有, 如有侵权,请联系我们删除。