[译]帝国时代中的网络编程

原文地址:

1500 Archers on a 28.8 Network Programming in Age of Empires and Beyond

概述

本文解释了在制作帝国时代1&2多人(网络)游戏中使用的设计架构,实现及经验,另外讨论了Ensemble Studios在他的游戏引擎中使用的当前及未来的网络方案。

帝国时代多人游戏:设计目标

在1996年早期帝国时代网络代码开始编写的那个时候,为了实现设想中的游戏体验,我们不得不面对了很多特定的任务。

  • 大量单位参与的历史史诗级战役
  • 支持8个玩家多人游戏
  • 确保平滑的模拟,无论是基于局域网,modern还是互联网
  • 支持目标平台:16MB Pentium 90 with a 28.8 modem
  • 通讯系统需要与现有的游戏引擎协同工作
  • 在最低的机器配置中仍能保持统一的15fps

Genie引擎运行良好,并且逐渐给予了单人玩家一个引人注目的游戏体验,Genie引擎是一个2d单线程游戏引擎,精灵图片绘制成256色,世界基于网格建立而成,随机生成的地图填满了成千上万的物体,从被砍伐的树木到跳跃的羚羊,对该引擎的主要运行任务粗略分解后(优化后)是这样的:30%的图形渲染,30%的ai及寻路,30%的运行逻辑。

在早期阶段,引擎已经很稳定,多人通讯需要工作于已经存在的代码,而非对已经存在的架构重新进行编码。

让事情更加复杂的是完成每个模拟步的时间有很大的不同:如果用户查看单位,滚动或者位于未探索的地形中,渲染时间会改变,另外Ai层面的大型的路径或者战略规划,都会使得游戏游戏回合波动相当的大(200ms左右)。

简单的一些快速计算就可知道,仅仅是传输一小撮单位的数据并保持实时更新,就会严重的限制与玩家交互的单位及物体的数量,即使仅传输x,y坐标,状态,动作,朝向及伤害,顶多也只能保证游戏中的移动单位到250个。

而我们想要做的是使用弩车,弓箭手,战士摧毁一个希腊城市,而同时它也被海上的战船围困,很明显,我们需要其他的方案。

同时模拟

相比于在游戏中传输所有单位的状态,我们更期望的是在每台机器运行同样的结果,在同一个时间传送每一个完全相同的指令集到每一台机器,pc们将会基本同步他们的游戏世界,允许玩家发布命令,然后用同样的方式,在同样的时间运行同样的命令,然后获得完全一致的游戏结果。

这种棘手的同步方案难以刚开始的时候就运行良好,但是确实相比其他方面有突出的优势。

改善基本模型

在最简单的概念层面,实现一个同时模拟似乎是相当简单,对于一些游戏来说,使用锁帧模拟及固定游戏时间甚至是直接可行的。

虽然这个方法能同时解决移动成千上万物体的问题,这个方案依旧需要面对互联网20到1000毫秒的延迟,及每帧处理的时间中解决这些变化。

发送玩家的指令,获取所有的消息,然后在进入下次回合前处理他们,这些导致的启停或者缓慢的指令周期,将会成为可玩性的梦魇,我们需要一套在后台等待通讯的同时继续处理游戏逻辑的方案。

Mark使用了一个标记命令系统,每条指令都将在两个通讯回合后执行(通讯回合在帝国时代中从实际的渲染帧中被分离出来)。

1

所以在回合1000运行的命令将会预定在1002回合中被运行(见图1),回合1001运行的是回合0999的命令,这样就能允许消息在被接收,确认及准备运行的阶段,游戏仍然在动以及运行游戏模拟。

回合通常设定为200ms,将这个回合中的所有命令发送出去,200ms后,回合结束,另外下一个回合开始,在游戏的任何时间,仅仅会执行一个回合的命令,接收及存储下一回合的命令,以及发送未来两个回合后的命令。

速度控制

