|
最近好久没有更新论坛帖子了,有点对不起大家,因为最近工作较忙,另外赶制和测试新版的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,无外乎你可以在点上增加两个属性,就是垂直距离,是否可以移动。- //地图文件头结构体
- struct _MapFileHead
- {
- char m_szMapName[MAX_MAPNAME]; //地图名称
- int m_nMapID; //地图的ID
- int m_nMapRow; //地图的宽度
- int m_nMapCol; //地图的高度
- };
- //一个矩形区域struct _AreaRect{ int m_nTopX; //起始点的X坐标 int m_nTopY; //起始点的Y坐标 int m_nWidth; //区域的宽度 int m_nHeight; //区域的高度
- //重载等于操作符 _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进入的时候,会触发一些事件。这样地图设计就可以非常的灵活,不是吗?
那么,我们来看看怎么组建一个游戏地图文件。- //打开地图文件
- FILE* pFile = fopen(pFileName, "wb");
- if(NULL == pFile)
- {
- printf("[map]Load map file (%s) error[%d].\n", pFileName, errno);
- getchar();
- return 0;
- }
- //写入地图头信息
- _MapFileHead MapFileHead;
- sprintf_s(MapFileHead.m_szMapName, MAX_MAPNAME, "Samplemap");
- MapFileHead.m_nMapRow = 256;
- MapFileHead.m_nMapCol = 256;
- MapFileHead.m_nMapID = 1;
- int nSize = fwrite(&MapFileHead, 1, sizeof(_MapFileHead), pFile);
- if(nSize != (int)sizeof(_MapFileHead))
- {
- printf("[map]Load map file (%s) error[%d].\n", pFileName, errno);
- fclose(pFile);
- getchar();
- return 0;
- }
- //写入地图热区
- int nAreaCount = 2;
- fwrite(&nAreaCount, 1, sizeof(int), pFile);
- _AreaRect mfh1;
- _AreaRect mfh2;
- int nAreaID1 = 101;
- int nLuaID1 = 0;
- mfh1.m_nTopX = 10;
- mfh1.m_nTopY = 10;
- mfh1.m_nWidth = 10;
- mfh1.m_nHeight = 10;
- int nAreaID2 = 101;
- int nLuaID2 = 0;
- mfh2.m_nTopX = 50;
- mfh2.m_nTopY = 50;
- mfh2.m_nWidth = 10;
- mfh2.m_nHeight = 10;
- fwrite(&nAreaID1, 1, sizeof(int), pFile);
- fwrite(&nLuaID1, 1, sizeof(int), pFile);
- fwrite(&mfh1, 1, sizeof(_AreaRect), pFile);
- fwrite(&nAreaID2, 1, sizeof(int), pFile);
- fwrite(&nLuaID2, 1, sizeof(int), pFile);
- fwrite(&mfh2, 1, sizeof(_AreaRect), pFile);
- //写入地图点阵
- unsigned char uState = 1;
- for(int i = 0; i < MapFileHead.m_nMapCol; i++)
- {
- for(int j = 0;j < MapFileHead.m_nMapRow; j++)
- {
- if(i == 2 && j == 2)
- {
- uState = 0;
- }
- else
- {
- uState = 1;
- }
- int nStateSize = (int)sizeof(unsigned char);
- nSize = fwrite(&uState, nStateSize, 1, pFile);
- if(nSize!= nStateSize)
- {
- printf("[map](%s)uState is error[%d].\n", pFileName, i * MapFileHead.m_nMapRow + j);
- fclose(pFile);
- getchar();
- return false;
- }
- }
- }
- fflush(pFile);
- printf("[map](%s)Create map OK.\n", pFileName);
- fclose(pFile);
复制代码
这里我们假设,一个游戏有两个热区,分别是_AreaRect mfh1和_AreaRect mfh2。其实这部分,完全可以做一个地图编辑器来实现,这里只讲原理,地图编辑器的话,自己去实现一个也不难。
这里最重要的就是写入地图点阵,我们建立了一个很大的数组,这个数组包含了一个地图节点能描述信息的所有。这里只包含了一个简单的属性,能走还是不能走。uState == 0是此节点不能行走,uState == 1是此节点可以行走。这里只是一个范例,我假设节点(2,2)是不能走的。这里要说明一下,这里的节点,并不是和游戏界面上的一个像素绑定,而是一个正方形的矩形区域,这个区域的大小就是粒度,可以根据游戏的定义来修改。你可以想象游戏地图就是一个网格扑在上面,有些网格能走,有些网格不能走。我们可以通过调节网格的大小实现对游戏精确度的控制。
好了,地图文件我具备了,那么在服务器上,怎么加载这个文件呢?- #ifndef _MAP_H
- #define _MAP_H
- #include <stdio.h>
- #include <errno.h>
- #include <vector>
- #include "mapdefine.h"
- #include "area.h"
- #define MAX_MAPCELLSIZE 20 //地图每个块的矩形区域大小
- #define MAX_FINDPOS_RANGE 4 //设置搜索节点有效位置的范围
- #define MAX_ASTAR_COUNT 1000 //最大Astar寻路的半径
- using namespace std;
- //地图信息
- struct _MapInfo
- {
- int m_nMapWidth; //地图的宽
- int m_nMapHight; //地图的高
- int m_nMapCellSize; //地图的块数
- };
- class CMapData : public CMapBaseData
- {
- public:
- CMapData();
- ~CMapData();
- //从文件中加载地图
- bool LoadMap(const char* pFileName);
- //显示地图信息
- void ShowMapInfo();
- //释放资源
- void Close();
- //给定一个点,判断是否可以行走
- bool IsCanGo(_MapPoint mappos);
- //得到地图的长
- int GetMapWidth();
- //得到地图的高
- int GetMapHigh();
- //获得一条路径,设置起始点和终止点,返回一个路径数组
- bool AStarFind(const _MapPoint PosStart, _MapPoint PosEnd, _MapPointPath* pMapPointPath); //AStar寻路算法
- //判断一个点是否可走,如果不可走返回一个可走最近的点。
- bool GetValidPos(_MapPoint& ptTarget, int nRange = MAX_FINDPOS_RANGE);
- private:
- //初始化8方向
- bool InitDX();
- //计算指定格子的权值
- inline void CalculateCost(const int nTarget, const int nEnd, const int nIndex, _WorldCostInfo& objWorldCostInfo);
- //将Index还原为nRow和nCol
- inline _MapPosCoord GetMapCoord(int nIndex);
- //返回方向权值
- inline int CalculateDirect(_MapPosCoord& MapQueenCoord, _MapPosCoord& MapEndCoord);
- //计算当前最合适的权值
- inline _WorldCostInfo CalculateEightQueen(int nTarget, int nEnd);
- //传入一个点,计算所在的格子,并返回格子的nIndex
- inline int GetMapPos(const _MapPoint PosTarget);
- //给定一个坐标点,返回所在的格子
- int GetMapIndex(_MapPoint mappos);
- //给定一个格子,返回这个格子中心坐标点
- _MapPoint GetMapIndexPoint(int nIndex);
- private:
- _MapPos* m_MapPos; //地图的块数组
- _MapFileHead m_MapFileHead; //地图头信息
- _MapInfo m_MapInfo; //地图信息
- CAreaGroup m_AreaGroup; //地图中的热区事件数组
- int m_nMapCellSize; //一个地图格子的大小(MAX_MAPNAME)
- _DX* m_pDX; //格子把方向的权值数组,用于计算路径
- };
- #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。 |
|