有没有大佬可以给我发一些守望先锋大佬的最佳或者亮眼表现的素材

原标题:阵亡镜头、全场最佳和煷眼表现 《守望先锋大佬》回放系统是如何设计的

大家好, 请关闭手机或者至少调至静音状态我们准备开始了。

欢迎来到“守望先锋大佬”(下文统称Overwatch) 回放技术分享今天的内容包括阵亡镜头、全场最佳和亮眼表现。我是Phil Orwig是Overwatch服务器团队开发工程师。

那么我们将深入到Overwatch里,這是暴雪在第一人称射击游戏领域的处女作它是一款团队合作竞技游戏,主要特色是各个身怀绝技的英雄

这次分享会覆盖回放系统(Replay System)的設计目标,深入架构与实现的细节以及深度打磨的过程。同时还会讨论我们遇到的挑战以及对应做出的折衷,最后会展望一下回放系統未来的目标

这次分享的标题就预示着必须给出***,对吧概要需求是创建一个单一的中央系统,能够支持阵亡镜头、全场最佳和亮眼表现除此之外我们还特别需要能够生成录像文件,在开发期间可以用来做内部调试

下面开始深入介绍每个议题。

每次玩家死亡时遊戏里就会显示临死前几秒钟的――大部分情况是以凶手(killer)视角来看的――死因及死亡过程。阵亡镜头可以帮助玩家理解他们是怎么死嘚以及为什么会死,有一定的教学作用

上面展示的狂鼠被半藏杀死的例子中,可以看到死亡过程中的几个关键节点玩家死亡时有3件徝得注意的事:玩家尸体布偶化(ragdoll,也叫布娃娃专用术语,全称Ragdoll physics是用在电子游戏的物理引擎中代替传统静态动画的可变性角色动画系统),方便判断死亡方式(译注:估计说的是炸死、摔死还是射死等);游戏镜头朝向凶手;凶手轮廓描边即使隔着墙也可以看见,从而使伱查看得更加清楚(译注:这一点在视频中没有得到展示)

我们会以淡入的方式进入阵亡镜头,展示死前一段时间以及死后几秒钟的画媔你能看见自己的尸体布偶,然后明白自己是怎么死的

我们希望阵亡镜头可以让你学到东西并提高技巧,但不是每个玩家都能享受这個目睹自己死亡的过程我个人就可以证明,看着自己的英雄一次次倒下带来的那种无力感如果持续体验这个过程,慢慢地会感觉这简矗是黑色幽默(macabre sense of humor)

另一方面,“全场最佳”是个赛后集会点在此你和伙伴们可以庆祝共同创造的精彩游戏体验,也是一个共同分享的伟大時刻它可以激励玩家为了得到最佳而拼尽全力,证明自己同时炫耀自己独有的个性化英雄形象。

在本部分结束前一起看下这个过程,我们会从队伍胜利播报开始切到玩家英雄特写,最后过渡到精彩的团队合作与高超技巧的展示

有点技术问题,稍等一下好了请欣賞我的“全场最佳”。

嘿嘿我没那么厉害啦,其实对面都是机器人 (众笑)(译注:视频中Phil拿了个4杀)

玩家如果觉得自己可能能成最佳,他们就会提前卖弄像下面这样。

上面展示的死神本来已经被查莉娅的大招“重力喷涌”困住了但是他通过幽灵形态成功逃脱,此時他的大招能量刚好充满他转身放了个喷漆在墙上,紧接着开大人挡杀人,佛挡杀佛

这里最值得注意到就是,这个玩家如此自信以臸于在开大前就喷了个漆:Well Played(译注守望先锋大佬中,时机把握得当这个行为就会被全场最佳一起记录下来,是高手炫耀的一种常见方式)那就好像是说:我这次肯定要拿最佳!

“全场最佳”带来了很多适于分享的话题,但是假如你没拿到最佳呢或者虽然你拿到了但昰恰好没有合适的录像软件记录这一切呢?对此我们的解决方案就是“亮眼表现”。12个玩家里面只有1个人能拿到最佳,而所有人都会嘚到“亮眼表现”(highlights)这个可以在游戏大厅入口处播放。有了这个功能玩家可以更容易地捕捉到自己的炫酷表现,并分享给好友

这個例子中亮眼表现的作用,与其说是炫耀技巧还不如说是用来搞笑。你可以看见一个路霸失手勾到了/networking-for-game-programmers/ )早已经把网络同步机制总结为彡类了:

  • 确定性帧同步(deterministic lockstep),常用于竞速游戏或者星际争霸2、魔兽争霸2这样的即时战略游戏;

注意我的幻灯片下方列出了一些参考文章正是這些文章启发了我们,设计出了我们自己的同步策略:

快速浏览一下这些技术第一个是确定性帧同步,左边有4个客户端都连接到同一個服务器上。服务器也可以由其中的一个客户端来兼任简单起见我们假定有个专属的某某云服务器。

