基于MMORPG的场景系统
发布时间:2010-07-30 09:35:59
发布时间:2010-07-30 09:35:59
目录:
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:所有类的基类,提供全局统一的ID和Name
CBaseRegion:地图的基本属性(名字、属性、宽、高、格子数据……)
CRegion:地图使用接口 (和游戏相关的东西放在这里)
CServerRegion:保存和服务器相关的属性(NPC初始化链表……)
CStorageRegion:地图显示相关的属性和操作(地面和建筑数据、显示接口……)
CClientRegion:处理和游戏客户端相关的东西
CEditRegion:提供给地图编辑器使用(显示网格、坐标、添加和删除元素……)
注:分层有助于代码的重用,我们在CBaseRegion和CStorageRegion中只保存是处理纯粹和地图相关的属性和操作,而在不同的游戏中只需要替换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个格子的状况。
比如:我们现在有TA和TB两种地表,那么当我们把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个点为中心,搜索周围的8个TILE,得到该点的过渡图片编号。
把该点设置为得到的过渡图片。
}
}
(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块产生连接关系,那么我们可以用4个BIT来表示周围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 Studio的Undo/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(),操作的数量很多,很容易漏掉或搞错,这点是最麻烦的,在做的时候一定要仔细。
行走与同步
人物阻挡设置:
游戏中当人物动起来的时候没有阻挡,但是当站立的时候,他脚下的格子要设置成为阻挡状态。下表列出了所有设置或取消阻挡的发生情况:
设置阻挡的情况 | 取消阻挡的情况 |
进入场景时 行走停下来时 瞬间移动到位时 被其他人物冲撞改变位置结束时 | 离开场景时 开始行走时 瞬间移动开始时 被其他人物冲撞改变位置开始时 |
鼠标点住一直行走:
在游戏中点一下鼠标,主角会走到指定的位置上,那么当主角移动到目的点后检测这时鼠标是否按住,如果按住就取鼠标的当前位置再发送一次行走指令,如此循环就实现了鼠标点住一直行走的功能。
大地图的场景管理
一张地图上的PLAYER和NPC等是存放在一个list中的,地图超大那么上面的PLAYER就可能超多(预计大于200),这样的话每个行走动作都要发送200条以上的消息,这对于服务器是一种很大的负担,而且这种负担是呈级数增长(10个玩家都走一步服务器将发送10*10=100条消息,而200个的话就是200*200=40000条消息!),可能任何服务器都无法负担。
通过思考和讨论,得到了以下几个方案:
方案一:
·服务器上每个场景用一个list来保存上面的player和NPC
·玩家行走、进入和离开等事件发给list中的所有player
·客户端的list保有该场景上的所有player和npc
优点:处理起来简单直接
缺点:发送的消息会随玩家数量的增加而暴增、客户端负担很重
方案二:
·服务器上每个场景用一个list来保存上面的player和NPC
·玩家行走、进入和离开等事件只发给该玩家一定范围内的Player
·客户端的list只保有本玩家附近的player和npc
·在服务器可以考虑使用hash表来优化查询速度
优点:减少了服务器发送消息的数量、减轻了客户端的负担
缺点:实现相对复杂,服务器负担大大加重(因为要不断判断玩家间的位置关系)
方案三:
·在服务器上把场景划分为小区域(大于屏幕大小)。每个区域对应一个list,场景中的所有对象按他们的位置加入到对应区域的list中,那么每次行走只需要把消息发送给最多4个相临区域的Player
·客户端的list只保有本玩家附近的player和npc
优点:大大减轻了服务器遍历list的负担、减少了发送消息的数量、减轻了客户端的负担
缺点:实现非常复杂、而且在服务器需要不断判断玩家是否跨越区域
方案四:
·服务器上场景的每个TILE保存一个Object指针用来绑定该格子上的player或NPC
·玩家行走、进入和离开等事件发给玩家周围一定范围内的player
·客户端保有该player周围一定范围内的player和npc
优点:处理起来极为直接、避免了耗时链表遍历(典型的以空间换时间)
缺点:地图每个TILE都要加入一个指针变量(管理不善容易出错)、每次发送场景广播要遍历所有TILE
方案五:
·服务器上每个场景用一个list来保存上面的player和NPC
·不使用事件通知,而使用状态位置通知的方式通过定时发送状态来更新客户端的player和npc状态
·客户端保有该player周围一定范围内的player和npc
优点:处理比较简单
缺点:实时性太低,对于要求同步比较精确的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