找回密码
 用户注册

QQ登录

只需一步,快速开始

查看: 7037|回复: 1

一个MMO游戏服务逻辑系统(一)

[复制链接]
发表于 2011-8-25 11:31:09 | 显示全部楼层 |阅读模式
最近好久没有更新论坛帖子了,有点对不起大家,因为最近工作较忙,另外赶制和测试新版的PurenessScopeServer 0.80版本,所以一直没有更新。新版的PurenessScopeServer 0.80添加了很多新功能,修复了一些测试出现的BUG,并提供了一个全新的服务器远程管理图形客户端工具,可以用来服务器插件管理(谢天谢地,终于实现了原来的目标),所有的dll或者so都可以看做U盘,实现远程管理热插拔。另外修改了消息的映射体制,同一个消息可以让一个或者多个逻辑模块订阅。windows下框架会自动生成dump文件,链接进入和断开的消息订阅,等等等等,增加了很多很实用的功能,并优化了系统,如果有兴趣大家期待一下吧,这里特别感谢badbrain朋友,在这里做出了很大的贡献。好了废话少说,来说正题。
市面上有很多MMO的游戏,但是真正介绍如何完整的构建一个MMO服务器内核的文章,却非常稀少,虽说有大量的开源服务器程序,不过要想在几万行代码中了解如何建立一个MMO服务器,绝对不是一件易事。而且网上部分叫嚣XX游戏服务器源代码的程序,大量的存在着无用的代码或者代码陷阱(不知道是不是放出者故意的),容易把真正的学习者引入歧途。在这里,我不敢说我的技术怎样,但是提供一种设计思路,为大家学习如何设计一个MMO游戏服务器而做出一种尝试。
以下代码都在windows和VS2005下测试通过(linux下亦可运行,因为我没有采用任何平台限制的API),希望通过下面的文字,能够让你觉得其实所谓大型MMO游戏服务器真的不过如此。还有一句想说,如果想变为卓越,光抄袭是没有意义的,而是学会在别人的基础上,总结出符合自己特点的理解和提升,就像武功一样,好的武功绝不流于形式,而是与自己心动合一。好的代码也是一样,必须经过你自己的咀嚼和消化,最后能够摒弃对自己无用的东西,吸收符合自身特点的理解,这样,才能越做越好。成功本无捷径,关键是,通过代码去了解自己,融合自己,表现自己。而并非仅仅在于代码的一招一式,最后做到码由心生,让代码为你的想法服务,服从于你的意志。
好了,先说说,要做一个MMO服务器而言,你最先想到的是什么?
1.我要让很多人在一个场景中。
2.场景中会有很多的NPC供玩家消遣,也可以玩家之间相互消遣。
3.场景中存在很多事件,当某些条件到达的时候,会被触发。
4.我在场景中行走,必须能看到身边人的变化,比如位置,状态等等。
5.或许会有一些神秘的地方,当玩家进入的时候,会进入另一个场景事件(副本)
6.在场景中存在各式各样的任务,我必须能灵活的选择。
以上问题,其实就是一个最普通的MMO游戏的基础要求,不要被纷繁的游戏场景,华丽的特技大招,以及漫天世界的任务, 多余牛毛的各种NPC吓到。其实在服务器端,只有几个技术点,把握好了,你就是一个世界的创世主。
那么,对于以上的需求,我服务器需要准备什么?
1.必须要有能够保证多路链接稳定的数据接收和传输系统(很多肤浅的文章把这部分认为是一个MMO服务器的核心,其实完全不是,真正游戏的核心应该是你的创意和算法,而不是通用的简单IO传输方法)
2.我会有各色各样的NPC,每个NPC上都会有各色各样的任务,我怎么能够做到方便的添加任务,修改任务?甚至是场景事件触发(比如白天黑夜的转换)
3.在处理行走的问题上,我怎么保证我能够看见身边玩家的最新动态?
4.寻路怎么做?
5.怎么保证服务器的运算不会被客户端的时间影响?
我将一个个作为解答,先将几个最基础的概念。
一个MMO游戏而言,我可以有任意多的场景,那么,什么是场景呢?你可以这样理解,一个场景可以包含一张或者N张地图,在这个场景内,我会有一个玩家列表,这个列表中的玩家不会隶属于其他场景,它所做的改变大部分只能影响到本场景内的事件变化(聊天全服务器喊话,消息除外)。对于一个场景内的玩家,最多最多我只需要告知通常境内的玩家,某一个玩家的事件产生了。(比如发招,集会,引怪等等),而在于其他场景的玩家,是否需要处理这样的信息呢?显然是不需要的。那么,我可以理解为,一个场景本身就是一个小进程,一个小世界,或者小线程。它不需要玩家也能正常的运行和存在。在这个世界中,无论玩家存在与否,某些事件(白天和黑夜的转换),该出现还是要出现的。场景间是独立的,一般一个 MMO可以有若干个场景,也就意味着有若干个线程或者进程(这里我主要用的是线程,毕竟进程间通讯的成本是比较高的。)
那么,对于通讯模块,我只要负责,把用户注册到某些线程中去,就完成了进入场景的需求,离开场景也是如此。同一个玩家是不能分身在两个场景中的,当然,策划需求另论,我这里只提供一个通用的想法。这样,就算是多线程,单个玩家数据也不必加锁解锁。它只会隶属于一个线程去维护和修改。这样设计能够提升效能。
恩,一个场景中,我需要一个或者N个地图,对吧。那么地图是怎么构建的呢?其实,对于地图而言,最重要的服务就是提供这里能走不能走,有什么对象在上面这两方面的需求,无论2D和3D都是如此。最简单的是,一个点阵结构,最基础的是一个X,Y坐标点,而对于3D,无外乎你可以在点上增加两个属性,就是垂直距离,是否可以移动。
  1. //地图文件头结构体
  2. struct _MapFileHead
  3. {
  4.         char m_szMapName[MAX_MAPNAME];   //地图名称
  5.         int  m_nMapID;                                    //地图的ID
  6.         int  m_nMapRow;                                 //地图的宽度
  7.         int  m_nMapCol;                                   //地图的高度
  8. };
  9. //一个矩形区域struct _AreaRect{        int m_nTopX;   //起始点的X坐标        int m_nTopY;   //起始点的Y坐标        int m_nWidth;  //区域的宽度        int m_nHeight; //区域的高度
  10.         //重载等于操作符        _AreaRect& operator = (const _AreaRect& ar)        {                this->m_nTopX   = ar.m_nTopX;                this->m_nTopY   = ar.m_nTopY;                this->m_nWidth  = ar.m_nWidth;                this->m_nHeight = ar.m_nHeight;                return *this;        };};