客户端接收玩家输入并上报给服务器服务器在接收到全部输入以前,保持锁定当前帧当全部客户端的输入都上报完成时,服务器才开始模拟游戏进程

处理好全部输入鉯后,服务器批量打包所有指令并转发给全部客户端。

然后客户端基于刚刚收到的转发指令开始各自模拟游戏过程。代码处理得当的話每个客户端都会得到相同的结果,看上去很美妙

基于帧同步实现一个回放系统很方便,只需要记录整个输入流即可实现回放而且輸入通常很容易压缩,所以数据量也不大

确定性帧同步,虽然架构简单但是也有一些缺陷。它的模拟过程需要严格一致同一帧内,楿同的输入集合在每个客户端必须产生同样的输出才行

如果做不到这一点,客户端之间就会产生不同步会对当前处于何种状态各执一詞。例如对浮点数依赖程度很高而浮点数运算很难做到严格一致,尤其在不同的平台架构下更是如此 一个简单的打印驱动失灵,就能茬中断控制器上改掉浮点数精度设置从而使模拟过程偏离轨道。而且这一点的确会发生

维护一个确定性的模拟过程,除了代码逻辑上嘚工作以外还有一个问题就是多玩家联网时产生的。

“等待全部玩家的输入”意味着任何一个停滞的玩家都会破坏模拟过程的及时性。游戏里玩家数量越多这个风险越高。简单计算一下假设一个5分钟时间的游戏,每个玩家卡顿的概率是2%4个玩家就会有8%的可能性,玩镓多达12个时已经是22%了。

我本应该画个图表来说明这一切的

上述缺陷都是可以克服的。 通过遵守合适的编程规范就可以开发出并维护一個确定性模拟系统的我们开发过很多这样的游戏,这样做绝对可行 或者可以耍点小聪明来隐藏缺失玩家输入引起的卡顿,例如你可以鼡倒带法(rewind)重新模拟或者维护一个预测(predict)窗口

Overwatch最终没有选择确定性帧同步方案,原因如下:首先我们不希望程序员和策划的因为偶现的不確定性而产生心理负担;除此之外,要支持中途加入游戏也不简单模拟操作本身就已经很耗了,如果5分钟的游戏过程每次都需要重头開始模拟的话,计算量太大

最后,实现一个阵亡镜头也会很困难因为需要客户端快照以及快播机制。按理说如果游戏支持录像文件的話快播自然就能做到,不过Overwatch并没有这一点

那么下一个选项就是“快照同步”了,与确定性帧同步相类似也是客户端接受玩家输入然後上报给服务器。然而这个模型里的服务器是可以忽略某个玩家输入缺失而继续向前模拟的。

这里不会转发输入操作给全体客户端取洏代之的是服务器模拟游戏世界,不停地生成整个世界状态的瞬时快照

然后服务器会把快照下发给客户端。客户端根据这些快照来更新各自的世界状态通常会用“插值”方法在两个相邻的快照间做平滑(译注,关于插值可以参考Source引擎,解释的比较权威)

这种方式运莋良好,概念也很简单同时避免了帧同步模型中的确定性和卡顿问题。事实上稍加改造,再结合一些压缩就能得到一个比较可靠地模型了。

差异计算过程比较耗费资源你可以分摊快照成本或者“增量”(Delta,下文中会多次出现并直接用英文单词)压缩,这样对于多個客户端来说只需要处理一次就好了。

如果你担心有人***你就需要维护一些客户端不应该看见的对象的实时数据,然后只下发客户端能看见的东西

基于快照同步实现回放系统的话,十分简便从一个完整世界状态开始,更新每帧时添加差异就好了广义上讲,这个方案就是依赖新技术的游戏客户端如何播放录像文件的问题客户端要做的仅仅是把网络游戏数据序列化到硬盘上。

这套架构实现阵亡镜頭同样很简单 你已经有全量的快照了,对吧所以只需要维护一个环形缓冲区,保存过去时间点到当前时间之间的世界状态历史然后偅播就行了。注意到我把这里快照(图片)的大小做成不同(译注:指的是上图中Replay Stream线上的4副图)的你可以图片大近似想象成每一帧的序列化数据尺寸。有一些帧变化的少所以尺寸就小。

这个例子里我同样使用了不同尺寸的管道(代表数据量的多少)。快照尺寸就相当於快照同步模型的阿喀琉斯之踵它限制了在每一帧内,世界状态变化的程度和次数需要同步的东西越多,初始快照尺寸就越大帧与幀之间变化越多,Delta就越大变化越多意味着需要的比特位就越多,而比特位需要消耗宝贵的带宽资源

对于一个高频互动的游戏,如果想偠在低带宽环境下正常运作最终能依靠的就只剩几种主流的同步模式了。

基于状态的同步 这一页幻灯片与之前的一模一样,客户端上報输入服务器执行模拟。

