游戏服务器之lua脚本系统

现在做游戏的较多用脚本 ,可以热加载代码。

脚本系统的目的是方便c++和lua之间的相互调用,实现c++和lua的 交互。

脚本系统设计:

(1)脚本系统创建时初始化虚拟机和库,析构时销毁。

(2)脚本加载:

主体入口脚本NPCEntry.lua:

含所有npc的脚本的表,含聊天接口函数。选择调用接口控制脚本QC.lua的接口,或者npc脚本的接口。

接口控制脚本QC.lua:

含接任务和交任务的接口(会使用任务表NPCQuest),检查接任务条件,来选择接任务还是返回聊天响应;检查交任务的条件,来选择交任务还是返回聊天信息。

各个npc脚本:

各个npc脚本以npc的id来命名,加载时存储在主体入口脚本的NPCEntry.lua的npc的表。

(3)配置加载:

NPCQuest表是配置在配置表,在脚本系统初始化时热加载

(4)函数调用:

(4-1)c++调用lua:把脚本参数存储到自定义列表,调用lua函数时压入函数和自定义参数到运行时栈。

(4-2)lua调用c++:使用tolua++导出需要调用的类的接口和成员变量。

脚本结构设计:

(1)主体入口文件NPCEntry.lua
(1-1)表npcTable  会按npcId 索引对应的npc lua文件的代码

(1-2)点击npc 函数 clickNPC 会返回对应的该npcId的所有的可接任务和可提交任务组成的字符串

(1-3)与npc聊天函数 talkNPC 会调用接任务或交任务接口,并返回其对话的字符串

(2)任务控制文件QC.lua 含任务接受(或对话接口)、任务提交(或对话)接口。

(3)npc 脚本 npc1.lua (npc2.lua ......). 含指定npcId的默认对话(可接可交任务)组成的字符串的接口。可拓展npc与玩家交互接口。

1、脚本系统定义

为了可以传入自定义类型的变量和原子变量到一个列表,再一次性压栈(其实函数的调用就是压栈弹栈的过程,c++和lua交互的过程也是如此,就是通过栈来实现变量的访问)。

(1)脚本系统的初始化和关闭

脚本系统的析构函数里需要关掉虚拟机

if(m_pLua)
{
lua_close(m_pLua);
m_pLua = NULL;
}

在构造函数里打开虚拟机

m_pLua = lua_open();
luaL_openlibs(m_pLua);

初始化脚本系统:

1)加载npc脚本和npc数据到lua虚拟机

2)把tolua++导出的接口导入到lua虚拟机

bool ScriptSystemLoad::init()
{
	if (!initNPCScript())//加载npc脚本和npc数据到lua虚拟机
	{
		printf("initNPCScript error\n");
		return false;
	}
	bool res = loadLuaServerInterface();//加载tolua++导出的接口到lua虚拟机
	if (!res)
	{
		printf("loadLuaServerInterface error\n");
		return false;
	}
	
	return true;
}

 

(2)调用脚本函数

步骤如下:

1)调用函数需要传入函数指针和参数,先压入需要调用的函数,然后是压入各个参数。最终执行是使用交互栈底的lua函数,而参数则是函数以上的指定个数的交互栈数据。

2)返回函数的执行结果,在交互栈的栈顶(结果可能多个)。

3)  调用过后就需要清栈。这是个编程习惯也是符合语言设计理念的(想下c++函数调用也是遵守函数调用约定,调用过后就清交互栈的)。


c++调用lua函数使用到的lua c的api 是:

LUA_API int   (lua_pcall) (lua_State *L, int nargs, int nresults, int errfunc);(参数:lua 虚拟机对象  函数参数个数  返回值个数 错误处理函数)

执行lua函数调用(参数:函数名、需要压入的参数、需要返回的结果)

