网络同步细节模糊不清的你看完一定会醍醐灌顶

网络同步细节模糊不清的你看完一定会醍醐灌顶

初始化游戏引擎失败什么意思_游戏引擎初始化失败_初始引擎失败化游戏怎么办

作者|杰里什

来源 | 关于游戏开发的那些事

我从2016年开始接触虚幻,到现在已经4年了。 最近读了很多关于网络同步的论文和书籍。 我终于了解了《Doom》和《Quake》等古董游戏的发展历史,并对它们的网络架构有了更深入的了解。 这次我想结合自己的工作和学习经历,从全局的角度来回顾一下虚幻网络模块,总结一下我们常见的一些问题。 相信如果你对UE同步的细节还比较模糊的话,读完之后你会恍然大悟。 。

在开始之前,让我给初学者一个建议。 如果你打算看UE4的同步源码,一般最好看一下这本书——《网络多人游戏架构与编程》。 基本涵盖了UE4同步框架的大部分内容,可以让你少走很多弯路。 。

游戏引擎初始化失败_初始化游戏引擎失败什么意思_初始引擎失败化游戏怎么办

让我们进入正题:

网络同步是为了使各个客户端上的角色表现一致。 它是游戏引擎的高级功能,所以我们一般将其归类在Gameplay模块中。 但具体的实施方案实际上会深刻影响底层网络架构(甚至整个游戏架构)。 我们不仅要决定使用哪种网络协议来完成它,还要决定游戏每个模块的循环执行顺序。 这不再只是“游戏玩法”级别的事情。

虚幻引擎属于标准CS架构(已被修改无数次),内置状态同步功能。 它的同步频率与游戏的帧率相同,并且是变长步长更新。 由于帧率完全受CPU和GPU性能的影响,因此网络同步的频率与整个项目的性能密切相关。 然而,我们需要认识到的一件事是,只要我们优化各个方面的性能,Unreal已经在尽可能快地发送和接收数据了。

RPC 和属性同步

在Unreal中,同步有两种方式,即RPC和属性同步(很多服务器引擎都是这种情况)。 与其说RPC是一种同步手段,不如说它是一种传输数据的方式。 优点是可以直接写成类函数的形式,容易理解。 同时不需要直接编写Socket,也不需要进行分包、拆包的处理。 在计算机网络的概念中,RPC被称为“远程过程调用”,本质上是一种传输数据的手段,其实现既可以是应用层的Http,也可以是传输层的TCP/UDP。 在Unreal中,因为很多游戏(比如FPS)的同步对网络延迟有严格的要求,所以我们放弃了需要三路握手的TCP,改用UDP(更不用说HTTP了)。 RPC 可以标记为可靠或不可靠。 可靠的RPC最终会到达目标终端,但不可靠的RPC不仅会在拥塞的网络环境中丢失,而且还可能在引擎节流时被提前阻塞。 RPC本身并不是一个可以持久化的对象。 我们只能通过 RPC 参数从一端向另一端发送“一次”数据,因此每次 RPC 调用只能执行“一次”(换句话说,他的生命周期只有一瞬间)。 如果RPC消息从网络上丢失,就会永久丢失(这里指的是不可靠的RPC),因此不适合游戏世界中各种对象的状态恢复。 它必须与能够维持对象状态的属性相结合。 另外,UE4中的RPC不支持回调,所有RPC函数的返回类型均为void。

属性同步本质上是一个上层的功能特性,是按对象进行处理的(不支持细粒度同步,但理论上可以通过条件属性进行部分调整,具体参见AACtor::PreReplicate)。 虚幻服务器会以一定的频率进行同步对象属性的数据发送和接收,同时处理回调函数。 创建属性同步是为了维护对象的状态。 它是一个在概念上非常接近“同步”一词的功能。 服务端的同步属性一旦发生变化,肯定会发送到客户端(注:属性同步仅针对服务端)。 同步到客户端(客户端到服务器没有任何通信),过程中可能会出现丢包和延迟(第一次同步时actor是可靠的),但是它内置的机制会保证属性的值最终传递给客户端。 借用一句经典的话,同步数据可能会迟到,但永远不会缺席。

无论是RPC还是属性同步,你都会发现它是基于UObject的,或者更准确地说,它是基于Actor(及其附属组件)的。 因为这两个函数一个使用类中的函数,另一个使用类对象的属性,所以它们都需要通过特定的对象来中介。 在UE架构中,设计是面向对象的,每个Actor都可以理解为游戏世界中的对象。

由于是基于Actor的,所以整个同步都是和GamePlay框架紧密相连的。 由于我们在发送同步数据时需要知道这个数据应该发送到哪个客户端,而客户端与服务器之间的链接信息(IP等)都在Playercontroller中氛围,因此同步逻辑与Playercontroller密切相关。 很多刚接触Unreal的朋友经常会遇到RPC数据无法发送或接收的问题。 他们没有意识到玩家控制器实际上包含客户端和服务器之间的连接信息。 最典型的例子是,如果你有10个玩家客户端连接到服务器,并且服务器上有一辆汽车,你要求它执行Client RPC,它如何知道将其发送给哪个客户端? 当然是通过这个车找到控制它的玩家控制器,然后找到对应客户端的IP。 如果这辆车不受任何客户端控制,那么他就不知道该把它发给谁。