与快照同步模型相反服务器不会为全体客户端生成单一更新,而是给每个玩家发送纯手工定制、公平贸易换來的、原生态的数据包(众笑)

那么,当我们谈论“世界状态”(world state)时是在谈论什么呢?

我想说的是任何用来表达游戏世界所需的必要狀态。可以想象一个代表个体位移、旋转、血量、动画参数和武器发射状态等等的分类数据表在快照同步模型中,客户端会收到全部状態数据并“原子化”执行一遍

基于状态的序列化,会把世界快照***成更小的、原子的数据块因为每个客户端收到的数据都是订制的,我们可以基于客户端相关性来设置更新的优先顺序这样就能够基于客户端的实测带宽来智能调整数据流了。

上图的例子里有个客户端收到1个数据包同时包含了大锤和法拉的更新信息。另一个客户端则收到了2个数据包其中1个是大锤,然后1个是法拉的信息这里最重要嘚一点就是对象可以独立更新,每个客户端的数据都是分别订制的

客户端网络状况参差不齐的情况下,这种模型就是最灵活的不过实現起来也是最复杂的。这就是Overwatch的实现方式

现在咱们深入了解一下Overwatch里是如何做到稳定的网络同步的,以及回放系统在这个同步范式下是如哬实现的

有些System是序列化过程的参与者(participants),它们负责同步游戏的某些方面(aspect)给接收者(receivers)或者复制目标(replication targets)接受者一般就是需要接收数据的活跃玩家戓者观战者(spectator)。最后复制目标会收到关于其他实体的更新消息。

通常我会把这一切简言之为:参与者把网络相关实体数据打包并发给接收鍺这些实体就叫“主体”(subject)。

下面来个具体点的例子假定你正在控制法拉(Overwatch英雄之一)你跳了一下然后飞起来准备低空轰炸,这个消息昰如何发给其他玩家的呢

当你开心地按下按钮,客户端预测就开始了同时(译注:原文是then,但其实这个操作不需要考虑前后顺序的)把这個命令上报给服务器

服务器收到连同你在内全部玩家的输入后,一个经过”官方批准”的模拟过程就开始了:一些实体移动了;一些英雄倒下了;抛射物爆炸了简直就像开Party一样,美妙极了

最终的每个变化和事件,我们统称为Delta例如”位置变化”就是个(服务器上的)Delta。

在垺务器端我们会累积所有对象状态的变化,然后保存在一个临时的“每帧脏数据集合”(per frame dirty set)里可以简单地认为它就是个字面意义的数学概念的集合,包含所有在那一帧发生改变的实体的ID

那如何把这些状态转发给客户端呢?我们对每个已连接的客户端也维护了一个“脏集合”(译注:应该指的是C1, C2…作者没有交代这些集合是如何生成的不过从下文的C1=C1-P可以推测出来)。这一页幻灯片右上方的F是当前帧脏集合每帧結束时,所有接收者的脏集合会与帧脏集F合并(还是数学上的并集概念)这样数据打包时就可以只包含那些该客户端真正需要的实体集匼了。在帧结束时这些操作都做完以后,帧的脏集会被清空下次更新(tick)时,一切重头开始

在同一个更新周期(tick)的后期,脏状态(译注:C1,C2…)会被序列化成一个数据包通过“网线”(这个年代到处都是网线)发给接收客户端。

数据一旦被序列化就会从脏集合中移除(C1=C1-P)。带寬是稀缺资源所以我们不会使用原生的状态数据,而是维护了一个经客户端确认收到的状态数据的历史记录这样就可以进行“增量编碼”来改进带宽使用模型,也就是减少带宽的占用

对于大多数帧,我们都有足够的带宽来序列化所有数据即使不够也没关系,这个例孓里只更新了法拉的状态,因为客户端带宽不足无法容纳大锤的数据了。无论如何接收者脏集合都会把它保存下来,最终会在将来嘚某个时刻成功序列化到某个下行包里

这就是状态同步模型的一大优点,可以基于带宽质量来调整下行数据大小

网络协议我们用的是萣制的UDP。众所周知UDP是不可靠协议,意思是说一个数据包可能会成功达到目的地也可能到达2次,或者乱序到达这都没关系,我们自己設计的协议可以感知丢包并在必要时重传脏数据

丢包发生时,只需要简单的把这个数据包合并回脏状态集合留待将来重传即可。在未來的包里我们可以把法拉移动这件事,连同该帧其他事件一起重传例如大锤开盾。

注意 如果在首次移动包发送完与收到丢包通知之間,法拉再次移动了那么无需重传旧状态,我们会把新的移动状态序列化到旧状态数据里

现在聊一下新加入游戏的玩家,是如何追赶仩最新的世界状态的

我们在服务器上维护了一个“永久”(lifetime)脏状态集合L,是个全量的脏状态集合s也就是服务器上所有曾经变脏的状态的“全集”。接下来的事情你肯定能猜到。

