游戏服务器之lua脚本系统

发表于2016-06-03
评论0 5.2k浏览
  现在做游戏的较多用脚本 ,可以热加载代码。
  脚本系统的目的是方便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* 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 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)进入> 
                     改变文本颜色       
                       改变文本样式      B=bold,U=underLine,S=strikeOut,I=italic 
                   改变字体大小      30)文字> 
"">                超链接         ""
  寻路到特定点 
]] 
function testGRT(player) 
    return "" green="" color="" text="">n" 
        .. "" have="" bold="" and="" strike="" underline="" style="">n" 
        .. "" a="" large="" size="" text="">n" 
        .. "" " nesting="" support!="">>>n" 
        .. "<@(main)back>" 
end 
   
print("npc1.lua loaded"this>50)this>30)文字>

(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..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  "..npcdefaulttalk[npcid].defaulttalk[idxrand]..">

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 = " ..="" strtalk="" "="">"-- 设置聊天的颜色 
        .. "nn<@(QC.acceptQuestStep," 
        .. npcId .. "," 
        .. questId .. "," 
        .. talkIdx+1 .. ")" 
        --.. player.m_sName ..":" 
        .. " ..="" strreply..="" "="">>" 
    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/

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引