爱收集资源网

网络同步问题:为什么细节模糊?

网络 2023-06-24 23:05

作者|Jerish

来源|游戏开发这些事

我从16年开始接触Unreal,到现在已然4年了。近来看了不少关于网路同步的论文和书籍,终于是理解了Doom和Quake这些古董级游戏的发展历史,对其网路构架也有了更深一层的认识。此次想按照自己的工作和学习经验,以一个全局的视角来重新回顾一下虚幻的网路模块,并总结一些我们常见的问题,相信对UE同步细节模糊不清的你看完后一定会醍醐灌顶。

开始前,我先给初学者一个建议。假如你准备看UE4的同步源码,最好先大致阅读一遍这本书——《网络多人游戏构架与编程》,上面基本囊括了UE4同步框架的大部份内容,可以让你少走不少弯路。

下边步入题外话:

网路同步,就是使各个顾客端上的角色表现保持一致,属于游戏引擎的中级功能,所以通常我们都将其归类于Gameplay模块当中。不过具体的实现方案似乎会深刻影响到底层的网路构架(甚至是整个游戏构架)。我们既要决定通过哪种网路合同来完成,又要决定游戏各个模块的循环执行次序,这早已不单单是“Gameplay”层面的东西了。

虚幻引擎属于标准的CS构架(经过无数次改版的),外置状态同步功能,其同步频度与游戏的画质相同,属于变长步更新。因为功耗完全受CPU、GPU性能的影响,所以网路同步的频度与整个项目的性能息息相关。不过,有一点我们要认识到,unreal早已是尽可能的根据自己最快的速率进行数据的发送与接收了,只要我们做好各方面的性能优化即可。

RPC与属性同步

在Unreal上面,同步有两种手段,即RPC与属性同步(好多服务器引擎都是这么)。与其说RPC是同步手段,不如说他是一种传输数据的方法,用处就是可以直接通过类的函数方式书写,便捷理解。同时不须要你直接写Socket,也不须要你处理封包和拆包。在计算机网路的概念上面,RPC称作“远程过程调用”,本质上就是一种传递数据的手段,而其实现方法既可以是应用层的Http,也可以是传输层的TCP/UDP。在虚幻上面,因为好多游戏的同步(例如FPS)对网路延后要求比较严苛,所以我们舍弃了须要三次握手的TCP而改用UDP(更不可能考虑HTTP了)。RPC既可以标记为可靠,也可以标记为不可靠。可靠的RPC最终一定会抵达目标终端,但不可靠的RPC不仅在网路拥挤的环境下遗失,也可能在引擎限流的情况下被提早困住。RPC本身并不是一个可以持续存在的对象,我们只能通过RPC参数“一次性”的将数据从一端发送到另一端,所以每位RPC调用只能“只执行一次”(换句话说,他的生命周期只有刹那间)。假如RPC消息从网路中遗失,这么他都会永久的遗失(这儿指不可靠的RPC),所以并不适宜游戏世界各类对象的状态恢复,必需要结合可以保持对象状态的属性才行。据悉,UE4上面RPC并不支持反弹,所有RPC函数的返回类型都是void。

属性同步,本质上属于一个比较下层的功能特点,是以每位对象为单位处理的(不支持更细细度的同步,但理论上可以通过条件属性做部份调整,详见AACtor::PreReplicate)。unreal的服务器会依照一定频度的去执行同步对象属性的数据发送和接收,同时处理反弹函数。属性同步的形成是为了维持对象的状态,是一个从概念上十分紧贴“同步”二字的功能,一旦服务器上的同步属性发生了变化,就一定会发送给顾客端(注意:属性同步只是服务器向顾客端的同步,不存在顾客端向服务器流通),其实中间会丢包会延后(actor首次同步时是reliable的),而且其外置的机制会保证属性的值最终送达到顾客端。借用一句精典的话来说就是,同步数据似乎会迟到,而且永远不会缺席。