bool ScriptSystem::exec(const char* fname, const ScriptValueList* args, ScriptValueList *results )
{
//清除堆栈(获取堆栈的数据个数,弹出堆栈的数据)
#define cleanStack()  do {\
				int stackNum = lua_gettop(m_pLua);\
				if (stackNum)\
				{\
					lua_pop(m_pLua,stackNum);\
				}\
		}while(0)

	int i;
	int args_count = 0;
	if (args)args_count = args->count;
	
	lua_getglobal(m_pLua,fname);//获取需要调用的lua函数

	//压入参数列表
	for (i = 0;i < args_count;i++)
	{
		ScriptValue scriptValue = args->values[i];//根据不同参数类型来压入参数
		if (ScriptValue::vNumber == scriptValue.type)
		{
			lua_pushnumber(m_pLua,scriptValue.data.d);
		}
		else if (ScriptValue::vInterger == scriptValue.type)
		{
			lua_pushinteger(m_pLua,scriptValue.data.i);
		}
		else if (ScriptValue::vString == scriptValue.type)
		{
			lua_pushstring(m_pLua,scriptValue.data.str);
		}
		else if (ScriptValue::vBool == scriptValue.type)
		{
			lua_pushboolean(m_pLua,scriptValue.data.i);
		}
		else if (ScriptValue::vPointer == scriptValue.type)
		{
			lua_pushlightuserdata(m_pLua,scriptValue.data.ptr);
		}
		else if (ScriptValue::vBaseObject == scriptValue.type)
		{
			tolua_pushusertype(m_pLua,scriptValue.data.ptr,"CBaseObject");
		}
		else if (ScriptValue::vEntity == scriptValue.type)
		{
			tolua_pushusertype(m_pLua,scriptValue.data.ptr,"CEntity");
		}
		else if (ScriptValue::vActor == scriptValue.type)
		{
			tolua_pushusertype(m_pLua,scriptValue.data.ptr,"CDoer");
		}
		else if (ScriptValue::vPlayer == scriptValue.type)
		{
			tolua_pushusertype(m_pLua,scriptValue.data.ptr,"CPlayer");
		}
		else
		{
			lua_pushnil(m_pLua);//压入空指针
		}
	}
	
	if (!results)//不需要返回值
	{
		int err = lua_pcall(m_pLua,args_count,0,0);//调用lua函数
		if (err)
		{
			const char* result = lua_tostring(m_pLua, -1);
			logError("Script Err:lua_pcall result:%s,fname %s\n",result,fname);
			cleanStack();
			return false;
		}
	}
	else//需要一个返回值
	{
		int err = lua_pcall(m_pLua,args_count,1,0);
		const char* result = lua_tostring(m_pLua, -1);//取出栈顶的执行结果
		if (err)//lua函数调用错误
		{
			logError("Script Err:lua_pcall result:%s,fname %s\n",result,fname);
			cleanStack();
			return false;
		}
		if (result)
		{
			logDebug("lua_pcall result:%s,fname %s\n",result,fname);
			results->push(result);//加入返回结果
		}
	}
	cleanStack();//清除堆栈
	return true;
}


(3)脚本变量列表

定义一些接口用于传入变量到列表。

在脚本系统执行c++调用lua接口时传入的参数列表和获取返回结果列表。

class ScriptValueList : public CBaseObject
{
public:
	static const int MaxValueCount = 8;//脚本值列表最多值数量
	int count;
	ScriptValue values[MaxValueCount];
	public:
	ScriptValueList(){ count = 0; memset(values, 0, sizeof(values)); }
	~ScriptValueList(){ clear(); }
	//添加一个整数值到列表
	bool push(int v);
	//添加一个浮点值到列表
	bool push(double v);
	//添加一个布尔值到列表
	bool push(bool b);
	//添加一个字符串值到列表
	bool push(const char* str);
	//添加一个指针值到列表
	bool push(void* ptr);
	//添加一个CBaseObject值到列表
	bool push(CBaseObject* ptr);
	//添加一个CEntity值到列表
	bool push(CEntity* ptr);
	//添加一个CDoer值到列表
	bool push(CDoer* ptr);
	//添加一个CPlayer值到列表
	bool push(CPlayer* ptr);
	//添加一个脚本值到列表
	bool push(const ScriptValue &v);
	//清空列表数据
	inline void clear()
	{
		for (int i= count-1; i>-1; --i)
		{
			values[i].clear();
		}
		count = 0;
	}
};

(4)变量

参数列表的需要的自定义的类型变量。

重载等号操作符来对不同类型的赋值到对应的成员中。

class ScriptValue
{
public: 
	//脚本值类型
	enum ValueType
	{
		vNumber = 0,
		vInterger = 1,
		vString = 2,
		vBool = 3,
		vPointer = 4,
		vBaseObject = 5,
		vEntity = 6,
		vActor = 7,
		vPlayer = 8,
	};
	~ScriptValue() { clear(); }
	
	inline void operator = (const int v){type = vInterger;data.i = v;}//从整数赋值
	inline void operator = (const double d){type = vNumber;data.d = d;}//从浮点数赋值
	inline void operator = (void* ptr){type = vPointer;data.ptr = ptr;}//从指针赋值
	inline void operator = (const bool b){type = vBool;data.i = b;}//从布尔赋值
	inline void operator = (const char* str){type = vString; data.str = str;}//从字符串指针赋值
	inline void operator = (CBaseObject* obj){type = vBaseObject; data.ptr = obj;}//从CBaseObject指针赋值
	inline void operator = (CEntity* obj){type = vEntity; data.ptr = obj;}//从CEntity指针赋值
	inline void operator = (CDoer* obj){type = vActor; data.ptr = obj;}//从CDoer指针赋值
	inline void operator = (CPlayer* obj){type = vPlayer; data.ptr = obj;}//从CPlayer指针赋值
	 
	inline void clear(){memset(&data,0,sizeof(data));}
	public:
	ValueType type;
	union {
		double d;
		int i;
		void* ptr;
		const char* str;
	}data;
};


(5)脚本加载

(5-1)加载字符串到虚拟机

用于加载字符串到虚拟机

