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]