freeeyes 发表于 2012-5-18 22:52:17

PSS应用之-游戏插件开发(二)

说道数据组织,这里不必多说了,我以前写的lua的帖子已经很多了,在这里就不再复述了。有兴趣的可以查看以前的帖子。
关于Lua的引擎部分,在这里我不在说了,我使用的就是以前的lua引擎的改良版。
那么下面说什么呢,让我们来看看,怎么组织一个架构吧。
那么,对于一个游戏来说,怎么样能做到高效呢?或许你会说,并发处理吧,比如,我有1000个房间,我可以创造10个线程去做,看上去或许很美,但是实际上,可能你要为线程间的开销而付出代价,而且在数据同步上,也要做很多的工作。
那么,回过来,是否可以这么想呢?我建立一个独立的场景,在这个场景下游1000个房间,所有进入的命令,都按照一个顺序排列,顺序执行,这样,我既不需要考虑数据锁的问题,也简化了代码,效能上怎么样呢?其实,如果是单线程的话,效能也不会很低,只要我把耗时的IO操作不在这里做即可。
那么,我就需要这么一个小世界,这个小世界可以给我提供如下功能:
(1)消息的队列,所有进入这个小世界的消息,都按顺序进入,并且按顺序执行。
(2)在这个小世界里,所有相关的IO操作,能避免的尽量避免,比如对数据源的读写。
(3)这个小世界还必须有定时器,可以满足我对有些定时需求的操作。
好了,让我们找找看,在ACE中有没有能满足我以上需求的类?
哦,还真有,ACE_Task正好就是我要的,恩,那么我们说干就干吧。
class CSenceManager : public ISenceManager
{
public:
        CSenceManager(void);
        virtual ~CSenceManager(void);

        virtual int open(void* args = 0);
        virtual int svc (void);

        virtual int handle_timeout(const ACE_Time_Value &tv, const void *arg);

        void Init();
        intClose();
        bool KillTimer();
        bool PutMessage(uint32 u4CommandID, _CommandInfo& objCommandInfo);

        bool StartTimer();

private:
        bool ProcessMessage(_QueueMessage* pQueueMessage);
        bool IsRun();

private:
        bool                           m_blRun;
        int                            m_nTimerID;
        uint32                         m_u4ProCount;         //处理包个数
        uint32                         m_u4TimeCost;         //处理包的时间消耗总时间
        CMessagePool                   m_objMessagePool;
        CMessageBlockPool            m_objMessageBlockPool;
        CLuaFn                         m_objLuaFn;
        CSMDataManager               m_objSMDataManager;
};先不理ISenceManager是什么,这个东东实际和Lua引擎相关,毕竟Lua里面的实现函数,都是静态的方法,所以,我需要一个桥接的东东。
呵呵,看上去很简单吧。
PutMessage()方法就是我进入这个小世界的地方,这个小世界就像一个盒子,盒子上有一个口,可以往里面按顺序放东西,这就是这个入口。我可以把消息放入里面,驱动这个小世界运转,很酷吧。当然ProcessMessage(),这个就是消息处理的地方,进入队列的消息,将在这里得到处理。那么,我们管它叫什么呢?CSenceManager好了,场景管理器。呵呵,聪明的你肯定会想,如果我有多个场景呢,那么一样,你只要多建立几个这样的场景类就可以的。这里为了测试我说的性能,我追加了m_u4ProCount和m_u4TimeCost来记录我的这个小世界的时间消耗,呵呵。为了提高性能,所有的消息在被创造的时候,都是从消息池里面拿出来的,这样减少了new和delete造成的时间消耗。m_objLuaFn就是我定义的Lua引擎,提供我这个小世界事件的启动,比如进入房间,离开房间,出牌等等。对应用户操作,该交给Lua处理的,就让它去处理吧。接下来,让我们看看,在这个小盒子里,我们如何切割若干个小房间呢?在设计房间的时候,我们必须有一个房间管理器,房间管理器中有一个房间,房间里面包含自己的属性,游戏的属性(德克萨斯扑克信息),以及玩家的属性。那么先看看,我们的玩家属性有什么//玩家信息
struct _Player
{
        uint32 m_u4PlayerID;                  //玩家ID
        char   m_szPlayerNick;   //玩家昵称
        uint32 m_u4Money;                     //玩家金钱
        uint32 m_objCard;   //玩家手上的牌
        uint32 m_u4CardCount;               //当前拥有牌的个数
        uint32 m_u4TimerID;                   //当前玩家身上的定时器
        _TimerInfo* m_pTimerInfo;             //当前的玩家定时器附加信息
        ACE_Time_Value m_tvUpdate;            //当前玩家更新最后时间