bool ScriptSystem::loadScript( const char* buffer )
{
	if (!buffer)
	{
		logError("buffer为空");
		return false;
	}
	if(!m_pLua)
	{
		logError("lua虚拟机为空");
		return false;
	}

	int err = luaL_dostring(m_pLua,buffer);//在虚拟机加载该语句
	
	if (!err)return true;//成功就直接返回,否则打印出错结果

	const char* result = lua_tostring(m_pLua, -1);
	logError("loadScript result:%s\n",result);
	return false;
}


(5-2)加载文件

加载文件内容的语句到虚拟机。

bool ScriptSystem::loadScriptFromFile( const char* fileName )
{
	if (!fileName)
	{
		logError("文件名为空");
		return false;
	}
	if(!m_pLua)
	{
		logError("lua虚拟机为空");
		return false;
	}
	int err = lua_dofile(m_pLua,fileName);//加载文件内容到虚拟机
	if (!err)return true;

	const char* result = lua_tostring(m_pLua, -1);
	logError("loadScriptFromFile error result:(%s)",result);
	return false;
}

2、加载数据

(1)加载npc数据(npc脚本和npc配置数据)到lua虚拟机

npc数据包括以下数据:

1)npc入口脚本

2)各个npc脚本

3)npc数据(配置的)

3-1)可接任务和可交任务数据

3-2)接收和完成任务的会话数据

3-3)npc默认对话数据

bool ScriptSystemLoad::initNPCScript()
{
	if(!g_Script)
	{
		//初始化脚本系统
		g_Script = new ScriptSystem();
		bool res = g_Script->loadScriptFromFile("./scripts/npc/NPCEntry.lua");//访问各个npc脚本的lua接口
		if (!res)
		{
			printf("g_NPCScript loadScriptFromFile error\n");
			return false;
		}
	}
	//加载各个场景的npc的脚本
	int i;
	lib::container::Array<PSceneNpc>* pSceneNpcList = g_ConfigManager->sceneDataAccessor.getSceneNpcList();
	for (i = (int)pSceneNpcList->length() -1;i >-1;i--)
	{
		SceneNpc* sceneNpc = (*pSceneNpcList)[i];
		if (sceneNpc)
		{       //npcId, npc名称,脚本位置
			//1,"npc1","./scripts/npc/npc1"
			//2,"npc2","./scripts/npc/npc2"
			//加载npc脚本到npc列表,npcTable[npcId] = npcScript
			bool loadNpcRes = loadNPCScript(sceneNpc->npcId,sceneNpc->name,sceneNpc->script);
			if (!loadNpcRes)
			{
				printf("load npc script error\n");
				return false;
			}
		}
	}
	//加载配置数据
	g_ConfigManager->missionDataProvider.makeNPCQuestData();//玩家可接收和完成任务列表
	g_ConfigManager->missionDataProvider.makeScriptQuestData();//脚本的任务数据(接收和完成任务的会话)
	g_ConfigManager->sceneDataAccessor.makeNpcDefaultTalkData();//npc的默认对话列表
	return true;
}

(2)加载所有的npc逻辑lua脚本

加载npc脚本到npc表(npc脚本数据有多个,方便索引,索引为npcId)

 参数为:npc Id , npc 名, npc 脚本位置

bool ScriptSystemLoad::loadNPCScript( unsigned long long npcId, const char* npcName, const char* fileName )
{
	char sBuffer[1024];
	char *ptr = sBuffer;
	
	if (npcId < 0 || !npcName || !fileName)
		return false;
	
	//NPC脚本文件读取后,将内容包装为一个向npcTable中的
	//npcId下标的值进行赋值的语句,并调用g_NPCScript->loadScript
	//加载到脚本中虚拟机中
	//npcTable[npcId] = npcScriptFile
	ptr += snprintf(ptr, sizeof(sBuffer)-1, 
	"local temp = require(\"scripts/npc/%s\") \r\n"
	"temp.npcId = %llu \r\n"//npcId
	"temp.npcName = \"%s\" \r\n" //npc名
	"npcTable[%llu] = temp \r\n "  //npc脚本
	, fileName, npcId, npcName, npcId);
	
	ptr[0] = 0;
	
	bool res = g_Script->loadScript(sBuffer);
	if (false == res)
	{
		printf("g_NPCScript loadScript error\n");
		return false;
	}
	return true;
}


(3)加载配置任务表数据

使用的是lua的C Api接口加载数据 
加载数据类型如下:

1)可接收和完成任务列表
2)接收和完成任务的会话
3)npc的默认对话列表

1)加载npc任务数据

在虚拟机中的,加载后的结果,全局表的结构如

NPCQuest[npcId] = {
accepts = { 1, 2, 3, },  npc可接的任务id
completes = { 100, 101, 102, },npc身上可完成任务id
},