你猜对了新玩家连上后,我们会把它的初始脏状态集合设置为“永久”脏集合L这里的设置僦是字面意义上的赋值。正常的打包流程最终会把所有变化都下发给这个客户端直到它赶上最新的进度

一切都很顺利,但是差点忘了 峩们是要做“回放”而我却一直在讲网络序列化。可以把回放数据流近似的认为就是正常游戏的网络数据流仅仅是接收者有点区别而已,回放系统的接收者叫做“回放快照接收者“(replay snapshot receiver)

所以实现回放系统的一个方式(并非最好,后面马上优化)就是在每一帧都保存一個完整世界的瞬时状态快照。

UDP包大小 是有MTU限制的然而回放流却没有这个限制,每一帧可达几百上千K字节直到内存耗尽。

但是我们如何保证每次都能得到恰好合适的全量快照呢

很容易,假装发给回放快照接收者的每个包都丢了就行然后把永久脏集合重新复制发给回放接收者。这样就可以保证每个快照上每一帧上的每个实体都是脏的但是就像我上面提示过的,这样做太浪费了

曾经提到过网络系统是囿增量编码的,所以可以基于先前已确认的状态来优化网络流量

我们又弄了第二个回放接收者,Delta接收者D这个接收者从不丢弃假想包,烸次更新都只包含上一帧到现在的变化所以基本上直接复制帧脏集然后序列化即可。

帧的结尾我们会把新的永久脏状态集合赋值给回放快照目标。同样也会把每帧脏集合序列化给回放Delta目标然后马上就会有个问题出现:这些数据最终都去哪了?

客户端正常接收者是通过網络包进行的而回放数据需要通过别的途径。我们维护了两个连续的缓冲区一个是给快照,一个给Delta每一帧都会记录这两个缓冲区的偏移和尺寸。

这些连续的缓冲区在整局游戏运行期间都会存在因为策划可能需要游戏开始到现在的一卷全场最佳镜头(reel,一段连续的影像无法直译,下文只好统称为“卷”)

实际看来,每个缓冲区每分钟需要1M的内存而每帧记录快照所需的内存量比一分钟1M要多得多。所以峩们把快照频率降低为每秒1个

你们中可能有些人想知道Delta的更新频率。服务器的频率是以62.5赫兹(16ms每帧)更新客户端的死亡重播和回放系统如果都运行在这个频率会使内存和带宽消耗达到3倍之多,所以我们把Delta的频率降低到20Hz

现在我们终于可以生成回放“卷”(replay reel ,译注:reel这里强调昰“卷”)了这是客户端所需的核心驱动数据。策划同学想要生成阵亡镜头或者全场最佳的时候他们请求一个“卷”即可。这些”卷”由一个快照加上一系列递增的Delta组成

要生成”卷”,就要找到”卷”所需开始时间之前的最近的那个快照然后不停添加Delta直到”卷”的截止时间。这里输出缓冲区的生成也很高效就是2次内存拷贝而已。

缓冲区会被压缩添加一些元数据,然后发给客户端阵亡镜头,亮眼表现和全场最佳都是用的这套机制而且都是通过这一个System支撑的。

如果这时候你开始怀疑这到底还是不是快照同步了你绝对是正确的。这是有意而为的两个World都运转良好:基于状态的模型的十分灵活,可以基于客户端网络质量动态调整;快照同步用于回放”卷”时也带來了简洁性

现在”卷”已经生产,需要发送给客户端了为了方便传输,”卷”会被切分为MTU大小的多个片段然后通过我们开发的BlockTransferSystem在网絡上传输。BlockTransferSystem会考虑到网络状况只有在带宽充足的情况下才会发送。

当然玩家死亡期间除了盯着凶手,发怒以外什么也做不了这一点吔让我们松了口气。因为他们太全神贯注了根本不会注意到因为传输大块数据而引起的微小抖动。

最后看一下同步的API长什么样这里省畧了一些参数;另外返回值应该是枚举而不是整形值,但是用来说明问题已经足够了

System通常会通过一个内部的通用序列化函数来实现网络參与者和回放参与者两个接口,这两个API的差异很小例如:网络参与者API会限制带宽,上面已经讲过很多次了而回放参与者API则完全不用担惢这一点。所以很多情况下回放接口会自行调用含有带宽限制的内部函数,来设置一些永远都达不到的阈值

网络和回放接口共享序列囮策略带来的结果就是,网络复制所节省下的每个字节在回放系统里也是如此。

这两个接口是每个接收者针对每个相关Subject(译注:之前囿提到,网络相关实体就是Subject)分别调用一次的。基本就是每个接收者都遍历一次完整的脏集合了

如果所有的参与者都认为他们对于某個Subject没有需要序列化的数据了,那就可以安全的把这个实体从接收者脏集合中删除了

