RPG游戏制作-04-接入脚本前的准备

    在RPG的开发中,一般情况下都会使用脚本,脚本在游戏开发中也很重要,在RPG游戏中,脚本就像剧本,来控制整个RPG游戏的流程。

    本游戏使用lua脚本语言,版本为5.3,没使用额外的库,这点和cocos2d-x不同,cocos2d-x使用的luajit是基于5.1版本的,并且为了能在lua文件中开发游戏做了很多工作。这两种选择没什么优劣,只是看个人的选择,如果是c++为主,lua为辅的话,推荐第一种,也就是本游戏所使用的。

    lua在和c/c++交互时有着其他语言无法比拟的优势,且因为其由ANSI C编写,在跨平台方面也很优秀。不过,如果为了跨平台和代码的一致性考虑的话,了解lua的一些内部代码是很必要的。为了保证跨平台,首先,需要在package.searchers中额外添加一个加载器,这么做的原因如下:

1.保证跨平台。lua中关于打开文件和检测文件是否存在使用的是c语言的FILE以及fopen等函数,在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个加载器,引用

http://www.cppblog.com/sunicdavy/archive/2014/04/16/206610.html

,为以下四个:

1. preload, 对已加载的module进行直接返回, 对应package.preload[modname]

2. lualoader, 对lua文件进行加载, 搜索路径为package.path

3. cloader, 对lua标准dll进行加载, 搜索路径为package.cpath

4. 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;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的为加密文件。

本节运行界面无任何变化。

猜你喜欢

转载自blog.csdn.net/bull521/article/details/79883541