void QuestAccessor::makeNPCQuestData()
{
	QuestConfig **ppQuestList = m_Quest.own_ptr();
	INT_PTR nCount = m_Quest.length();
	lua_State* L = g_Script->getLuaState();
	lua_newtable(L);//创建npc任务总表到栈顶
	for (INT_PTR i = 1; i < nCount; ++i)
	{
		if (ppQuestList[i]->nStartNpc != -1)
		pushToNPCQuestAccepts(L, ppQuestList[i]->nStartNpc, ppQuestList[i]->nQid);//加载可接收任务表
	}
	for (INT_PTR i = 1; i < nCount; ++i)
	{
		if (ppQuestList[i]->nEndNpc != -1)
		pushToNPCQuestCompletes(L, ppQuestList[i]->nEndNpc, ppQuestList[i]->nQid);//加载可完成任务表
	}
	lua_setglobal(L, "NPCQuest");//设置npc任务总表为全局成员(栈顶被弹出)
}


压入可接任务列表

加入接收任务列表到虚拟机
把npc的一些任务数据加入到可接任务表里

void QuestAccessor::pushToNPCQuestAccepts(lua_State* L, int NpcId, unsigned short questID)
{
	size_t tLen = 0;
	//获取为NPCID为下标的表到栈顶
	lua_rawgeti(L, -1, NpcId);//栈顶是表NPCQuest
	if (!lua_istable(L, -1))//如果没有该表就创建一张该npc的任务表
	{
		lua_pop(L, 1);//弹出空值nil
		lua_newtable(L);//创建该npc的任务表
		lua_pushvalue(L, -1);//设置该npc的任务表的引用到栈顶
		lua_rawseti(L, -3, NpcId);//栈顶作为该npc的任务表被设置到任务总表NPCQuest里(下标为NpcId,设置完后会自动弹出栈顶)
	}
	
	//向accept表添加可接任务ID
	lua_getfield(L, -1, "accepts");//栈顶是该npc的任务表
	if (!lua_istable(L, -1))//没有该accepts表就创建一张
	{
		lua_pop(L, 1);//弹出空值
		lua_newtable(L);//创建accepts表
		lua_pushvalue(L, -1);//设置accepts表的引用到栈顶
		lua_setfield(L, -3, "accepts");//栈顶为accepts表被设置到该npc的任务表里(下标为字符串"accepts",设置完后会自动弹出栈顶)
	}
	tLen = lua_objlen(L, -1);//获取accepts表的长度
	lua_pushinteger(L, questID);
	lua_rawseti(L, -2, int(tLen + 1));//栈顶作为任务ID被设置到accepts表(下标为tLen+1,也就是往accepts表后面添加任务ID)
	lua_pop(L, 1);//弹出accepts表
	
	//弹出该npc的任务表(任务总表中NPCID为下标的表)
	lua_pop(L, 1);
}

 

2)加载任务具体数据

在虚拟机中的,加载后的结果,全局表的结构如

QuestData[questId] = { 
name = "任务名",
acceptTalk = { "111", "222", "333" }, 
acceptReply = {"11", "22", "33"},
completeTalk = { "aaa", "bbb", "ccc" },
completeReply = {"aa", "bb", "cc"},
}

加载代码如下:

创建任务数据表

void QuestAccessor::makeScriptQuestData()
{
	QuestConfig** ppQuestList = m_Quest.own_ptr();
	INT_PTR nCount = m_Quest.length();
	lua_State* L = g_Script->getLuaState();
	lua_newtable(L);
	for (INT_PTR i = 1; i < nCount; ++i)
	{
		pushToQuestData(L, ppQuestList[i]);
	}
	lua_setglobal(L, "QuestData");
}

加载配置数据到任务数据表

void QuestAccessor::pushToQuestData(lua_State* L, QuestConfig *quest)
{
	lua_newtable(L);
	lua_pushvalue(L, -1);
	lua_rawseti(L, -3, quest->nQid);

	//name = "quest-name"
	lua_pushstring(L, quest->sName);
	lua_setfield(L, -2, "name");

	const QuestAccessor::TalkStruct* pTalkStruct =  &(m_questTalkList.get(quest->nQid));
	if (NULL != pTalkStruct)
	{
		if (pTalkStruct->accept.nCount > 0)
		{
			lua_newtable(L);
			lua_pushvalue(L, -1);
			lua_setfield(L, -3, "acceptTalk");
			for (int i = 0; i < pTalkStruct->accept.nCount; ++i)
			{
				lua_pushstring(L, pTalkStruct->accept.talkList[i].c_str());
				lua_rawseti(L, -2, i + 1);
			}
			lua_pop(L, 1);

			lua_newtable(L);
			lua_pushvalue(L, -1);
			lua_setfield(L, -3, "acceptReply");
			for (int i = 0; i < pTalkStruct->accept.nCount; ++i)
			{
				lua_pushstring(L, pTalkStruct->accept.replyList[i].c_str());
				lua_rawseti(L, -2, i+1);
			}
			lua_pop(L, 1);
			
		}
		if (pTalkStruct->complete.nCount > 0)
		{
			lua_newtable(L);
			lua_pushvalue(L, -1);
			lua_setfield(L, -3, "completeTalk");
			for (int i = 0; i < pTalkStruct->complete.nCount; ++i)
			{
				lua_pushstring(L, pTalkStruct->complete.talkList[i].c_str());
				lua_rawseti(L, -2, i + 1);
			}
			lua_pop(L, 1);

			lua_newtable(L);
			lua_pushvalue(L, -1);
			lua_setfield(L, -3, "completeReply");
			for (int i = 0; i < pTalkStruct->complete.nCount; ++i)
			{
				lua_pushstring(L, pTalkStruct->complete.replyList[i].c_str());
				lua_rawseti(L, -2, i+1);
			}
			lua_pop(L, 1);
		}
	}
	lua_pop(L, 1);
}