由于模拟必须总是拥有相同的输入,游戏实际上只能与最慢的机器运行的一样快,速度控制就是我们可以动态改变回合的长度,为了在通讯延迟及处理速度不一致的情况下,保证动画及玩法更为平滑。

  • 如果一台机器掉帧,而其他的机器将会处理他们的命令,渲染分配时间的所有东西,然后继续等待下一个回合,哪怕是微小的停顿都会立即注意到;
  • 通讯延迟,由于网络延迟及丢包也会让玩家需要等待足够的数据包来完成本次回合。

每台客户端计算一个平均帧率,他被统一的计算为若干帧的平均处理时间,由于随着游戏的进程,视角,单位数量,地图大小及其他因素都会被改变,所以这个数据将会在本次回合结束的消息包中被捎带上。

每台客户端也会定期的测量它与其他客户端一个往返的响应时间,他将会发送与其他客户端的最长的平均响应时间到回合结束的消息包中(总共两个字节被用在速度控制中)。

绘图1

每个回合,被指定的主机将会分析回合结束的消息包,计算出一个目标帧率及网络延迟的调节因素,主机将会发送一个新的帧率及通讯回合长度给所有客户端使用,图3到图5显示了通讯回合在不同条件下的分解情况。

QQ截图20150827193001

通讯回合粗略的预估为一个消息往返的响应时间(RTT),这个响应时间分分解了若干的模拟帧,执行这些模拟帧的时间需要在最慢的机器中也能完成。

通讯回合的时间跨度会被加权,这样才能保证在网络延迟时动态变化,慢慢趋向于一个可以保证游戏持续的最佳稳定速度。游戏只会在最糟糕的峰值出现停顿或者减缓)——命令延迟将会上升,但是保持平滑(每回合进调整少量的毫秒数)以逐渐调整到最好的游戏速度。这个方案给予了适应环境变化的同时提供最平滑的游戏体验。

保证送达

网络层使用了UDP,因而命令顺序,丢包检测及重传在每个客户端自行处理,每个消息使用了一系列字节用来标示即将被运行的回合数及每个消息的序列号,如果一个需要被之前回合数被执行,他会被丢弃,否则将会存储给将来的回合运行,由于UDP的性质,Mark假定消息接收规则为“当消息被怀疑的时候,就丢弃它“,如果一个消息不是按顺序被接收,接收方会立即发送一个重传请求给被丢弃的消息,如果一个确认包比预期的晚送达,发送方会预期到这个消息可能已经丢失,重新发送这个消息包,而不需要接收方请求。

潜在的好处

由于游戏的结果取决于所有玩家运行同样的结果,所以修改及欺骗客户端变为极其困难,任何不同的模拟结果都会被标记为“不同步“,然后游戏停止,当然,欺骗本地客户端用来显示更多的信息还是可能的,但是这些轻微的问题相对来说很容易处理,可以由后续的补丁及修订版本修复,安全上无形中已经获得了巨大的胜利。

潜在的问题

起初看起来,让两块相同的代码运行相同的结果应该是相当容易及直接的,然而事实并非如此,微软的产品经理Tim Znamenacek在很早的时候就告诉Mark:“在每个项目中,总有一个顽固的错误影响了所有的地方——我想“不同步”就是这个顽固的问题。“, 他是对的,找出不同步错误的困难点在于每个微小的差异都会随着时间不停的被放大,一只小鹿在生成地图的时候轻微的不对齐,都会导致搜寻饲料的时候有轻微的不同,然后几分钟过去后,村民的路径也会有轻微的偏差导致他的长矛可能会刺不中,然后回家的时候没有食物,所以当由于不同的食物数量的校验和不同时,有时候你很难追溯到造成该问题的本源。

我们差不多校验了世界,物体,寻路,目标及其他所有系统,但似乎总有一些事情被漏掉,巨大的消息(50MB)追踪及世界物体转存进行筛选让这个问题愈发困难,部分的困难是概念上的——程序员们不习惯于编写某些代码,比如不得不使用同样数量的调用来随机游戏模拟。(是的,随机数种子也需要同步)。

我们获得了哪些经验?

我们在开发帝国时代的多人游戏中,获得了一些重要的经验,这些经验也适用于开发其他游戏的多人系统。

