游戏服务器之lua脚本系统
发表于2016-06-03
现在做游戏的较多用脚本 ,可以热加载代码。
脚本系统的目的是方便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)、脚本系统的初始化和关闭
脚本系统的析构函数里需要关掉虚拟机
1 2 3 4 5 | if (m_pLua) { lua_close(m_pLua); m_pLua = NULL; } |
在构造函数里打开虚拟机
1 2 | m_pLua = lua_open(); luaL_openlibs(m_pLua); |
初始化脚本系统:
1)、加载npc脚本和npc数据到lua虚拟机
2)、把tolua++导出的接口导入到lua虚拟机
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | bool ScriptSystemLoad::init() { if (!initNPCScript())//加载npc脚本和npc数据到lua虚拟机 { printf( "initNPCScript errorn" ); return false; } bool res = loadLuaServerInterface();//加载tolua++导出的接口到lua虚拟机 if (!res) { printf( "loadLuaServerInterface errorn" ); 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函数调用(参数:函数名、需要压入的参数、需要返回的结果)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | 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 %sn" ,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 %sn" ,result,fname); cleanStack(); return false; } if (result) { logDebug( "lua_pcall result:%s,fname %sn" ,result,fname); results->push(result);//加入返回结果 } } cleanStack();//清除堆栈 return true;
|
(3)、脚本变量列表
定义一些接口用于传入变量到列表。
在脚本系统执行c++调用lua接口时传入的参数列表和获取返回结果列表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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)、变量
参数列表的需要的自定义的类型变量。
重载等号操作符来对不同类型的赋值到对应的成员中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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)、加载字符串到虚拟机
用于加载字符串到虚拟机
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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:%sn" ,result); return false; } |
(5-2)、加载文件
加载文件内容的语句到虚拟机。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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默认对话数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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 errorn" ); return false; } } //加载各个场景的npc的脚本 int i; lib::container::Array 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 errorn" ); 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 脚本位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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") rn" "temp.npcId = %llu rn" //npcId "temp.npcName = "%s" rn" //npc名 "npcTable[%llu] = temp rn " //npc脚本 , fileName, npcId, npcName, npcId); ptr[ 0 ] = 0 ; bool res = g_Script->loadScript(sBuffer); if (false == res) { printf( "g_NPCScript loadScript errorn" ); return false; } return true; } |
(3)、加载配置任务表数据
使用的是lua的C Api接口加载数据
加载数据类型如下:
1)、可接收和完成任务列表
2)、接收和完成任务的会话
3)、npc的默认对话列表
1)、加载npc任务数据
在虚拟机中的,加载后的结果,全局表的结构如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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的一些任务数据加入到可接任务表里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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"},
}
加载代码如下:
创建任务数据表
1 2 3 4 5 6 7 8 9 10 11 12 | 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" ); } |
加载配置数据到任务数据表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | 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,表结构如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 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虚拟机。
1 2 3 4 5 6 7 8 9 10 11 | bool ScriptSystem::loadServerInterface() { if (!m_pLua) { printf( "loadServerInterface m_pLua is nulln" ); return false; } //注册c++类和接口到lua虚拟机 luaopen_serverLuaInterface(m_pLua); return true; } |
4、lua接口文件的应用实例
(1)、NPC访问接口文件
NPC访问接口文件(NPCEntry.lua)主要对外(对c++)提供接口调用。加载npc总表及其访问接口。
点击函数调用具体NPC的代码模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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的所有任务的对该玩家的状态.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | module(..., package.seeall) function main(player) local str1 = "<@(testGRT)TestGRT>" --str1 = str1 .. QC.formatQuestState(player) str1 = QC.formatQuestState(player) --返回 return str1 end --返回字符串的格式要求 --[[ <@(函数)显示名称> 与NPC对话 <@(func1)进入>
]] function testGRT(player) return " " color=" " text=" ">n" .. " " bold=" " and=" " strike=" " underline=" " style=" ">n" .. " " large=" " size=" " text=" ">n" .. " .. "<@(main)back>" end print ( "npc1.lua loaded" ) |
(3)、任务控制接口
任务控制接口文件QC.lua ,含对任务的所有操作:遍历所有任务、获取接任务的对话、获取交任务的对话。
1)、获取所有任务及其状态
检查是否可接或可交任务,返回客户端所有可接收和可完成的任务的组成的应答字符串(包含请求任务接口、npc id 、任务名)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | function formatQuestState(player) local npcid = player.m_NpcTalk.m_nTalkNpcID; --角色正在对话的npc Id npcid = tonumber (npcid) local idxRand = math.random ( 3 ) local defaultRet = "n 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)、接任务或其对话
接收任务的对话或接任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | 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 = " .. "nn<@(QC.acceptQuestStep," .. npcId .. "," .. questId .. "," .. talkIdx+ 1 .. ")" --.. player.m_sName ..":" .. " end return strRet end
|
5、脚本系统测试用例
(1)、c++调用lua函数
玩家类型是用tolua++导出了公开的类的接口的,只要把玩家指针压入lua的栈,在lua中获取到该玩家指针时就可以调用玩家指针的接口了
调用NPCEntry.lua里的talkNPC函数(传入参数并返回结果),然后调用模块QC.lua 文件里的 acceptQuestStep 函数,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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)
1 | local boolean bCanAccept = player.m_Quest:checkCanAccept(questId) --检查是否是可接的 |
6、lua虚拟机调用栈调试
测试用例:
c++调用lua函数testSceneManager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 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: [cpp] view plain copy 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 , ud=0x7fffffffe480) at ../ldo.c:116
#7 0x0000000000485288 in luaD_pcall (L=0x741e90, func=0x47e570 , 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/