找回密码
 用户注册

QQ登录

只需一步,快速开始

查看: 5880|回复: 0

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

[复制链接]
发表于 2012-5-18 22:52:17 | 显示全部楼层 |阅读模式
说道数据组织,这里不必多说了,我以前写的lua的帖子已经很多了,在这里就不再复述了。有兴趣的可以查看以前的帖子。
关于Lua的引擎部分,在这里我不在说了,我使用的就是以前的lua引擎的改良版。
那么下面说什么呢,让我们来看看,怎么组织一个架构吧。
那么,对于一个游戏来说,怎么样能做到高效呢?或许你会说,并发处理吧,比如,我有1000个房间,我可以创造10个线程去做,看上去或许很美,但是实际上,可能你要为线程间的开销而付出代价,而且在数据同步上,也要做很多的工作。
那么,回过来,是否可以这么想呢?我建立一个独立的场景,在这个场景下游1000个房间,所有进入的命令,都按照一个顺序排列,顺序执行,这样,我既不需要考虑数据锁的问题,也简化了代码,效能上怎么样呢?其实,如果是单线程的话,效能也不会很低,只要我把耗时的IO操作不在这里做即可。
那么,我就需要这么一个小世界,这个小世界可以给我提供如下功能:
(1)消息的队列,所有进入这个小世界的消息,都按顺序进入,并且按顺序执行。
(2)在这个小世界里,所有相关的IO操作,能避免的尽量避免,比如对数据源的读写。
(3)这个小世界还必须有定时器,可以满足我对有些定时需求的操作。
好了,让我们找找看,在ACE中有没有能满足我以上需求的类?
哦,还真有,ACE_Task正好就是我要的,恩,那么我们说干就干吧。
  1. class CSenceManager : public ISenceManager
  2. {
  3. public:
  4.         CSenceManager(void);
  5.         virtual ~CSenceManager(void);
  6.         virtual int open(void* args = 0);
  7.         virtual int svc (void);
  8.         virtual int handle_timeout(const ACE_Time_Value &tv, const void *arg);
  9.         void Init();
  10.         int  Close();
  11.         bool KillTimer();
  12.         bool PutMessage(uint32 u4CommandID, _CommandInfo& objCommandInfo);
  13.         bool StartTimer();
  14. private:
  15.         bool ProcessMessage(_QueueMessage* pQueueMessage);
  16.         bool IsRun();
  17. private:
  18.         bool                           m_blRun;
  19.         int                            m_nTimerID;
  20.         uint32                         m_u4ProCount;         //处理包个数
  21.         uint32                         m_u4TimeCost;         //处理包的时间消耗总时间
  22.         CMessagePool                   m_objMessagePool;
  23.         CMessageBlockPool              m_objMessageBlockPool;
  24.         CLuaFn                         m_objLuaFn;
  25.         CSMDataManager                 m_objSMDataManager;
  26. };