        _Player()
        {
                m_u4TimerID       = TIMER_ERROR_IN;
                m_pTimerInfo      = NULL;
                Clear();
        }

        void Clear()
        {
                m_u4PlayerID      = PLAYER_INVALID_ID;
                m_u4Money         = 0;
                m_u4CardCount   = 0;
                m_szPlayerNick = '\0';
                m_tvUpdate      = ACE_OS::gettimeofday();

                for(int i = 0; i < PLAYER_CODE_SIZE; i++)
                {
                        m_objCard = CARD_INVALID_ID;
                }
                m_u4CardCount = 0;

                if(m_pTimerInfo != NULL)
                {
                        //如果Timer附加的指针没有得到释放,则在这里提出警告
                        OUR_DEBUG((LM_ERROR, "m_pTimerInfo is not NULL.\n"));
                }

                m_u4TimerID       = TIMER_ERROR_IN;
                m_pTimerInfo      = NULL;
        }
};上面的注释我想写的应该比较清楚了,呵呵,对于玩家,我需要一些列的属性,当然,你觉得不够的话,你可以添加。那么,Room是什么样的呢?//房间结构体
struct _Room
{
        uint32      m_u4RoomID;                                    //房间ID
        char      m_szRoomName;                     //房间名称
        char      m_szRoomDesc;                  //房间信息描述
        _Player   m_objPlayer;               //最大房间内的玩家
        _Player   m_objVisitPlayer;    //最大房间参观者玩家
        bool      m_blState;                                     //是否开放,true开放,false不开放
        uint32      m_u4State;                                     //房间状态,0为准备开始,1为已经开始
        uint32      m_u4PlayerCount;                               //玩家个数
        uint32      m_u4VisitorCount;                              //参观玩家个数
        uint32      m_u4TimerID;                                 //房间定时器ID
        _TimerInfo* m_pTimerInfo;                                  //房间定时器的附加参数
        ACE_Time_Value m_tvUpdate;                                 //当前房间更新最后时间

        _GameInfo   m_objGameInfo;                                    //房间游戏相关信息

        _Room()
        {
                m_u4RoomID = ROOM_ERROR_NOEXIST;
                m_u4TimerID= TIMER_ERROR_IN;
                m_pTimerInfo = NULL;
                Clear();
        }