这些带Fixed的函数,给参与者提供了一个机会去同步那些与实体无关的信息,在网络侧就包括带外控制消息和握手之类的操作。

由于采用了不可靠协议每个参与网络同步的System,对于每个数据包都需要接受一个ACK或者NACK消息来判断是否收到或者丢失。

我们就是靠之前已经确认收到过的状态结合内部帧压缩来增量的发送更新。

另┅方面回放参与者完全不需要收包确认和丢包通知,因为它们从不“丢”包取而代之的是在序列化过程开始前和结束后使用一对回调函数。如果要举例说明如何在完成回放帧的序列化之后立即使用这些函数,那就是对所有来自Delta接收者的内部序列化状态立即ACK,对所有赽照接收者上的数据立即NACK

上图是发送数据给网络接收者的API使用的简化版用例。对于该接收者的每个参与者都执行 Fixed调用,然后获取到该接收者的脏Subject集合并允许每个System都参与到Subject的序列化过程。

这个例子忽略了很多技术细节而这些细节几乎可以拿出来单独开设分享议题了,包括:脏状态追踪相关性,优先级过滤,带宽模型丢包与确认,以及其他能单独分享的技术

现在回头看一下参与者API,大家可能会囿疑问为什么这些System的设计没有基于组件,而是基于实体 而一个良好的ECS架构实现应该是基于组件的。起初我们的同步API确实是基于组件的例如MovementSystem简单遍历所有Mover组件然后序列化即可。不幸的是这种范式有些潜在的关联,假如带宽受限会发生什么呢通过组件序列化意味着一個System就有可能扼杀其他System序列化能力。除此之外还增加了同一实体被多个包更新的可能性所以我们团队的实用主义者在这个案例中,从纯粹嘚ECS转向了非纯ECS而且是完全可以接受的,也是为了更好地玩家体验

好了,上面讲过的都是干货现在来点好玩的:挑战和躺坑。如果说の前讲的是“如何做”那么现在讲的就是“何时”与“何地”,而“为什么”最开始我就说过了。

首先何时下发阵亡镜头的”卷”?阵亡镜头有几个固定时间点:死亡、重生

这两个点之间的距离是由重生定时器控制的,不同的地图、游戏模式、是否加时及所有策划鈳以配置的情况下这个定时器的值都不同。

为了使得阵亡镜头看起来更合理下发数据时需要比死亡那一刻再超前一点点,这样看起来僦没那么突兀了避免出现一脸懵bi的情况下突然被人爆头。

另外我们也不想在玩家死亡的那一瞬间就突然停止回放,那样会很突兀因為你的大脑需要一些时间来处理上下文,最好能看到死亡后几秒钟内周围都发生了什么看着尸体布偶化,然后再从死亡的痛苦中恢复过來

现在这幻灯片越来越拥挤了。

在死后的尾声阶段和重生之间,才需要观看阵亡镜头对吧?这本来就是要开发阵亡镜头系统的真正原因

所以这里只剩下一个很小的时间窗口,可以让我们把回放数据发下去大概是半秒钟左右的时间。然而在低带宽条件下半秒钟是鈈够传输数据的。

这些情况也进一步地促使我们开发团队节省带宽事实再一次证明,我们用来减少带宽消耗的所有努力同时也减少了囙放数据的大小。实践中我们严格遵守这个纪律,最终实现了一个可靠的阵亡镜头体验即使低带宽高丢包的网络环境下也是如此。

这裏我们还有另外一种选择与其一次性传输大块数据,不如分开成几次:死亡时立即发送死前(pre-death)数据之后再陆续下发增量Delta数据。

这个方法會需要一些工程上的折衷总带宽消耗会变大,因为没办法对整个“卷”进行压缩了另外还需要一些控制逻辑:即使收到阵亡镜头的第┅个数据块也不能立即开始播放,因为数据还不足够有可能引起回放镜头卡顿,没人想要这种体验

所有困难都是可以克服的,我们也鈈一定非要这样做不过这也是选项之一。

哦这部分是我的最爱:可打断的阵亡镜头。“复活”是个麻烦事因为我们已经把“卷”发給客户端了,客户端也开始播放了但是因为回放随时可能被取消或者打断,所以我们不能完全销毁live世界的状态也不能停止接收live的网络數据,因为live的游戏脚本可能会需要根据这些数据控制UI、特效触发事件来表明你被“复活”了。

我们没有使用快照同步模型所以没办法保持住整个世界的状态,我们没做过类似的游戏也从来没有实现过。

回放可能立即就会被打断怎么办呢?

我之前提到过Overwatch的游戏架构用嘚是ECS于是乎我有了一个想法,为什么不弄一个独立的”域”(domain)来实现回放呢或者用Overwatch的术语来说,一个独立的EntityAdmin通过这两个不相干的ECS域,峩们可以把回放和live环境隔离开来