当然,RPC和属性同步的不同实现原理也决定了它们有很多差异。 由于属性同步遵循每个实例对象,因此不存在“随用随用”。 也就是说,属性同步需要在每一帧的特定时间通过统一的引擎接口写入发送缓冲区(sendbuffer)。 这样带来的问题是,只有你在同一帧中修改的属性的最后一个值才会传递给客户端,这将导致你的回调函数只被执行一次。 RPC 则不同。 每次执行时,数据都会立即填充到发送缓冲区中,以确保不会丢失 RPC 调用(如果 RPC 可靠)。

初始引擎失败化游戏怎么办_初始化游戏引擎失败什么意思_游戏引擎初始化失败

另外,这里还有一个深坑,就是关于Actors和Components的同步顺序。 对象的同步首先必须在客户端上的对象和服务器上的对象之间建立关联,这样当服务器上的A发生变化时,它会告诉客户端上的A也发生变化。 但A是一个对象,对象之间也需要同步。 一个场景中有很多物体,必须按顺序进行同步。 这样,经常会出现A的对象有很多同步指针属性指向B对象,但A对象出现时B还没有同步,所以在A的Beginplay中访问B是不可能的。 那么如何解决这个问题呢? 答案是使用属性回调。 一旦执行属性回调,就可以确保A的B指针存在。 然而,属性回调并不能解决所有问题。 如果B对象也有指向C对象的指针,则回调期间C尚未同步。 如果你想用B访问C,你会发现它又是一个空指针。 目前虚幻引擎中还没有完美的解决这个问题的办法,所以我们必须尽可能的避免这种情况(我自己也在尝试实现一些可行的方法)。 与此类似的更详细的问题还有很多,稍后我会列出其中的一些。

初始化游戏引擎失败什么意思_游戏引擎初始化失败_初始引擎失败化游戏怎么办

移动同步理解

两种同步方式已经介绍完毕,现在重点介绍网络同步方案。 游戏中的同步本质上是客户端之间的性能同步,而RPC和属性同步只是数据同步,我们需要将其与画面性能结合起来。 说白了,屏幕表现包括物体的显示和隐藏、动画、位置等。位置同步是最复杂的,因为游戏中的角色每一帧都可能在移动,而运动分量(movement component)是来解决这个问题。 并诞生了。

移动组件非常复杂。 需要考虑各种情况的延迟和抖动,解决不同客户端、不同角色的流畅性问题,实现各种插值方法。 在网络同步中,角色总是存在三种形态,分别是本地玩家控制、服务器控制和其他玩家控制。 在unreal中,它们分别对应Autonomous、Authority和Simulate。 这三种类型的存在本质上代表了角色的控制者是谁(哪一端可以通过命令直接操作他),而从另一个角度来看,这种分类实际上代表了玩家操作是否存在网络延迟和延迟。 尺寸。 对于本地控制的Autonomous角色,他可以直接在本地响应你的操作。 如果要将操作发送到服务器,就需要经历客户端-服务器延迟,而服务器想要将这个操作同步给其他客户端,就需要另一个服务器——客户端延迟。

初始化游戏引擎失败什么意思_初始引擎失败化游戏怎么办_游戏引擎初始化失败

同步中最困难的其实就是如何有效对抗这种延迟。 因此,延迟补偿等同步策略就会诞生,即本地客户端收到其他客户端的消息时,将所有本地角色回滚到【当前时间-网络延迟时间】的位置2d素材,然后进行处理计算消息。 。

(UE4在默认引擎中没有这个操作,但在Unreal Tournament中有。如下图,红色为当前结束的具体位置,黄色为回滚预测的位置)。

初始引擎失败化游戏怎么办_游戏引擎初始化失败_初始化游戏引擎失败什么意思

移动组件本地客户端使用不可靠的RPC到服务器,而服务器使用属性同步到其他客户端。 为什么使用RPC? 由于客户端只能通过RPC向服务器发送消息,因此属性同步仅用于将服务器同步到客户端。 Unreal在同步位置时会记录每个客户端和服务器的时间戳。 通过位置缓冲区缓存、每帧不停发送、判断时间戳、调整位置、回滚等操作,达到了理想的效果。 本质上,《守望先锋》的帧同步+状态同步是一样的(参见:)。 但Unreal并没有使用ECS,无法支持架构中所有的逻辑回滚。

