首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

3.2  服务端对象的更新频率

John M. Olsen,Microsoft

infix@xmission.com

考虑这篇文章是否有用时,可能有人会对自己说:“从服务端发送大量的数据有什么大不了的?现在不是都有宽带(broadband)了吗?”在任何MMP游戏的预算中,带宽都是一项重要支出,它可以花去订阅费用中的很大一部分。在本文写作时,带宽的批发价格通常在5美元/GB左右。只需要把数以万计(如果幸运的话,可能是数以十万计)并发玩家的数据需求累计起来,就可以知道为什么对发送数据量进行管理是那么重要了。

本文会从一些基本原理出发来指出那些在决策过程中需要用到的关键信息,随后介绍一个裁减和组织数据包的方法以对网络进行有效的运用。

3.2.1  视觉连贯性与精确度

所有的实时在线游戏都会在某种程度上和延迟问题作斗争,它们努力地寻求各种方法来对游戏状态进行精确而可信的描述。游戏开发人员的目标是要交付一个视觉上和感觉上都很好的游戏,即使以牺牲一小部分视觉精确度为代价。

如果玩家可以看到一个在视觉上连贯一致的游戏状态,并且其他玩家角色(Player Character,PC)和由计算机控制的非玩家角色(Non-Player Character,NPC)都以一个可信的方式移动,那么他们就不会察觉到存在的任何微小的延迟错误。

如果另一个玩家或是非玩家角色从一个位置跳跃到另一个位置,玩家会很快察觉到。这通常是由网络包丢失(网络以及硬件限制)或是数据发送频率过低(实现问题)造成的延迟引起的。此外,开发人员应该知道一致的延迟通常比变化的延迟更加容易补偿。

有些游戏会以自适应的方式根据延迟自行调整,它们会对位置进行平滑的调整以把对象带到预期的地方。根据需要修正的误差量的不同,这可能会引起一个难以察觉的调整,也可能会导致高速的移动或是怪异的滑行。

在对那些用于寻找合适数据更新频率的方法进行评价时,本文的指导方针是“如果玩家发现不了,那就不要紧”。

3.2.2  需要发送哪些数据

服务端需要在玩家和服务器之间发送多种类型的数据,每一种都需要根据它们是否需要及时传递来采取不同的处理方法。

1.环境变化

环境变化具有非常高的优先级。如果玩家眼中的游戏世界和服务端的游戏世界不一致,就会造成大量的碰撞和同步问题。譬如当玩家本地信息和服务端信息不同时,他会惊讶为什么他总会错过一个移动的平台。由于环境变化非常重要,游戏开发人员需要花费大量的精力来减少对这类数据进行的传输。

环境中的活动地形元素(active terrain element)只是数据流中一个非常小的部分。这些数据应该包含正被打开或是关闭的门以及像旋转的风车之类的循环移动,游戏的设计人员希望所有玩家和它们的碰撞都可以与它们的视觉表示一致。如果一个玩家从风车的翼板上笔直穿过而另一个玩家却撞上了两翼之间的空档,那看上去一定很傻。

2.玩家之间以及玩家和NPC之间的交互

由玩家或NPC触发的交互包括战斗、交易以及各种形式的聊天。在大多数情况下,这在数据流中占据的比重并不大。交易和聊天并不需要非常及时,服务端可以很方便地在玩家不会觉察到的情况下把它们延迟一小段时间。另一方面,战斗信息至少需要和任何环境变化一样及时。如果游戏支持通过服务器中转的语音传输,那就必须给语音数据设置一个很高的优先级,而且它还会占据带宽的很大一部分。

3.PC和NPC的移动

最重要的是,服务端必须以一个足够高的频率来发送PC和NPC的移动信息,这样才能使玩家在视觉上感觉是连续的,但是也不能过于频繁,否则我们的网络就会陷入困境。

3.2.3  带宽限制

目前市场上的游戏对带宽的用量都差不多,大约在每个玩家1KB/s左右。有两个原因使得这个数据传输率成为一个有用的基准。首先,它是在调制解调器允许的范围之内的(modem-friendly),因为即使是28.8KB的低速调制解调器也可以维持这个数据传输率。其次,这对于预算来说也是一个很好的数字。即使某些玩家在一个月里全天挂在游戏上游戏开发人员仍然可以从他们的月费中获得一些剩余资金来支付硬件、办公室以及雇员等开销。