无论是RPC,还是属性同步,你会发觉他都是基于UObject的,或则更准确的讲都是基于Actor的(以及其附属组件)。由于这两种功能一个是借助类中的函数,另一个是借助类对象的属性,她们都须要与某一个具体的对象作为媒介,而在UE的构架中,设计都是面向对象的,每位Actor都可以理解为游戏世界的对象。

既然是基于Actor的,这么整个同步就与GamePlay框架紧密相连。因为我们在发送同步数据的时侯须要晓得这个数据应当发向那个顾客端,而顾客端与服务器的链接信息(IP等)又在Playercontroller上面,所以同步的逻辑与playercontroller密切相关。好多刚接触unreal的同学常常会碰到RPC数据发不出去或则收不到的问题,就是没有认识到playercontroller虽然是包含顾客端与服务器的联接信息的。最典型的,如果你有服务器上连着10个玩家顾客端,服务器上有一辆车,让他执行ClientRPC,他如何晓得发给那个顾客端?其实是通过这个车找到控制他的playercontroller,之后找到对应顾客端的IP,假如这个车不被任何顾客端控制,那他就不晓得要发给谁。

其实,RPC与属性同步的实现原理不同也决定了她们有好多差别。因为属性同步是跟随每一个实例对象走的,所以不存在“随用随发”。也就是说,属性同步须要在每帧特定的时机通过统一的引擎插口讲到发送缓存(sendbuffer)上面。这样带来的问题就是,你在同一帧上面更改的属性只有最后的哪个值会传到顾客端那儿,因而造成你的反弹函数也只会执行一次。而RPC不同,每次执行时就会立即将数据塞到发送缓存上面,因而保证不会遗失任何一次RPC的调用(如果RPC是可靠的)。

守望先锋小美身材数据_守望先锋守望之心辅助_更新守望先锋传输数据

另外,这儿面还有一个大坑,就是关于Actor以及Component的同步次序问题。一个对象的同步首先要给顾客端上的对象与服务器上的对象构建关联,这样服务器的A变化了就会告诉顾客端上的A也去变化。而且A是一个对象,对象也是须要同步的,一个场景上面有这么多的对象,同步肯定是按次序的来的。这样才会常常出现A的对象上面有好多指向B对象的同步表针属性,并且A对象出现的时侯B还没同步过来,所以在A的Beginplay上面访问B是不行的。这么怎么解决这个问题?答案是用属性反弹,一旦执行了属性反弹,就可以确保A的B表针是存在的。不过,属性反弹并不能解决所有问题。如果B对象还有C对象的表针,反弹的时侯C还没同步过来,你想用B去访问C发觉又是空表针。这问题目前在现今的虚幻引擎上面还没有完美的解决方案,所以我们要尽可能的避开这些情况(我本人正在尝试实现一些可行的方式)。类似引起的更细节的问题还有好多,前面我会列出一些。

联通同步理解

两种同步手段早已介绍完毕,我们如今把视角锁定在网路同步的解决方案上。游戏中同步本质上是同步顾客端之间的表现,而RPC与属性同步都只是数据上的同步,我们须要将其与画面表现结合上去。画面表现说白了就是物体的显示与隐藏、动画、位置等,其中位置同步就是最复杂的一项,由于游戏中的角色可能是每帧都在联通的,联通组件(movementcomponent)就是为了解决这个问题而诞生的。

联通组件很复杂,他须要考虑到各类情况的延后、抖动,须要解决不同顾客端不同角色的流畅性问题,须要实现各类配准手段。在网路同步中,仍然存在三种方式的角色,分别是本地玩家控制的、服务器控制的以及其他玩家控制的,在unreal中分别对应着Autonomous、Authority与Simulate。这三种类型的存在本质上代表着角色的控制者是谁(那个端可以直接通过命令操作他),而从另一个角度讲这些分类虽然是代表着玩家的操作是否有网路延后以及延后的大小。对于本地控制的Autonomous角色,他可以在本地直接响应你的操作,假如想把操作发给服务器,则须要经历一个client——server的延后,而服务器想把这个操作同步给其他顾客端又须要一个server——client的延后。