最后问了一圈,从工程师到团队领导到技术总监,我基本可以确定在我们这个游戏项目中CPU绝对不会荿为瓶颈。不过这也可能是错的我的意思是,永远不会成为计算密集型(sim-bound可以参考CPU Bound和I/O Bound的定义)程序,看起来是那么的明智

而且对于我们來说这是一个清理旧架构中设计不合理部分的机会,例如对全局变量和静态变量的内部依赖有了两个完全不相关的域以后,所有的参数嘟需要限制在域的范围内所以借此机会整理代码也不错。

大概花了几周的时间我们把所有东西都隔离了,并且有了两个域(Domain)一个做实時游戏,一个做回放在这个模型里,实时游戏数据来自网络直接进入实时渲染的世界里。而来自回放的数据块直接进入了回放模块進入阵亡镜头时,实际上两个域的数据都在同时处理

这一页的视频里会演示一个简单的阵亡镜头。回放开始时会有一个黑色淡入效果這代表我们停止运行实时游戏,转而渲染回放域等到回放结束时又会转移回去。

总得来说执行“域”的实验很成功或许是过于成功了,所以我们就在想既然2个域是ok的,那为什么不搞4个呢

实际上这并不是一点代价都没有的,我的意思是说我们需要实现一套机制,可鉯在过渡时供域之间进行协调、通信。现在大部分域内部冲突都依赖于EntityAdmin这个全局管理器通过脚本化行为来协调何时进入何域。

那么这個额外的模拟过程的代价又是什么呢我们最终还是计算密集型的了,尤其是在主机和低端PC机上多域模型带来了这个额外负载,但它绝鈈是唯一一个打破性能预算的系统有了回放系统以后,我们就可以用录像文件来做整体性能优化

录像文件使得我们可以直接在主机上發布60帧的FPS游戏,也在低端PC机上达到了性能目标

继续讲下打磨过程,这一部分最后才讲也是我最喜欢的一个话题。

先说炮台吧对于我們来说,炮台击杀敌人是个棘手问题建造炮台的玩家会得到击杀奖励。炮台杀人的时候主人可能已经在半个地图以外了。更糟的是主人在这时候被干掉了。

下面就是一个托比昂的炮台拿到全场最佳的例子炮台杀人无数,咱们可以数一下它杀了几个

两个(注意视频裏托比昂死在了悬崖边上)。

三个(众笑因为这时候托比昂的尸体已经掉到悬崖中部了)

真的很滑稽,这段视频是从beta版来的那时候我們还没修复这个bug呢。后来我们就不再把全场最佳送给死掉的英雄了也就解决了这个问题。(译注守望先锋大佬最初的版本确实有这类全場最佳镜头,也是十分搞笑)

所以托比昂视角的阵亡镜头(译注:感觉这里有点问题,作者强调的应该是全场最佳)只有在他自己而非炮台杀人时才有意义。

那么该如何处理(炮台击杀时的阵亡镜头)呢基本上是脚本硬编码的。我们最初调整了炮台的阵亡镜头角度使咜看起来是真的站在炮台的视角看的。但不幸的是炮台是由AI控制的,而不是一个真实的玩家并没有用第一人称视角在那里打***。更重偠的是炮台AI与真人瞄准的方式也是完全不同的。

通过创建一个基于炮台动画骨骼的定制化摄像机我们解决了这个问题。从那个骨骼位置后方一点发射出一个由点组成的环,来得到稳定的视角这种处理方式对于秩序之光的炮台(哨兵炮)尤其有用,因为它们很小经瑺被放在天花板上,或者角落里、墙上另外让炮台和受害者都处于画面中也是很重要的。

下面的视频展示了一个炮台摄像机的例子注意玩家死亡时,尸体布偶化摄像机自动对准凶手。阵亡镜头期间摄像机会追踪玩家,他从角落里一露头就可以看见炮台把他打到人倳不省。

这里你还会有个额外收获可视化的摄像机位置调试信息。这里绘制了一些绿色的点表示摄像机的朝向,用来在阵亡镜头期间保证炮台始终位于画面中央的。

最后的提示是关于debug功能的即使是在回放过程中也依然可用。如果发现bug了同样可以依赖脚本系统的调試功能去解决。

你应该能注意到摄像机目标位置会有一些插值可能还有些其他的被掩盖了,但是这些就是基本原理了(nuts and bolts螺母和螺栓)。

下面是一个炮台摄像机未能按照预期工作的例子发行版本也是如此。可以看见死亡发生时摄像机再次指向了凶手但是这个例子里,陣亡镜头里的摄像机没有成功地把炮台聚焦到画面中央

如果你注意到那些天花板上的龙骨,就知道摄像机的位置没办法更好了实在没辦法。

除了炮台以外还有其他几种情况,需要我们来实现定制化的阵亡镜头

当你跌出地图边缘时,我们会解除摄像机绑定然后追踪伱的布偶直到沃斯卡亚的冰河里。

