Cocos版本: 3.10
Lua版本: 5.1.4
環(huán)境: window Visual Studio 2013
Lua
Lua
作為一種腳本語言, 它的運行需要有宿主的存在,通過Lua虛擬棧進行數(shù)據(jù)交互。
它的底層實現(xiàn)是C
語言,C語言封裝了很多的API接口,使得C/C++與Lua之間可以很方便的通信交互。
Lua的官網(wǎng): https://www.lua.org/
在cocos2dx中, Lua與C++的交互是通過**tolua++**進行的,**tolua++**實質(zhì)上是對Lua C API的一層封裝。
通過tolua++ 設定的接口,使得Lua很方便的調(diào)用C++提供的 cocos API接口。
Lua的運行需要有宿主的存在,在cocos2d-x中,C++可以作為Lua的宿主, 而Lua運行的時候需要通過虛擬棧進行數(shù)據(jù)交互,這個運行的環(huán)境,通常被稱為Lua_State
。
Lua虛擬棧
棧的特點是先進后出的, 在Lua的虛擬棧中它有著如下的特點:
- 棧中數(shù)據(jù)通過索引進行獲取數(shù)據(jù)
- 索引的數(shù)值可以為正數(shù),也可為負數(shù); 正數(shù)為1的永遠表示棧底,負數(shù)為-1的永遠表示棧頂
如下圖所示(圖片來源:Lua調(diào)用原理展示):
)
假設我們想使用C++
訪問Lua
文件中的數(shù)據(jù)
-- test.lua
str = "Get Lua Data Sucess!!!"
function Add(num1, num2)
return num1 + num2
end
以C++
獲取Lua
文件中的字符串為例,其數(shù)據(jù)交互的流程是:
-
C/C++將參數(shù)
str
放入Lua棧的棧頂中 -
Lua從棧中獲取參數(shù)
str
,并將棧頂置為空 -
Lua從全局表中查找參數(shù)str對應的數(shù)據(jù)
-
全局表將參數(shù)str的數(shù)據(jù)反饋給Lua
-
Lua將參數(shù)str的返回值放入堆棧中,此時返回值位于棧頂
-
C++從棧中獲取數(shù)據(jù)
C++
調(diào)用Lua
需要進行環(huán)境配置:
1. 新建項目,選擇Empty Project,在項目的Source Files新增.cpp文件
2. 若有Lua的相關環(huán)境,可將Lua/5.1目錄下的include,lib文件夾拷貝到與.cpp文件同目錄下
若無,則推薦LuaForWindows
其網(wǎng)址為:http://files.luaforge.net/releases/luaforwindows/luaforwindows
它會自動配置lua的環(huán)境,并安裝SciTE工具相關,以后就可以在控制臺,SciTE輸入lua相關代碼進行調(diào)試
屬性配置,打開項目屬性:
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的基本函數(shù),在lua.h中的函數(shù)均已"lua_"為前綴
#include "lua.h"
// 定義lua的標準庫函數(shù),比如table, io, math等
#include "lualib.h"
// 提供了輔助庫相關,以"luaL_"為前綴
#include "lauxlib.h"
}
void main(){
// 創(chuàng)建lua環(huán)境,并加載標準庫
lua_State* pL = lua_open();
luaL_openlibs(pL);
// 加載lua文件,返回0表示成功
int code = luaL_loadfile(pL, "test.lua");
if (code != 0){
return;
}
// 執(zhí)行l(wèi)ua文件,參數(shù)分別為,lua環(huán)境,輸入?yún)?shù)個數(shù),返回值個數(shù)
lua_call(pL, 0, 0);
// 重置棧頂索引,設置為0表示棧清空
lua_settop(pL, 0);
// ------------- 讀取變量 -------------
//lua_getglobal 主要做了這么幾件事: 將參數(shù)壓入棧中,lua獲取參數(shù)的值后再將返回的結(jié)果壓入棧中
lua_getglobal(pL, "str");
// 判定棧頂值類型是否為string,返回1表示成功,0表示失敗
int isStr = lua_isstring(pL, 1);
if (isStr == 1) {
// 獲取棧頂值,并將lua值轉(zhuǎn)換為C++類型
std::string str = lua_tostring(pL, 1);
std::cout << "str = " << str.c_str() << std::endl;
}
// ------------- 讀取函數(shù) -------------
lua_getglobal(pL, "Add");
// 將函數(shù)所需要的參數(shù)入棧
lua_pushnumber(pL, 1); // 壓入第一個參數(shù)
lua_pushnumber(pL, 2); // 壓入第二個參數(shù)
/*
lua_pcall與lua_call類似,均用于執(zhí)行l(wèi)ua文件,其方法分別為:
void lua_call(lua_State *L, int nargs, int nresults);
int lua_pcall(lua_State *L, int nargs, int nresults, int errfunc);
兩者的區(qū)別在于:
前者在出現(xiàn)錯誤,程序會崩潰。后者多了一個errfunc索引,用于準確定位錯誤處理函數(shù)。
函數(shù)執(zhí)行成功返回0,失敗后可通過獲取棧頂信息獲取錯誤數(shù)據(jù)
兩者的共同之處在于:
會根據(jù)nargs將參數(shù)按次序入棧,并根據(jù)nresults將返回值按次序填入棧中
若返回值結(jié)果數(shù)目大于nresults時,多余的將被丟棄;若小于nresults時,則按照nil補齊。
*/
int result = lua_pcall(pL, 2, 1, 0);
if (result != 0) {
const char *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!!!"
------------- 棧底 -------------
因此如下的索引獲取數(shù)字索引可以使用-1或者2
*/
int isNum = lua_isnumber(pL, -1);
if (isNum == 1) {
double num = lua_tonumber(pL, -1);
std::cout << "num = " << num << std::endl;
}
// 關閉state環(huán)境,即銷毀Lua_State對象,并釋放Lua動態(tài)分配的空間
lua_close(pL);
system("pause");
}
注意:
- C++在獲取不同文件下的方法時,在通過
include
引用后,然后就直接調(diào)用 - Lua通過
luaL_loadfile
進行加載,然后等lua_call/lua_pcall
執(zhí)行后才能獲取數(shù)據(jù)
Lua這么處理的原因在于: 全局變量表中是不會存儲相關數(shù)據(jù)的
Lua C API
這里簡要說明下常用的API相關,有助于對了解后面的**tolua++**有幫助。 主要方法有:
/*
獲取棧頂索引即棧中元素的個數(shù),因為棧底為1,所以棧頂索引為多少,就代表有多少個元素
*/
int lua_gettop(lua_State *L);
/*
將棧頂索引設置為指定的數(shù)值
若設置的index比原棧頂高,則以nil補足。若index比原棧頂?shù)?,高出的部分舍棄?比如: 棧中有8個元素,若index為7,則表示刪除了一個棧頂?shù)脑亍H鬷ndex為0,表示清空棧
注意: index為正數(shù)表示相對于棧底設置的,若為負數(shù)則相對于棧頂而設置的
*/
void lua_settop(lua_State *L, int index);
/*
將棧中索引元素的副本壓入棧頂
比如:從棧底到棧頂,元素狀態(tài)為10,20,30,40;若索引為3則元素狀態(tài)為:10,20,30,40,30
類似的還有:
lua_pushnil: 壓入一個nil值
lua_pushboolean: 壓入一個bool值
lua_pushnumber: 壓入一個number值
*/
void lua_pushvalue(lua_State *L, int index);
/*
刪除指定索引元素,并將該索引之上的元素填補空缺
比如:從棧底到棧頂,元素狀態(tài)為10,20,30,40;若索引為-3則元素狀態(tài)為10,30,40
*/
void lua_remove(lua_State *L, int index);
/*
將棧頂元素替換索引位置的的元素
比如:從棧底到棧頂,元素狀態(tài)為10,20,30,40,50;若索引為2則,元素狀態(tài)為10,50,30,40
即索引為2的元素20被棧頂元素50替換
*/
void lua_replace(lua_State *L, int index);
/*
獲取棧中指定索引元素的類型,若失敗返回類型LUA_TNONE。其它類型有:
LUA_TBOOLEAN, LUA_TNUMBER, LUA_TSTRING, LUA_TTABLE
LUA_TFUNCTION, LUA_USERDATA等
*/
int lua_type(lua_State *L, int idx);
/*
檢測棧中元素是否為某個類型,成功返回1,失敗返回0; 類似的還有:
lua_isnumber, lua_isstring, lua_iscfunction, lua_isuserdata
*/
int lua_isXXX(lua_State *L, int index);
// 將棧中元素轉(zhuǎn)換為C語言指定類型
lua_Number lua_tonumber(lua_State *L, int idx);
lua_Integer lua_tointeger(lua_State *L, int idx);
int lua_toboolean(lua_State *L, int idx);
const char* 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庫,并在對浮點計算,循環(huán)等進行了優(yōu)化
-
luasocket: 網(wǎng)絡庫相關
-
tolua: tolua++庫相關,實質(zhì)是對Lua C庫進行的再封裝
-
xxtea: cocos2d-x 自帶的加密相關,目前項目使用的較少,很容易被破解
Lua的在引擎中的封裝,主要是:
-
LuaEngine
封裝的對Lua的管理引擎類 -
Lua_Stack
對Lua運行環(huán)境Lua_State的封裝,LuaEngine主要管理的就是Lua_Stack
項目啟動
關于LuaEngine的初始化,主要在項目啟動的時候初始化的,它僅僅是項目啟動的一個小的環(huán)節(jié)。
如果想了解引擎的整個啟動流程,可參考博客:cocos2dx 的啟動和結(jié)束流程
而對于LuaEngine的啟動,可以簡單的分為三個步驟:
- 初始化
LuaEngine
,通過LuaStack
獲取LuaState
運行環(huán)境 - 通過**tolua++**提供的接口,將C++ 不同的模塊進行注冊
- 執(zhí)行Lua腳本
其主要代碼實現(xiàn)在:
bool AppDelegate::applicationDidFinishLaunching() {
// 初始化LuaEngine
// 在getInstance中會初始化LuaStack,LuaStack初始化Lua環(huán)境相關
auto engine = LuaEngine::getInstance();
// 將LuaEngine添加到腳本引擎管理器ScriptEngineManager中
ScriptEngineManager::getInstance()->setScriptEngine(engine);
// 獲取Lua環(huán)境
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"));
// 執(zhí)行Lua腳本文件
if (engine->executeScriptFile("main.lua")) {
return false;
}
return true;
}
LuaEngine初始化
通過LuaEngine::getInstance()
,我們了解下**LuaStack::init()**的相關實現(xiàn)
extern "C" {
#include "lua.h"
#include "tolua++.h"
#include "lualib.h"
#include "lauxlib.h"
}
bool LuaStack::init(void)
{
// 初始化Lua環(huán)境并打開標準庫
_state = lua_open();
luaL_openlibs(_state);
toluafix_open(_state);
// 注冊全局函數(shù)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環(huán)境中
g_luaType.clear();
register_all_cocos2dx(_state);
// ...
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
// 導入android下調(diào)用java相關API
LuaJavaBridge::luaopen_luaj(_state);
#endif
addLuaLoader(cocos2dx_lua_loader);
return true;
}
針對于addLuaLoader
,它是Lua的加載器,主要將 cocos2dx_lua_loader
方法添加到Lua全局變量package下的loaders成員中。
當Lua通過 requires
加載腳本時,Lua會借助package下的loaders中的加載器 cocos2dx_lua_loader
加載。
該加載器支持我們自定義設置搜索路徑相關,且拓展實現(xiàn)對腳本的加密解密相關??聪滤膶崿F(xiàn):
extern "C" {
int cocos2dx_lua_loader(lua_State *L) {
// 后綴為luac和lua
static const std::string BYTECODE_FILE_EXT = ".luac";
static const 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讀取數(shù)據(jù)
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());
// 判定文件內(nèi)容是否獲取成功
if (chunk.getSize() > 0) {
// 加載文件
LuaStack* stack = LuaEngine::getInstance()->getLuaStack();
stack->luaLoadBuffer(L, reinterpret_cast<const char*>(chunk.getBytes()),
static_cast<int>(chunk.getSize()), chunkName.c_str());
} else {
CCLOG("can not get file data of %s", chunkName.c_str());
return 0;
}
return 1;
}
}
這個的實現(xiàn)其實就是Lua語言關于require
實現(xiàn)的本質(zhì),比如:
- Cocos2dx 是如何實現(xiàn)搜索指定的Lua文件的
- Lua通過
require
是如何檢索引用文件的 - 關于Lua文件查找不到,報錯的路徑信息是如何獲取的
想了解更多內(nèi)容,參考:lua 之 require
繼續(xù)代碼研究,看下luaLoadBuffer
的實現(xiàn):
nt LuaStack::luaLoadBuffer(lua_State *L, const char *chunk, int chunkSize, const char *chunkName) {
int r = 0;
// 判定是否加密,若lua腳本加密,則解密后在加載腳本文件
// luaL_loadbuffer 用于加載并編譯Lua代碼,并將其壓入棧中
if (_xxteaEnabled && strncmp(chunk, _xxteaSign, _xxteaSignLen) == 0) {
// decrypt XXTEA
xxtea_long len = 0;
unsigned char* result = xxtea_decrypt((unsigned char*)chunk + _xxteaSignLen,
(xxtea_long)chunkSize - _xxteaSignLen,
(unsigned char*)_xxteaKey,
(xxtea_long)_xxteaKeyLen,
&len);
skipBOM((const char*&)result, (int&)len);
r = luaL_loadbuffer(L, (char*)result, len, chunkName);
free(result);
} else {
skipBOM(chunk, chunkSize);
r = luaL_loadbuffer(L, chunk, chunkSize, chunkName);
}
// 判定內(nèi)容是否存在錯誤
#if defined(COCOS2D_DEBUG) && COCOS2D_DEBUG > 0
if (r) {
switch (r) {
case LUA_ERRSYNTAX:
// 語法錯誤
CCLOG("[LUA ERROR] load \"%s\", error: syntax error during pre-compilation.", chunkName);
break;
case LUA_ERRMEM:
// 內(nèi)存分配錯誤
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:
// 運行錯誤處理函數(shù)時發(fā)生錯誤
CCLOG("[LUA ERROR] load \"%s\", while running the error handler function.", chunkName);
default:
// 未知錯誤
CCLOG("[LUA ERROR] load \"%s\", error: unknown.", chunkName);
}
// 通過lua的堆棧,獲取棧頂?shù)腻e誤信息,將錯誤日志打印出來(-1永遠表示棧頂)
const char* error = lua_tostring(L, -1);
CCLOG("[LUA ERROR] Error Result: %s", error);
lua_pop(L, 1);
}
#endif
return r;
}
這里對Lua做的事情主要是:
- 檢測Lua是否加密,如果是,則進行解密; 否則加載并編譯Lua代碼
- 如果是測試版本,會對Lua的內(nèi)容進行安全檢測。
最后我們梳理下關于LuaEngine
對Lua的操作流程:
- 獲取
LuaEngine
的接口,調(diào)用LuaStack
對Lua所需要的環(huán)境進行初始化 - 通過
addLuaLoader
將Lua的變量等信息添加到加載器中進行解析 - 解析文件后,通過
loadBuffer
加載Lua文件數(shù)據(jù)并進行安全檢測。
到這里Lua的文件數(shù)據(jù)相關算是初始化成功了。
tolua++
**tolua++**是cocos官方提供的一個將C++代碼相關轉(zhuǎn)換為指定格式文件的工具,用于實現(xiàn)Lua對C++的調(diào)用, 簡單的看下引擎在項目啟動中關于tolua++封裝的接口相關:
TOLUA_API int register_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);
return 1;
}
int lua_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";
return 1;
}
**tolua++**的特點就是開頭必帶前綴: tolua_
-
tolua_usertype
用于聲明一個自定義的類型,將其注冊到Lua中 -
tolua_cclass
聲明一個C++類,并將其注冊到Lua中。參數(shù)中的第一個是Lua中的類名,第二個是C++類名,第三個是父類名,第四個是模板參數(shù)(可選),第五個是模板名(可選) -
tolua_beginmodule/tolua_endmodule
用于定義一個模塊,并將接口函數(shù)注冊到該模塊中 -
tolua_function
將一個C++類的成員函數(shù)或靜態(tài)函數(shù)注冊為Lua中的函數(shù), 方便調(diào)用
任意看一段函數(shù)的實現(xiàn):
int lua_cocos2dx_Ref_getReferenceCount(lua_State* tolua_S) {
int argc = 0;
cocos2d::Ref* cobj = nullptr;
bool ok = true;
#if COCOS2D_DEBUG >= 1
tolua_Error tolua_err;
#endif
// 從Lua棧中獲取cocos對象類型,是否為cc.Ref
#if COCOS2D_DEBUG >= 1
if (!tolua_isusertype(tolua_S,1,"cc.Ref",0,&tolua_err)) goto tolua_lerror;
#endif
// 將數(shù)據(jù)轉(zhuǎn)換為Ref對象,若失敗則提示:無效的對象
cobj = (cocos2d::Ref*)tolua_tousertype(tolua_S,1,0);
#if COCOS2D_DEBUG >= 1
if (!cobj)
{
tolua_error(tolua_S,"invalid 'cobj' in function 'lua_cocos2dx_Ref_getReferenceCount'", nullptr);
return 0;
}
#endif
// 獲取參數(shù)數(shù)目,-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);
return 0;
}
unsigned int ret = cobj->getReferenceCount();
tolua_pushnumber(tolua_S,(lua_Number)ret);
return 1;
}
luaL_error(tolua_S, "%s has wrong number of arguments: %d, was expecting %d \n", "cc.Ref:getReferenceCount",argc, 0);
return 0;
}
其他的cocos2d-x提供的Lua可調(diào)用方法不再贅述,與之類似。
結(jié)語
前面的內(nèi)容將cocos2dx對Lua的封裝,以及tolua++的使用,說了很多,主要原因在于:文章來源:http://www.zghlxwxcb.cn/news/detail-713994.html
- 官方提供的一些接口,僅支持在C++中使用,不支持在Lua中使用,比如骨骼動畫中的一些復雜邏輯處理
- 項目的拓展需要有底層的支持
- 項目如果將cocosStudio替換為FairyGUI,需要了解這些。
最后祝大家學習生活愉快!文章來源地址http://www.zghlxwxcb.cn/news/detail-713994.html
到了這里,關于cocos2d-x C++與Lua交互的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!