复制代码
先不理ISenceManager是什么,这个东东实际和Lua引擎相关,毕竟Lua里面的实现函数,都是静态的方法,所以,我需要一个桥接的东东。
呵呵,看上去很简单吧。
PutMessage()方法就是我进入这个小世界的地方,这个小世界就像一个盒子,盒子上有一个口,可以往里面按顺序放东西,这就是这个入口。我可以把消息放入里面,驱动这个小世界运转,很酷吧。
当然ProcessMessage(),这个就是消息处理的地方,进入队列的消息,将在这里得到处理。
那么,我们管它叫什么呢?CSenceManager好了,场景管理器。
呵呵,聪明的你肯定会想,如果我有多个场景呢,那么一样,你只要多建立几个这样的场景类就可以的。
这里为了测试我说的性能,我追加了m_u4ProCount和m_u4TimeCost来记录我的这个小世界的时间消耗,呵呵。
为了提高性能,所有的消息在被创造的时候,都是从消息池里面拿出来的,这样减少了new和delete造成的时间消耗。
m_objLuaFn就是我定义的Lua引擎,提供我这个小世界事件的启动,比如进入房间,离开房间,出牌等等。对应用户操作,该交给Lua处理的,就让它去处理吧。
接下来,让我们看看,在这个小盒子里,我们如何切割若干个小房间呢?
在设计房间的时候,我们必须有一个房间管理器,房间管理器中有一个房间,房间里面包含自己的属性,游戏的属性(德克萨斯扑克信息),以及玩家的属性。
那么先看看,我们的玩家属性有什么
  1. //玩家信息
  2. struct _Player
  3. {
  4.         uint32 m_u4PlayerID;                  //玩家ID
  5.         char   m_szPlayerNick[MAX_BUFF_20];   //玩家昵称
  6.         uint32 m_u4Money;                     //玩家金钱
  7.         uint32 m_objCard[PLAYER_CODE_SIZE];   //玩家手上的牌
  8.         uint32 m_u4CardCount;                 //当前拥有牌的个数
  9.         uint32 m_u4TimerID;                   //当前玩家身上的定时器
  10.         _TimerInfo* m_pTimerInfo;             //当前的玩家定时器附加信息
  11.         ACE_Time_Value m_tvUpdate;            //当前玩家更新最后时间
  12.         _Player()
  13.         {
  14.                 m_u4TimerID       = TIMER_ERROR_IN;
  15.                 m_pTimerInfo      = NULL;
  16.                 Clear();
  17.         }
  18.         void Clear()
  19.         {
  20.                 m_u4PlayerID      = PLAYER_INVALID_ID;
  21.                 m_u4Money         = 0;
  22.                 m_u4CardCount     = 0;
  23.                 m_szPlayerNick[0] = '\0';
  24.                 m_tvUpdate        = ACE_OS::gettimeofday();
  25.                 for(int i = 0; i < PLAYER_CODE_SIZE; i++)
  26.                 {
  27.                         m_objCard[i] = CARD_INVALID_ID;
  28.                 }
  29.                 m_u4CardCount = 0;
  30.                 if(m_pTimerInfo != NULL)
  31.                 {
  32.                         //如果Timer附加的指针没有得到释放,则在这里提出警告
  33.                         OUR_DEBUG((LM_ERROR, "[_Player::Clear]m_pTimerInfo is not NULL.\n"));
  34.                 }
  35.                 m_u4TimerID       = TIMER_ERROR_IN;
  36.                 m_pTimerInfo      = NULL;
  37.         }
  38. };
复制代码
上面的注释我想写的应该比较清楚了,呵呵,对于玩家,我需要一些列的属性,当然,你觉得不够的话,你可以添加。
那么,Room是什么样的呢?
  1. //房间结构体
  2. struct _Room
  3. {
  4.         uint32      m_u4RoomID;                                    //房间ID
  5.         char        m_szRoomName[MAX_BUFF_20];                     //房间名称
  6.         char        m_szRoomDesc[MAX_BUFF_200];                    //房间信息描述
  7.         _Player     m_objPlayer[ROOM_PLAYER_SIZE];                 //最大房间内的玩家
  8.         _Player     m_objVisitPlayer[ROOM_PLAYER_VISITOR_SIZE];    //最大房间参观者玩家
  9.         bool        m_blState;                                     //是否开放,true开放,false不开放
  10.         uint32      m_u4State;                                     //房间状态,0为准备开始,1为已经开始
  11.         uint32      m_u4PlayerCount;                               //玩家个数
  12.         uint32      m_u4VisitorCount;                              //参观玩家个数
  13.         uint32      m_u4TimerID;                                   //房间定时器ID
  14.         _TimerInfo* m_pTimerInfo;                                  //房间定时器的附加参数
  15.         ACE_Time_Value m_tvUpdate;                                 //当前房间更新最后时间
  16.         _GameInfo   m_objGameInfo;                                    //房间游戏相关信息
  17.         _Room()
  18.         {
  19.                 m_u4RoomID = ROOM_ERROR_NOEXIST;
  20.                 m_u4TimerID  = TIMER_ERROR_IN;
  21.                 m_pTimerInfo = NULL;
  22.                 Clear();
  23.         }
  24.         //清理房间
  25.         void Clear()
  26.         {
  27.                 for(int i = 0; i < ROOM_PLAYER_SIZE; i++)
  28.                 {
  29.                         m_objPlayer[i].m_u4PlayerID = PLAYER_INVALID_ID;
  30.                 }
  31.                 for(int i = 0; i < ROOM_PLAYER_VISITOR_SIZE; i++)
  32.                 {
  33.                         m_objVisitPlayer[i].m_u4PlayerID = PLAYER_INVALID_ID;
  34.                 }
  35.                 if(m_pTimerInfo != NULL)
  36.                 {
  37.                         //如果Timer附加的指针没有得到释放,则在这里提出警告
  38.                         OUR_DEBUG((LM_ERROR, "[_Room::Clear]m_pTimerInfo is not NULL.\n"));
  39.                 }
  40.                 m_u4TimerID  = TIMER_ERROR_IN;
  41.                 m_pTimerInfo = NULL;
  42.                 m_u4PlayerCount  = 0;
  43.                 m_u4VisitorCount = 0;
  44.                 m_blState        = true;
  45.                 m_u4State        = 0;
  46.                 m_tvUpdate       = ACE_OS::gettimeofday();
  47.         }
  48. };