        //清理房间
        void Clear()
        {
                for(int i = 0; i < ROOM_PLAYER_SIZE; i++)
                {
                        m_objPlayer.m_u4PlayerID = PLAYER_INVALID_ID;
                }

                for(int i = 0; i < ROOM_PLAYER_VISITOR_SIZE; i++)
                {
                        m_objVisitPlayer.m_u4PlayerID = PLAYER_INVALID_ID;
                }

                if(m_pTimerInfo != NULL)
                {
                        //如果Timer附加的指针没有得到释放,则在这里提出警告
                        OUR_DEBUG((LM_ERROR, "m_pTimerInfo is not NULL.\n"));
                }

                m_u4TimerID= TIMER_ERROR_IN;
                m_pTimerInfo = NULL;

                m_u4PlayerCount= 0;
                m_u4VisitorCount = 0;
                m_blState      = true;
                m_u4State      = 0;
                m_tvUpdate       = ACE_OS::gettimeofday();
        }
};同理,和玩家一样过,在这里,我使用了数组而不是STL去解决用户个数的问题,原因是,我觉得在极少的个数面前,使用STL的容器可能显得比较臃肿,而且,我需要达成的是,内存是可控的。我可以控制我的内存大小。那么接下来,我们创建一个房间的管理器吧。struct _RoomManager
{
        _Room m_objRoom;

        _RoomManager()
        {
                for(int i = 0; i < ROOM_COUNT; i++)
                {
                        m_objRoom.m_u4RoomID = i;
                }
        }"。。。"是房间管理器的各种调用方法。提供给外层使用,代码太多了,就不贴在这里了,这里我只是简单的介绍一下架构。那么接下来,你可能会问,那么,用户登录登出,我怎么能把数据源和游戏服务器隔离呢?问的好,如果你能想到这个,说明你已经看懂了一大半我讲的东西了。那么,我们就来说说共享内存吧。我以前在PSS里面提供了一个共享内存跨进程访问的例子,可惜,那个例子可能看的不是很明白。在这里,我正式整合到我的这个德克萨斯扑克中去吧。好的,那么,首先,我们来想想,如何隔离数据源的访问呢?如果我有一个进程,提供数据的读取与写入,将从数据源获得的数据,写入一个公共的地方,当有用户访问的时候,我提供给这个进程一个消息,说我要获得一个指定的数据,这个进程先帮我在共享内存中寻找,如果有直接返回给我一个位置。如果没有,则从数据源填充到公共的地方,然后返回给我一个位置。就和看电影一样,电影院在门口买票,人们拿着票走到指定的座位上,这时候已经坐下看电影的人,不必等着拿票进来找坐位的人,而是可以直接观赏电影。这岂不是很美?恩,那么,我们需要一个这样的模式,来解决我们的数据问题。看吧,其实很简单,你只要造一个虚拟的电影院就可以了。那么,什么是共享内存呢?如果你刚刚接触这个,可能会对此有点晕,没关系,你可以在这里找到我关于共享内存的帖子,好好看一下。我这里使用的共享内存引擎,就是我以前帖子里的。那么,在消费者(电影院厅)进程中,我们只需要提供这样的类功能就可以了typedef map<uint32, _UserPlayer*> mapOnlineUserPlayer;
typedef list<_UserPlayer*> listDeleteUserPlayer;

class CSMDataManager
{
public:
        CSMDataManager(void);
        ~CSMDataManager(void);

        bool         Init();                                    //打开共享内存
        _UserPlayer* GetUserPlayer(uint32 u4Pos);               //根据偏移获得数据指针


private:
        CSMPool<_UserPlayer> m_objUserPlayerPool;   //玩家用户池
        SMKey                m_objKey;                //共享内存key

};
#endif只有一个方法,那就是GetUserPlayer()顾名思义,就是,我有一张电影票,我拿着这个票能找到固定的座位。这个座位就是我的。那么,数据源维护进程是怎么做的呢?typedef map<string, _UserPlayer*> mapOnlineUserPlayer;
typedef map<string, _UserPlayer*> mapOfflineUserPlayer;
typedef list<_UserPlayer*> listDeleteUserPlayer;

class CSMDataManager
{
public:
        CSMDataManager(void);
        ~CSMDataManager(void);

        bool         Init();                                    //打开共享内存
        bool         ReadDBData();                              //从数据库中填充玩家信息
        int32      ReadDBData(const char* pPlayerNick);       //从玩家昵称获得玩家信息
        int32      ReadPlayerSMPos(const char* pPlayerNick);//获得玩家当前内存地址的偏移位置
        bool         LeaveDBData(const char* pPlayerNick);      //玩家离开
        bool         WriteDBData(const char* pPlayerNick);      //写入数据源玩家的数据
        _UserPlayer* GetUserPlayer(uint32 u4Pos);               //根据偏移获得数据指针
        void         ReadSMPool();                              //还原数据在线,离开和可以删除的数据列表

private:
        uint32       GetRandNumber();                           //获得一个随机数
        bool         DeleteUserPlayer();                        //删除最后一个应该删除链表的指针
        _UserPlayer* FindPlayerLeave(const char* pPlayerNick);//在离线列表中寻找是否已有数据
        _UserPlayer* FindPlayerDelete(const char* pPlayerNick); //在可删除列表中寻找是否已有数据

private:
        CSMPool<_UserPlayer> m_objUserPlayerPool;   //玩家用户池
        SMKey                m_objKey;                //共享内存key
        mapOnlineUserPlayerm_mapOnlineUserPlayer;   //在线玩家表
        mapOfflineUserPlayer m_mapLeaveUserPlayer;    //已经离开玩家表
        mapOfflineUserPlayer m_mapDeleteUserPlayer;   //可以删除的玩家表(用于高效查找)
        listDeleteUserPlayer m_listDeleteUserPlayer;//可以删除的玩家表(用于淘汰算法)
};
#endif呵呵,这里提供的方法比较多,可能看着有些头晕,但是实际上,你只需要看Public提供的方法就行了,那些Private方法提供的是为了LRU而设计的,保持内存的高效性,也就是说,如果用户退出了,且我已经会写成功了数据源,我并不会删除这个用户的数据,如果它下次登录上来,我会现在内存中查找,如果有就直接返回之。(在Linux的release下,每秒每个链接3000次访问稳定输出)如果共享内存满了,我会淘汰最后最不常用的回写成功的数据。这样尽量做到高效。我做的例子很简单,通过用户昵称去获得数据,当然,数据源我没有使用数据库,为了保证样例的完整性,我这里使用了一个简单的随机算法。当然,你有兴趣的话,完全可以替换我这块变成你的数据源访问代码。呵呵,注释写的很清楚了吧。有了这两样,我们就可以很轻松的分布数据请求了。也为下一步我们的动作做好了充足的准备。写到这里,我个人用了差不多2天的时间,我想聪明的你一定能够从中找到一些自己可以借鉴的东西。好了,下一章,我会讲,如何把以上这些应用串联起来。呵呵,让他们统一行动。
页: [1]
查看完整版本: PSS应用之-游戏插件开发(二)