复制代码

这是一个标准的地图头信息。我们记录一个地图的大致信息,那么下面,我们用一个数组,来记录地图的每个点的信息。_AreaRect是什么呢?我们可以这样想,在一个地图中,会存在若干个"热区",当某些玩家或者NPC进入的时候,会触发一些事件。这样地图设计就可以非常的灵活,不是吗?
那么,我们来看看怎么组建一个游戏地图文件。
  1.         //打开地图文件
  2.         FILE* pFile = fopen(pFileName, "wb");
  3.         if(NULL == pFile)
  4.         {
  5.                 printf("[map]Load map file (%s) error[%d].\n", pFileName, errno);
  6.                 getchar();
  7.                 return 0;
  8.         }
  9.         //写入地图头信息
  10.         _MapFileHead MapFileHead;
  11.         sprintf_s(MapFileHead.m_szMapName, MAX_MAPNAME, "Samplemap");
  12.         MapFileHead.m_nMapRow = 256;
  13.         MapFileHead.m_nMapCol = 256;
  14.         MapFileHead.m_nMapID  = 1;
  15.         int nSize = fwrite(&MapFileHead, 1, sizeof(_MapFileHead), pFile);
  16.         if(nSize != (int)sizeof(_MapFileHead))
  17.         {
  18.                 printf("[map]Load map file (%s) error[%d].\n", pFileName, errno);
  19.                 fclose(pFile);
  20.                 getchar();
  21.                 return 0;
  22.         }
  23.         //写入地图热区
  24.         int nAreaCount = 2;
  25.         fwrite(&nAreaCount, 1, sizeof(int), pFile);
  26.         _AreaRect mfh1;
  27.         _AreaRect mfh2;
  28.         int nAreaID1   = 101;
  29.         int nLuaID1    = 0;
  30.         mfh1.m_nTopX   = 10;
  31.         mfh1.m_nTopY   = 10;
  32.         mfh1.m_nWidth  = 10;
  33.         mfh1.m_nHeight = 10;
  34.         int nAreaID2   = 101;
  35.         int nLuaID2    = 0;
  36.         mfh2.m_nTopX   = 50;
  37.         mfh2.m_nTopY   = 50;
  38.         mfh2.m_nWidth  = 10;
  39.         mfh2.m_nHeight = 10;
  40.         fwrite(&nAreaID1, 1, sizeof(int), pFile);
  41.         fwrite(&nLuaID1, 1, sizeof(int), pFile);
  42.         fwrite(&mfh1, 1, sizeof(_AreaRect), pFile);
  43.         fwrite(&nAreaID2, 1, sizeof(int), pFile);
  44.         fwrite(&nLuaID2, 1, sizeof(int), pFile);
  45.         fwrite(&mfh2, 1, sizeof(_AreaRect), pFile);
  46.         //写入地图点阵
  47.         unsigned char uState = 1;
  48.         for(int i = 0; i < MapFileHead.m_nMapCol; i++)
  49.         {
  50.                 for(int j = 0;j < MapFileHead.m_nMapRow; j++)
  51.                 {
  52.                         if(i == 2 && j == 2)
  53.                         {
  54.                                 uState = 0;
  55.                         }
  56.                         else
  57.                         {
  58.                                 uState = 1;
  59.                         }
  60.                         int nStateSize = (int)sizeof(unsigned char);
  61.                         nSize = fwrite(&uState, nStateSize, 1, pFile);
  62.                         if(nSize!= nStateSize)
  63.                         {
  64.                                 printf("[map](%s)uState is error[%d].\n", pFileName, i * MapFileHead.m_nMapRow + j);
  65.                                 fclose(pFile);
  66.                                 getchar();
  67.                                 return false;
  68.                         }
  69.                 }
  70.         }
  71.         fflush(pFile);
  72.         printf("[map](%s)Create map OK.\n", pFileName);
  73.         fclose(pFile);
