韩懿留的博客
导航
C++博客
首页
新随笔
联系
聚合
管理
<
2024年4月
>
日
一
二
三
四
五
六
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
公告
注册个cppblog真费劲。
留言簿
(1)
给我留言
查看公开留言
查看私人留言
文章分类
BW使用
(rss)
Linux
(rss)
Mysql
(rss)
RPC调用
(rss)
服务器通讯(1)
(rss)
服务器杂类
(rss)
游戏模块逻辑(3)
(rss)
文章档案
2012年3月 (1)
2012年1月 (3)
阅读排行榜
评论排行榜
常用链接
我的随笔
我的评论
我参与的随笔
统计
随笔 - 0
文章 - 4
评论 - 2
引用 - 0
最新评论
1. re: 哎...MFC真头疼...
@過客
OK, I'll try it... thx for advise~
--韩懿留
2. re: 哎...MFC真头疼...
試著用QT, 應該會容易許多
--過客
[转]一个MMO游戏服务逻辑系统(一)
最近好久没有更新论坛帖子了,有点对不起大家,因为最近工作较忙,另外赶制和测试新版的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。
posted on 2012-01-10 15:43
韩懿留
阅读(613)
评论(0)
编辑
收藏
引用
所属分类:
游戏模块逻辑
只有注册用户
登录
后才能发表评论。
相关文章:
[转]一个MMO游戏服务逻辑系统(三)
[转]一个MMO游戏服务逻辑系统(二)
[转]一个MMO游戏服务逻辑系统(一)
网站导航:
博客园
IT新闻
BlogJava
知识库
博问
管理
Powered by:
C++博客
Copyright © 韩懿留