1.更新频率

必须注意更新频率会影响总体数据的大小。在网络上发送的每个数据包都有一个不同大小的附加头部,像UDP(没有确认的数据包)的头部大小是28个字节,而TCP(具有确认的)数据包在满载时头部大小是72字节。

更新频率越高就意味着这个额外头部数据的发送频率也越高。理想情况下,服务端应该把需要发送给某个特定玩家的所有信息打包在一起作为一个网络数据包,一下子发送出去,即使它包含的是玩家所需要的各种不同类型的信息。如果游戏服务端在服务器和玩家之间同时使用TCP和UDP数据包,就需要把这些信息分成两部分独立发送,但是它应该可以把所有的TCP数据合并成一个数据包,把所有的UDP数据合并为另一个数据包。

如果对数据进行打包发送的频率过高,数据包头部会消耗不必要的额外带宽。这样一来,当游戏中的行为发生变化时,玩家更容易发现延迟。网络传输层可能会自动把小的数据包合并起来,但是服务端无法控制它们进行这种合并的时机和方式。

如果发送更新的频率过低,随着服务端降低向玩家更新位置的频率,PC和NPC在视觉上的跳跃和偏差也会加重。发送数据的频率过低还会增大数据包,这是因为网络会把它们打碎并在另一端重新组合起来。虽然这一切都是由网络层透明处理的,这些大的数据包也更容易受到丢包的影响,因为消息任何部分的丢失都意味着必须丢弃整个消息并且重新发送。

2.分区(Zone)的地形和连续的地形(Continuous Terrain)

从网络的角度来看,这两种地形模式具有相同的需求。无论使用哪种模式,游戏系统都需要以某个方式来向玩家更新游戏世界的状态,并且把重点放在那些在玩家附近发生的事情上。分区地形系统会自动为最远范围赋予一个上限,而在连续地形系统中,游戏设计人员则需要使用额外的代码来决定界限。

3.2.4  每个用户在服务端需要的数据

在为整理数据以及调整数据优先级定义一个良好的方案之前,必须先确认在服务端要为每个活动玩家保存哪些信息。虽然每个游戏在具体实现时可能会有很大的差别,但是基本上每个玩家在服务器上都应该有一个发送队列。

这个发送队列包含了那些需要从服务端发送给玩家的消息。服务端表示每个玩家的数据结构中还应该包含一个列表,其中包含了最近几次发送更新时玩家的位置。这个位置列表的长度应该和发送队列相同,稍后本文将对此进行详细讨论。

3.2.5  管理数据大小

当角色数量增加时,保存角色数据所需要的内存可能会成指数级增长,游戏管理人员必须避免这种情况的发生。在n个玩家之间可能的关系数量是n2,因此游戏管理人员需要尽可能地在可接受的范围内把n减到最小。

理想情况下,游戏开发人员应该建立某种机制来把游戏环境划分成区域,在这些区域中管理人员可以很方便地知道在某个特定玩家的视野中有哪些玩家。在一个基于门户(portal)的系统中,管理人员应该确认对于每个门户来说,哪些门户是重要的,如果另一个门户的距离它很近,那就需要知道那个区域中的PC和NPC信息。

如果不使用这种机制,游戏管理人员就需要进行周期性的范围判断并且把每个玩家能够看到的其他PC、NPC和环境对象保存下来,不过这种方案会占用大量的内存和CPU时间。

3.2.6  更新队列

每个玩家的发送队列都是由一系列时间槽(time slot)组成的,如表3-1所示,这个列表的头表示的是马上要被发送出去的数据。在对某个玩家进行更新时,服务端会把第一个槽中的所有数据更新拼接为尽可能少的网络包后发送给他。从服务端的角度来看,这意味着每当槽中包含了任何网络包,服务端就会马上对玩家进行更新。这与客户端的屏幕刷新率无关,事实上它比屏幕刷新率低多了。