复制代码
同理,和玩家一样过,在这里,我使用了数组而不是STL去解决用户个数的问题,原因是,我觉得在极少的个数面前,使用STL的容器可能显得比较臃肿,而且,我需要达成的是,内存是可控的。我可以控制我的内存大小。
那么接下来,我们创建一个房间的管理器吧。
  1. struct _RoomManager
  2. {
  3.         _Room m_objRoom[ROOM_COUNT];
  4.         _RoomManager()
  5.         {
  6.                 for(int i = 0; i < ROOM_COUNT; i++)
  7.                 {
  8.                         m_objRoom[i].m_u4RoomID = i;
  9.                 }
  10.         }
复制代码
"。。。"是房间管理器的各种调用方法。提供给外层使用,代码太多了,就不贴在这里了,这里我只是简单的介绍一下架构。
那么接下来,你可能会问,那么,用户登录登出,我怎么能把数据源和游戏服务器隔离呢?
问的好,如果你能想到这个,说明你已经看懂了一大半我讲的东西了。
那么,我们就来说说共享内存吧。我以前在PSS里面提供了一个共享内存跨进程访问的例子,可惜,那个例子可能看的不是很明白。在这里,我正式整合到我的这个德克萨斯扑克中去吧。
好的,那么,首先,我们来想想,如何隔离数据源的访问呢?
如果我有一个进程,提供数据的读取与写入,将从数据源获得的数据,写入一个公共的地方,当有用户访问的时候,我提供给这个进程一个消息,说我要获得一个指定的数据,这个进程先帮我在共享内存中寻找,如果有直接返回给我一个位置。如果没有,则从数据源填充到公共的地方,然后返回给我一个位置。就和看电影一样,电影院在门口买票,人们拿着票走到指定的座位上,这时候已经坐下看电影的人,不必等着拿票进来找坐位的人,而是可以直接观赏电影。这岂不是很美?
恩,那么,我们需要一个这样的模式,来解决我们的数据问题。看吧,其实很简单,你只要造一个虚拟的电影院就可以了。
那么,什么是共享内存呢?如果你刚刚接触这个,可能会对此有点晕,没关系,你可以在这里找到我关于共享内存的帖子,好好看一下。我这里使用的共享内存引擎,就是我以前帖子里的。
那么,在消费者(电影院厅)进程中,我们只需要提供这样的类功能就可以了
  1. typedef map<uint32, _UserPlayer*> mapOnlineUserPlayer;
  2. typedef list<_UserPlayer*> listDeleteUserPlayer;
  3. class CSMDataManager
  4. {
  5. public:
  6.         CSMDataManager(void);
  7.         ~CSMDataManager(void);
  8.         bool         Init();                                    //打开共享内存
  9.         _UserPlayer* GetUserPlayer(uint32 u4Pos);               //根据偏移获得数据指针
  10. private:
  11.         CSMPool<_UserPlayer> m_objUserPlayerPool;     //玩家用户池
  12.         SMKey                m_objKey;                //共享内存key
  13. };
  14. #endif
复制代码
只有一个方法,那就是GetUserPlayer()顾名思义,就是,我有一张电影票,我拿着这个票能找到固定的座位。这个座位就是我的。
那么,数据源维护进程是怎么做的呢?
  1. typedef map<string, _UserPlayer*> mapOnlineUserPlayer;
  2. typedef map<string, _UserPlayer*> mapOfflineUserPlayer;
  3. typedef list<_UserPlayer*> listDeleteUserPlayer;
  4. class CSMDataManager
  5. {
  6. public:
  7.         CSMDataManager(void);
  8.         ~CSMDataManager(void);
  9.         bool         Init();                                    //打开共享内存
  10.         bool         ReadDBData();                              //从数据库中填充玩家信息
  11.         int32        ReadDBData(const char* pPlayerNick);       //从玩家昵称获得玩家信息
  12.         int32        ReadPlayerSMPos(const char* pPlayerNick);  //获得玩家当前内存地址的偏移位置
  13.         bool         LeaveDBData(const char* pPlayerNick);      //玩家离开
  14.         bool         WriteDBData(const char* pPlayerNick);      //写入数据源玩家的数据
  15.         _UserPlayer* GetUserPlayer(uint32 u4Pos);               //根据偏移获得数据指针
  16.         void         ReadSMPool();                              //还原数据在线,离开和可以删除的数据列表
  17. private:
  18.         uint32       GetRandNumber();                           //获得一个随机数
  19.         bool         DeleteUserPlayer();                        //删除最后一个应该删除链表的指针
  20.         _UserPlayer* FindPlayerLeave(const char* pPlayerNick);  //在离线列表中寻找是否已有数据
  21.         _UserPlayer* FindPlayerDelete(const char* pPlayerNick); //在可删除列表中寻找是否已有数据
  22. private:
  23.         CSMPool<_UserPlayer> m_objUserPlayerPool;     //玩家用户池
  24.         SMKey                m_objKey;                //共享内存key
  25.         mapOnlineUserPlayer  m_mapOnlineUserPlayer;   //在线玩家表
  26.         mapOfflineUserPlayer m_mapLeaveUserPlayer;    //已经离开玩家表
  27.         mapOfflineUserPlayer m_mapDeleteUserPlayer;   //可以删除的玩家表(用于高效查找)
  28.         listDeleteUserPlayer m_listDeleteUserPlayer;  //可以删除的玩家表(用于淘汰算法)
  29. };
  30. #endif
复制代码
呵呵,这里提供的方法比较多,可能看着有些头晕,但是实际上,你只需要看Public提供的方法就行了,那些Private方法提供的是为了LRU而设计的,保持内存的高效性,也就是说,如果用户退出了,且我已经会写成功了数据源,我并不会删除这个用户的数据,如果它下次登录上来,我会现在内存中查找,如果有就直接返回之。(在Linux的release下,每秒每个链接3000次访问稳定输出)
如果共享内存满了,我会淘汰最后最不常用的回写成功的数据。这样尽量做到高效。
我做的例子很简单,通过用户昵称去获得数据,当然,数据源我没有使用数据库,为了保证样例的完整性,我这里使用了一个简单的随机算法。当然,你有兴趣的话,完全可以替换我这块变成你的数据源访问代码。
呵呵,注释写的很清楚了吧。
有了这两样,我们就可以很轻松的分布数据请求了。也为下一步我们的动作做好了充足的准备。
写到这里,我个人用了差不多2天的时间,我想聪明的你一定能够从中找到一些自己可以借鉴的东西。
好了,下一章,我会讲,如何把以上这些应用串联起来。呵呵,让他们统一行动。

您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|手机版|小黑屋|ACE Developer ( 京ICP备06055248号 )

GMT+8, 2024-4-28 03:06 , Processed in 0.012705 second(s), 5 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表