了解你的用户。了解用户的期望对于多人游戏的性能,感知延迟及命令延迟至关重要,每个游戏类型都是不同的,你需要理解哪些东西对于你特定的玩法及控制是正确的。

在开发的早期,Mark与主设计师坐在一起,原型化了通讯延迟(这个是贯穿我们开发阶段不断重试的部分),从单人游戏出发,很容易模拟不同区间的命令延迟来获取玩家反馈,什么时候感觉正确、缓慢、急促或者很糟糕。

对RTS游戏而言,250毫秒的命令延迟并不会被察觉,250至500毫秒延迟是可玩的,而大于500毫秒延迟开始能被玩家察觉到,这里也有件有趣的事情值得注意,玩家会自行脑补出一种“游戏节奏”,这是当他们点击直到获得响应精神期望上的延迟。一个始终如一的慢响应比一个忽快忽慢的命令延迟好得多(比如80到500毫秒的延迟),在这个例子中,保持在500毫秒延迟更可玩,而变化的延迟会感觉很卡且难以使用。

实际上这导致了大部分的编程重点转移在了平滑上——选择一个的更长的回合长度肯定会比尽快运行加上一些偶然的减慢更一致也更平滑。任何速度上的改变都应该逐渐缓慢的改变。

我们也计算过系统上玩家的主要数据——他们平均每1.5到2秒的时间发布一次命令(移动,攻击,砍树),在剧烈的战斗中,会提升到每秒3到4个命令的峰值,由于我们游戏的行为是逐渐复杂起来的,所以最大的通讯需求往往在游戏的中后期。

当你花时间学习你的用户行为时,你将会注意到他们怎么玩游戏,这将会帮助你处理网络问题,在帝国时代中,当玩家兴奋的攻击时,他会不停的点击,这些行为会每秒发布非常多的命令,造成短暂的峰值,如果他们对一个巨大的群组进行寻路是,也会产生巨大的网络峰值需求。一个简单的过滤器可以丢弃在同一个位置的重复性命令,这样能大幅度的减少这个行为的影响。

总之,观察用户行为可以帮助你:

  • 明白玩家的延迟期望
  • 尽早原型化多人游戏的部分
  • 观察会影响多人游戏性能的行为

测量为王

你将会发现一个令人惊讶的事情,如果你更早的将测量数据对外,使得测试者可以看到该数据,可以帮助他们理解网络引擎底层所做的事情。

经验:当Mark过早的将测量数据移除,在最终代码加入后,一些帝国时代的通讯问题无法重新校验消息(长度及频率)层级,无法检测一些问题,比如偶然性的AI竞争条件,困难的计算路径,不良的结构命令包在一些被调优过的系统将造成巨大的性能问题

当越过一些边界条件的时候,你的系统是否会通知测试者及开发者?——当开发过程中某项任务对系统产生压力的时候,程序猿及测试人员将会观察到,并在早期就让你知晓,进而去做某些处理。

花些时间对你的测试人员进行一些培训,帮助他们理解你的通讯系统如何工作的,另外暴露及解释一些主要的测试数据给他们——当网络代码不可避免的遇到一些奇怪的错误时,你可能会受益于他们的一些发现。

总之,你的测量数据应该:

·对于测试人员更加可读及易于理解

·暴露瓶颈,速度降低及问题

·不占用太多性能,并保持运行

培训你的开发者  对于那些习惯于单机游戏开发的开发者,让他们开始思考关于命令发布,接收及处理的分拆,你发布的命令可能并不会发生,或者再几秒后发生,这个很容易忘记。命令必须在发送及接受的时候进行双重检测。

在同步模型中,编程人员也必须认识到,当运行游戏逻辑时,代码必须不依赖于本地因素(比如特定的硬件,设置等)。代码在所有机器上的结果必须匹配,比如游戏逻辑中的随机地形可能导致游戏行为不同(保存及重新生成随机种子负责这个事情,我们需要随机,但是不能改变游戏模拟)。