本文使用这个数据队列来向玩家发送所有的更新,因此它会包含移动数据、聊天消息、商业信息以及所有其他从服务端到客户端的数据传输。

表3-1                                        发送数据前的数据队列

槽1

槽2

槽3

槽4

槽n

更新A

更新C

更新D

更新F

更新G

更新B

更新E

在发送槽1中的每个更新时,服务端会判断何时需要再次发送这个更新。如果这是一个循环更新(譬如说玩家位置),服务端会把它移动到其他槽中,这样它很快就会被再次发送。如果这是一个一次性更新(譬如说聊天消息),服务端在发送以后就会丢弃它。一旦完成对这个槽的处理,服务端就把这个空闲槽移动到列表尾部,并且把其他槽向前移动一个位置,如表3-2所示:

表3-2                                        发送数据后的数据队列

槽2

槽3

槽4

槽n

槽1

更新C

更新D

更新F

更新G

更新A

更新E

注意在表3-2中,更新A被移动到槽2中去了,它会被立即重新发送(这可能是对对手位置的更新),而更新B被删除了(这可能是一个聊天消息)。槽2现在是队列的头。有时服务端会故意推迟发送数据,因此游戏管理人员需要特别注意收集这些数据的时机和方式。服务端通过特定的更新消息来指定需要发送的数据(譬如说某个玩家的位置),但是直到发送时它才会去获取实际的信息(譬如说这个位置是X、Y、Z),这么做可以使它当前发送的信息总是最新的。

游戏管理人员应该对每个槽中所包含的更新数量作柔性的限制。如果有一个槽中装满了更新,就意味着已经接近预期的最大带宽限制,因此应该有选择地推迟几个更新。如果管理人员想要放置更新的那个槽已经满了,就应该向后查找第一个还有空间的槽。这种方法只适用于那些次序并不重要的更新(譬如说位置更新)。文本信息的次序可不能被颠倒。

有些数据在发送时不能有任何延迟,还有一些则需要确保正确的发送次序。这时,服务端要么在这个槽中放入更多的更新,要么把这个槽尾部的另一个更新移动到下一个槽的头部。如果下一个槽也满了,就会引起连锁反应。如果这种溢出情况经常发生,就意味着网络的槽太小了,或者是发送数据的频率太高了。游戏开发人员需要测量整个网络的吞吐量,从而确定问题出在哪里。

3.2.7  缺省的更新频率

应该怎样决定某个信息的发送频率呢?应该怎样决定对于特定的玩家来说,在每次决定某个NPC的位置更新时应该向后移动几个槽呢?这些问题不仅决定了玩家可以用多高的频率获得NPC的更新,同时也决定了对于玩家来说,这个NPC的行动有多流畅。

通常,服务端希望对近距离的物体进行频繁的更新,也就是在每次发送更新数据时都会包含这些物体的信息,这很可能是每秒一次甚至是两次。远处的物体可以更新得慢一点。当然,总是存在某些情况使这个一般规律需要被改变,本文会在稍后对此进行讨论。

3.2.8  计算范围

精确的范围计算需要大量的CPU时间。为得到两点间的距离通常需要对它们的正交分量间距离的平方和计算平方根,如等式3-1所示:

                                                                          (等式3-1)

如果不想花那么多的时间,就必须寻找一条捷径,回到这篇文章最早的主题之一:如果玩家不能发现,那就不要紧。实际上玩家永远都不会直接看到计算出的范围,因此可以使用曼哈顿距离(Manhattan Distance),如等式3-2所示。它之所以这样命名是因为用它可以计算出从一点走到另一点时所需经过的正方形街区数。在允许的精度范围内也可以使用其他方法进行估计。

                                                      (等式3-2)

一旦知道了近似距离,就可以根据NPC和玩家间的距离来把这个更新放到某个槽中。最简单的方式是使用一个线性函数。如果游戏管理人员可以知道最大的可视范围并且估计出某个特定物体的距离,就可以通过简单的计算知道应该把更新放入第几个槽中,如等式3-3所示:

                               ×                   (等式3-3)

由于使用了近似值,管理人员应该总是把槽的下标限制(clamp)在有效范围内,因为它有可能会超出列表的长度。

