基于MMORPG的场景系统

发布时间:2010-07-30 09:35:59

地图

基于MMORPG的场景系统

目录:

1、 规格和结构

2、 地图编辑器框架

3、 建筑和人物的排序

4、 动态预读

5、 地表自动拼接

6、 墙壁自动拼接

7、 揭锅盖(建筑揭房顶)

8、 反悔/重做(Undo/Redo

9、 小地图

10、 寻路

11、 行走与同步

12、 大地图的场景管理

13、 分布式场景设计

14、 即时战斗系统

地图编辑器截图:



1、规格和结构:

1、 地图基本格子大小40*20(阻挡、切换点等……)

2、 地面基本图块大小80*20(可在一张大图中选取)

3、 建筑层使用整图

4、 地图显示使用动态预读

5、 地图上的房屋采用揭屋顶的方式处理。

6、 地表文件存放在Element\xxx\下面、建筑文件存放在Build\xxx\下面

类的层次结构:

CBaseObject:所有类的基类,提供全局统一的IDName

CBaseRegion:地图的基本属性(名字、属性、宽、高、格子数据……)

CRegion:地图使用接口 (和游戏相关的东西放在这里)

CServerRegion:保存和服务器相关的属性(NPC初始化链表……)

CStorageRegion:地图显示相关的属性和操作(地面和建筑数据、显示接口……)

CClientRegion:处理和游戏客户端相关的东西

CEditRegion:提供给地图编辑器使用(显示网格、坐标、添加和删除元素……)

注:分层有助于代码的重用,我们在CBaseRegionCStorageRegion中只保存是处理纯粹和地图相关的属性和操作,而在不同的游戏中只需要替换CRegion(公用) CServerRegion(服务器) CClientRegion(壳户端)3个接口就达到了重用地图的目的。



3、建筑和人物的排序:

每个建筑有三个参考点,用于计算建筑与人物的排序与遮挡关系。

在游戏中,建筑最基本的样式如下,方块为三个参考点。红色为主参考点

只要人物处于红线与兰线的夹角之内就是建筑遮挡人物,如果人物处于两线的夹角下方就是人物遮挡建筑。

但是由于有两条线,所以在计算时要将建筑以主参考点为界,左右分别进行计算。

计算的基本原理是:

人的斜率 = 人物所在坐标到原点的连线的斜率

参考斜率 = 一个参考点到原点的连线的斜率

if ( 人的斜率 < 参考斜率 )

{

人物遮挡建筑

}

else

{

建筑遮挡人物

}

建筑之间的排序方法:

因为建筑的大小是不固定的,而且每个建筑有3个参考点,所以不能简单的和人物一样进行排序,我们采用的方法是为每个建筑设定了一个排序参考点,这个点的缺省位置等同于建筑的主参考点位置。

建筑之间的先后顺序直接按建筑的排序参考点的Y值决定,Y值越大越后显示。



4、动态预读:

如下图所示:

我们在客户端以屏幕为中心分3个区域:

1、 屏幕:玩家在屏幕上可以看到的区域。

2、 可视区域:(比屏幕大一圈)进入该区域中的地表和建筑元素就会被载入内存。

3、 缓冲区域:(比可视区域大一圈)离开这个区域的东西就会被释放掉。

在实际的应用中,屏幕大小是800*600,可视区域是3*3个屏幕大小,缓冲区域是5*5个屏幕大小。这样无论多大的地图,我们最多只需要同时载入5*5=25个屏幕大小的图片资源。内存节约相当明显。



5、地表自动拼接

(1)地表文件命名规则:

单一地表单元:DB???.bmp

过渡地表单元:DB???-DB???.bmp

说明:???代表数字,在过渡元素这里,前面的“DB???”是过渡中里面的元素名

在制作单一地表单元的时候,把4种单元元素参照竖排的方式进行排列,按照出现的几率,把地图中显现频率较高的单元放在最上面, 下面的几率依次变小。

单一地表单元,统一放置在/element/base/ 目录下面

过渡地表单元,统一放置在/element/transition/ 目录下面

DB016-DB020.BMP(TA) DB020-DB016.BMP(TB)

(2)原理:

地图中的一块地表和周围的8块产生过渡关系,现在我们只考虑两种地表之间的关系,那么我们可以用一个8位的BTYE来表示周围的8个格子的状况。

比如:我们现在有TATB两种地表,那么当我们把TA铺到地图上的时候,就以该点为中心计算周围8个格子,如果该格是TA那么标记为1,否则就是0

根据排列组合,我们一共可以得到2^8种组合,也就是256种。

把这256种情况用一张表列出来,如下:

// 自动拼接的256种判定方式

CEditRegion::stTileTable CEditRegion::s_stTileTable[256] =

{

// 00000000

TB,1,1, TA,2,2, TA,2,1, TA,2,1, TA,2,0, TA,2,1, TA,2,1, TA,2,1,

TA,1,0, TB,0,2, TB,0,2, TB,0,2, TA,1,0, TB,0,2, TB,0,2, TB,0,2,

// 00010000

TA,0,0, TA,1,1, TA,1,1, TB,0,2, TA,1,0, TB,0,2, TB,0,2, TB,0,2,

TA,1,0, TB,0,2, TB,0,2, TB,0,2, TA,1,0, TB,0,2, TB,0,2, TB,0,2,

// 00100000

TA,0,1, TA,1,1, TA,1,1, TA,1,1, TA,1,1, TA,1,1, TA,1,1, TA,1,1,

TB,2,2, TA,1,1, TA,1,1, TA,1,1, TB,2,2, TA,1,1, TA,1,1, TA,1,1,

..

……

}

表中的每一项由三个单位组成:TA/TB应该使用那张材质图片。后面两个数值表示在材质图片中的位置编号,一个材质由9个基本块组成,编号为(0,0)-(2,2)

自动拼接的过程:

{

在地图中的(x,y)点下左健;

把该点铺成我们选定的材质TA;

for(int I=0; I<8; I++)

{

(x,y)周围第I个点为中心,搜索周围的8TILE,得到该点的过渡图片编号。

把该点设置为得到的过渡图片。

}

}

(3)待解决的问题

所有的计算都是围绕着基本块展开的(位置为1,1的图块),如果是两个过渡图块之间就不能实现自动拼接了。



6、墙壁自动拼接

(1)墙壁文件命名规则:

我们游戏中的墙壁为4方向,所以一共有16种基本的墙体元素如下表:

迷宫墙壁的文件放在build\下面的任意一个目录中,如build\wall\,文件名从00.bmp – 15.bmp

注意在地图编辑器中只有放置00.bmp时才会进行墙壁的自动拼接。

00

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

(2)原理:

一块墙壁和周围的4块产生连接关系,那么我们可以用4BIT来表示周围4个格子的状况。

比如:我们在一个格子中放入一个建筑,当我们把建筑放到地图上的时候,就以该点为中心计算周围的4个格子,如果该格是建筑那么标记为1,否则就是0

根据排列组合,我们一共可以得到2^4种组合,也就是16种。

16种情况就是上面的表所列出来的。

例如:

1   1

x

0   1

0代表空地,1代表有墙壁,x是我们需要计算的格子。

我们以左上角为起点逆时针旋转就可以得到一个值:1011 (2进制) = 11 (10进制)

然后我们查上面的那张表,11.bmp正好就是我们需要的那个图案。

这是自动拼接之前的:

自动拼接之后的:

7、

揭锅盖原理(揭房顶)

主角在屋外,锅盖盖上 主角进到屋里,锅盖揭开

人物在进入房屋后,房顶会自动揭开,我们称之为揭锅盖操作。

这种会被揭开的建筑我们称为“锅盖”。

原理是:

在锅盖覆盖的地图格子中存放该锅盖的指针(锅盖也是一个建筑,所以有一个指向建筑的指针指向它),那么一旦在客户端检测到主角行走到了锅盖覆盖的区域上后,就不显示该建筑。当主角走出来之后锅盖恢复原状。

在实际游戏中我们还使用了一个m_lPanAlpha值,表示当锅盖刚刚揭开或刚刚盖上时候的建筑图片的透明度,实际看起来就是该建筑逐渐消失和逐渐出现。



8、反悔/重做(Undo/Redo

用过VC++的人都知道,Visual StudioUndo/Redo功能非常强大,它可以保存文件打开后你对该文件的所有操作,然后可以一步步的返回到最初的状态。

如果我们的地图编辑器也提供这个功能,不仅仅为以后的地图编制人员提供了极大的方便,而且给人的感觉也会非常酷、非常专业不是。

但这是怎么做的呢?

通常有两种做法,

一、 保存每一步的操作,在Undo的时候做对应的反操作。

二、 在操作之前保存整个文件,Undo的时候把刚才保存的整个文件复制回来。

第一种方法只用保存操作,所以占用的空间非常小,几乎可以进行不限步数的‘反悔/重做’,但是我们必须为每一种操作都提供一个反操作的功能,在一些比较复杂的系统中,操作非常烦多,而且每添加一种新操作的同时也必须提供相应的反操作,代价是非常大的。

第二种方法在实现上非常的简单,因为不用关心使用者做了什么操作,只要一发生改动,那么就把整个文件都记录下来,不过有利就有弊,这种方法对于一些比较小的文件还很适合,但是文件一旦很大,那么每次复制整个文件的代价就太高了,而且对内存的需求也非常高。该方法比较适合对小文件进行有限步数的Undo/Redo

考虑到我们的编辑器操作比较丰富,而且要根据功能进行不断的扩充和改进,所以我采用的是第二种方法,为编辑者提供10步之内的Undo/Redo功能。

主要的定义和方法如下:

private:

enum { MAX_UNDOSTEP = 10 }; // 最大的反悔步数

vector<CEditRegion*> m_vectorUndoRegion; // 保存undo列表,CEditRegion是我们的地图数据

long g_lCurUndoStep; // 当前Undo的步子

public:

void BackupUndo(); // 备份当前地图,把当前地图压入undo列表,在每次改变地图之前调用

void Undo(); // 反悔一步 Ctrl-Z

void Redo(); // 重做一步 Ctrl-Y

看起来Undo/Redo方法虽然简单,但是我们必须跟踪每一种操作,在它改变文件之前调用BackupUndo(),操作的数量很多,很容易漏掉或搞错,这点是最麻烦的,在做的时候一定要仔细。



行走与同步

人物阻挡设置:

游戏中当人物动起来的时候没有阻挡,但是当站立的时候,他脚下的格子要设置成为阻挡状态。下表列出了所有设置或取消阻挡的发生情况:

设置阻挡的情况

取消阻挡的情况

进入场景时

行走停下来时

瞬间移动到位时

被其他人物冲撞改变位置结束时

离开场景时

开始行走时

瞬间移动开始时

被其他人物冲撞改变位置开始时

鼠标点住一直行走:

在游戏中点一下鼠标,主角会走到指定的位置上,那么当主角移动到目的点后检测这时鼠标是否按住,如果按住就取鼠标的当前位置再发送一次行走指令,如此循环就实现了鼠标点住一直行走的功能。

大地图的场景管理

  一张地图上的PLAYERNPC等是存放在一个list中的,地图超大那么上面的PLAYER就可能超多(预计大于200),这样的话每个行走动作都要发送200条以上的消息,这对于服务器是一种很大的负担,而且这种负担是呈级数增长(10个玩家都走一步服务器将发送10*10=100条消息,而200个的话就是200*200=40000条消息!),可能任何服务器都无法负担。

通过思考和讨论,得到了以下几个方案:

方案一:

 ·服务器上每个场景用一个list来保存上面的playerNPC

 ·玩家行走、进入和离开等事件发给list中的所有player

 ·客户端的list保有该场景上的所有playernpc

 优点:处理起来简单直接

 缺点:发送的消息会随玩家数量的增加而暴增、客户端负担很重

方案二:

 ·服务器上每个场景用一个list来保存上面的playerNPC

 ·玩家行走、进入和离开等事件只发给该玩家一定范围内的Player

 ·客户端的list只保有本玩家附近的playernpc

 ·在服务器可以考虑使用hash表来优化查询速度

 优点:减少了服务器发送消息的数量、减轻了客户端的负担

 缺点:实现相对复杂,服务器负担大大加重(因为要不断判断玩家间的位置关系)

方案三:

 ·在服务器上把场景划分为小区域(大于屏幕大小)。每个区域对应一个list,场景中的所有对象按他们的位置加入到对应区域的list中,那么每次行走只需要把消息发送给最多4个相临区域的Player

 ·客户端的list只保有本玩家附近的playernpc

 优点:大大减轻了服务器遍历list的负担、减少了发送消息的数量、减轻了客户端的负担

 缺点:实现非常复杂、而且在服务器需要不断判断玩家是否跨越区域

方案四:

 ·服务器上场景的每个TILE保存一个Object指针用来绑定该格子上的playerNPC

 ·玩家行走、进入和离开等事件发给玩家周围一定范围内的player

 ·客户端保有该player周围一定范围内的playernpc

 优点:处理起来极为直接、避免了耗时链表遍历(典型的以空间换时间)

 缺点:地图每个TILE都要加入一个指针变量(管理不善容易出错)、每次发送场景广播要遍历所有TILE

方案五:

 ·服务器上每个场景用一个list来保存上面的playerNPC

 ·不使用事件通知,而使用状态位置通知的方式通过定时发送状态来更新客户端的playernpc状态

 ·客户端保有该player周围一定范围内的playernpc

 优点:处理比较简单

 缺点:实时性太低,对于要求同步比较精确的ARPG不太适合

我们目前使用的方法场景管理方式:

服务器上的每个Region按屏幕大小分割为若干AREA,每个AREA保存处于该区域上的SHAPE链表,客户端只有一个ShapeList,保存主角周围9个屏幕内的Shape

server

一个SHAPE在服务器上从一个AREA转到另一个AREA的时候,发这个SHAPE的进入消息给新的几个附近AREA,并把这几个AREA中的SHAPE发送给该SHAPE

client:

当一个SHAPE离开主角的距离超过主角的视野范围(9屏)后,在客户端上把这个SHAPE删除。

如果client收到一个服务器发来的消息,但是在client却没有该shape,那么发送一个请求给server,要求取得该shape的数据。

1、什么时候发送局部场景消息:(只发给周围9个屏幕内的玩家)

(1) 进入场景

(2) 离开场景

(3) 行走(特殊处理)

(4) 场景聊天

(5) 攻击、释法

(6) 瞬间移动(特殊处理)

(7) 丢物品

(8) 其他操作

2、什么时候发送全部场景消息:

(1) 全场景喊话

// CMessage中的几个不同的发送消息函数

long SendTo(CMySocket* =NULL); // 发送消息

long SendToRegion(CRegion* =NULL, CPlayer* =NULL); // 发送给一个场景的所有玩家

long SendToArea(CArea* =NULL, CPlayer* =NULL); // 发送给一个AREA的所有玩家

long SendToAround(CPlayer* pMain, CPlayer* pException=NULL); // 发送给一个玩家周围的玩家

long SendAll(CMySocket* =NULL); // 发送给所有客户端



即时战斗系统



汪疆 soft

2003.9

www.gpgame.net

基于MMORPG的场景系统

相关推荐