现在做游戏的较多用脚本 ,可以热加载代码。
脚本系统的目的是方便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与玩家交互接口。
为了可以传入自定义类型的变量和原子变量到一个列表,再一次性压栈(其实函数的调用就是压栈弹栈的过程,c++和lua交互的过程也是如此,就是通过栈来实现变量的访问)。
脚本系统的析构函数里需要关掉虚拟机
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; }
步骤如下:
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; }
在脚本系统执行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; } };
参数列表的需要的自定义的类型变量。
重载等号操作符来对不同类型的赋值到对应的成员中。
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; };
用于加载字符串到虚拟机
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; }
加载文件内容的语句到虚拟机。
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; }
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; }
加载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; }
1)可接收和完成任务列表
2)接收和完成任务的会话
3)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任务总表为全局成员(栈顶被弹出) }
压入可接任务列表
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); }
在虚拟机中的,加载后的结果,全局表的结构如
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); }
创建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); }
使用的实现过程使用到一个叫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; }
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" -- 导入控制接口文件
NPC模块含主函数main,需要实现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")
任务控制接口文件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
调用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);
利用tolua++导出的类和其接口,lua模块中可以调用c++的对象以及其接口
如之前lua代码的acceptQuestStep函数中,检查玩家指针的任务模块能否接受任务(ID为questId)
local boolean bCanAccept = player.m_Quest:checkCanAccept(questId)--检查是否是可接的
测试用例:
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/