如果是因为有人把你推下去我们就会转而从他的视角回放死亡过程。

半藏的神龙之魂有同样的炮台問题。他甚至可以从重生室里放大然后他的龙可以飞跃整个地图直到干掉你,然而半藏的本尊还在重生室里潇洒呢

在这种情况下显示來自凶手视角的阵亡镜头显然没什么意义,取而代之的是我们使用了一个第三人称视角的摄像机去追踪龙的飞行过程直到杀死你的那一刻 。

实际上还有好几种需要定制的阵亡镜头但是没时间全部覆盖到了。上面讲的是最常见的几种对每一种情况进行打磨都使得阵亡镜頭的体验更好,可以减少玩家突然被弄死时的困惑

好吧,插值提示(interpolation hinting)从服务器的角度来看,进攻者永远都是在“过去”采取行动的如果没有额外提示的话,客户端播放阵亡镜头时就会看到准星瞄的永远都是目标后方,这样不好会导致论坛上很多投诉。

为了修复这个問题我们在回放系统里记录了每个玩家的插值延迟提示,然后在客户端播放回放期间把这个和插值延迟缓冲区集成到了一起。这样就保证了阵亡镜头可以准确显示服务器端进攻者的瞄准动作

接下来我会用“时间缩放”的方式来连续播放两个阵亡镜头过程(内部回放文件支持这么做)。重点观察开火反冲(recoil)那一刻士兵76的位置Overwatch里,武器的操作是不会有任何延迟的一旦你扣下***,立马就能看到开火特效和其怹相关的表现包括爆炸、拖尾等。

结果就是因为客户端帧率的粒度较低,你能看见一点点偏离但是开火反冲却是由该帧脚本精确控淛的。

在爆头的瞬间(从视频中可以看出)目标刚好处于准星的位置,完美

再看看同样的情况下,没用插值的表现(译注从视频上來看,这种情况下目标已经越过准星了,弹道和击杀提示才出现)

好吧,现在讲一下”快进”(fast forwarding)前面已经说了每秒钟记录一次快照,洏死后还有2秒的收尾过程(译注:正如前面讲的摄像机对准凶手,再看看周围环境的变化)会发生什么呢?如果所需“卷”开始之前朂近的一次快照超前了整整1秒的话(译注:也就是说,“卷”开始时该快照刚好结束),收尾过程会被截短只剩下1秒完全不够你用來思考世界的。

解决方案就是把回放过程快进到“卷”开始的时间这是个很小的工作,却很重要保证了阵亡镜头体验的一贯性。

服务器超载时会落后1到2帧然后再赶上来这种情况下,当前模拟的帧可能得不到所需要的时长了如果此时客户端执行回放的话,它能得到的幀数就会比预期要少实际表现就是回放结束的很突兀。

解决方案也很直接我们在beta版 的客户端采用更高的帧率。同时记录客户端期望时長和服务器实际时长这样的话就可以在期望的时间窗口内完成回放。

游戏中大多数消息都是针对单个玩家的回放系统只需要其中的一蔀分消息,观战者也有同样的问题不是所有发给指定客户端的消息都应该分发给观战者。

为了实现这一点我们把每个游戏消息都加上紸释,来指定它是否需要被包含到回放或者观战数据流当中然后在把这些消息委托给适当的接收者。

客户端实际回放期间我们会过滤掉那些不是发给当前主体的消息。

如果没有这些打磨的话“卷”里就会缺失受击提示、命中确认、UI变化和一些语音对话。这些处理帮我們改善了回放的体验

提高Delta录制频率。之前有提到过我们的Delta刷新频率是20HZ,这基本上是精确度期望值对带宽消耗妥协的结果顶级玩家可鉯做到(也的确这么做了)在快速旋转的同时进行射击,但我们当前的频率达不到那么精确

一个办法就是在回放数据里增加一些朝向提礻,就像插值提示那样另外一个办法就是加快Delta录制频率,使之能够配合当前上下行包的频率这两个方法都会使得带宽与存储空间的消耗急剧增长。玩家移动的数据包约占到总带宽的一半

那为什么这会是个问题呢?这里我找了一个游戏高手人非常好,在录制完成最终嘚视频以前一遍一遍地尝试爆我的头。他有一手绝活你可以看见准星经过爆头点。但是因为没有更细粒度的Delta更新我们只能用插值填滿这50毫秒的范围。

注意准星虽然经过了头的位置,但是这里就能看出增加Delta的重要性了(译注:视频中弹道已经明显偏离准星位置了就昰因为Delta的频率不够高所致)。

优化回放文件:快进倒带,拖放 (scrub随意指定开始位置去播放)。当前的回放文件只在起始处有一个快照,所以它其实就是一个普通的基于输入的回放机制你唯一能做的就是从头向后顺序播放。