3)加载Npc默认聊天表

创建Npc默认聊天表NpcDefaultTalk,表结构如下

NpcDefaultTalk[npcid] = { 
DefaultTalk= { "111", "222", "333" }, 
}

void SceneAccessor::makeNpcDefaultTalkData()
{
	SceneNpc **ppNpcList = m_sceneNpcList.own_ptr();
	INT_PTR nCount = m_sceneNpcList.length();
	lua_State *L = g_Script->getLuaState();
	lua_newtable(L);//建立个npc的聊天的表
	for (INT_PTR i = 1; i < nCount; ++i)
	{
		pushToNpcDefaultTalk(L, ppNpcList[i]->defaultTalk[0], ppNpcList[i]->npcId);
		pushToNpcDefaultTalk(L, ppNpcList[i]->defaultTalk[1], ppNpcList[i]->npcId);
		pushToNpcDefaultTalk(L, ppNpcList[i]->defaultTalk[2], ppNpcList[i]->npcId);
	}
	lua_setglobal(L, "NpcDefaultTalk");
}

void SceneAccessor::pushToNpcDefaultTalk(lua_State *L, const char* talk, int npcid)
{
	size_t nLen = 0;
	//打开NPCID为下标的表
	lua_rawgeti(L, -1, npcid);//获取NPCID为下标的表到栈顶
	if (!lua_istable(L, -1))
	{
		lua_pop(L, 1);//弹出栈顶元素
		lua_newtable(L);
		lua_pushvalue(L, -1);
		lua_rawseti(L, -3, npcid);
	}

	//向DefaultTalk表添加内容
	lua_getfield(L, -1, "DefaultTalk");//获取NPCID为下标的表中的DefaultTalk表
	if (!lua_istable(L, -1))
	{
		lua_pop(L, 1);
		lua_newtable(L);
		lua_pushvalue(L, -1);
		lua_setfield(L, -3, "DefaultTalk");
	}
	nLen = lua_objlen(L, -1);
	lua_pushstring(L, talk);//压入DefaultTalk表中的聊天字符串
	lua_rawseti(L, -2, int(nLen+1));
	lua_pop(L, 1);

	//关闭NPCID为下标的表
	lua_pop(L, 1);
}


3、加载复杂对象(tolua++)

使用的实现过程使用到一个叫tolua++的第三方库,这个库可以跨windows和linux平台的。使用也会十分方便。

脚本系统的初始化需要初始化tolua++注册的类和函数。

函数luaopen_serverLuaInterface是tolua++根据设计文件生成的接口,里面包含所有需要注册的类和函数。
目的是把需要的类的接口导出到lua虚拟机。

bool ScriptSystem::loadServerInterface()
{
	if (!m_pLua)
	{
		printf("loadServerInterface m_pLua is null\n");
		return false; 
	}
	//注册c++类和接口到lua虚拟机
	luaopen_serverLuaInterface(m_pLua);
	return true;
}


4、lua接口文件的应用实例

(1)NPC访问接口文件

NPC访问接口文件(NPCEntry.lua)主要对外(对c++)提供接口调用。加载npc总表及其访问接口。

点击函数调用具体NPC的代码模块

npcTable = {}--包含所有的npc lua代码
--点击NPC执行的入口函数,调用npc模块的主函数
function clickNPC(npcId, player)
	local npc = npcTable[npcId]--获取npc id 为npcId的npc主代码模块
	if npc ~= nil then
		return npc.main(player)--调用npc模块的主函数
	else
		return "missing npc script table"
	end
end
--与NPC对话功能的函数入口,调用的是任务控制模块QC
function talkNPC(npcId, player, funcName, ...)--npcid,玩家指针,调用模块和其函数,npcId,任务id,聊天索引
	local mDotStart,mDotEnd = string.find(funcName, "QC")--格式为如QC.acceptQuestStep
	if mDotStart then
		local mdName = string.sub(funcName, mDotStart, mDotEnd)--mdName为如QC
		local md = _G[mdName]--获取模块,如QC.lua
		if md then
			funcName = string.sub(funcName, mDotEnd + 2)--函数名如acceptQuestStep
			local func = md[funcName]--模块中的函数名,如QC中的acceptQuestStep
			if func then
				return func(player, unpack(arg))--调用该函数
			else
				return "missing function " .. funcName .. " at module " .. mdName
			end
		else
			return "missing module " .. mdName
		end
	else
		local npc = npcTable[npcId]--获取npc数据
		if npc ~= nil then
			local func = npc[funcName]--调用npc的功能函数
			if func ~= nil then
				return func(player, unpack(arg))
			else
				return "missing npc function "..funcName
			end
		else
			return "missing npc script table"
		end
	end