3.2.9  调整优先级

上文已经讲解了应该怎样根据距离来确定一般更新的频率了。还有不少方法可以对数据流做进一步的优化。首先,某些PC和NPC会在很长时间内保持静止。服务端可以一开始就把这些位置更新发送给玩家以使玩家得到起始位置,之后就只需偶尔更新一下以防数据包的丢失。

服务端还希望从某些PC获取更频繁的更新。对于那些在游戏中与服务端处于同一群组中的其他玩家来说,游戏管理人员应该使用一个较小的槽偏移或是特殊的槽范围限制(clamp)来提高他们向我们发送更新的频率。如果玩家把任意PC或者NPC选为目标,服务端也应该同样地增加其更新频率。因为这表示玩家出于某种原因想要了解更多关于这个PC或NPC的信息,因此我们有必要对它们进行更频繁的更新。

还有一种方法可以调整优先级,但是需要付出更多的努力。服务端可以使用玩家过去位置的列表来判断他们的位置是否可预测,并以此对玩家的优先级作出调整。如果某个玩家在一个位置停留超过几秒,服务端就应该开始调整优先级以使这个玩家的更新频率越来越低。同样地,如果玩家以直线奔跑,服务端可以根据已知数据精确地推断出他们的位置。如果玩家时而奔跑,时而停止,或是不规则地进行转向和移动,就需要对他们使用更高的更新频率。

对那些很少移动并且已经静止了很长时间的物体,可以使用最极端的调整方法。当这种物体第一次加入队列时,使用通常的方式进行发送就可以了。但是当服务端判断它今后应该如何更新时,可以为它加上一个“不要发送”标记。一旦设置了这个标记,以后就可以跳过这个更新的发送,除非在之后的判断中,它的状态有所改变。

为了减少判断时间,我们在做出判断后,应该把具有这个标记的物体放到最后的那个槽中,从而尽可能地降低对它们进行判断的频率。

在服务端更新玩家状态同时,应该周期性地调整PC和NPC的优先级。由于这个队列系统具有内建的延迟,通常服务端不必立即把这些改变发送出去。

3.2.10  调整队列

在大多数情况下,服务端可以让队列把数据打散,并且按照它们进入队列的时间发送更新。有时候,这可能会导致一些不连续性(在前面已经讨论过)的发生,譬如说当玩家在一个地方坐了很长时间后突然四处奔跑。

如果玩家的优先级调整突然发生很大的改变,服务端就应该在队列中放入一个新的更新。仅当这个更新可以在没有副作用的情况下进行多次发送时才可以这样做,因为原来的更新也在队列中等待处理。由于另一个更新也在队列中,所以这个新加的更新必须被标识为一次性的,以免在队列中一直出现对同一个PC或NPC的两个更新。

当PC和NPC进入玩家范围时,服务端也需要加入一个更新,并且在它们离开范围时删除这个更新。这不仅包括通常的移动,还包括进入和退出游戏。当它们第一次进入范围时,服务端需要在队列中加入一个更新,这个更新应该使用前面所讨论的标准的队列槽计算方法。

这里有一条捷径可以把那些超出范围之外的更新删除。因为在每次发送更新时服务端都必须组织数据,所以可以把这些超出范围的物体在将要发送时裁减掉。这样就不必为了在每个玩家的队列中搜索那些失效更新而浪费时间了。这时服务端应该发送一个代替消息,告诉客户端要更新的这个对象已经在视野之外了。

3.2.11  总结

带宽管理是一个非常关键的问题,它可以为游戏的开发节约很多开支,可以很方便地使用所支付或是所节省的钱来衡量管理数据流的工作质量。这里的难点在于怎样在带宽开销和愉快的玩家体验之间找到平衡,并且在有限的网络资源情况下尽可能地保证游戏环境的平滑。玩家能够获得的游戏体验有多流畅,一部分取决于服务端向他们发送数据的频率,这也决定了玩家对游戏设计的满意程度。

使用这里所描述的技术就可以提供给玩家他们期待的游戏体验,并且也不会浪费不必要的网络带宽,从而在这两者之间找到一个完美的平衡。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论