复制代码

这里我们假设,一个游戏有两个热区,分别是_AreaRect mfh1和_AreaRect mfh2。其实这部分,完全可以做一个地图编辑器来实现,这里只讲原理,地图编辑器的话,自己去实现一个也不难。
这里最重要的就是写入地图点阵,我们建立了一个很大的数组,这个数组包含了一个地图节点能描述信息的所有。这里只包含了一个简单的属性,能走还是不能走。uState == 0是此节点不能行走,uState == 1是此节点可以行走。这里只是一个范例,我假设节点(2,2)是不能走的。这里要说明一下,这里的节点,并不是和游戏界面上的一个像素绑定,而是一个正方形的矩形区域,这个区域的大小就是粒度,可以根据游戏的定义来修改。你可以想象游戏地图就是一个网格扑在上面,有些网格能走,有些网格不能走。我们可以通过调节网格的大小实现对游戏精确度的控制。
好了,地图文件我具备了,那么在服务器上,怎么加载这个文件呢?
  1. #ifndef _MAP_H
  2. #define _MAP_H
  3. #include <stdio.h>
  4. #include <errno.h>
  5. #include <vector>
  6. #include "mapdefine.h"
  7. #include "area.h"
  8. #define MAX_MAPCELLSIZE       20      //地图每个块的矩形区域大小
  9. #define MAX_FINDPOS_RANGE     4       //设置搜索节点有效位置的范围
  10. #define MAX_ASTAR_COUNT       1000    //最大Astar寻路的半径
  11. using namespace std;
  12. //地图信息
  13. struct _MapInfo
  14. {
  15.         int m_nMapWidth;        //地图的宽
  16.         int m_nMapHight;        //地图的高
  17.         int m_nMapCellSize;     //地图的块数
  18. };
  19. class CMapData : public CMapBaseData
  20. {
  21. public:
  22.         CMapData();
  23.         ~CMapData();
  24.         //从文件中加载地图
  25.         bool LoadMap(const char* pFileName);
  26.         //显示地图信息
  27.         void ShowMapInfo();
  28.         //释放资源
  29.         void Close();
  30.         //给定一个点,判断是否可以行走
  31.         bool IsCanGo(_MapPoint mappos);
  32.         //得到地图的长
  33.         int GetMapWidth();
  34.         //得到地图的高
  35.         int GetMapHigh();
  36.         //获得一条路径,设置起始点和终止点,返回一个路径数组
  37.         bool AStarFind(const _MapPoint PosStart, _MapPoint PosEnd, _MapPointPath* pMapPointPath);  //AStar寻路算法
  38.         //判断一个点是否可走,如果不可走返回一个可走最近的点。
  39.         bool GetValidPos(_MapPoint& ptTarget, int nRange = MAX_FINDPOS_RANGE);
  40. private:
  41.         //初始化8方向
  42.         bool InitDX();
  43.         //计算指定格子的权值
  44.         inline void CalculateCost(const int nTarget, const int nEnd, const int nIndex, _WorldCostInfo& objWorldCostInfo);
  45.         //将Index还原为nRow和nCol
  46.         inline _MapPosCoord GetMapCoord(int nIndex);
  47.         //返回方向权值
  48.         inline int CalculateDirect(_MapPosCoord& MapQueenCoord, _MapPosCoord& MapEndCoord);
  49.         //计算当前最合适的权值
  50.         inline _WorldCostInfo CalculateEightQueen(int nTarget, int nEnd);
  51.         //传入一个点,计算所在的格子,并返回格子的nIndex
  52.         inline int GetMapPos(const _MapPoint PosTarget);
  53.         //给定一个坐标点,返回所在的格子
  54.         int GetMapIndex(_MapPoint mappos);
  55.         //给定一个格子,返回这个格子中心坐标点
  56.         _MapPoint GetMapIndexPoint(int nIndex);
  57. private:
  58.         _MapPos*     m_MapPos;       //地图的块数组
  59.         _MapFileHead m_MapFileHead;  //地图头信息
  60.         _MapInfo     m_MapInfo;      //地图信息
  61.         CAreaGroup   m_AreaGroup;    //地图中的热区事件数组
  62.         int          m_nMapCellSize; //一个地图格子的大小(MAX_MAPNAME)
  63.         _DX*         m_pDX;          //格子把方向的权值数组,用于计算路径
  64. };
  65. #endif
