• (转)Lua与C++的逻辑舞步


      以前写过一篇Lua的基础文章,属于基础的技术普及贴,具体请参阅精华区。以下的文章,希望你能稍微了解一些Lua的基础知识,再来阅读更有益处。 
      在网上,很多人都在说Lua适合做游戏,但是怎么做?说的却知之甚少,更多的是长篇累牍的基础贴。脚本与C++的结合,如何能达到较高的效率?什么样的数据适合Lua去表现?什么样的数据适合C++去处理?怎么利用两者的关系?在这里我给出一种解决方法。 
      很多人说,你可以把C++的对象封装到Lua里面去。在Lua里面提供对C++类操作的各种方法。这样的手段见仁见智,但是我个人却并不赞同,原因是,Lua是一个面向过程的语言,它和python,js是完全不同的,就像C和C++,为什么要把面向过程和面向过程完全分开,是因为它们服务于不同的需求,看过一些朋友把C++的对象模式做了一个Lua的粘合层,很是感觉有些繁复。在我看来,面向过程就要体现出面向过程的精华,不要把过多对象的概念引入其中,这样,代码简洁,易于维护管理,同时也能发挥各自最高的价值。C++是面向对象的,就让它去处理它擅长的对象,Lua是面向过程的,那么我就让lua去处理它擅长的过程组织,两个互不干扰,这样,双方组合后的能量才能真正爆发出来。 
    还是那句话,在我看来,代码只是一种表现形式,更重要的是思想,如何思考问题的方法。如果你是Lua初学者,我希望更多的是去学会如何思考问题,并将问题化解到你的思维模式中,形成流。这样,你才不会过分的依赖语言本身,毕竟,基础的语法在谷歌上我们还是能大量获得的。但是好的想法,却需要日积月累的总结。 
    先来讲个故事,我以前做游戏,经常碰到这样的策划: 
      "你来帮我实现这么一个东东,玩家到一个NPC那里,NPC展现出一个列表,上面写着若干任务描述,玩家选择一个,然后找到另外一个NPC或者到野外打够任务需要的素材和条件,回来交给这个NPC,完成任务。","玩家找到一个NPC,直接展开一个副本,里面有很多怪物啊关卡啊等等,玩家进去就可以乒乒乓乓。做完任务就可以退出来,并获得奖励。" 
      这样的需求我在游戏开发中会经常遇到,有时候我很纠结,为什么这么说?因为更多的时候,策划是不会特别考虑细节的。而程序实现的时候,往往面对的是非常细节的东西。好不容易实现了策划的想法,保不齐哪天策划要变化,你难道再跟着策划改一遍吗?要知道,就算你写程序再牛,你也赶不上策划天马行空的速度。在"我觉得应该是这样..."这样的语句面前,你的任何技术积淀都可能瞬间化为浮云。东西做不出来,策划肯定认为是你的能力不行,而程序呢,认为策划太BT。最后双方变成了敌人,在很多游戏团队中都存在这样的问题。 
      那么能不能有一种模式,策划策划的过程,让他们自己去实现?而且可以满足他们自己去不断纠正,同时不影响我们的程序实现?方法是有的,就是让策划写一个剧本,这个剧本可以加载到程序里面去执行。不过,不要指望策划会用C++,这是不现实的。你可以把他们想象成一个厨师,你是一个配料师,按照厨师的要求,你给他准备好各种配料,至于做什么菜,那是厨师的事情了。对了,这就是脚本语言带给策划和程序开发模式的改变。也正是源于此,我们需要找到一种能够清晰的描述事件过程的语言,来配合我C++为厨师们准备灶台。炒菜的东西我给你准备好了,都是按照厨师要求的,如果厨师做不出来,那么是厨师自己的手艺问题了。如果配料你做不出来,那么就是你配料师水平不行。责任明确,问题容易找。 
      那么好了,我们先以lua来说,怎么做这样的架构? 
      首先,既然我们是配料师,那么一定要提供一系列的配料才行。我再强调一次,对于面向过程的语言,尽量不要扯入对象的概念。那么好,看看我们手里有什么材料? 
    玩家的数据资料,比如:血量,名称,身上背负的任务编号,身上的装备,材料,格子。。。。等等等等。 
    我们要做的是,给策划提供一系列的方法,让他们可以随意修改这个值。这是第一步。光有这个还是不够的,我们还要提供一些事件的触发,可以让某些事件发生的时候,调用到策划所写的脚本里面去。完成这两项,我们作为配料师的责任就完成了。简单吧。 
    策划呢,拿到这些材料,来组织你的故事吧。当然,你得会基本的lua语法,lua本身并不复杂,很适合轻松上手。首先,你需要处理程序给你的事件,并决定你对这些事件的反应。然后,组织你的数据过程,完成你对数据流动的预想。 
    好了,说道理简单,让我们看看如何去做这样的架构吧,用代码来实现一个高弹性,可扩展的逻辑舞步,嘿嘿,很兴奋吧。 
    我先建立一个简单的CPlayer对象,姑且这就是最简单的玩家属性吧,当然,你可以扩展之,我只是展现这样的思维流程。这里包含了一个对所有CPlayer的数据管理对象。

    #include <string>
    #include <map>
    
    using namespace std;
    
    class CPlayer
    {
    public:
            CPlayer(void) {};
            ~CPlayer(void) {};
    
    //创建一个玩家信息
            void CreatePlayer(int nPlayerID, string strPlayerName)
            {
                    m_nPlayerID      = nPlayerID;                   //玩家的ID
                    m_nMessionID     = 0;                           //玩家的任务
                    m_nItemID        = 0;                                //玩家所持有的物品ID
                    m_strPlayerName  = strPlayerName;     //玩家的昵称
                    m_strMessionDesc = "";                     //任务的描述
            };
    
            int GetPlayerID()
            {
                    return m_nPlayerID;
            };
    
            bool SetMessionID(int nMessionID)
            {
                    if(m_nMessionID != 0)
                    {
                            return false;
                    }
    
                    m_nMessionID = nMessionID;
                    return true;
            };
    
            int GetMessionID()
            {
                    return m_nMessionID;
            }
    
            void FinishMessionID()
            {
                    m_nMessionID = 0;
            };
    
            void SetMessionDesc(const char* pDesc)
            {
                    m_strMessionDesc = pDesc;
            };
    
            void SetItemID(int nItemID)
            {
                    m_nItemID = nItemID;
            };
    
            int GetItemID()
            {
                    return m_nItemID;
            }
    
    private:
            int    m_nPlayerID;        //玩家ID
            string m_strPlayerName;    //玩家姓名
            int    m_nMessionID;       //玩家任务ID
            string m_strMessionDesc;   //玩家任务描述
            int    m_nItemID;          //玩家所持物品ID
    };
    
    
    class CPlayerManager
    {
    private:
            CPlayerManager() {};
    
    public:
            static CPlayerManager& Instance()
            {
                    if(m_pPlayerManager == NULL)
                    {
                            m_pPlayerManager = new CPlayerManager();
                    }
    
                    return *m_pPlayerManager;
            };
    
            ~CPlayerManager()
            {
                    Close();
            };
    
            bool AddPlayer(CPlayer* pPlayer)
            {
                    if(NULL == pPlayer)
                    {
                            return false;
                    }
    
                    mapPlayerManager::iterator f = m_mapPlayerManager.find(pPlayer->GetPlayerID());
                    if(f != m_mapPlayerManager.end())
                    {
                            return false;
                    }
    
                    m_mapPlayerManager.insert(mapPlayerManager::value_type(pPlayer->GetPlayerID(), pPlayer));
                    return true;
            };
    
            CPlayer* GetPlayer(int nPlayerID)
            {
                    mapPlayerManager::iterator f = m_mapPlayerManager.find(nPlayerID);
                    if(f != m_mapPlayerManager.end())
                    {
                            return (CPlayer* )f->second;
                    }
                    else
                    {
                            return NULL;
                    }
            };
    
            void Close()
            {
                    mapPlayerManager::iterator b = m_mapPlayerManager.begin();
                    mapPlayerManager::iterator e = m_mapPlayerManager.end();
    
                    for(b; b != e; b++)
                    {
                            CPlayer* pPlayer = (CPlayer* )b->second;
                            if(NULL == pPlayer)
                            {
                                    delete pPlayer;
                                    pPlayer = NULL;
                            }
                    }
            };
    
    private:
            typedef map<int, CPlayer*> mapPlayerManager;
            mapPlayerManager m_mapPlayerManager;
            static CPlayerManager* m_pPlayerManager;
    };

    呵呵,这个类很简单,里面包含了一些玩家的基本信息,并提供了一个CPlayerManager()的管理类,管理所有已经注册的CPlayer对象,这些CPlayer对象提供给我了修改玩家种种属性的方法。
    好了,我还需要一个让Lua能够方便调用我的C++这些玩家的修改方法的类。来完成和lua的互联互通。这里,既然Lua是面向过程的语言,那么我就让我的方法全部是面向过程的。

    #ifndef LUAFNREGISTER_H
    #define LUAFNREGISTER_H
    
    //lua下的C++注册类,负责处理lua中用到的C++函数
    //add by freeeyes
    
    #include "LuaDefine.h"
    #include <vector>
    #include <string>
    using namespace std;
    
    typedef int (*FuncExecute)(lua_State* plua_State);
    
    struct _FuncInfo
    {
            string      m_strFuncName;
            FuncExecute m_FuncExecute;
    };
    
    //这个类负责管理所有注册的lua对C++函数类
    class CLuaFnRegister
    {
    public:
            CLuaFnRegister() {};
            ~CLuaFnRegister() {};
    
            void Close()
            {
                    m_vecLuaFnRegCollection.clear();
            };
    
            bool AddFunc(const char* pFuncName, FuncExecute pFn)
            {
                    _FuncInfo objFuncInfo;
                    objFuncInfo.m_strFuncName = pFuncName;
                    objFuncInfo.m_FuncExecute = pFn;
                    
                    m_vecLuaFnRegCollection.push_back(objFuncInfo);
                    return true;
            };
    
            int GetSize()
            {
                    return (int)m_vecLuaFnRegCollection.size();
            };
    
            _FuncInfo* GetFuncInfo(int nIndex)
            {
                    if(nIndex >= (int)m_vecLuaFnRegCollection.size())
                    {
                            return NULL;
                    }
                    else
                    {
                            return (_FuncInfo* )&m_vecLuaFnRegCollection[nIndex];
                    }
            };
    
    
    private:
            typedef vector<_FuncInfo> vecLuaFnRegCollection;
            vecLuaFnRegCollection m_vecLuaFnRegCollection;
    };
    
    #endif

    首先,我用一个类,来管理所有我的修改方法在lua中的注册。一个函数名外加一个函数指针,就完成了一个注册。这个类用于管理所有已知我需要让lua调用我的方法。
    好了,再来看看,如果策划给我们一个任务的需求,我们需要给予策划什么样的接口呢?

    #ifndef _LUAFN_MESSION_H
    #define _LUAFN_MESSION_H
    
    #include "LuaDefine.h"
    #include "Player.h"
    
    //执行远程函数
    static int LuaExecute_Func(lua_State* pState, const char* pFuncName, int nPlayerID)
    {
            lua_getglobal(pState, pFuncName);
            lua_pushnumber(pState, nPlayerID);
    
            int nRet = lua_pcall(pState, 1, 0, 0);
            if (nRet != 0)
            {
                    printf("[LuaExecute_Func]call function(%s) error(%s).\n", pFuncName, lua_tostring(pState, -1));
                    return false;
            }
    
            lua_settop(pState, -1);
            return 0;
    }
    
    //输出文字
    static int LuaFn_Mession_Print(lua_State* pState)
    {
            char* pGlobal = (char* )lua_tostring(pState, -1);
            printf_s("[LuaFn_Mession_Print]Data=%s.\n", pGlobal);
            return 0;
    }
    
    //输入描述信息
    static int Lua_Mession_Desc(lua_State* pState)
    {
            int nPlayerID  = (int)lua_tonumber(pState, -2);
            char* pDesc    = (char* )lua_tostring(pState, -1);
    
            //printf_s("[Lua_Mession_Desc]nPlayerID=%d, pDesc=%s.\n", nPlayerID, pDesc);
            CPlayer* pPlayer = CPlayerManager::Instance().GetPlayer(nPlayerID);
            if(NULL == pPlayer)
            {
                    lua_pushboolean(pState, (int)false);
            }
            else
            {
                    pPlayer->SetMessionDesc(pDesc);
                    lua_pushboolean(pState, (int)true);
            }
    
            return 1;
    }
    
    //输入描述信息
    static int Lua_Mession_Error(lua_State* pState)
    {
            int nPlayerID  = (int)lua_tonumber(pState, -2);
            char* pDesc    = (char* )lua_tostring(pState, -1);
    
            CPlayer* pPlayer = CPlayerManager::Instance().GetPlayer(nPlayerID);
            if(NULL == pPlayer)
            {
                    lua_pushboolean(pState, (int)false);
            }
            else
            {
                    pPlayer->SetMessionDesc(pDesc);
                    lua_pushboolean(pState, (int)true);
            }
    
            return 1;
    }
    
    //当前用户是否存在任务,如果有则返回False
    static int Lua_Mession_IsHave(lua_State* pState)
    {
            int nPlayerID  = (int)lua_tonumber(pState, -1);
    
            CPlayer* pPlayer = CPlayerManager::Instance().GetPlayer(nPlayerID);
            if(NULL == pPlayer)
            {
                    lua_pushboolean(pState, (int)false);
            }
            else
            {
                    if(pPlayer->GetMessionID() != 0)
                    {
                            lua_pushboolean(pState, (int)true);
                    }
                    else
                    {
                            lua_pushboolean(pState, (int)false);
                    }
            }
    
            return 1;
    }
    
    //得到用户任务ID
    static int Lua_Mession_GetID(lua_State* pState)
    {
            int nPlayerID  = (int)lua_tonumber(pState, -1);
    
            CPlayer* pPlayer = CPlayerManager::Instance().GetPlayer(nPlayerID);
            if(NULL == pPlayer)
            {
                    lua_pushnumber(pState, 0);
            }
            else
            {
                    int nMessionID = pPlayer->GetMessionID();
                    lua_pushnumber(pState, nMessionID);
            }
    
            return 1;
    }
    
    //得到用户任务ID
    static int Lua_Mession_SetID(lua_State* pState)
    {
            int nPlayerID  = (int)lua_tonumber(pState, -2);
            int nMessionID = (int)lua_tonumber(pState, -1);
    
            CPlayer* pPlayer = CPlayerManager::Instance().GetPlayer(nPlayerID);
            if(NULL != pPlayer)
            {
                    pPlayer->SetMessionID(nMessionID);
            }
    
            return 0;
    }
    
    //给与玩家道具ID
    static int Lua_Mession_SetItemID(lua_State* pState)
    {
            int nPlayerID  = (int)lua_tonumber(pState, -3);
            int nMessionID = (int)lua_tonumber(pState, -2);
            int nItemID    = (int)lua_tonumber(pState, -1);
    
            CPlayer* pPlayer = CPlayerManager::Instance().GetPlayer(nPlayerID);
            if(NULL != pPlayer)
            {
                    pPlayer->SetItemID(nItemID);
            }
    
            return 0;
    }
    
    //得到玩家道具ID
    static int Lua_Mession_GetItemID(lua_State* pState)
    {
            int nPlayerID  = (int)lua_tonumber(pState, -2);
            int nMessionID = (int)lua_tonumber(pState, -1);
    
            CPlayer* pPlayer = CPlayerManager::Instance().GetPlayer(nPlayerID);
            if(NULL != pPlayer)
            {
                    int nItemID = pPlayer->GetItemID();
                    lua_pushnumber(pState, nItemID);
            }
            else
            {
                    lua_pushnumber(pState, 0);
            }
    
            return 1;
    }
    
    //完成任务
    static int Lua_Mession_Done(lua_State* pState)
    {
            int nPlayerID  = (int)lua_tonumber(pState, -2);
            int nMessionID = (int)lua_tonumber(pState, -1);
    
            CPlayer* pPlayer = CPlayerManager::Instance().GetPlayer(nPlayerID);
            if(NULL != pPlayer)
            {
                    pPlayer->FinishMessionID();
            }
    
            return 0;
    }
    
    //调用远程事件处理函数
    static int Lua_Mession_Events(lua_State* pState)
    {
            int nPlayerID  = (int)lua_tonumber(pState, -2);
            int nMessionID = (int)lua_tonumber(pState, -1);
    
            //lua_pop(pState, -1);
    
            //调用远程接口
            char szFuncName[100] = {'\0'};
            sprintf(szFuncName, "Event_%d_Request", nMessionID);
    
            LuaExecute_Func(pState, szFuncName, nPlayerID);
    
            return 0;
    }
    
    #endif

    好了,我提供了一批方法给策划,这就是作料,如果策划觉得不够,我可以随意添加。这里你可以看到,我所有传给lua的方法,都是以基础类型为参数的,避免了设计中的类传递,这也是我抵触把类给Lua的实现,因为在实现细节上,任何修改都应该可以转变成对基础类型的修改。所以,你没有必要把类给策划,让他自己去找繁复的接口。你给他们的,都是清晰可见的,而且是独立的。这样,减少了策划误用错用你的接口的可能性。
    还记得我在Lua基础贴中的那个CLuaFn类吗?如果你读了我以前的Lua的文章,对它应该不陌生,好的,那么让我扩展一下它吧。让它支持我所需要的接口。

    bool CLuaFn::InitClass()
    {
            if(NULL == m_pState)
            {
                    printf("[CLuaFn::InitClass]m_pState is NULL.\n");
                    return false;
            }
    
            tolua_open(m_pState);
            tolua_module(m_pState, NULL, 0);
            tolua_beginmodule(m_pState, NULL);
    
            m_LuaFnRegister.AddFunc("LuaFn_Mession_Print", LuaFn_Mession_Print);
            m_LuaFnRegister.AddFunc("Lua_Mession_Error", Lua_Mession_Error);
            m_LuaFnRegister.AddFunc("Lua_Mession_Desc", Lua_Mession_Desc);
            m_LuaFnRegister.AddFunc("Lua_Mession_IsHave", Lua_Mession_IsHave);
            m_LuaFnRegister.AddFunc("Lua_Mession_GetID", Lua_Mession_GetID);
            m_LuaFnRegister.AddFunc("Lua_Mession_SetID", Lua_Mession_SetID);
            m_LuaFnRegister.AddFunc("Lua_Mession_SetItemID", Lua_Mession_SetItemID);
            m_LuaFnRegister.AddFunc("Lua_Mession_GetItemID", Lua_Mession_GetItemID);
            m_LuaFnRegister.AddFunc("Lua_Mession_Done", Lua_Mession_Done);
            m_LuaFnRegister.AddFunc("Lua_Mession_Events", Lua_Mession_Events);
    
            for(int i = 0; i < m_LuaFnRegister.GetSize(); i++)
            {
                    _FuncInfo* pFuncInfo = m_LuaFnRegister.GetFuncInfo(i);
                    if(NULL != pFuncInfo)
                    {
                            tolua_function(m_pState, pFuncInfo->m_strFuncName.c_str(), pFuncInfo->m_FuncExecute);
                    }
            }
            tolua_endmodule(m_pState);
    
    
            return true;
    }

    呵呵,看到了吧,一点也不复杂。
    好了,让我们来看看策划要做什么。
    策划需要实现两个lua。
    第一个NPC_freeeyes.lua

    --NPC的ID,在程序里面对应NPC
    
    NPC_1001_NAME="自由之眼"
    
    function NPC_1001_OnDefault(nMapID, nPlayerID)
        blMessionState = Lua_Mession_IsHave(nPlayerID)
        if blMessionState == true then
            --如果任务有,则判断是否需要完成
            nMessionID = Lua_Mession_GetID(nMapID, nPlayerID)
            NPC_1001_OnSubmit(nMapID, nPlayerID, nMessionID)
        else
            Lua_Mession_SetID(nPlayerID, 200001)
            Lua_Mession_Desc(nPlayerID, "请选择你要接受的任务信息,1是要土豆,2是要黄铜。")
        end
    end
    
    function NPC_1001_OnEvents(nMapID, nPlayerID)
        nMessionID = Lua_Mession_GetID(nMapID, nPlayerID)
        --print("[NPC_1001_OnEvents]nMessionID="..nMessionID)
        if nMessionID ~= 0 then
            Lua_Mession_Events(nPlayerID, nMessionID)
        end
    end
    
    function NPC_1001_OnSubmit(nMapID, nPlayerID, nMessionID)
                    nItemID = Lua_Mession_GetItemID(nPlayerID, nMessionID)
                    if nMessionID == 200001 then
                                    if nItemID == 300 then
                                                    Lua_Mession_Done(nPlayerID, nMessionID)
                            --LuaFn_Mession_Print("200001 完成了。")
                                    end
                    end
                    if nMessionID == 200002 then
                                    if nItemID == 301 then
                                                    Lua_Mession_Done(nPlayerID, nMessionID)
                            --LuaFn_Mession_Print("200002 完成了。")
                                    end
                    end
    end

    呵呵,看看,我把自己做成了一个NPC,当然,如果你愿意,你可以建立无数个这样的NPC在这个游戏中。这里面提供了两个最简单的方法,当玩家碰触这个NPC,我们调用NPC_1001_OnDefault()方法,这里会验证玩家身上有没有任务,有,则会调用是否完成的函数。我准备了两个任务,200001和200002
    好了,那么,我们再看看第二个lua完成了什么?

    function Event_200001_Request(nPlayerID)    
        --LuaFn_Mession_Print("200001 给你一个土豆。")
        Lua_Mession_SetItemID(nPlayerID, 200001, 300)
    end
    
    function Event_200002_Request(nPlayerID)
        --LuaFn_Mession_Print("200002 给你一个黄铜。")
        Lua_Mession_SetItemID(nPlayerID, 200002, 301)
    end

    我们姑且命名为Mession_Event_200001.lua(其实这个名字并不是很合适,呵呵,例子嘛)
    为什么这么设计,因为我需要策划也需要建立一个完整的思维体系。任务的创建和任务的接收,是在很多的lua文件中完成的,一个任务体系一个lua,这样很清晰。关于任务中条件的达成,都是各个Event的lua去做的。这样,你可以让策划写起来很舒服,而且从程序的角度来讲,也会很舒服。事件的触发过程,由策划在另一个lua里面完成。
    比如,我创建一个获取黄铜的任务,我在Event被触发的时候,就给他一个黄铜。具体可以参考我的源代码。
    这样,一个基于过程的逻辑实现体系就完成了,当然,这是一个很简单的。那么如何在游戏中和事件绑定呢?这又是另一个话题了,等我有时间好好的讲讲,如何在服务器上控制事件的触发,这些事件会根据条件,调用不同的lua形成结果。
    最后,作为程序员的我,自然要对这个架构测试一下性能,看看能否满足我的要求,于是,我让一个玩家接受100万次任务,获得道具,交还任务。看看花费了多少时间。
    结果如下: (windowsXP,CPUE5300,2G内存的的条件下),运行时间是13.3秒。也就是每秒可以执行7.5万次任务交接,获得道具和任务完成的流程。呵呵,这个速度,我还是比较满意的。
    以下代码在windowsXP和VS2005下测试通过。


    如果你喜欢或者对你有帮助,我很高兴,当然,也欢迎你多多提问。
    还有,代码永远是基础的,思想才是最有价值的东西。不要把自己的思路固定在代码实现的层面上。

  • 相关阅读:
    (紫书,感谢作者)第7章暴力求解法
    明日更新
    明天更新
    UVa11882最大的数(dfs+剪枝)
    UVa12569树上的机器人的规划
    es6中的reduce方法?
    浏览器是如何渲染页面的?
    判断是不是一个数组?
    判断是否是一个数组?
    var与let的区别?
  • 原文地址:https://www.cnblogs.com/sdlypyzq/p/3013779.html
Copyright © 2020-2023  润新知