其他经验教训 这应该是常识——如果你依赖于第三方的网络库(我们这里用的是DirectPlay),编写一个独立的测试环境,检查该库所说的消息包保证抵达,保证包顺序等功能是没有问题的,另外测试该产品在处理消息通讯的时候是否有潜在的瓶颈或者奇怪的行为。

准备好创建模拟程序还有压力测试模拟,我们最终编写了三个不同的小的测试程序,每个测试软件都为了突出不同的问题,比如连接泛滥,同时配对连接,丢失保证抵达的包等。

开发过程中,尽早的用modem测试,并持续这个开发过程(这很痛苦)。因为很难隔离出问题(可能因为ISP,游戏,通讯软件,modem,配对服务或者其他可能的原因导致突然的性能下降),另外当用户习惯于实时的LAN连接速度后,真的不太愿意再使用拨号网络。你需要保证在局域网以及modem连接中投入一样的热情,这很重要。

帝国时代2的改进

在《帝国时代2:帝王世纪》中,我们增加了一些多人游戏的功能,比如游戏录像,文件传输,区域内的持续的状态追踪。我们还优化的原先的多人系统,比如DirectPlay的集成,修改速度控制的bug以及帝国时代1发布之后的一些性能问题。

游戏录像功能是一个本来你偶然发现“我可以用他来进行调试”的功能,后面变成了一个成熟的游戏功能,游戏录像在粉丝网站非常的流行,因为他们允许玩家交换及分析游戏数据,观看有名的对战,还有回看他们玩的游戏。作为一个调试工具,游戏录像非常有价值,因为我们的模拟是确定的,游戏录像与多人游戏的采用同样的方式,游戏录像提供给我们一个不断重试播放特定bug录像的机会,因为录像必须保证每次播放的内容都是一致的。

我们集成了一个区域内的比赛匹配系统,原来一代只能简单的运行多人游戏,在二代中,我们扩展了这个功能,允许增加运行参数控制,并且对数据报告进行存储。虽然没有一个完整的由内而外的系统,但是我们利用了DirectPlay的大厅启动功能,允许这个区域通过游戏前置表格控制某些特定的游戏设置,当游戏开始的时候,锁定这些功能。这允许玩家更好的寻找他们想要玩的游戏。因为他们可以在比赛匹配层看到这些参数,而不是直接进入游戏初始化的等待界面。在后台,我们实现了持久化的数据报告及追踪。我们为区域提供了通用的结构,这些数据在游戏结束之后被提交,这些数据用来记录玩家排名及等级,玩家可以通过区域网站观察到这些数据。

RTS3多人游戏:目标

RTS3是Ensembles的下一代策略游戏的内部代号,RTS3的设计基于成功的帝国时代系列设计理念,另外增加了一系列新的功能及多人游戏需求。

  • 基于帝国一二代的功能集,网络设计需要支持互联网游戏,各式各样大型的地图,数以千计的可控制单位;
  • 3d-RTS3是一个全3d游戏,拥有动画补间及非分面的单位位置及专项;
  • 更多的玩家——可能支持多于8个玩家;
  • TCP/IP支持,56k的TCP/IP互联网连接是我们的主要目标;
  • 家庭网络支持——支持终端用户的家庭网络配置(包含防火墙及NAT设置);

在RTS3早期中,我们做了一个决定,使用与帝国1代2代同样的网络模型——同步模拟——因为RTS3的设计以同样的方式能够发挥这种架构的优势。在一代二代中,我们依赖于DirectPlay来传输数据及会话管理,但是在RTS3中我们决定创造一个核心的网络库,用来作为我们最基础的socket库。

全3d的世界意味着我们不得不对帧频问题及多人游戏的模拟平滑更加的敏感,尽管如此,这也意味着我们的模拟更新时间及帧率更容易出现变化,我们可能需要花费更多的时间用来渲染。在Genie引擎中,单位转向是分面的,动画是帧率固定的——然而突然之间,我们突然被允许进行随意的单位转向以及平滑的动画,这意味着游戏将会对视觉更加的敏感(延迟的效果以及摇摆不定的帧频)。

