使用虚幻引擎4年,我想再谈谈他的网络架构
The following article is from 游戏开发那些事 Author Jerish
作者 | 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个玩家客户端,服务器上有一辆车,让他执行Client RPC,他怎么知道发给哪个客户端?当然是通过这个车找到控制他的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发现又是空指针。这问题目前在现在的虚幻引擎里面还没有完美的解决方案,所以我们要尽可能的避免这种情况(我本人正在尝试实现一些可行的方法)。类似引发的更细节的问题还有很多,后面我会列举一些。
https://zhuanlan.zhihu.com/c_164452593 《Exploring in UE4》网络同步原理深入(上)《Exploring in UE4》网络同步原理深入(下)《Exploring in UE4》网络同步的理解与思考《Exploring in UE4》移动组件详解《Exploring in UE4》回放系统分析(待更)
1.Client的RPC并不能保证一定在客户端执行。在服务器上,如果有一个没有connection信息的actor(比如不是同步的,完全由AI控制的。或者说他的remote role等于none),那么他的clientRPC只会在自己的客户端上面执行。最后可能造成的后果就是函数调用栈的无限循环进而崩溃。
2.beginplay在客户端服务器都会执行,如果在beginplay执行另外一个actor的生成。可能会触发客户端和服务器都生成一遍自己的actor,结果客户端存在了两个Actor(一个自己生成的,一个服务器生产的)。之后在调用RPC的时候很可能会出现RPC执行失败,因为本地生成的Actor没有任何connection信息。 3.客户端上对象的Beginplay是可能执行多次。在unreal中,如果一个actor是服务器创建并同步给客户端,那么服务器可以随时关闭这个对象的同步。一旦这个对象距离玩家角色非常远或者服务器主动关闭同步,客户端上的对象就会被删除掉。后期如果玩家又靠近了这个对象,那么就会重新同步到客户端,再执行一次Beginplay。这样某些数据进行两次初始化,可能不是我们想要的。 4.我们经常会遇到“游戏状态恢复”的场景,比如网络游戏中的断线重连。然后你就可能会遇到一些对象在重连后状态不对,因为很多对象的变化是通过RPC去做的,RPC是一次性的。当你重连后,RPC不会再执行一次,所以客户端重连的状态与服务器其实是不同的。这时候需要使用属性同步来解决问题,但是属性回调在断线重连的时候你也并不一定想执行,所以要重新审视一下回调函数里面的内容。 5.不要把随时可能被destroyed的对象传进RPC的参数里面,RPC参数里面没有判断对象是否是合法的。如果传递的过程中对象被destroy掉,后续可能触发序列化找不到NETGUID的相关崩溃。 6.一般情况下,同步顺序在一个character内是严格按照属性的声明顺序的,不同actor无法保证。 7.一般回调会调到的函数,要注意里面有没有判空return的情况,这个时候其他actor的指针是有可能为空的。 8.一个UObject指针类型的数组属性,可能会触发多次回调,最后一次可以确保所有指针都有值。 9.属性回调执行的前提是客户端与服务器的值不同,如果你本地先修改一个值,然后服务器修改的与客户端相同,那么是不会触发回调的。 10.一般来说当Actor与PC解绑后,Actor就无法保证RPC的执行了。这种情况往往发生在角色死亡后执行unpossess时,所以在这时应该注意RPC的执行情况。 11.如果属性没有同步到客户端或者不执行回调,注意一下是否使用了自定义的条件属性 12.所有设置定时器来判断同步属性是否收到的逻辑都是不规范的,一旦服务器或者客户端变卡(一开始没有表现,但是随着游戏内容的增加可能出现各种诡异的bug)就可能导致信息丢失。
【End】
CSDN 618程序员购物日:显示器、键盘、蓝牙耳机、扫地机器人、任天堂游戏机、AirPods Pro等超多IT人的心仪好物,全场超低价出售,让1亿程序员买到爽!
更多精彩推荐
☞发送0.55 ETH花费近260万美元!这笔神秘交易引发大猜想
☞京东姚霆:推理能力,正是多模态技术未来亟需突破的瓶颈!