end

require "./scripts/npc/QC" -- 导入控制接口文件


(2)NPC代码模块

NPC模块含主函数main,需要实现NPC主函数调用分派。

如下是npc1的主代码模块,这里是获取玩家正在聊天的npc的所有任务的对该玩家的状态.

module(..., package.seeall)

function main(player)
    local str1 = "<@(testGRT)TestGRT>"
	--str1 = str1 .. QC.formatQuestState(player)
	str1 = QC.formatQuestState(player)--返回
	return str1
end
--返回字符串的格式要求
--[[
<@(函数)显示名称>					与NPC对话			<@(func1)进入>
<C(颜色)文本>						改变文本颜色		<C(FFFF0000)文字>
<F(BUSI)文本>						改变文本样式		B=bold,U=underLine,S=strikeOut,I=italic
<E(字体大小)文本>					改变字体大小		<E(30)文字>
<U(http://地址)显示名称>				超链接			<U(http://www.moon.com)主页>
<M(地图ID:x:y:行为:对象名)显示名称>	寻路到特定点
]]
function testGRT(player)
	return "<C(FF00FF00)may be green color text>\n"
	    .. "<F(BUS)text may have bold and strike and underline style>\n"
	    .. "<E(50)this got a large size text>\n"
	    .. "<C(FF800080)this <F(BUS)shows <E(60) nesting support!>>>\n"
	    .. "<@(main)back>"
end

print("npc1.lua loaded")

(3)任务控制接口

任务控制接口文件QC.lua ,含对任务的所有操作:遍历所有任务、获取接任务的对话、获取交任务的对话。

1)获取所有任务及其状态

检查是否可接或可交任务,返回客户端所有可接收和可完成的任务的组成的应答字符串(包含请求任务接口、npc id 、任务名)

function formatQuestState(player) 
	local npcid = player.m_NpcTalk.m_nTalkNpcID;--角色正在对话的npc Id
	npcid = tonumber(npcid)
	local idxRand = math.random(3)
	local defaultRet = "\n<C(FFEDCB5D)"..NpcDefaultTalk[npcid].DefaultTalk[idxRand]..">"--返回默认对话的格式
	local strRet = defaultRet
	
	if type(NPCQuest[npcid]) ~= "table" then 
		return strRet
	end
	
	if type(NPCQuest[npcid].accepts) == "table" then 
		local accept = NPCQuest[npcid].accepts
		for k,v in pairs(accept) do -- 遍历所有接收的任务
			--调用C++接口查看是否可接
			local quest = QuestData[v]--npc身上可接的任务id
			local boolean bCanAccept = player.m_Quest:checkCanAccept(v)
			if bCanAccept then
				strRet = strRet .. "\n<@(QC.acceptQuestStep," -- 
				.. npcid .. ",".. v .. ",".. "1)".. quest.name
				.. ">"
			end
		end
	end 
	
	if type(NPCQuest[npcid].completes) == "table" then
		local complete = NPCQuest[npcid].completes
		for k,v in pairs(complete) do-- 遍历所有完成的任务
			--查看是否可交任务
			local quest = QuestData[v]--npc身上可完成的任务(id为v)的数据
			local boolean bCanSubmit = player.m_Quest:checkCanSubmit(v)--玩家指针的任务模块的函数checkCanSubmit检查玩家能否提交任务v
			if bCanSubmit then --添加可以提交任务添加npc id和任务名称到返回的字符串
				strRet = strRet .. "\n<@(QC.completeQuestStep,"
				.. npcid .. ",".. v .. ",".. "1)".. quest.name
				.. ">"
			end
		end
	end
	
	return strRet
end

2)接任务或其对话

 接收任务的对话或接任务