二代开发完成后,我们想要总结出哪些是最重要的功能——那些预先规划设计还有工具化能对调试时间产生最大帮助的功能。我们也认识到迭代测试对于我们游戏的重要性,所以早早的将多人游戏的部分提到最高优先度。

RTS3通讯架构

6

一个OO的方案,RTS的网络架构是一个强面向对象的方式(见图6),强面向对象的设计抽象化了特定的平台,协议、拓扑结构及系统,有助于我们支持不同的网络配置。特定的协议以及特定版本的网络拓扑结构采用尽可能少的代码。大多数的功能被抽离到了高层的逻辑中。实现一个新的协议,我们仅仅需要扩展那些网络对象,实现特定的网络协议代码(比如Client、Session,这些需要基于不同的协议做不同的处理),系统中的其他对象不需要进行修改(比如Channels,TimeSync等),因为他们仅与Client及Session的高层抽象接口调用。

7

点对点的拓扑结构。Genie引擎支持点对点的拓扑结构(所有的客户端直连其他客户端,星形的网络结构),RTS3我们继续使用这个结构,因为这种结构在使用同步模拟的时候有一些与生俱来的优势。点对点的拓扑结构意味着在一次会话中已连接的客户端是一种星形的配置(见图7)。也就是说,所有客户端都连接了其他所有客户端。这是一代二代采用的设置。

点对点的优势:

  • 减少网络延迟,客户端直连比客户端-服务端-客户端需要更少的往返时间。
  • 没有中心点故障为题-如果客户端掉线(哪怕是主机),游戏还可以继续进行。

点对点的劣势:

  • 更多的活动连接数——意味着更多的潜在失败节点及潜在延迟
  • 没办法支持某些NAT配置

Net.lib 我们设计RTS3通讯架构的目标是适合于策略游戏的系统,但是同时我们也希望他能用来支持我们的内部工具以及我们将来的游戏。为了达到这个目标,我们创建了多层级的架构,它能够支持游戏级别的对象,比如Client或者Session,也能支持底层级的传输对象,比如一个链路或者一个网络地址。

RTS3基于我们的次时代BANG!引擎创建,该引擎是一个模块化的架构,由各种组件组成,比如音乐,渲染和网络,网络子系统在这里适合作为一个组件添加到BANG!引擎中(同时也是一个内部工具)。我们的网络模型分离为4个不同的服务器层级,看起来有点像,但不完全像OSI网络模型(见图8)。

8

Socks,第一层

第一层,sock层,提供基本的socket c api,它抽象化为不同的操作系统提供通用的底层网络接口,这个接口类似于berkley socket,这个层级的代码主要为更高等级的网络库使用,而不打算用直接在应用层代码。

链接,第二层

链接层提供传输层级的服务,这个层级的对象,比如Link,Listener,NetWorkAddress,Packet代表着用来连接和发送消息的有用对象。

  • Packet:我们的基本消息结构——一个可扩展的对象,它通过link对象发送消息的时候,会自动处理序列化及反序列化(通过纯虚方法)
  • Linker:两个网络终端的连接,这也可以是一个回路(loopback)连接,即两端都为同一台机器,Linker上的Send和Receive方法用来处理消息,另外有个void*的数据缓冲对象
  • Listener:Link生成器,这个对象监听一个连入的连接,当连接完成后生成一个Link对象
  • Data Steam:这个是一个任意长度的数据流,用来通过一个给定的Link——比如用来实现文件传输
  • Net Address:与协议无关的网络地址对象
  • Ping:一个简单的Ping类,用来报告给定Link的网络延迟

多人游戏,第三层

多人游戏层是net.lib api中最高层级的对象,RTS3使用这个层级来组合低等级的api, 比如links为更有用的对象,比如Client或者Session。

