在RPG的开发中,一般情况下都会使用脚本,脚本在游戏开发中也很重要,在RPG游戏中,脚本就像剧本,来控制整个RPG游戏的流程。
本游戏使用lua脚本语言,版本为5.3,没使用额外的库,这点和cocos2d-x不同,cocos2d-x使用的luajit是基于5.1版本的,并且为了能在lua文件中开发游戏做了很多工作。这两种选择没什么优劣,只是看个人的选择。
lua在和c/c++交互时有着其他语言无法比拟的优势,且因为其由ANSI C编写,在跨平台方面也很优秀。不过,如果为了跨平台和代码的一致性考虑的话,了解lua的一些内部代码是很必要的。为了保证跨平台,首先,需要在package.searchers中额外添加一个加载器,这么做的原因如下:
1.保证跨平台
lua中打开文件和检测文件是否存在 使用的是c语言的FILE以及fopen等函数,在android下,如果lua读取的是压缩包中的文件的话,必然会失败;如果脚本文件是在android的可读写路径下的话则没什么问题。
2.一致性和功能性
在解析文件的同时可以添加一个hook(回调函数),以实现解密操作,同时又不会失去lua的特性。
其实,对package.searchers的操作就是为了在脚本文件中能使用require。(也可以不使用require,但是需要在程序运行开始前全部加载所需要的脚本文件,且每次有新的脚本文件都要对c++代码进行修改,不推荐)
那么问题来了,应该如何添加加载器函数?或者在何处添加?又或者如何添加呢?先看看require的相关代码:
static int ll_require (lua_State *L)
{
//获取对应的名称
const char *name = luaL_checkstring(L, 1);
//如果已经加载过了,则不再加载
lua_settop(L, 1);
lua_getfield(L, LUA_REGISTRYINDEX, "_LOADED");
lua_getfield(L, 2, name);
if (lua_toboolean(L, -1))
return 1;
//从栈中弹出lua_getfile()的结果
lua_pop(L, 1);
//发现加载器,若发现成功,栈中应该存在模块(类型为function)和字符串
findloader(L, name);
//压入必要的参数入栈
lua_pushstring(L, name);
lua_insert(L, -2);
//运行加载器来加载模块
lua_call(L, 2, 1);
/* _LOADED[name] = returned value */
if (!lua_isnil(L, -1))
lua_setfield(L, 2, name);
//没有返回值则让 _LOADED[name] = true
if (lua_getfield(L, 2, name) == LUA_TNIL)
{
lua_pushboolean(L, 1);
lua_pushvalue(L, -1);
lua_setfield(L, 2, name);
}
return 1;
}
然后看看findloader函数。
static void findloader (lua_State *L, const char *name) {
int i;
luaL_Buffer msg;
//仅初始化该结构体
luaL_buffinit(L, &msg);
/* 把 'package.searchers' 放到索引为3的栈中 */
if (lua_getfield(L, lua_upvalueindex(1), "searchers") != LUA_TTABLE)
luaL_error(L, "'package.searchers' must be a table");
/* 遍历可用的加载器 */
for (i = 1; ; i++) {
//searchers表中没有更多的加载器,则加载失败
if (lua_rawgeti(L, 3, i) == LUA_TNIL) {
lua_pop(L, 1);
luaL_pushresult(&msg);
luaL_error(L, "module '%s' not found:%s", name, lua_tostring(L, -1));
}
//压入形参,并执行函数
lua_pushstring(L, name);
lua_call(L, 1, 2);
//索引为-2的是函数,表示加载器加载块成功,直接返回
if (lua_isfunction(L, -2))
return;
//索引为-2的是字符串,添加该错误描述
else if (lua_isstring(L, -2)) {
lua_pop(L, 1);
luaL_addvalue(&msg);
}
else
lua_pop(L, 2);
}
}
在findloader函数中,对在package.searchers中的加载器从头开始遍历,并尝试加载对应的模块,如果栈中索引为-2的是函数,则表示加载成功,该函数直接返回。否则会尝试其他的加载器,直到遍历结束。
默认情况下,lua为package.searchers分配了4个加载器,参考网址,分配器为以下四个:
- preload, 对已加载的module进行直接返回, 对应package.preload[modname]
- lualoader, 对lua文件进行加载, 搜索路径为package.path
- cloader, 对lua标准dll进行加载, 搜索路径为package.cpath
- croot, 官方文档说的是all-in-one加载器, 感觉很神奇, 感兴趣可以自行参考源码
因此,为了保证不同平台的代码运行的一致性,我们添加的加载器应该在 preload和lualoader之间,且没必要把lua中的其他加载器函数设置为空。
GameScene.h新增的代码如下:
private:
EventLayer* m_pEventLayer;
MapLayer* m_pMapLayer;
PlayerLayer* m_pPlayerLayer;
EffectLayer* m_pEffectLayer;
Character* m_pViewpointCharacter;
lua_State* m_pLuaState;
public://脚本相关
//为lua添加加载器
void addLuaLoader(lua_CFunction func);
//添加lua文件搜索路径
void addLuaSearchPath(const char* path);
//执行脚本文件
int executeScriptFile(const char* filename);
void pushInt(int intValue);
void pushFloat(float floatValue);
void pushBoolean(bool boolValue);
void pushString(const char* stringValue);
void pushNil();
bool GameScene::initializeScript()
{
m_pLuaState = luaL_newstate();
luaL_openlibs(m_pLuaState);
//添加自定义的lua加载器
this->addLuaLoader(sdl_lua_loader);
//添加路径
this->addLuaSearchPath("Resources/script");
int ret = this->executeScriptFile("script/Map02_NPC02.lua");
if (ret != LUA_OK)
{
auto errMsg = luaL_checkstring(m_pLuaState,-1);
printf("%s", errMsg);
}
return true;
}
该函数在init函数中调用。在initializeScript函数中,添加了自定义的加载器,以及路径。需要注意的是,addLuaSearchPath()中的路径是在package.path添加,而这个路径在require等用到。
void GameScene::addLuaLoader(lua_CFunction func)
{
//空指针,直接返回
if (func == nullptr)
return ;
//获取对应的表
lua_getglobal(m_pLuaState, "package");
lua_getfield(m_pLuaState, -1 ,"searchers");
lua_pushcfunction(m_pLuaState, func);
//把searchers表原来的2以后的项往后移1位
for (int i = (int)lua_rawlen(m_pLuaState, -2) + 1; i > 2; i--)
{
//获取项到栈顶
lua_rawgeti(m_pLuaState, -2, i - 1);
//转移
lua_rawseti(m_pLuaState, -3, i);
}
//searchers[2] = func
lua_rawseti(m_pLuaState, -2, 2);
lua_setfield(m_pLuaState, -2, "searchers");
//清除栈顶,即清除 package表
lua_pop(m_pLuaState, 1);
}
addLuaLoader函数的功能就是先在位置2中腾出一个位置,之后把加载器(函数)添加到位置2。
void GameScene::addLuaSearchPath(const char* path)
{
//获取path
lua_getglobal(m_pLuaState, "package");
lua_getfield(m_pLuaState, -1,"path");
const char* curPath = luaL_checkstring(m_pLuaState, -1);
//添加
lua_pushfstring(m_pLuaState, "%s;%s/?.lua", curPath, path);
lua_setfield(m_pLuaState, -3, "path");
lua_pop(m_pLuaState, 2);
}
int GameScene::executeScriptFile(const char* filename)
{
FileUtils* utils = FileUtils::getInstance();
string fullpath = utils->fullPathForFilename(filename);
int ret = LUA_ERRRUN;
unsigned int size = 0;
unique_ptr<char> chunk = utils->getUniqueDataFromFile(fullpath, &size);
//加载并执行
if (chunk != nullptr)
{
const char* data = chunk.get();
ret = luaL_loadbuffer(m_pLuaState, data, size, nullptr);
if (ret == LUA_OK)
{
ret = lua_pcall(m_pLuaState, 0, 0, 0);
}
chunk.reset();
}
return ret;
}
为了保证跨平台,应该多使用引擎提供的函数(cocos2d-x没有getUniqueDataFromFile函数,可用getDataFromFile()代替)。
void GameScene::pushInt(int intValue)
{
lua_pushinteger(m_pLuaState, (lua_Integer)intValue);
}
void GameScene::pushFloat(float floatValue)
{
lua_pushnumber(m_pLuaState, (lua_Number)floatValue);
}
void GameScene::pushBoolean(bool boolValue)
{
int value = boolValue ? 1 : 0;
lua_pushboolean(m_pLuaState, value);
}
void GameScene::pushString(const char* stringValue)
{
lua_pushstring(m_pLuaState, stringValue);
}
void GameScene::pushNil()
{
lua_pushnil(m_pLuaState);
}
简单调用了lua的函数。
然后就是加载器函数的编写。
int sdl_lua_loader(lua_State* pL)
{
//后缀为 luac 和lua
static const std::string BYTECODE_FILE_EXT = ".luac";
static const std::string NOT_BYTECODE_FILE_EXT = ".lua";
//取出文件名
std::string filename(luaL_checkstring(pL, 1));
//去掉扩展名,如果有的话
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.size())
{
filename = filename.substr(0, pos);
}
}
//将所有的.替换成/
pos = filename.find_first_of(".");
while (pos != std::string::npos)
{
filename.replace(pos, 1, "/");
pos = filename.find_first_of(".", pos + 1);
}
//先在package.path中搜索脚本
FileUtils* utils = FileUtils::getInstance();
std::unique_ptr<char> chunk = nullptr;
size_t chunkSize = 0;
string chunkName;
//获取package.path
lua_getglobal(pL, "package");
lua_getfield(pL, -1, "path");
std::string searchpath(luaL_checkstring(pL, -1));
lua_pop(pL, 1);
size_t begin = 0;
size_t next = searchpath.find_first_of(";", 0);
//遍历path中的所有路径,结合文件名进行组装
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");
chunkName = prefix.substr(0, pos) + filename + BYTECODE_FILE_EXT;
if (utils->isFileExist(chunkName))
{
chunk = std::move(utils->getUniqueDataFromFile(chunkName, &chunkSize));
break;
}
else
{
chunkName = prefix.substr(0, pos) + filename + BYTECODE_FILE_EXT;
if (utils->isFileExist(chunkName))
{
chunk = std::move(utils->getUniqueDataFromFile(chunkName, &chunkSize));
break;
}
}
begin = next + 1;
next = searchpath.find_first_of(";", begin);
}while (begin < (int)searchpath.length());
//可在此处进行解密
if (chunk != nullptr)
{
const char* buffer = chunk.get();
luaL_loadbuffer(pL, buffer, chunkSize, chunkName.c_str());
chunk.reset();
lua_pushstring(pL, chunkName.c_str());
return 2;
}
lua_pushstring(pL, "can not get file data");
return 1;
}
这个函数和cocos2d-x中Cocos2dxLuaLoader.cpp的cocos2dx_lua_loader类似,其主要作用就是根据package.path中的路径,再加上模块名,然后检测该文件是否存在,如果存在则获取其数据并加载,否则则遍历直到结束。另外,这个函数的返回值其实并不重要,重要的是,如果操作完成,栈顶即索引为-1为字符串,索引为-2为函数(cocos2dx_lua_loader的返回值不是如此)。
扩展名为.luac的为加密文件。
本节运行界面无任何变化。