function acceptQuestStep(player, npcId, questId, talkIdx)
	npcId = tonumber(npcId)//需要把字符串转成数字
	questId = tonumber(questId)
	talkIdx = tonumber(talkIdx)
	
	--获取对应的npc
	local npc = NPCQuest[npcId]-- npc 任务列表NPCQuest,是配置在配置表,在脚本系统初始化时热加载
	
	if type(npc) ~= "table" then
		return "npc is not a table,npcid:".. npcId .. ", questID:" .. questId ..", talkIdx:" .. talkIdx -- 找不到npcId的npc任务表,返回错误信息
	end
	
	local qData
	for k,v in pairs(npc.accepts) do -- 遍历该npc表里的可接任务数据
		if questId == v then -- 选择需要的任务
			qData = QuestData[v] -- 任务数据表
			break
		end
	end
	if type(qData) ~= "table" then --检查是否是表
		return "questData is not a table,npcid:".. npcId .. ", questID:" .. questId ..", talkIdx:" .. talkIdx  -- 找不到npcId的可接任务表,返回错误信息
	end
	
	local curTalkList = qData.acceptTalk -- id为npcId任务表里的可接任务表里的接任务聊天表
	if type(curTalkList) ~= "table" then
		return "curTalkList is not a table,npcid:".. npcId .. ", questID:" .. questId ..", talkIdx:" .. talkIdx -- 找不到npcId的可接任务表里的聊天表,返回错误信息
	end
	
	local curReplyList = qData.acceptReply -- id为npcId任务表里的可接任务表里的任务响应聊天表
	if type(curReplyList) ~= "table" then
		return "curReplyList is not a table,npcid:".. npcId .. ", questID:" .. questId ..", talkIdx:" .. talkIdx
	end
	
	local nTalkCount = #curTalkList
	local strTalk = curTalkList[talkIdx]--获取npc响应消息
	local strReply = curReplyList[talkIdx]-- 获取npc主动回应消息
	local strRet
	
	if talkIdx > nTalkCount then --  聊完天就接任务(根据聊天的索引判断)
		--防止网络延时,造成重复接,再做一次判断
		local boolean bCanAccept = player.m_Quest:checkCanAccept(questId)--检查是否是可接的
		if bCanAccept then
			player.m_Quest:acceptQuest(questId)--接收任务
			--strRet = "<@(close)关闭>"
			player.m_NpcTalk:sendCloseTalk()--接完任务就直接发送关闭窗口消息
			return strRet
		end
	else--需要继续聊天
		--格式化下一个聊天响应消息,并返回
		strRet = "<C(FFEDCB5D)" .. strTalk .. ">"-- 设置聊天的颜色
		.. "\n\n<@(QC.acceptQuestStep,"
		.. npcId .. ","
		.. questId .. ","
		.. talkIdx+1 .. ")"
		--.. player.m_sName ..":"
		.. "<C(FFFFBA00)" .. strReply.. ">>"
	end
	
	return strRet
end


5、脚本系统测试用例

(1)c++调用lua函数

玩家类型是用tolua++导出了公开的类的接口的,只要把玩家指针压入lua的栈,在lua中获取到该玩家指针时就可以调用玩家指针的接口了

调用NPCEntry.lua里的talkNPC函数(传入参数并返回结果),然后调用模块QC.lua 文件里的 acceptQuestStep 函数,

args.clear();//清空栈参数,然后压入参数作为函数参数
//接任务
args.push(2);//npcId
args.push(player);//玩家指针
args.push("QC.acceptQuestStep");//调用的函数,  脚本QC里的函数acceptQuestStep,脚本QC 是接口控制文件
args.push(1);//npcID
args.push(1);//questID
args.push(1);//talkIdx
g_Script->exec("talkNPC",&args,&results);//执行lua接口,talkNPC函数,参数args ,返回results(压入lua虚拟机栈,栈顶是lua函数talkNPC,然后是所有的参数)。需要先获取脚本的函数talkNPC到栈顶,再压入参数args

args.clear();
//完成任务
args.push(2);//npcId
args.push(player);
args.push("QC.completeQuestStep");
args.push(1);//npcID
args.push(1);//questID
args.push(1);//talkIdx
g_Script->exec("talkNPC",&args,&results);


(2)lua调用c++接口

利用tolua++导出的类和其接口,lua模块中可以调用c++的对象以及其接口

如之前lua代码的acceptQuestStep函数中,检查玩家指针的任务模块能否接受任务(ID为questId)

local boolean bCanAccept = player.m_Quest:checkCanAccept(questId)--检查是否是可接的


6、lua虚拟机调用栈调试

测试用例:

c++调用lua函数testSceneManager

ScriptValueList args;
ScriptValueList results;

CPlayer* player = new CPlayer();
strcpy(player->m_sName,"playerName");
player->m_nAccountId = 100;
player->m_nX = 200;
player->m_nY = 300;
//player->m_NpcTalk.setNpcID(1);

args.clear();
args.push(player);
g_Script->exec("testSceneManager",&args, &results );

args.clear();

lua模块中的函数testSceneManager:
function testSceneManager(player)
	print("testSceneManager")
	local duplicateId = 1
	local duplicate = g_DuplicateManager:createDuplicate(duplicateId)
	print(duplicate.m_Guid)
	print(duplicate.m_duplicateId)
	print(duplicate.m_duplicateName)
	print("m_playerNum")
	print(duplicate.m_playerNum)
	duplicate:addPlayerToFirstScene(player)
	print("m_playerNum")
	print(duplicate.m_playerNum)
	duplicate:delPlayer(player)
	print("m_playerNum")
	print(duplicate.m_playerNum)
end


调试步骤(具体代码参考脚本系统测试用例):

1)c++中通过lua_pcall 调用lua脚本中的testSceneManager。

2)执行testSceneManager,从调用栈可以看出lua虚拟机在执行代码时会调用luaV_execute

3)testSceneManager会调用g_DuplicateManager:createDuplicate 

4)这个类函数对应的c++对象通过tolua++导出的访问接口是tolua_serverLuaInterface_DuplicateManager_createDuplicate00 

5)tolua_serverLuaInterface_DuplicateManager_createDuplicate00 实际上使用的也是lua的C api来访问lua的交互栈。

6)获取栈上的数据并转换成c++对象的类型,并调用该对象的接口(栈底的是DuplicateManager对象指针,栈2位置是参数duplicateId)