复制代码

好了,这里就是一个简单的游戏地图了,这里,我要给外围提供几个服务。
1.寻路。
2.地点验证。
比如,一个玩家或者NPC要移动,对象会给地图一个目的点,那么地图就要推算出这个对象要移动的路径,躲开障碍物。那么,我们怎么存放这个路径呢?在我看来,路径只是一系列的点,而路径中的每个点,都是一个拐点。中间的点我们不必放在路径进去。因为对于任何一个可以移动的对象,给你的都是一个起始的(x,y)和一个终止的(x,y),如果是最理想的情况,中间没有任何阻碍,那么路径中只会存在两个点(一条直线),就是起点的(x,y)和终点的(x,y)。如果对象移动路途中存在障碍物,我记录的是一系列拐弯的位置(x,y)就可以了,那么,对象只要按照自己的速度移动过去就行了。这里涉及到了A*算法,如果想知道实现原理,请参考我以前写的http://www.acejoy.com/bbs/viewthread.php?tid=3044&extra=page%3D2
好了,我对外提供这些接口,是为游戏对象的基础服务,至于实现细节,可以参考我的cpp。
这里说一下,或许大家会觉得GetValidPos()这个函数有些奇怪。呵呵,这里我要说明一下,由于在MMO中,玩家每次点击的点未必就是可以行走的。如果直接不走,可能会给游戏体验造成负面影响,所以,每次玩家给与服务器一个目标点的时候,无论可走与否,我都会去寻找一个附近最近的可以移动的点。这样游戏体验感会非常好。
好了,先说到这里,下一讲我将会讲解,如何理解NPC这个对象,以及如何创造可以配置的AI。
发表于 2013-1-7 13:42:40 | 显示全部楼层
很感谢,学习中
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

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

GMT+8, 2024-4-29 07:41 , Processed in 0.014035 second(s), 6 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

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