在BAND!网络库中最有趣的对象可能就是这些在多人游戏层级的了,以下的API代表了与游戏层级交互的绝大部分对象集。然而实现上我们还是维持与游戏无关的方案。

  • Client:它是最基本的网络节点的抽象,它可以配置为一个远程客户端(Link)或者本地客户端(loopback Link),Clients无法被直接创建,但是可以由Session生成。
  • Session:这个对象用来创建,连接,收集及管理各种Client,Session包含了所有其他多人游戏层级的对象,想使用这个对象的话,应用程序简单的调用host或者join,给它一个本地或者远程地址,然后Session就会处理好剩下的。它的责任包含了创建及销毁Clients,通知Session事件及派发到合适的对象。
  • Channel及Ordered Channel:改对象代表了一个虚拟的消息管道,通过某个Channel发送的消息将会自动的分离及接收到远程节点对应的通道对象上。一个Ordered Channel与TimeSync对象协同工作,保证所有客户端在Channel上收到的消息次序是相同的。
  • Shared Data:代表着通过所有客户端共享的一些数据,你可以扩展这个对象来创建特定的实例(包含你自己的数据类型)。使用内建的方法可以通过网络自动同步更新这些数据。
  • Time Sync:管理在Session中的所有客户端同步网络时间的平滑过程。

游戏通讯,第四层

通讯层级是RTS3游戏逻辑的部分,它主要搜集了游戏与网络库接口的部分系统,他实际上存在于游戏代码中,通讯曾提供了许多有用的工具方法,用来创建及管理多人游戏层级的网络层级的网络对象,另外试图归纳游戏的联网游戏部分为一个易于使用的接口。

新的功能和更好的工具

改善同步系统 在帝国时代开发团队中没有人不需要可能的更好的同步工具,在任何项目中,当你回头整理开发过程的时候,某些区域总是花费你最多的时间,但是如果你做了更多预先的工作会省掉你非常多的时间。同步的调试工具可能是我们在开发RTS3中最重要的工具。RTS3同步追踪系统主要是为了快速定位每轮的同步问题,开发它的其他要点是开发者的使用上的易用性,它能够处理任意巨大数量的同步数据,也能够从发布版中不编入同步的那部分代码,最终能够通过某些变量或者配置文件随意修改这项配置,而不是重新编译。

RTS3的同步检查主要是采用两个宏

#define syncRandCode(userinfo) gSync->addCodeSync(cRandSync, userinfo, __FILE__, __LINE__)

#define syncRandData(userinfo, v) gSync->addDataSync(cRandSync, v, userinfo, __FILE__, __LINE__)

(每个同步“标签”都有一系列sync打头的宏,每个标签代表需要同步的系统,在这里例子中代表的随机数生成器cRandSync)这些宏都带着一个userinfo的字符串参数,这是一个名字或者显示需要同步的描述,比如,一次这样的调用

SyncRandCode(“syncing the random seed”, seed);

同步控制台命令和配置变量

这对于开发过程来说,意义重大(任何Quakemod的创造者都会证实这点)。控制台命令一般是简单的函数调用,可以通过启动配置文件,在游戏的控制台中, 或者UI 的钩子中, 调用任意的游戏功能。 配置变量被称为数据类型,通过简单的get, set, define和toggle 函数,来做各种测试和配置参数。

Paul 扩展了一个支持对人游戏的控制台命令和配置变量系统。我们可以很容易将一个普通的配置变量(例如, enableCheating), 通过添加一个标记, 加入到对人游戏的配置变量。如果使用了这个标记, 在多人游戏中就会传输这个配置变量,同步的游戏进程也会跟这个值有关(例如, 是否允许资源免费)。 多人游戏的控制台命令也是类似的概念: 调用一个多人游戏下的控制台命令, 会传输到网络中的玩家, 并同步执行。

通过应用这两个工具,开发者可以很简单的使用多人系统, 而不用写一行代码。他们能快速的添加测试工具和配置, 并加入到网络环境中。

总结

点对点的网络同步模型,在《帝国》系列游戏中获得了成功。关键之处在于明白花时间在创建工具和技术上的重要性(例如同步和网络测量)。证明了在实时战略游戏上应用这种架构的可行性。后续在RTS3 中做的改进, 保证了多人游戏的体验和单机时几乎没有区别,除非在最烂的网络环境下。

[译]帝国时代中的网络编程》上有3条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注