以下是gdb调试调用栈的信息

#0  DuplicateManager::createDuplicate (this=0x72b228, duplicateId=1) at ../logic/map/mapManager.cpp:329
#1  0x000000000044582e in tolua_serverLuaInterface_DuplicateManager_createDuplicate00 (tolua_S=0x741e90)
    at ../logic/scriptSystem/luaInterface/ServerLuaInterface.cpp:3780
#2  0x0000000000484c11 in luaD_precall (L=0x741e90, func=0x742290, nresults=1) at ../ldo.c:319
#3  0x0000000000499264 in luaV_execute (L=0x741e90, nexeccalls=1) at ../lvm.c:590
#4  0x0000000000484e9f in luaD_call (L=0x741e90, func=0x742260, nResults=1) at ../ldo.c:377
#5  0x000000000047e59f in f_call (L=0x741e90, ud=0x7fffffffe480) at ../lapi.c:805
#6  0x0000000000483f14 in luaD_rawrunprotected (L=0x741e90, f=0x47e570 <f_call>, ud=0x7fffffffe480) at ../ldo.c:116
#7  0x0000000000485288 in luaD_pcall (L=0x741e90, func=0x47e570 <f_call>, u=0x7fffffffe480, old_top=16, ef=0)
    at ../ldo.c:463
#8  0x000000000047e645 in lua_pcall (L=0x741e90, nargs=1, nresults=1, errfunc=0) at ../lapi.c:826
#9  0x0000000000448ab8 in ScriptSystem::exec (this=0x742970, fname=0x4bbf40 "testSceneManager", args=0x7fffffffe600, 
    results=0x7fffffffe570) at ../logic/scriptSystem/scriptSystem.cpp:147
#10 0x00000000004484f4 in ScriptSystemLoad::testScriptSystemLoad () at ../logic/scriptSystem/scriptSystem.cpp:323
#11 0x00000000004674d6 in CConfigManager::initSystem (this=0x72aff8) at ../config/configManager.cpp:83
#12 0x0000000000424cbd in CGameServer::loadDataConfig (this=0x71fcf0) at ../logic/server/logicServer.cpp:87
#13 0x0000000000424fde in CGameServer::runGameServer (this=0x71fcf0) at ../logic/server/logicServer.cpp:148
#14 0x0000000000471aa0 in main (argc=1, argv=0x7fffffffe8c8) at ../main.cpp:18


调用栈分析:

栈0:

DuplicateManager::createDuplicate  是c++里的副本模块的创建。

栈1:

TOLUA_DISABLE_tolua_serverLuaInterface_DuplicateManager_createDuplicate00 是tolua++对的DuplicateManager类的createDuplicate接口的导出代码。

导出的代码如下:

获取栈上的数据并转换成c++对象的类型,并调用该对象的接口

/* method: createDuplicate of class  DuplicateManager */
#ifndef TOLUA_DISABLE_tolua_serverLuaInterface_DuplicateManager_createDuplicate00
static int tolua_serverLuaInterface_DuplicateManager_createDuplicate00(lua_State* tolua_S)
{
#ifndef TOLUA_RELEASE
 tolua_Error tolua_err;
 if (//lua运行时虚拟机内的接口和参数类型检查
     !tolua_isusertype(tolua_S,1,"DuplicateManager",0,&tolua_err) ||
     !tolua_isnumber(tolua_S,2,0,&tolua_err) ||
     !tolua_isnoobj(tolua_S,3,&tolua_err)
 )
  goto tolua_lerror;
 else
#endif
 {
  DuplicateManager* self = (DuplicateManager*)  tolua_tousertype(tolua_S,1,0);
  int duplicateId = ((int)  tolua_tonumber(tolua_S,2,0));
#ifndef TOLUA_RELEASE
  if (!self) tolua_error(tolua_S,"invalid 'self' in function 'createDuplicate'", NULL);
#endif
  {
   SceneManager* tolua_ret = (SceneManager*)  self->createDuplicate(duplicateId);
    tolua_pushusertype(tolua_S,(void*)tolua_ret,"SceneManager");
  }
 }
 return 1;
#ifndef TOLUA_RELEASE
 tolua_lerror:
 tolua_error(tolua_S,"#ferror in function 'createDuplicate'.",&tolua_err);
 return 0;
#endif
}
#endif //#ifndef TOLUA_DISABLE

栈8:

c++(通过lua_pcall )调用lua脚本的函数testSceneManager

栈2-7:

lua运行时的函数的栈调用。

DuplicateManager* self = (DuplicateManager*)  tolua_tousertype(tolua_S,1,0),就是获取栈中的第一个值,转成实例对象的指针。


c函数和c++类导出到虚拟机的实现:

对于 C 函数,会添加到 Lua 的全局名字空间中,而每一个 C++ 类,则会注册一个与类名相同的 table,并添加到全局名字空间,再将类函数添加到这个 table中.

  详细参考:http://dualface.github.io/blog/2012/08/25/tolua-plus-plus-implement/


你可能感兴趣的:(游戏服务器之lua脚本系统)