zhigu 发表于 2007-6-1 22:11:00

潘多拉的盒子---无缝服务器

<strong><br/><br/></strong>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;刚好看到gameres上面有人在说无限大地图,在服务器要实现无限大地图的逻辑处理,从地图管理、物理模拟、逻辑处理、网络处理到服务器架构都会和传统的分块式地图和服务器有很大差别。我认为这是潘多拉的盒子,打开了是五彩缤纷,琳琅满目,更深层处则是危机重重、举步维艰,决不会像前面各位网友想的那么简单。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;下面的文字大部分摘自《大型多人在线游戏开发》中服务器开发的《无缝服务器--优点和缺点》,并加入了一些自己的看法和想法,本人看法的不当和错误,还望各位指出,更希望有丰富的无缝服务器设计经验和相关资料的朋友能共享一下自己的思路和想法。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;一、游戏服务器的组织方式:分块和无缝<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;经典的MMO游戏服务器一般采用的是分块方式进行游戏世界管理。也就是把世界按世界的二维坐标或城池、地名分成不直接连通的区域(一般通过传送NPC、传送点、任务等方式进入不同世界分块),这些小的游戏区域基本上在逻辑、物理、地图上都是不相干的,从而对这些分块进行单独管理。注:按照地图大小和其负载能力不同,这些分块可能位于多个地图--逻辑服务器簇中,也可以位于单个地图--逻辑服务器进程中。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;分块方式的服务器相对来说逻辑单一、物理也不相关、同步和互斥问题也不严重,在设计和逻辑处理上来说就比较简单&nbsp;。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;无缝服务器的地方式为游戏提供一个超级大陆,除了少数地方需要传送和任务触发做为特殊处理方式外(比如岛屿、任务副本、任务洞穴等等),绝大部分地图都是直接连通的,即玩家在地图中任意一点A到地图任意另一点B之间都可以通过有限的路径线段到达,并且这些线段都被玩家所在的地图包含,玩家在从A点到B的过程中,玩家始终在一个地图中,不存在逻辑切换和地图切换。无缝服务器相对来说,其逻辑难度就大、互斥和同步要求就高了很多,设计和调试的难度也绝对是按几何级数上升。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;二、简单考虑一下无缝世界的实现方案<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;通常的做法是先把地图分块小块,然后在相邻的块之间加入一些服务器通讯以使每个服务器做出自主的反应。要实现这一点,可以让服务器互相通知那些与边界接近的对象,这样以来,每个服务器都可以通过他们的本地代理来对远程对象进行控制。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1.如何分割游戏世界?<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;第一个问题就是游戏世界的划分问题,这个问题本身没有最优解,因游戏的类型、大小不同而不同。通常的MMORPG和MMOARPG往往是由玩家控制人物在基于地形上进行漫游和执行逻辑任务。于是一般的划分方案都是基于地形坐标的。在2D游戏里我们往往用基于TILE或九宫格的方式进行地图划分,而在3D里则常用的四叉树、八叉树、场景图等方式进行场景管理。这些方案的实现无论在客户端或是服务器端看起来都不复杂,但在后面的讨论中我们将了解到其中所面临的难题,这是无缝世界的第一个要点。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;2.区块应该分多大?区块间的共享区域应该有多大?<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;由于我们的世界是分块管理的,而一旦某对象接近某区块的临界区时我们将不得不通知此临界区附近其它区块服务器。这儿我们将要决定到底把分块间的共享区域设为多大,区块本身应该有多大?<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;这是一个要慎重考虑的问题,因为区块的大小和区块间共享区域的大小直接决定了服务器间的数据交互和玩家的可玩性感受。最小的情况下,共享区域大小应该与客户端的游戏对象和玩家的可感知范围中最大的一个一样大。因为如果再小一点的话,怪物或NPC在服务器边界附近时,他视野中的对象可能和另一边的玩家完全不相同,甚至这个玩家一直受到某个亡灵法师的攻击,但却看不到那个攻击他的亡灵法师。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3.详细代理还是精确代理?<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;上面我们提到在共享区域的时候,这些边界对象可能会有多个远程代理。因为他们要进行跨越服务器边界和其它世界对象进行交互。这就必然要求当一个玩家进入共享区域时,之前他所在的服务器确保通知其它相邻区块的服务器,而每个相邻的服务器都会创建一个此对象的远程代理来表示这一对象。同样地,当这个对象从边界区到移动到另一个区块的服务器的时候,必须销毁原来区域服务器里的该对象及其它服务器里该对象的远程代理。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;另一个问题是:代理对象应该为区块服务器提供多少该对象的信息。如果信息太少,就很难在异步条件下正确地执行相关游戏逻辑,而如果信息太多,就需要对大量的数据保持同步,在大量对象处于共享区域的时候,这必然会导致严重的服务器集的性能问题。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;一个实用的代理至少要包括位置、方向、碰撞信息、对象类型等基本信息。它还应该包括其它依赖于游戏且不会发生改变的属性等。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;为了减少服务器之间的通讯,也可以为对象的属性赋上代理的优先级。也就是说:高优先级的属性要尽快更新,但低优先级的属性可以在空闲的时候或延迟进行更新。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;4.区块的边界定义:刚性的还是柔性?<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;一个刚性的边界是当一个对象越过服务器边界的时候,它的所有权立即转移给另一个服务器,没有任何延迟。而一个柔性的服务器边界则是在转移对象所有权前允许“小许地夸越服务器边界”。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;事实上转移一个对象的代价可能是很大的,对于刚性边界来说,这可能带来严重的性能问题。比如当玩家和怪物在边界上来回追赶做战的时候,服务器性能会受很大的影响。柔性边界可以在一定程度上减少这类问题的发生,但仍然不能完全避免。而且柔性边界会使程序设计和编码变得更加复杂和困难。比如有些代码需要对远程代理的对象使用不同的方式进行处理,那就不能简单地用“是否在本服务器范围内”来判断本地对象和代理对象了。事实上这时对象刚进入A区块,但其对象还保留在B区块里,这时A和B的是否本地对象的判断都将有误,并可能采用错误的事件处理方式。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;5.静态边界还是动态边界?<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;即是否在游戏运行的过程中动态地改变服务器的边界。静态的世界边界相对来说简单一些,但不利于服务器性能的充分利用。动态地改变区块边界可能是基于服务器的动态负载均衡要求的。要正确的设计和实现服务器边界的动态调整是非常复杂的。比如说:服务器必须使用一个类似于数据库的事务这样的原子操作把一组对象集体地转移到另一个服务器上去,并且不能产生任何玩家可感觉出来的延迟。而如果边界和两个以上的服务器相关,则这必须涉及到3个以上的服务器边界调整。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;事实上就算以上调整能较好实现,但对于判断在何时、何地、如何调整服务器边界也是非常困难的。且过于频繁的调整也会带来严重的性能问题和资源消耗,过少则又可能使某些服务器的负担过重。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;6.如何处理某区块服务器的异常?<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;如果某个区块的服务器突然发生异常关闭或重起,相邻或相关的服务器如何应对这种情况?比如当某对象P正在从A到B的转移的时候,B发生了异常关闭,将如何处理P。这可能会引起物品、金钱的复制问题。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;三、无缝服务器的优势<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1.玩家拥有更大的游戏空间<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;2.扩展性和可靠性更高<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;扩展性表现在游戏世界的无限性上面。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;而对于架构良好的无缝服务器来说,某一个或几个服务器崩溃时可以自动把上面的玩家迁移到相邻的服务器上,这大大降低了玩家掉线的概率,从而提高了游戏的稳定性。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3.不需要等待加载地图<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;对玩家来说不用等待加载地图的无聊时间,这是一个显而易见的优点。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;四、无缝服务器的缺点:<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1.代理和真实对象之间的不同步,会致使许多的问题变得不确定。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;2.程序的设计将会变得非常困难。因为一个夸越边界的对象会出现海量的竞争和失败问题,要设计和诊断这些问题是非常的困难和棘手的。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;事实上使用了无缝服务器的架构,将使游戏里几乎所有的逻辑系统设计难度都大增加。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3.对游戏设计师的影响<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;分块的制作和边界的衔接都将是困难的,不同人员的合作将使这工作的难度变得更大。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;4.美术的制作也变得更为复杂<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;如果一个物体夸越了两服务器以上的世界边界,这将在逻辑、显示上都造成莫名其妙的错误。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;5.在无缝世界中一个玩家送给另一个玩家一件装备的示例<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a.在一个基于分块方式的经典MMO中,实现一个玩家送给另一个玩家的逻辑可能如下所示:<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;def&nbsp;Give(srcPlayer&nbsp;,tarPlayer,itemId):<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;not&nbsp;srcPlayer.HasItem(itemId):<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;WRN("赠与失败,src=%s&nbsp;没有&nbsp;itemId=%s&nbsp;号物品可以赠与",srcPlayer,itemId)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;srcPlayer.SendMessage(GIVE_FAILED,itemId)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;elif&nbsp;not&nbsp;srcPlayer.CanRemoveItem(itemId):<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;srcPlayer.SendMessage(GIVE_FAILED,itemId)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;elif&nbsp;not&nbsp;tarPlayer.CanAddItem(itemId):<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;srcPlayer.SendMessage(GIVE_FAILED,itemId)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else:<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;srcPlayer.RemoveItem(itemId)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tarPlayer.AddItem(itemId)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;srcPlayer.SaveToDatabase()<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tarPlayer.SaveToDatabase()<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;srcPlayer.SendMessage(GIVE_SUCESS,itemId)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tarPlayer.SendMessage(RECIVE_SUCESS,itemId)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;这是一个无竞争、要处理的失败情况也很少的才10几行的逻辑代码。但如果到了无缝服务器中,我们将看到这有多复杂。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;假定P1打算把一个物品item送给P2,下面给出大致的事件序列:<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1.服务器收到一个从P1客户端发过来的GIVE物品请求:<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a)进行HasItem和CanRemoveItem检查,如果检查失败,如前所示代码一样返回。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b)否则,对物品进行加锁,防止P1对其进行其它任何操作。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;i&gt;这个加锁是非持久的,不会保存到数据库中,也不会在P转移到其它服务器时继续保留锁定状态。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;c)发送一个GiveRequest消息给P2<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;d)如果GiveRequest发送失败,解锁物品,发送GIVE_FAILED消息给P1,终止请求&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;2.P2收到P1发送的GiveRequest消息<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a)进行CanAddItem检查<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;i&gt;由于此物品不一定是本地对象,所以GiveRequest应该包含足够的信息以符合检查的需要。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b)把CanAddItem的结果放在一个GiveCanAddItem消息中发送给P1<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;i&gt;这儿不做GiveCanAddItem消息是否失败的处理,否则更复杂<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3.P1收到一个P2的GiveCanAddItem消息<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a)如果返回FALSE,则解锁物品,发送GIVE_FAILED消息给P1,终止请求<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b)否则从P1背包中删除item,此时不更新数据库和P1的客户端<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;c)向P2发送一个GiveAddItem消息<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;d)如果GiveAddItem发送失败,则把物品放回P1背包,解锁物品,然后发送给P1失败的消息,结束请求<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;4.P2收到P1发送的GiveAddItem消息<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a)P2检查CanAddItem是否成功,因为这期间可能P2已经改变了状态,如果检查失败,向P1发送GiveFailed消息,然后终止请求<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b)否则,把这个物品暂时加入P2的背包,不要更新P2的客户端和数据库<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;c)向P1发送GiveAddedItem消息<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;d)如果这个GiveAddedItem失败,则把物品从P2背包删除,然后停止<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;5.P1收到P2的GiveFailed(4.a发出的)消息,终止请求<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;6.P1收到P2的GiveAddedItem消息<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a)从P1背包中删除此item,然后更新数据库和客户端<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b)向P2发送GiveAddedItemAck消息<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;c)如果GiveAddedItemAck发送失败,则必须要恢复P1背包里的item,然后更新数据库和客户端<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;7.P2收到P1的GiveAddedItemAck消息<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a)转让成功,更新P2的背包,更新数据库和客户端<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;8.P2收到P1的GiveCancel消息(任何时刻)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a.结束请求。
页: [1]
查看完整版本: 潘多拉的盒子---无缝服务器