游戏服务器之lua脚本系统

来源:互联网 发布:淘宝找不到卖家中心 编辑:程序博客网 时间:2024/04/30 00:30

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

脚本系统的目的是方便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] = npcScriptbool 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] = npcScriptFileptr += 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);//栈顶是表NPCQuestif (!lua_istable(L, -1))//如果没有该表就创建一张该npc的任务表{lua_pop(L, 1);//弹出空值nillua_newtable(L);//创建该npc的任务表lua_pushvalue(L, -1);//设置该npc的任务表的引用到栈顶lua_rawseti(L, -3, NpcId);//栈顶作为该npc的任务表被设置到任务总表NPCQuest里(下标为NpcId,设置完后会自动弹出栈顶)}//向accept表添加可接任务IDlua_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 thenreturn npc.main(player)--调用npc模块的主函数elsereturn "missing npc script table"endend--与NPC对话功能的函数入口,调用的是任务控制模块QCfunction talkNPC(npcId, player, funcName, ...)--npcid,玩家指针,调用模块和其函数,npcId,任务id,聊天索引local mDotStart,mDotEnd = string.find(funcName, "QC")--格式为如QC.acceptQuestStepif mDotStart thenlocal mdName = string.sub(funcName, mDotStart, mDotEnd)--mdName为如QClocal md = _G[mdName]--获取模块,如QC.luaif md thenfuncName = string.sub(funcName, mDotEnd + 2)--函数名如acceptQuestSteplocal func = md[funcName]--模块中的函数名,如QC中的acceptQuestStepif func thenreturn func(player, unpack(arg))--调用该函数elsereturn "missing function " .. funcName .. " at module " .. mdNameendelsereturn "missing module " .. mdNameendelselocal npc = npcTable[npcId]--获取npc数据if npc ~= nil thenlocal func = npc[funcName]--调用npc的功能函数if func ~= nil thenreturn func(player, unpack(arg))elsereturn "missing npc function "..funcNameendelsereturn "missing npc script table"endendendrequire "./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 str1end--返回字符串的格式要求--[[<@(函数)显示名称>与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>"endprint("npc1.lua loaded")

(3)任务控制接口

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

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

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

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

2)接任务或其对话

 接收任务的对话或接任务

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


5、脚本系统测试用例

(1)c++调用lua函数

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

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

args.clear();//清空栈参数,然后压入参数作为函数参数//接任务args.push(2);//npcIdargs.push(player);//玩家指针args.push("QC.acceptQuestStep");//调用的函数,  脚本QC里的函数acceptQuestStep,脚本QC 是接口控制文件args.push(1);//npcIDargs.push(1);//questIDargs.push(1);//talkIdxg_Script->exec("talkNPC",&args,&results);//执行lua接口,talkNPC函数,参数args ,返回results(压入lua虚拟机栈,栈顶是lua函数talkNPC,然后是所有的参数)。需要先获取脚本的函数talkNPC到栈顶,再压入参数argsargs.clear();//完成任务args.push(2);//npcIdargs.push(player);args.push("QC.completeQuestStep");args.push(1);//npcIDargs.push(1);//questIDargs.push(1);//talkIdxg_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 = 1local 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/


0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 车辆挂牌时公司名称打错了怎么办 经营异常名录查不到怎么办移除 企业被列入经营税务异常名录怎么办 手机号被别人绑定了企业微信怎么办 刚出生一天的婴儿不肯吃奶怎么办 9个月孩子不好好吃奶怎么办 孩子2个月吃奶老是呛到怎么办 孩子4个月不好好吃奶怎么办? 3个月的孩子不吃奶肚子胀怎么办 刚出生的婴儿吃完奶打嗝怎么办 宝宝刚生下来一直睡觉不吃奶怎么办 刚生下来的婴儿不吃奶怎么办 刚生下来的小猫不吃奶怎么办 刚生的小羊羔不吃奶怎么办 刚出生的婴儿不会吸奶怎么办 生完小孩七十天妈妈咳嗽怎么办 带欣的名字三个字儿的怎么办 二十多岁的儿子沉迷游戏网络怎么办 为什么打开游戏网络却用不了怎么办 打来微信网页版显示证书错误怎么办 开了家定制家具店生意不好怎么办 宝宝起风疹怎么办要注意的问题 超市买的内裤西铁牌子没去掉怎么办 没申请生产许可证贴标了怎么办 淘宝没有品牌非要我写品牌怎么办没 意外怀孕明明一直用安全套的怎么办 找不到百度网盘的dns地址怎么办 小米众筹到了发货时间不发货怎么办 不知道电脑宽带连接账号密码怎么办 电脑如果宽带账号密码忘记了怎么办 xp电脑用户名和密码忘了怎么办 电脑的用户名和密码忘记了怎么办 电脑重置后需要用户名和密码怎么办 电脑登录用户名和密码忘记了怎么办 电脑登录用户名和密码忘了怎么办啊 微信无意中点了允许登录怎么办 qq号码登录微信无法验证怎么办 注册微信公众号邮箱激活不了怎么办 不是自己申请的qq号忘密码怎么办 联通宽带拨号账号密码忘记了怎么办 忘了路由器的用户名和密码怎么办