网络同步发展,已基本形成。 从早期的Lockstep到指令流水线再到预测回滚TimeWarp,这些都是通用的同步优化方法。 目前的趋势是状态同步和帧同步的各种机制互相学习、互相促进。 除了移动同步之外,我们对于其他方面比如动作同步、隐藏显示等一般要求不太严格,因为它们不需要每一帧都处理。 一般情况下,RPC用于一次性通知修改。

关于同步,还有一个细节大家平时不太关注,那就是同步频率。 前面提到游戏引擎初始化失败,UE4会尽可能快地发送同步数据。 如果客户端的性能很好,帧数很高,那么一帧就会产生很多移动RPC。 理论上,如果没有丢包,即使服务器帧率很低,服务器也会一一模拟客户端发送的数据。 最终两端的结果会是一样的,依然顺利。 但如果移动的RPC中途丢失了一部分(引擎内部会限制传输速率),可能会导致服务端的计算结果与客户端不同,不断拉回客户端,造成滞后。

一般来说,RPC和属性同步在某些场景下是可以相互替代的。 对于简单且实时性要求不高的情况,可以使用RPC。 对于服务器需要保持实时控制和持续同步的情况,我们可以使用属性同步。 属性同步本身已经优化了,成本也没有那么高。 您可以通过各种条件设置其同步规则。 但要注意的是,量变会带来质变。 如果不加限制地使用所有属性同步,那么actor(和属性)遍历的开销将是相当可观的,所以合理使用还是非常重要的。 理论上有很多领域可以优化。 例如,Actor可以设置同步范围(类似于AOI),距离玩家较远的物体不需要同步; Actor可以按照一定的规则对某些客户端关闭属性复制功能(休眠)。 、关闭ActorChannel并同时将其从NetConnection中移除; 使用复制图来划分空间并消除不太相关的对象以减少带宽使用(但此解决方案仅适用于大型世界游戏)。 理论上我们还可以添加更多的优化方法和更细的粒度来进行调整,但具体的解决方案必须根据游戏类型灵活处理。

游戏引擎初始化失败_初始化游戏引擎失败什么意思_初始引擎失败化游戏怎么办

(Replicationgraph表示每个宝箱都放置在其影响的所有格子中,只有玩家进入这些格子才会收到宝箱的同步信息)

初始化游戏引擎失败什么意思_游戏引擎初始化失败_初始引擎失败化游戏怎么办

回放系统

回放看似是一个非常高级的功能,但实际上它早在20世纪90年代就与Lockstep算法一起诞生了。 UE4内置了Demenetdriver系统来处理播放和录制,但由于它使用的是状态同步而不是帧同步,所以实现起来比较复杂。 基本思想是在本地创建一个虚拟服务器。 录制时,本机作为服务器,回放时,本机作为客户端。 当游戏进行时游戏引擎初始化失败,本地录制开始,并将播放相关的数据序列化到数据流(可以是内存、磁盘或网络数据包)中,然后在播放时从相应的数据流中读出。 该框架虽然存在,但仍处于未完成阶段,并且使用中存在不少陷阱(例如,播放时不会执行过期的多播事件)。 对于死亡回放和精彩镜头实时切换的需求,涉及到的逻辑比较复杂(比如现实世界和回放世界之间的切换和隐藏)。 有时间我会写一篇文章详细讲一下这个。

游戏引擎初始化失败_初始引擎失败化游戏怎么办_初始化游戏引擎失败什么意思

(官方射击游戏Demo-ShooterGame包含简单的播放演示功能)

初始化游戏引擎失败什么意思_游戏引擎初始化失败_初始引擎失败化游戏怎么办

底层框架

说完上层网络同步,我们简单说一下下层。 虚幻引擎诞生于20世纪90年代,想必参考了很多其他游戏的设计,比如《雷神之锤》、《星际围攻:部落》等。当时,雷神之锤是最早采用“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类型同步属性的更新可以在发送数据包时完成。 之前更新过(这是一个坑,请注意),具体可以参考我的知乎文章《深入探究UE4网络同步原理(下)》第五部分第八节。

至此,我已经对虚幻引擎的网络模块进行了全面的重组。 更多详情请参考我的知乎专栏《UE4探索》中的文章《》及相关文章。

《UE4中的探索》深入网络同步原理(上)

《UE4中的探索》深入网络同步原理(下)

《探索UE4》中对网络同步的理解和思考

《Exploring in UE4》中移动组件详解

《UE4中的探索》播放系统分析(待更新)

最后总结一下同步过程中经常遇到的一些问题。 这些都是我经历了无数坑之后总结出来的。 代替大家的“观看”或“转发”并不过分。

【结尾】

CSDN 618程序员购物日:显示器、键盘、蓝牙耳机、扫地机器人、任天堂游戏机、AirPods Pro等众多IT人最爱的单品全部超低价出售,让一亿程序员买的开心!

初始引擎失败化游戏怎么办_初始化游戏引擎失败什么意思_游戏引擎初始化失败

文章来源:https://blog.csdn.net/csdnnews/article/details/106798813