同步中最难的当然就是怎样有效的对抗这些延后。所以,会诞生例如延后补偿这些同步策略,即本地顾客端收到其他顾客端消息的时侯将本地的所有角色回滚到【当前时间-网路延后时间】时的位置再进行消息的处理和估算。

守望先锋小美身材数据_更新守望先锋传输数据_守望先锋守望之心辅助

(UE4默认引擎上面没有这些操作,虚幻竞技场上面有。如右图,蓝色是当后端的具体位置,红色是回滚预测的位置)。

联通组件本地顾客端到服务器采用的是不可靠的RPC,而服务器到其他顾客端采用的是属性同步。为何使用RPC?由于顾客端向服务器发送消息只能通过RPC,属性同步只是拿来服务器同步给顾客端用的。unreal在同步位置时记录了各个顾客端以及服务器的时间戳,通过位置buffer缓存、每帧不停的发送位置、判断时间戳调整位置与回滚等操作实现比较理想的疗效,本质上守望先锋的帧同步+状态同步是相同的(详见:)。不过虚幻并没有采用ECS,并不能在构架上挺好的支持所有逻辑的回滚。

网路同步发展至今,虽然基本已然成形。从初期的Lockstep到指令流水线化再到预测回滚TimeWarp,大体的同步优化手段都是这种,现今的趋势就是状态同步与帧同步里的各类机制相互借鉴相互推动。不仅联通同步,其他的例如动作同步和隐藏显示我们通常要求不这么严苛,由于她们不须要每帧都做处理,通常采用RPC做一次性的通知更改就可以了。

关于同步,还有一个你们平常不是很在乎的细节,那就是同步频度。上面谈到了UE4会根据尽可能快的速率去发送同步数据,假如顾客端的性能十分好帧率十分高,这么一帧都会形成极其多的联通RPC。理论上来说,假如没有丢包的话,就算服务器分辨率很低,服务器也会根据顾客端发来数据挨个模拟,最后两端结果相同,依然是流畅的。并且,假如中间遗失了部份联通的RPC(引擎内部才会对发送进行限流),就可能导致服务器估算结果与顾客端不同因而不断拉回顾客端,导致卡顿。

总的来说,RPC与属性同步有些场景是可以互相取代的。对于简单且实时性要求不高的使用RPC就可以,而对于须要服务器实时保有主控故此持续性同步的状态我们就可以使用属性同步。属性同步本身早已做了优化消耗没有这么大,你可以通过各类条件来设置他的同步规则。并且注意,量变形成质变,倘若不加节制的全部使用属性同步,这么actor(以及属性)遍历的开支与会相当可观,所以还是合理的使用还是十分重要的。这块理论上有好多可以优化的地方,例如Actor可以设置同步的范围(类似AOI),距离玩家很远的对象不须要同步;Actor可以依据一些规则关掉对个别顾客端的属性复制功能(Dormancy),同时关掉ActorChannel并从NetConnection里移除;采用replicationgraph对空间进行界定,剔除相关性不强的对象因而降低带宽的占用(然而这个方案只适宜大世界类型的游戏)。理论上,我们还可以添加更多的优化方法以及更细的细度来进行调整,不过具体方案就要按照游戏类型来灵活处理了。

(Replicationgraph示意,每位宝箱被放置到他所影响的所有条纹上面。玩家只有步入这种条纹上面就会收到宝箱的同步信息)

回放系统

回放看上去是个很高大上的功能,但似乎早在上世纪90年代就随着Lockstep算法一起诞生了。UE4外置了一套Demonetdriver系统来处理回放和录制,但因为采用的是状态同步而不是帧同步,所以实现上去比较复杂。基本思路就是在本地创建一个虚拟的服务器,录制的时侯本地当作一个服务器,回放的时侯本地又当作一个顾客端。在游戏进行的时侯,本地开始录制并把回放相关的数据序列化到数据流上面(可以是显存、磁盘或则是网路包),播放的时侯再去对应的数据流上面读下来。其实框架是有的,但还处于一个未完成的阶段,用上去坑也是相当的多(例如过期的多播风波在回放中不会被执行到)。对于死亡回放以及精彩镜头这些实时切换的需求,涉及到的逻辑要更复杂一些(例如真实世界和回放世界的切换与隐藏),这块有时间我会再写文章来仔细讲讲。

