所有Unreal网游开发者都应该看的文章:使用虚幻引擎4年,再谈UE的网络架构( 四 )



所有Unreal网游开发者都应该看的文章:使用虚幻引擎4年,再谈UE的网络架构
文章图片

Replicationgraph示意 , 每个宝箱被放置到他所影响的所有格子里面 。 玩家只有进入这些格子里面才会收到宝箱的同步信息


三、回放系统


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

所有Unreal网游开发者都应该看的文章:使用虚幻引擎4年,再谈UE的网络架构
文章图片

官方射击游戏Demo——ShooterGame中就含有一个简单的回放演示功能


四、网络框架

【所有Unreal网游开发者都应该看的文章:使用虚幻引擎4年,再谈UE的网络架构】
说完了上层的网络同步 , 再简单谈谈底层 。 虚幻引擎诞生于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的方式 。 在虚幻引擎中 , 网络包的更新顺序是“收数据——逻辑更新——发数据” , 但并不是所有的同步更新逻辑都在收包的时候做


推荐阅读