正因为如此就不可能支持拖放,快进也是通过从头开始快速播放实现的而且倒带实现方式也不咋地,真正想用这个功能的人一定很失望因为还是从头快速播放到你想要的位置の前多少秒钟。

目前都是团队内部在使用但是不能排除将来会发布的特性会用到这些。

这一页给出了一个简洁的修复方式:插入更多快照!

考虑到目前快照的尺寸我们大概可以在回放文件里增加几个10秒钟粒度的快照,在不用对文件尺寸做过多妥协的情况下至少可以以10秒钟的步幅做快进。

如果我们真的在回放文件增加那些快照会有一个额外的好处:可扩展的观战功能。增加一个代理或者服务器与实時游戏服务器独立开来,专门接收带Delta的回放快照即可越来越多的玩家都想要在诸如锦标赛之类的比赛中实时观战,只需要下发快照给这些玩家然后不断塞Delta数据流给他们就行了。所有这一切都发生在一个隔离的服务器上完全不用担心实时游戏服务器过载什么的,这一点非常非常重要

增加一点缓冲区,画几个UI再加上可扩展的观战功能这事就成了。当初设计这组内部功能时真是太明智了。

尽管后来发現我其实没那么聪明,这个点子也绝非原创它的实现方式与Valve公司的那个叫做“Source TV”的观战系统如出一辙,我猜Dota2的游戏内观战也是基于这個架构的

最后,如果不谈一下把回放系统开发给玩家使用那就太过分了。玩家需要回放而我们希望玩家开心,做到以下几点就够了

老实说,最大的阻碍就是找到一种方法来保留全部回放数据

根据当前帧率,一周的回放文件大约需要200TB的存储空间这还是压缩后的。鈈幸的是压缩需要消耗CPU资源,而CPU是我们当前最大的瓶颈

除此之外,这200T数据也不是需要永久保存的我们现在正在研究如何在存储需求囷留存(retention)需要之间做权衡,以使得这个功能尽快上线

我们希望可以允许玩家保存一段合理的时间内,把回放文件保存在他们自己的机器上我们也考虑过允许玩家在比赛结束时下载回放文件,但是那样会增加本已捉襟见肘的服务器带宽资源所以也行不通。

谈到留存就不鈳避免的提到补丁和序列化兼容性问题。为了能够序列化网络和回放数据的结构定义,都依赖于资源定义如果补丁中含有任何与stream有关嘚东西,都会使得回放文件失效

从2016年5月游戏发布到2016年12月份,我们打了大概22个补丁也就带来了22个潜在的不兼容的可能性,谁都不希望看箌

所以我们的客户端必须支持加载上一个补丁版本的资源的能力,这样才能正确播放回放文件这也意味着,如果硬盘上没有所需资源就要从网络上下载。还要支持某种形式的新旧版本资源覆盖从CDN按需下载,显示进度条需求越列越多。

我们到现在还在想象这个功能應该做成什么样但是这多少会给你们一些信息,了解我们正在克服哪些阻碍突破什么边界。

快要结束了现在回顾一下。

回放系统很酷上面的例子已经展示了我们影院级的回放镜头支持。

耶子弹时间!想象一下,把这个功能和一些时间压缩、暂停技术相结合就能嘚到类似电影《黑客帝国》中的子弹时间。在我们游戏玩法预告里大量的采用了这个功能。所以这里取消暂停旋转一下摄像机,这样僦有了一个更好的角度

到目前为止都学到了哪些知识呢?设计网络同步模型时就考虑到回放的需求;不相关的执行域提供了更大的灵活性和隔离性;网络带宽优化重于回放文件尺寸优化能省一个字节就省一个字节;社区都很喜欢分享亮眼表现和全场最佳,能在Reddit和其他论壇上看到这些分享感觉真的很棒。

如果你在开发时不支持回放文件那就太悲惨了。你应该说服你的工程师或者如果你自己就是工程師的话,你真的应该先实现这个功能对于debug来说太有用了。回放文件对于性能分析也很有用可以提早优化,使得游戏可以运行于任何硬件

更多的域意味着更多的负载。这一点绝对有些意外让人头疼,在域之间平滑过渡就需要大量人力开发。我们做了很多工作去保证无论什么背景场景,场景更新都保持在一个低频模式以减少CPU的消耗。

服务器定制的序列化很昂贵定制越多,优化就越多如果你决萣采用这种方式的话,就要对困难有所准备

最后一件事,你或许已经注意到幻灯片里的API与发行版本用的很相似所以,一定要尽早开发哃步模式这样的话API会干净很多。

版权声明:本文为博主原创文章遵循 版权协议,转载请附上原文出处链接和本声明



导入时一般选择文件最大的即是人物模型
剩余的要做什么事,美术大佬就各自发挥吧
特别说明:建议Blender版本2.79,守望版本1.4此文档仅作为经验的积累,大佬不喜勿喷有需求提取参考就行,建议别做商用盗取版权

参考资料

 

随机推荐