(官方射箭游戏Demo——ShooterGame中就富含一个简单的回放演示功能)

底层框架

说完了下层的网路同步,再简单说说底层。虚幻引擎诞生于90年代,也肯定参考了好多其他游戏的设计,例如“雷神之锤(Quake)”,“星际围攻:部落(Tribe)”等。当时Quake是最早一批采用基于“CS构架状态同步”的游戏,而Tribe将模块进行分拆和封装,是第一个建立了比较健全的网路同步构架的游戏。UE4的构架与Tribe很像,通过NetDriver+NetConnection+Channel+Actor/Uobject具象分层实现了目前的同步方法。好多人总是责怪虚幻引擎把底层搞得太复杂,但这似乎有好多历史缘由以及技术上的权衡,官方团队在过去的20年里肯定也无数次地思索过这些问题,这儿也不过多赘言。其实,从网路层面上说,UE4高度耦合的网路框架不适宜帧同步(这儿指lockstep),同时也很难改导致ECS构架。不过,我个人也同样认为好多游戏没必要非要追求帧同步,两种同步开发各有各的坑,真做上去游戏虽然都没这么简单(其实踩UE官方的坑可能会让你更不爽一点,虽然不是自己写的)。

守望先锋守望之心辅助_守望先锋小美身材数据_更新守望先锋传输数据

关于网路合同,游戏界经过大量的测试很早就公认——对于高频同步的游戏,使用UDP同步的疗效要好于TCP。为此,Unreal使用的就是UDP合同,并且为了保证数据的可靠性,须要在下层封装一个可靠的UDP,也就是NetDriver+NetConnection+Channel那一套。上面的逻辑很复杂但是涉及到好多模块,确实有一些冗余。据悉,即使是可靠的,并且在属性同步和RPC的处理方法上并不相同,属性同步只保证最后的数据是可靠的,中间的结果可能会遗失,而RPC则可以保证消息一定按序送达。针对其外置的RUDP的重发机制,UE虽然早已做过好多次的优化和调整了,之前任何的丢包和正序就会立即触发重发,4.24上面早已添加了循环队列来收包矫治收包的顺序,一定程度上减轻了毋须要的重传。消息的接收和发送默认还是在主线程处理的(我们可以决定是否启用多线程),因为UDP不须要窃听多个Socket并且针对收包采用多线程意义也不大,所以也没有采用iocp或则其他异步IO的形式。在虚幻引擎中,网路包的更新次序是“收数据——逻辑更新——发数据”,但并不是所有的同步更新逻辑都在收包的时侯做,UObject类型同步属性的更新可能就是在分包前更新的(这块是一个坑,要注意),具体可以参考我的知乎文章“《ExploringinUE4》网络同步原理深入(下)”中的第五部份第8小节。

到此,我早已比较全面的把虚幻引擎的网路模块重新梳理一遍,更多的细节请参考文章“”以及我的知乎专栏《ExploringinUE4》相关文章。

《ExploringinUE4》网络同步原理深入(上)

《ExploringinUE4》网络同步原理深入(下)

《ExploringinUE4》网络同步的理解与思索

《ExploringinUE4》移动组件解读

《ExploringinUE4》回放系统剖析(待更)

最后,我们再总结一些在同步中常常会碰到的问题,那些都是我踩了无数坑才总结下来的,拿你们的“在看”或“转发”换一下不过分吧。

【End】

CSDN618程序员购物日:显示器、键盘、蓝牙扬声器、扫地机器人、任天堂游戏机、AirPodsPro等超多IT人的心仪好物,全场超优价转让,让1亿程序员买到爽!

更新守望先锋传输数据