精华内容
下载资源
问答
  • 网络同步

    千次阅读 2017-01-13 12:47:27
    同步在竞技类网络游戏中的应用 帧同步在网上可以搜的资料比较少,关于游戏的更是没有,不过,实现的原理也比较简单,最近几天就写了份关于帧同步的文档,当作给同事扫扫盲,顺便也在这里发发,可以给其他...

    帧同步在竞技类网络游戏中的应用

    帧同步在网上可以搜的资料比较少,关于游戏的更是没有,不过,实现的原理也比较简单,最近几天就写了份关于帧同步的文档,当作给同事扫扫盲,顺便也在这里发发,可以给其他人参考参考

        --竞技类网络游戏设计方案

     

    一、        前言

     帧同步,根据wiki百科的定义是,一种对同步源进行像素级同步显示的处理技术,对于网络上的多个接入者,一个信号将会通过主机同步发送给其他人,并同步显示在各个终端上。同步信号可以是每帧的像素数据,也可以是影响数据变化的关键事件信息。

    帧同步在网络游戏中的应用,设计上有异于传统的mmorpg游戏,因为可以承载更大量的后台计算,实现类单机的效果,所以可应用在类似射击类、飞机类中实现弹幕计算或者格斗类的高精度打击体验

    本文将主要介绍下帧同步与传统mmorpg设计框架的异同点以及相关的几个设计方案,最后,深入展开对其中一种实现方案的分析,而相关的反外挂和断线重连机制等技术难点暂不在本文讨论。

    二、        帧同步在游戏中的应用

    网络游戏中,游戏服务的架构大致可以分为2种模式,分别是cs模式和p2p模式

    cs模式框架如图1(c为客户, GSS为游戏状态服务器)

             


                                       图1

    图1,游戏状态服务器(GSS)单独部署,负责对网络上各个接入者提供服务,当GSS状态发生变化时,将状态同步发送给各个接收者。

    p2p模式框架如图2(c为客户,GSS为游戏状态服务器):

             



                                       图2

    图2中,游戏状态服务器存在于各个客户主机上,游戏状态的改变直接来自于各个客户端的输入。

    以上2个服务框架中,cs模式,由于GSS服务器只有一个,游戏状态能保证绝对一致,但GSS可能同时服务上万个玩家,由于机器性能以及网络带宽等硬件资源限制,服务器对大部分情况都无法进行非常严格的检查和处理;p2p模式相对于cs模式,同时连接的玩家有限,所以可以进行比较精细的运算,可实现类似射击类、飞机类的弹幕计算或者格斗类的高精度打击体验,但是,由于端到端的通讯方式,随着同时接入用户的增加,通讯量呈指数级增长,所以,其对同时接入的数量上会限制得比较严格,适合少量同屏的竞技类等游戏。

    p2p模式中,由于存在多份的GSS,如何保证各个GSS一致也需要特殊考虑,       帧同步算法在游戏中的应用,主要就是为了解决p2p模式下的GSS一致性问题。实现原理是将游戏处理细化为帧,对于每帧,在同样的运行环境中,保证同样的输入的情况下,将得到同样的输出结果。

                              

                                                         图3

    图3中,初始状态都为1,序列帧第二帧时,输入加1操作,则状态变为2,第三帧时无输入,状态不变,第四帧时,输入加1操作,状态变为3.对于同个运行环境的各个客户端来说,相同的输入状况下,将得到相关的输出结果,如图4效果。

                      


                                                                           图4

    通常,为了用户的输入能及时的响应以及游戏状态的过度能够平滑,会将GSS设置为20到30帧以上。并且,由于客户端机器性能或者设置的差异,GSS的状态无法与游戏渲染帧实现一一对应,所以,GSS与表现层必须做到完全的分离,否则将因为某些细小的误差被放大最终导致游戏出现完全不同的结果。

                               


                                              图5

    图5,非确定的渲染层的输出,完全由GSS来驱动,GSS保证帧数的稳定,即使出现网络延迟,也必须在确保收到该帧的所有输入后才执行该帧的处理。

    实现方案上,大致可以分出3种,分别是无主机结构、有主机结构、服务器主机结构

    u  无主机结构

    图2的拓扑结构中,所有GSS功能对等,该方案需要进行特殊的对帧处理,确保所有客户端都已经同步并且收到所有的输入。但是,由于网络上的各个客户端完全对等,一旦某个用户网络状况出现延迟或者中断等异常,将影响其他用户的操作体验,所以该方案简单公平但体验容易受限

     

    u  有主机结构

                                        


                                                        图6

    图6,在各个客户端中随机选择一个的GSS作为主机,同时负责对帧控制及输入输出管理,其他GSS仅跟GSS主机通讯,GSS之间互相不通讯。该方案的好处是,游戏的体验只受主机与本机的网络与本机器状况的影响,其他GSS出现的任何故障都不会影响其他人,当GSS主机完全失去联系时,其他GSS也可以重新仲裁得出新的GSS主机来,但该结构主机在客户端,容易给外挂有可乘之机,对输入对帧等能进行特殊处理,最终导致游戏丧失公平性。此方案能保证玩家体验,但安全性较低

    u  服务器主机结构

    服务器主机结构,是将图6的结构中的GSS主机的的对帧控制及输入输出管理功能放在服务器上,降低GSS客户端的客观影响,保证了大部分玩家的体验,且其中有玩家作弊,也能马上检测到,保证游戏的公平性,但结构上已脱离p2p设计,通讯流量随用户增加,负额指数级增长。该方案安全性高,保证玩家体验,但对服务负载有一定的要求。

    u  其他

    融合有/无主机与服务器主机的结构。服务器主机结构的特点在于控制权在服务端,在有状态的网络游戏中,可以有效防止游戏数据修改、游戏加速等外挂,在服务端硬件资源方面,可以增加有/无主机结构减轻负担,大部分功能用有/无主机结构处理,关键操作由服务器主机结构处理等,让GSS主机与服务器主机协同服务

     

    三、        服务器主机结构设计

    服务器主机结构的特点如上所述,这里再深入展开对该结构的分析与设计。

             服务器设计

             


                                                                 图7

    服务器主要是起到控制作用,进行客户端的对帧控制和输入输出管理。如图7,服务器每帧都发驱动帧驱动客户端执行帧处理,当客户端有输入被服务器接收到,则服务器当前帧内将输入同步输出给各个客户端.

    网络上由于客户端的状况多种多样,客户端帧数可能跟不上服务器,如图8所示,如果客户端出现掉帧情况,则在收到驱动帧后需要加速执行,以追上其他客户端的速度,避免掉帧的用户一直在对过去的事件进行响应。

    游戏应该优先保证正常用户的体验,所以当有玩家出现卡帧情况的时候,不应选择暂停其他玩家,而是让他慢慢的追赶上来,设计上,服务器即可以采用客户端的正常速度,按帧驱动客户端,但当网络都出现突发状况的时候,如图9,通讯异常时,2个客户端都对帧数2缺失,如果服务器照常运行,到恢复网络状况时,会出现情况是,每个客户端都卡了几帧之后,加速拉了几帧。所以,针对这种情况,增加客户端的对帧操作,即客户端执行第1帧时,跟服务器说可以播放第二帧了,然后服务器开始驱动第二帧动作,考虑网络延迟情况,可以提前对帧第n帧的,效果如图9,左边客户端第二个对帧操作使服务器开始推动第二帧进行,而右边客户端的第二个对帧动作其实不起任何作用

                              


                                                                           图8

                              


                                                                          图9

     

    伪代码

     代码不贴了

    客户端设计

                              


                                                                 图10

    客户端设计由两部分组成,分别是GSS模块和渲染模块。

    GSS模块包含物品系统、角色系统、AI系统、场景系统还有其他相关系统等,同时,输入输出和帧数控制也一起集成在GSS模块中。GSS中各系统功能分别是:

             物品系统:       游戏物品以及物品的效果

             角色系统:       角色包括玩家角色、npc及apc等

             ai系统:          驱动apc行动的控制模块

             场景系统:     场景物件、地图、寻路等

             其他系统:      其他类似技能、状态等系统

             输入输出模块:       监听玩家输入,将玩家输入上报服务器,同时监听服务器输入,绑定当前帧输出

             帧数控制模块:      监听服务器驱动帧,驱动执行每帧处理

    GSS模块中各个系统的执行,由帧数驱动,不引入其他时间线。有如物品持续时间、状态持续时间等都以帧数作为唯一的时间轴。帧与帧之间的播放频率,则由服务器统一控制,但由于网络抖动等影响,帧的频率并不是太稳定,为避免播放抖动,帧数控制器需要进行一定的平滑处理。

                      


                                                         图11

    客户端的渲染层,由GSS模块驱动,为减少模块间的耦合,GSS模块使用事件通知机制驱动渲染层表现。具体细分事件类型如图12(具体项目具体事件拆解)

                      

    由于渲染层与GSS只做到事务级的同步,而GSS与渲染层的播放速率有可能不同,则为保证较好的表现效果,GSS的逻辑帧需要与渲染层的渲染帧做固定比率的绑定,譬如图13的1:2,当GSS逻辑帧数不变的情况下,渲染帧掉帧时,能经过换算得到当前逻辑帧对应的渲染帧数,出现GSS帧数暂停时,则逻辑帧也跟着一起暂停

                      


                                                                 图13

    逻辑帧与渲染帧绑定算法(伪代码)

             代码不贴了

    其中  OnUpdate由引擎在每帧调用,GetNewestFrame获得逻辑帧通知过来的最新帧,这样,保证了逻辑帧中关键帧进行伤害计算时,渲染帧不会脱帧严重。

     

     

    四、        反外挂与断线重连

             稍等后续文章



    对于一个游戏来讲,战斗就是灵魂,如果战斗做不到极致,其他方面做的再好也是徒劳,这几年,也参与了很多游戏的开发,其中有很多游戏是从决策到死掉全程参与,深有感慨。从端游到页游 从页游再到手游 每一个新市场机会出现的时候,都是从闭着眼睛就能赚钱到大部分赚不到钱过度,对游戏的开发技术都要求也是越来越高,只不过每个市场到成熟的时间都被大大缩短了。对于战斗来讲很多都是策划脑补的跟实际做出来的完全不是一回事,很多原因都是战斗方案选型就是错误的。

      从程序角度来讲,我把战斗从两个维度分类:

      1、从操作方式上分为回合操作、即时操作。

      2、从交互方式上分为 离线战斗 联网战斗,这个地方需要说明一下,有些游戏虽然也能进攻别人,例如COC 但是因为战斗的时候,另外一个人是不可以操作的,类似于这样的战斗也可以称为离线战斗。

      基本上所有的战斗都是以上两种方式在某种程度的组合而已,例如梦幻西游可以认为是联网战斗、回合操作类型。

      最近比较火的全民超神,王者荣耀,属于 (联网战斗|离线战斗)即时操作类型。

      最新网易出的功夫熊猫 属于 (联网战斗|离线战斗)即时操作类型,相对于dota类的全民超神、王者荣耀,他对延迟要求的更高了。

      如果在立项初期,项目计划时候不确定游戏的操作类型,以及网络要求,做得后期要想调整的话,改动是致命的,假设按照之前时空猎人的方式实现的纯离线战斗及时游戏,最多也只能做做离线PVP,如果想增加联网PVP的功能的话,对于程序来讲几乎需要重写战斗。

      随着移动游戏市场越来越成熟,对于战斗的要求也在提高,原来做一款ARPG,只有单机玩法就足够了,不需要开发实时PVP,但是现在市面上的ARPG不仅可以联网PVP,甚至可以联网组队PVE了,所以我感觉如果现在再去做游戏的话,只是一个单机玩法,或者是离线PVP玩法已经远远不能满足现代玩家的口味了。

      我认为现在战斗系统需要满足一下几点。

      1、一定要有离线PVE玩法,或者离线PVP玩法,可以在让玩家在网络不好的时候消遣,节省流量。(全民超神、王者荣耀在5V5匹配时候都有一定几率匹配到离线战斗,这个时候是不耗流量的,其他人全是AI控制的)

      2、一定要有在线PVP,在线PVE,能够让玩家在网络比较好的时候,实时竞技。增加可玩性。

      3、战斗中,尽最大程度节省玩家的流量,例如全民超神这款游戏,一场30分钟的战斗基本上要消耗掉20M的流量,而且此类游戏大部分是玩的联网战斗,基本上在非wifi情况下没法玩。

      4、需要有战斗回放机制,可以让策划设计离线玩法的时候更自由,例如COC,战斗回放基本变成了它游戏的一部分。

      5、防作弊,如果有离线玩法的话,一定有机制对离线玩法的结果进行验证,要不然等你游戏真火了,你就知道错了。

      6、实现难度相对较低。

      对于联网游戏来讲,同步的方式主要分为两种,状态同步、帧同步。

      1、状态同步:顾名思义,是指的将其他玩家的状态行为同步的方式,一帮情况下AI逻辑,技能逻辑,战斗计算都由服务器运算,只是将运算的结果同步给客户端,客户端只需要接受服务器传过来的状态变化,然后更新自己本地的动作状态、Buff状态,位置等就可以了,但是为了给玩家好的体验,减少同步的数据量,客户端也会做很多的本地运算,减少服务器同步的频率以及数据量。

      2、 帧同步:RTS游戏常采用的一种同步技术 ,上一种状态同步方式数据量会随着需要同步的单位数量增长,对于RTS游戏来讲动不动就是几百个的单位可以被操作,如果这些都需要同步的话,数据量是不能被接受的,所以帧同步不同步状态,只同步操作,每个客户端接受到操作以后,通过运算可以达到一致的状态(通过随机种子保证所有客户端随机序列一致),这样的情况下就算单位再多,他的同步量也不会随之增加。

      下面我们从以上的5个方面对各自实现方式进行描述:



      总结一下:

      1、对于回合制战斗来讲,其实选用哪种方式实现不是特别重要了,因为本身实现难度不是很高,采用状态同步也能实现离线战斗验证。所以采用帧同步的必要性不是很大。

      2、对于单位比较多的RTS游戏一定是帧同步,对于COC来讲,他虽然是离线游戏,但是他在一样输入的情况下是能得到一样结果的,所以也可以认为他是用帧同步方式实现的战斗系统。

      3、对于对操作要求比较高的,例如MOBA类游戏有碰撞(玩家、怪物可以互相卡位)、物理逻辑,纯物理类即时可玩休闲游戏,帧同步实现起来比较顺畅,(有开源的Dphysics 2D物理系统可用 它是Determisti的)。

      4、对于战斗时大地图MMORPG的,一个地图内会有成千上百的玩家,不是小房间性质的游戏,只能使用状态同步,只同步自己视野的状态。    

      5、帧同步有个缺点,不能避免玩家采用作弊工具开图。
    展开全文
  • UE4网络同步(二)——深入同步细节

    万次阅读 多人点赞 2017-10-29 11:54:18
    前言 UE同步是一块比较复杂而庞大的模块,里面设计到了很多设计思想,...PS:如果只是想知道怎么使用同步,不建议阅读这篇文章,不过可以参考我另外一篇博客 UE4网络同步(一)——理解同步规则 另外,博主参考的源...

    前言

    UE同步是一块比较复杂而庞大的模块,里面设计到了很多设计思想,技巧,技术。我这里主要是从同步的流程分析,以同步的机制为讲解核心,给大家描述里面是怎么同步的,会大量涉及UE同步模块的底层代码,稍微涉及一点计算机网络底层(Socket相关)相关的知识。
    PS:如果只是想知道怎么使用同步,不建议阅读这篇文章,不过可以参考我另外一篇博客 UE4网络同步(一)——理解同步规则
    另外,博主参考的源码版本比较旧,有些细节略有差异,大家可以作为参考。有时间我会对此进行更新
    目录

    一.基本概念

    二.通信的基本流程

    三.连接的建立

      1. 服务器网络模块初始化流程
      1. 客户端网络模块初始化流程
      1. 服务器与客户端建立连接流程

    四.Actor的同步细节

    五.属性同步细节

      1. 属性同步概述
      1. 重要数据的初始化流程
      1. 发送同步数据流程分析
      1. 属性回调函数执行
      1. 关于动态数组与结构体的同步

    六.RPC执行细节

    七.其他网络特性

      1. Reliable与可靠数据传输

    一. 基本概念

    UE网络是一个相当复杂的模块,这篇文档主要是针对Actor同步,属性同步,RPC等大致的阐述一些流程以及关键的一些类。这里我尽可能将我的理解写下来。
    在UE里面有一些和同步相关的概念与类,这里逐个列举一下并做解释:

    底层通信:

    • Bunch
      一个Bunch里面主要记录了Channel信息,NGUID。同时包含其他的附属信息如是否是完整的Bunch,是否是可靠的等等,可以简单理解为一个数据包,该数据包的数据可能不完整,继承自FNetBitWriter
      InBunch:从Channel接收的数据流串
      OutBunch:从Channel产生的数据流串
    • FBitWriter
      字节流书写器,可以临时写入比特数据用于传输,存储等,继承自FArchive
    • FSocket
      所有平台Socket的基类。
      FSocketBSD:使用winSocket的Socket封装
    • Packet
      从Socket读出来/输出的数据
    • UPackageMap
      生成与维护Object与NGUID的映射,负责Object的序列化。每一个Connection对应一个UPackageMap
      (Packet与Bunch的区别:Packet里面可能不包含Bunch信息)

    基本网络通信:

    • NetDriver
      网络驱动,实际上我们创建使用的是他的子类IPNetDriver,里面封装了基本的同步Actor的操作,初始化客户端与服务器的连接,建立属性记录表,处理RPC函数,创建Socket,构建并管理当前Connection信息,接收数据包等等基本操作。NetDriver与World一一对应,在一个游戏世界里面只存在一个NetDriver。UE里面默认的都是基于UDPSocket进行通信的。
    • Connection
      表示一个网络连接。服务器上,一个客户端到一个服务器的一个连接叫一个ClientConnection。在客户端上,一个服务器到一个客户端的连接叫一个ServerConnection。
    • LocalPlayer
      本地玩家,一个客户端的窗口ViewportClient对应一个LocalPlayer,Localplayer在各个地图切换时不会改变。
    • Channel
      数据通道,每一个通道只负责交换某一个特定类型特定实例的数据信息。ControlChannel:客户端服务器之间发送控制信息,主要是发送接收连接与断开的相关消息。在一个Connection中只会在初始化连接的时候创建一个该通道实例。
      VoiceChannel:用于发送接收语音消息。在一个Connection中只会在初始化连接的时候创建一个该通道实例。
      ActorChannel:处理Actor本身相关信息的同步,包括自身的同步以及子组件,属性的同步,RPC调用等。每个Connection连接里的每个同步的Actor都对应着一个ActorChannel实例。
      常见的只有这3种:枚举里面还有FileChannel等类型,不过没有使用。
    • PlayerController
      玩家控制器,对应一个LocalPlayer,代替本地玩家控制游戏角色。同时对应一个Connection,记录了当前的连接信息,这和RPC以及条件属性复制都是密切相关的。另外,PlayerController记录他本身的ViewTarget(就是他控制额Character),通过与ViewTarget的距离(太远的Actor不会同步)来进行其他Actor的同步处理。
    • World
      游戏世界,任何游戏逻辑都是在World里面处理的,Actor的同步也受World控制,World知道哪些Actor应该同步,保存了网络通信的基础设施NetDriver。
    • Actor
      在世界存在的对象,没有坐标。UE4大部分的同步功能都是围绕Actor来实现的。
    • Dormant
      休眠,对于休眠的Actor不会进行网络同步

    属性同步相关:

    • FObjectReplicator
      属性同步的执行器,每个Actorchannel对应一个FObjectReplicator,每一个FObjectReplicator对应一个对象实例。设置ActorChannel通道的时候会创建出来。
    • FRepState
      针对每个连接同步的历史数据,记录同步前用于比较的Object对象信息,存在于FObjectReplicator里面。
    • FRepLayOut
      同步的属性布局表,记录所有当前类需要同步的属性,每个类或者RPC函数有一个。
    • FRepChangedPropertyTracker
      属性变化轨迹记录,一般在同步Actor前创建,Actor销毁的时候删掉。

    二. 通信的基本流程

    如果我们接触过网络通信,应该了解只要知道对方的IP地址以及端口号,服务器A上进程M_1_Server可以通过套接字向客户端B上的进程M_1_Client发送消息,大致的效果如下:
    这里写图片描述

    • 图2-1 远程进程通信图

    而对于UE4进程内部服务器Server与客户端Client1的通信,与上面的模型基本相似:
    这里写图片描述

    • 图2-2 UE4远程进程通信图

    那这个里面的Channel是什么意思呢?简单理解起来就是一个通信轨道。为了实现规范与通信效率,我们的一个服务器针对某个对象定义了Channel通道,这个通道只与客户端对应的Channel通道互相发送与接收消息。这个过程抽象起来与TCP/UDP套接字的传输过程很像,套接字是在消息发送到进程前就进行处理,来控制客户端进程A只会接收到服务器对应进程A的消息,而这里是在UnrealEditor.exe进程里面处理,让通道1只接收到另一端通道1发送的消息。
    上面的只是针对一个服务器到客户端的传输流程,那么如果是多个客户端呢?
    这里写图片描述

    • 图2-3 Channel通信图

    每一个客户端叫做一个Connection,如图,就是一个server连接到两个客户端的效果。对于每一个客户端,都会建立起一个Connection。在服务器上这个Connection叫做ClientConnection,对于客户端这个Connection叫做ServerConnection。每一个Channel都会归属于一个Connection,这样这个Channel才知道他对应的是哪个客户端上的对象。
    接下来我们继续细化,图中的Channel只标记了1,2,3,那么实际上都有哪些Channel?这些Channel对应的都是什么对象?其实,在第一部分的概念里我已经列举了常见的3中Channel,分别是ControlChannel,ActorChannel,以及VoiceChannel。一般来说,ControlChannel与VoiceChannel在游戏中只存在一个,而ActorChannel则对应每一个需要同步的Actor,所以我们再次细化上面的示意图:
    这里写图片描述

    • 图2-4 Connection下的Channel通信图

    到这里我们基本上就了解了UE4的基本通信架构了,下面我们进一步分析网络传输数据的流程。首先我们要知道,UE4的数据通信是建立在UDP-Socket的基础上的,与其他的通信程序一样,我们需要对Socket的信息进行封装发送以及接收解析。这里面主要涉及到Bunch,RawBunch,Packet等概念,建议参考第一部分的基本概念去理解,很多注释已经加在了流程图里面。如图所示:
    这里写图片描述

    • 图2-5 发送同步信息流程图

    这里写图片描述

    • 图2-6 接收同步信息流程图


    三. 连接的建立

    前面的内容已经提到过,UE的网通通信是基于Channel的,而ControlChannel就是负责
    控制客户端与服务器建立连接的通道,所以客户端与服务器的连接信息都是通过UControlChannel执行NotifyControlMessage函数处理的。下面首先从服务器与客户端的网络模块初始化说起,然后描述二者连接建立的详细流程:


    1.服务器网络模块初始化流程

    从创建GameInstance开始,首先创建NetDriver来驱动网络初始化,进而根据平台创建对应的Socket,之后在World里面监听客户端的消息。
    这里写图片描述

    • 图3-1 服务器网络模块初始化流程图


    2.客户端网络模块初始化流程

    客户端前面的初始化流程与服务器很相似,也是首先构建NetDriver,然后根据平台创建对应的Socket,同时他还会创建一个到服务器的ServerConnection。由于客户端没有World信息,所以要使用一个新的类来检测并处理连接信息,这个类就是UpendingNetGame。
    这里写图片描述

    • 图3-2 客户端网络模块初始化流程图

    3.服务器与客户端建立连接流程

    二者都完成初始化后,客户端就会开始发送一个Hello类型的ControlChannel消息给服务器(上面客户端初始化最后一步)。服务器接收到消息之后开始处理,然后会根据条件再给客户端发送对应的消息,如此来回处理几个回合,完成连接的建立,详细流程参考下图:
    (该流程是本地局域网的连接流程,与在线查找服务器列表并加入有差异)
    这里写图片描述

    • 图3-3 客户端服务器连接建立流程图

    四. Actor的同步细节

    Actor的同步可以说是UE4网络里面最大的一个模块了,里面包括属性同步,RPC调用等,这里为了方便我将他们拆成了3个部分来分别叙述。
    有了前面的描述,我们已经知道NetDiver负责整个网络的驱动,而ActorChannel就是专门用于Actor同步的通信通道。
    这里对Actor同步做一个比较细致的描述:服务器在NetDiver的TickFlush里面,每一帧都会去执行ServerReplicateActors来同步Actor的相关内容。在这里我们需要做以下处理:

    1. 获取到所有连接到服务器的ClientConnections,首先获取引擎每帧可以同步的最大Connection的数量,超过这个限制的忽略。然后对每个Connection几乎都要进行下面所有的操作
    2. 找到要同步的Actor,只有被放到World.NetworkActors里面的Actor才会被考虑,Actor在被Spawn时候就会添加到这个NetworkActors列表里面
    3. 找到客户端玩家控制的角色ViewTarget(ViewTaget与摄像机绑定在一起),这个角色的位置是决定其他Actor是否同步的关键
    4. 验证Actor,对于要销毁的以及所有权Role为ROLE_NONE的Actor不会同步
    5. 是否到达Actor同步时间,Actor的同步是有一定频率的,Actor身上有一个NetUpdateTime,每次同步前都会通过下面这个公式来计算下一次Actor的同步时间,如果没有到达这个时间就会放弃本次同步Actor->NetUpdateTime = World->TimeSeconds + FMath::SRand() * ServerTickTime + 1.f/Actor->NetUpdateFrequency;
    6. 如果这个Actor设置OnlyRelevantToOwner,那么就会放到一个特殊的列表里面OwnedConsiderList然后只同步给属于他的客户端。否则会把Actor放到ConsiderList里面
    7. 对于休眠状态的Actor不会进行同步,对于要进入休眠状态的Actor也要特殊处理关闭同步通道
    8. 查看当前的Actor是否有通道Channel,如果没有,还要看看Actor是否已经加在了场景,没有加载就跳过同步
    9. 接第8个条件——没有Channel的情况下,还会执行Actor::IsNetRelevantFor判断是否网络相关,对于不可见的或者太远的Actor会返回false,不会同步
    10. Actor的同步数量可能非常大,所以有必要对所有的Actor进行一个优先级的排列
      处理完上面的逻辑后会对优先级表里的所有Actor进行排序
    11. 排序后,如果连接没有加载此 actor 所在的关卡,则关闭通道(如果存在)并继续
      每 1 秒钟调用一次 AActor::IsNetRelevantFor,确定 actor 是否与连接相关,如果不相关的时间达到 5 秒钟,则关闭通道
      如果要同步的Actor没有ActorChannel就给其创建一个并绑定Actor,执行同步并更新NetUpdateTime = Actor->GetWorld()->TimeSeconds + 0.2f * FMath::FRand();
      如果此连接出现饱和剩下的 actor会根据连接相关时间判断是否在下一个时钟更新
    12. 执行UActorChannel::ReplicateActor执行真正的Actor同步以及内部数据的同步,这里会将Actor(PackageMap->SerializeNewActor),Actor子对象以及其属性序列化(ReplicateProperties)封装到OutBunch并发送给客户端
      (备注:我们当前版本下面的逻辑都是写在UNetDriver::ServerReplicateActors里面,4.12以后的UE4已经分别把Connection预处理,获取同步Actor列表,优先级处理等逻辑封装到单独的函数里了,详见ServerReplicateActors_BuildConsiderlist, ServerReplicateActors_PrioritizedActors, ServerReplicateActors_ProsessPrioritizedActors等函数
      优先级排序规则是什么?答案是按照是否有controller,距离以及是否在视野。通过FActorPriority构造代码可以定位到APawn::GetNetPriority,这里面会计算出当前Actor对应的优先级,优先级越高同步越靠前,是否有Controller的权重最大)
      总之,大体上Actor同步的逻辑就是在TickFlush里面去执行ServerReplicateActors,然后进行前面说的那些处理。最后对每个Actor执行ActorChannel::ReplicateActor将Actor本身的信息,子对象的信息,属性信息封装到Bunch并进一步封装到发送缓存中,最后通过Socket发送出去。

    下面是服务器的同步Actor的发送Bunch堆栈:(与UE默认的有些不同)
    这里写图片描述

    • 图4-1 服务器同步Actor堆栈图

    下面描述客户端是如何接收到服务器同步过来的Actor的。首先客户端TickDispatch检测服务器的消息,收到消息后通过Connection以及Channel进行解析(第二部分已经讲解),最后一步解析出完整数据的操作在UActorChannel::ProcessBunch执行,在这个函数里面:

    1. 如果发现当前的ActorChannel对应的Actor为NULL,就对当前的Bunch进行反序列化Connection->PackageMap->SerializeNewActor(Bunch, this, NewChannelActor);解析出Actor的内容并执行PostInitializeComponents。如果Actor不为NULL,跳过这一步(参考下面图一堆栈)
    2. 随后根据Bunch信息找到同步过来的属性值并对当前Actor对应的属性进行赋值
    3. 最后执行PostNetInit调用Actor的BeginPlay。(参考下面图二堆栈)

    下面截取了客户端接收到同步Actor并初始化的调用堆栈:
    在这里插入图片描述

    • 图4-2 客户端接收并序列化同步的Actor堆栈图

    • 图4-3 客户端初始化同步过来Actor堆栈图

    从上面的描述来看,基本上我们可以很容易的分析出当前的Actor是否被同步,比如在UActorChannel::ReceivedBunch里面打个断点,看看当前通道里有没有你要的Actor就可以了。
    至于里面更详细的内容,就建议大家去代码里面调试吧。


    五. 属性同步细节

    1.属性同步概述

    属性同步是一个很复杂的模块,我在另一个关于UE4网络的思考文档里面讲解了属性同步相关的使用逻辑以及注意事项。这里我尽可能的分析一下属性同步的实现原理。
    有一点需要先提前说明一下,服务器同步的核心操作就是比较当前的同步属性是否发生变化,如果发生就将这个数据通过到客户端。如果是普通逻辑处理,我们完全可以保存当前对象的一个拷贝对象,然后每帧去比较这个拷贝与真实的对象是否发生变化。不过,由于同步数据量巨大,我们不可能给每个需要同步的对象都创建一个新的拷贝,而且这个逻辑如果暴露到逻辑层的话会使代码异常复杂难懂,所以这个操作要统一在底层处理。那么,UE4的基本思路就是获取当前同步对象的空间大小,然后保存到一个buffer里面,然后根据属性的OffSet给每个需要同步的属性初始化。这样,就保存了一份简单的“拷贝”用于之后的比较。当然,我们能这么做的前提是存在UE的Object对象反射系统。
    下面开始进一步描述属性同步的基本思路:我们给一个Actor类的同步属性A做上标记Replicates(先不考虑其他的宏),然后UClass会将所有需要同步的属性保存到ClassReps列表里面,这样我们就可以通过这个Actor的UClass获取这个Actor上所有需要同步的属性,当这个Actor实例化一个可以同步的对象并开始创建对应的同步通道时,我们就需要准备属性同步了。
    首先,我们要有一个同步属性列表来记录当前这个类有哪些属性需要同步(FRepLayout,每个对象有一个,从UClass里面初始化);其次,我们需要针对每个对象保存一个缓存数据,来及时的与发生改变的Actor属性作比较,从而判断与上一次同步前是否发生变化(FRepState,里面有一个Staticbuff来保存);然后,我们要有一个属性变化跟踪器记录所有发生改变同步属性的序号(可能是因为节省内存开销等原因所以不是保存这个属性),便于发送同步数据时处理(FRepChangedPropertyTracker,对各个Connection可见,被各个Connection的Repstate保存一个共享指针)。最后,我们还需要针对每个连接的每个对象有一个控制前面这些数据的执行者(FObjectReplicator)。
    这四个类就是我们属性同步的关键所在,在同步前我们需要对这些数据做好初始化工作,然后在真正同步的时候去判断与处理。


    2.重要数据的初始化流程

    下面的两个图分别是属性同步的服务器发送堆栈以及客户端的接收堆栈。
    在这里插入图片描述

    • 图5-1服务器发送属性堆栈图

    这里写图片描述

    • 图5-2客户端接收属性堆栈图

    从发送堆栈中我们可以看到属性同步是在执行ReplicatActor的同时进行的,所以我们也可以猜到属性同步的准备工作应该与Actor的同步准备工作是密不可分的。前面Actor同步的讲解中我们已经知道,当Actor同步时如果发现当前的Actor没有对应的通道,就会给其创建一个通道并执行SetChannelActor。这个SetChannelActor所做的工作就是属性同步的关键所在,这个函数里面会对上面四个关键的类构造并做初始化,详细的内容参考下图:

    这里写图片描述

    • 图5-3 SetChannelActor流程解析图

    图中详细的展示了几个关键数据的初始化,不过第一次看可能对这个几个类的关系有点晕,下面给大家简单画了一个类图。
    这里写图片描述

    • 图5-4 属性同步相关类图

    具体来说,每个ActorChannel在创建的时候会创建一个FObjectReplicator用来处理所有属性同步相关的操作,同时会把当前对应通道Actor的同步的属性记录在FRepLayOut的Parents数组里面(Parents记录了每个属性的UProperty,复制条件,在Object里面的偏移等),同时把这个RepLayOut存储到RepState里面,该RepState指针也会被存储到FObjectReplicator里面,RepState会申请一个缓存空间用来存放当前的Object对象(并不是完整对象,只包含同步属性,但是占用空间大小是一样的)。FRepChangedPropertyTracker在创建RepState的同时也被创建,然后通过FRepLayOut的Parents数量来初始化他的记录表的大小,并记录对应的位置是否是条件复制属性,RepState里面保存一个指向他的指针。
    (关于Parents属性与CMD属性:Replayout里面,数组parents示当前类所有的需要同步的属性,而数组cmd会将同步的复杂类型属性【包括数组、结构体、结构体数组但不包括类类型的指针】进一步展开放到这里面。比如ClassA里面有一个StructB属性,这个属性被标记同步,StructB属性会被放到parents里面。由于StructB里面有一个Int类型C属性以及D属性,那么C和D就会被放到Cmd数组里面。有关结构体的属性同步第5部分还有详细描述)


    3.发送同步数据流程分析

    前面我们基本上已经做好了同步属性的基本工作,下面开始执行真正的同步流程。
    

    在这里插入图片描述

    • 图5-5服务器发送属性堆栈图

    再次拿出服务器同步属性的流程,我们可以看到属性同步是通过FObjectReplicator::
    ReplicateProperties函数执行的,进一步执行RepLayout->ReplicateProperties。这里面比较重要的细节就是服务器是如何判断当前属性发生变化的,我们在前面设置通道Actor的时候给FObjectReplicator设置了一个Object指针,这个指针保存的就是当前同步的对象,而在初始化RepState的同时我们还创建了一个Staticbuffer,并且把buffer设置和当前Object的大小相同,对buffer取OffSet把对应的同步属性值添加到buffer里面。所以,我们真正比较的就是这两个对象,一般来说,staticbuffer在创建通道的同时自己就不会改变了,只有当与Object比较发现不同的时候,才会在发送前把属性值置为改变后的。这对于长期同步的Actor没什么问题,但是对于休眠的Actor就会出现问题了,因为每次删除通道并再次同步强制同步的时候这里面的staticbuff都是Object默认的属性值,那比较的时候就可能出现0不同步这样奇怪的现象了。真正比较两个属性是否相同的函数是PropertiesAreIdentical(),他是一个static函数。
    这里写图片描述

    • 图5-6 服务器同步属性流程图

    4.属性回调函数执行

    虽然属性同步是由服务器执行的,但是FObjectReplicator,RepLayOut这些数据可并不是仅仅存在于服务器,客户端也是存在的,客户端也有Channel,也需要执行SetChannelACtor。不过这些数据在客户端上的作用可能就有一些变化,比如Staticbuffer,服务器是用它存储上次同步后的对象,然后与当前的Object比较看是否发生变化。在客户端上,他是用来临时存储当前同步前的对象,然后再把通过过来的属性复制给当前Object,Object再与Staticbuffer对象比较,看看属性是否发生变化,如果发生变化,就在Replicator的RepState里面添加一个函数回调通知RepNotifies。
    在随后的ProcessBunch处理中,会执行RepLayout->CallRepNotifies( RepState, Object );处理所有的函数回调,所以我们也知道了为什么接收到的属性发生变化才会执行函数回调了。
    在这里插入图片描述

    • 图5-7 客户端属性回调堆栈图

    5.关于动态数组与结构体的同步

    结构体:UE里面UStruct类型的结构体与C++的Struct不一样,在反射系统中对应的是UScriptStruct,他本身可以被标记Replicated并且结构体内的数据默认都会被同步,而且如果里面有还子结构体的话也也会递归的进行同步。如果不想同步的话,需要在对应的属性标记NotReplicated,而且这个标记只对UStruct有效,对UClass无效。这一段的逻辑在FRepLayout::InitFromObjectClass处理,ReplayOut首先会读取Class里面所有的同步属性并逐一的放到FRepLayOut的数组Parents里面,这个Parents里面存放的就是当前类的继承树里面所有的同步属性。随后对Parents里面的属性进一步解析(FRepLayout::InitFromProperty_r),如果发现当前同步属性是数组或者是结构体就会对其进行递归展开,将数组的每一个元素/UStruct里面的每一个属性逐个放到FRepLayOut的Cmds数组里面,这个过程中如果遇到标记了NotReplicate的UStruct内部属性,就跳过。所以Cmds里面存放的就是对数组或者结构体进一步展开的详细属性。
    在这里插入图片描述

    • 图5-8 Parents内部成员截图

    在这里插入图片描述

    • 图5-9 Cmds内部成员截图

    Struct结构内的数据是不能标记Replicated的。如果你给Struct里面的属性标记Replicated,UHT在编译的时候就会提醒你编译失败"Struct members cannot be replicated"。这个提示多多少少会让人产生误解,实际上这个只是表明UStruct内部属性不能标记Replicated而已。最后,UE里面的UStruct不可以以成员指针的方式在类中声明。
    数组:数组分为两种,静态数组与动态数组。静态数组的每一个元素都相当于一个单独的属性存放在Class的ClassReps里面,同步的时候也是会逐个添加到RepLayOut的Parents里面,参考上面的图5-8。UE里面的动态数组是TArray,他在网络中是可以正常同步的,在初始化RepLayOut的Cmds数组的时候,就会判断当前的属性类型是否是动态数组(UArrayProperty),并会给其cmd.type做上标记REPCMD_DynamicArray。后面在同步的时候,就会通过这个标记来对其做特殊处理。比如服务器上数组长度发生变化,客户端在接收同步过来的数组时,会执行FRepLayout::ReceiveProperties_DynamicArray_r来处理动态数组。这个函数里面会矫正当前对象同步数组的大小。
    (之前发生过休眠动态数组同步不正常的情况,但是现在无法重现,如果出现同步不正常的情况请告知我一下,以便进一步完善这个文档。)


    六. RPC执行细节

    RepLayOut参照表不止同步的对象有,函数也同样有,RPC的执行同样也是通过属性同步的这个框架。比如我们在代码里面写了一个Client的RPC函数ClientNotifyRespawned,那UHT会给我们生成一个.genenrate.cpp文件,里面会有这个函数的真正的定义如下:

    void APlayerController::ClientNotifyRespawned(class APawn* NewPawn, bool IsFirstSpawn)
    {
    	PlayerController_eventClientNotifyRespawned_Parms Parms;
    	Parms.NewPawn=NewPawn;
    	Parms.IsFirstSpawn=IsFirstSpawn ? true : false;
    	ProcessEvent(FindFunctionChecked(ENGINE_ClientNotifyRespawned),&Parms);
    }
    

    而我们在代码里的函数之所以必须要加上_Implementation,就是因为在调用端里面,实际执行的是.genenrate.cpp文件函数,而不是我们自己写的这个。同时结合下面的RPC执行堆栈,我们可以看到在Uobject这个对象系统里,我们可以通过反射系统查找到函数对应的UFuntion结构,同时利用ProcessEvent函数来处理UFuntion。通过识别UFunction里面的标记,可以知道这个函数是不是一个RPC函数,是否需要发送给其他的端。
    当我们开始调用CallRemoteFunction的时候,RPC相关的初始化就开始了。NetDiver会进行相关的初始化,并试着获取RPC函数的Replayout,那么问题是函数有属性么?正常来说,函数本身就是一个执行过程,函数名是一个起始的执行地址,他本身是没有内存空间,更不用说存储属性了。不过,在UE4的反射系统里面,函数可以被额外的定义为一个UFunction,从而保存自己相关的数据信息。RPC函数的参数就被保存在UFunction的基类Ustruct的属性链表PropertyLink里面,RepLayOut里面的属性信息就是从这里获取到的。
    一旦函数的RepLayOut被创建,也同样会放到NetDiver的RepLayoutMap里面。随后立刻调用FRepLayout::SendPropertiesForRPC将RPC的参数序列化封装与RPC函数一同发送。
    这里写图片描述

    • 图6-1 RPC函数的RepLayOut初始化堆栈图

    简单概括了RPC的发送,这里再说一下RPC的接收。当客户端收到上面的RPC发来的数据后,他需要一步一步的解析。首先,他会执行ReceivePropertiesForRPC来接收解析RPC函数传来的参数并做一些判断确定是否符合执行条件,如果符合就会通过ProcessEvent去处理传递过来的属性信息,找到对应的函数地址(或者说函数指针)等,最后调用该RPC函数。
    这里的ReplayOut里面的Parents负责记录当前Function的属性信息以及属性位置,在网络同步的过程中,客户端与服务器保存一个相同的ReplayOut,客户端才能在反序列化的时候通过OffSet位置信息正确的解析出服务器传来的RPC函数的N个参数。
    这里写图片描述

    • 图6-2 接收RPC函数的传递的参数堆栈图

    这里写

    • 图6-3 客户端执行RPC函数堆栈图

    最后客户端是怎样调用到带_Implementation的函数呢?这里又需要用到反射的机制。我们看到UHT其实会给函数生成一个.genenrate.h文件,这个文件就有下面这样的宏代码,把宏展开的话其实就是一个标准的C++文件,我们通过函数指针最后找到的就是这个宏里面标记的函数,进而执行我们自己定义的_Implementation函数。

    virtual void ClientNotifyRespawned_Implementation(class APawn* NewPawn, bool IsFirstSpawn);\ 
    DECLARE_FUNCTION(execClientNotifyRespawned) \
    { \
    	P_GET_OBJECT(APawn,NewPawn); \
    	P_GET_UBOOL(IsFirstSpawn); \
    	P_FINISH; \
    	this->ClientNotifyRespawned_Implementation(NewPawn,IsFirstSpawn); \
    } \
    


    七. 其他网络特性(待更新)

    1.Reliable与可靠数据传输
    我们知道RPC函数可以通过标记Reliable来保证远程执行,这个Reliable其实就是通过UE的上层机制来实现的。一般来说,如果使用TCP协议,我们在代码层面不需要做任何处理就可以实现可靠数据传输。但是,考虑到TCP协议的三次握手,保持连接,拥塞控制等等机制会影响传输效率,所以UE4仍然使用UDP作为运输层协议,通过上层机制来保证数据的可靠性。
    对于RPC函数:在执行UNetDriver::InternalProcessRemoteFunction会判断当前函数是否标记了Reliable
    if (Function->FunctionFlags & FUNC_NetReliable)
    {
    Bunch.bReliable = 1;
    }
    对于Actor及其属性:在执行UActorChannel::ReplicateActor 时只要Actor的bNetTemporary为false,那么Actor的同步就是可靠的。(bNetTemporary表示Actor只在创建时同步一次)
    Bunch.bReliable = !Actor->bNetTemporary;
    关于UDP实现可靠数据传输的思路与TCP相同,主要是解决丢包与包的顺序不对的问题。进一步来讲,就是发送端给数据包标号,接收端按照标号的顺序接收,如果接收到正常顺序的包就发送ACK应答给发送端,发送端如果没有收到ACK就表示丢包,需要重发。这里面的细节并没有去仔细研究,大家如果有兴趣可以参考下面几个函数。网上有篇博客对这部分有简单的讲解 —— UE4网络模块分析。
    UChannel::PreBunch,UNetConnection::ReceivePacket,UNetConnection::ReceiveNak,UChannel::ReceiveNak,UChannel::ReceivedAcks。

    最后再推荐几个博客
    http://www.jianshu.com/p/b4f1a5412cc9
    http://www.cnblogs.com/ghl_carmack/
    https://www.zhihu.com/people/fjz13/posts

    原文链接(转载请标明):http://blog.csdn.net/u012999985/article/details/78384199

    展开全文
  • UE4网络同步详解(一)——理解同步规则

    万次阅读 多人点赞 2017-10-15 21:41:40
    这篇文章主要以问题的形式,针对UE同步的各个方面的内容,...如果想深入了解同步的实现原理,可以参考UE4网络同步(二)——深入同步细节 问题一:如何理解Actor与其所属连接? 附加:1. Actor的Role是ROLE_Autho...

        这篇文章主要以问题的形式,针对UE同步的各个方面的内容,做一个详细而充分的讨论。对于新手理解UE的同步机制非常有帮助,对于有一定的基础而没有深入的UE程序也或许有一些启发。如果想深入了解同步的实现原理,可以参考  UE4网络同步(二)——深入同步细节

            问题一:如何理解Actor与其所属连接?

                附加:1. Actor的Role是ROLE_Authority就是服务端么?

            问题二:你真的会用RPC么?

      附加:1. 多播MultiCast RPC会发送给所有客户端么?

            问题三:COND_InitialOnly怎么用?

            问题四:客户端与服务器一致么?

            问题五:属性同步的基本规则是?

                附加:1.  结构体的属性同步有什么特别的?

     问题六:组件同步的基本规则是?

            Tips:同步注意的一些小细节

     

     

    问题一:如何理解Actor与其所属连接?

          https://api.unrealengine.com/CHN/Gameplay/Networking/Actors/OwningConnections/index.html。UE4官网关于网络链接这一块其实已经将的比较详细了,不过有一些内容没有经验的读者看起来可能还是比较吃力。

          按照官网的顺序,我一点点给出我的分析与理解。首先,大家要简单了解一些客户端的连接过程。

     主要步骤如下:

          1.客户端发送连接请求。

          2.如果服务器接受连接,则发送当前地图。

          3.服务器等待客户端加载此地图。

         4.加载之后,服务器将在本地调用 AGameMode::PreLogin。这样可以使 GameMode 有机会拒绝连接

          5.如果接受连接,服务器将调用 AGameMode::Login该函数的作用是创建一个 PlayerController,可用于在今后复制到新连接的客户端。成功接收后,这个 PlayerController 将替代客户端的临时PlayerController (之前被用作连接过程中的占位符)。

         此时将调用 APlayerController::BeginPlay。应当注意的是,在此 actor 上调用RPC 函数尚存在安全风险。您应当等待 AGameMode::PostLogin 被调用完成。

         6.如果一切顺利,AGameMode::PostLogin 将被调用。

         这时,可以放心的让服务器在此 PlayerController 上开始调用RPC 函数。

          那么这里面第5点需要重点强调一下。我们知道所谓连接,不过就是客户端连接到一个服务器,在维持着这个连接的条件下,我们才能真正的玩“网络游戏”。通常,如果我们想让服务器把某些特定的信息发送给特定的客户端,我们就需要找到服务器与客户端之间的这个连接。这个链接的信息就存储在PlayerController的里面,而这个PlayerController不能是随随便便创建的PlayerController,一定是客户端第一次链接到服务器,服务器同步过来的这个PlayerController(也就是上面的第五点,后面称其为拥有连接的PlayerController)。进一步来说,这个Controller里面包含着相关的NetDriver,Connection以及Session信息。

         对于任何一个Actor(客户端上),他可以有连接,也可以无连接。一旦Actor有连接,他的Role(控制权限)就是ROLE_AutonomousProxy,如果没有连接,他的Role(控制权限)就是ROLE_SimulatedProxy 。

         那么对于一个Actor,他有三种方法来得到这个连接(或者说让自己属于这个连接):

         1.设置自己的owner为拥有连接的PlayerController,或者自己owner的owner为拥有连接的PlayerController。也就说官方文档说的查找他最外层的owner是否是PlayerController而且这个PlayerController拥有连接。

          2.这个Actor必须是Pawn并且Possess了拥有连接的PlayerController。这个例子就是我们打开例子程序时,开始控制一个角色的情况。我们控制的这个角色就拥有这个连接。

         3.这个Actor设置自己的owner为拥有连接的Pawn。这个区别于第一点的就是,Pawn与Controller的绑定方式不是通过Owner这个属性。而是Pawn本身就拥有Controller这个属性。所以Pawn的Owner可能为空。 (Owner这个属性在Actor里面,蓝图也可以通过GetOwner来获取)

         对于组件来说,那就是先获取到他所归属的那个Actor,然后再通过上面的条件来判断。

         我这里举几个例子,玩家PlayerState的owner就是拥有连接的PlayerController,Hud的owner是拥有连接的PlayerController,CameraActor的owner也是拥有连接的PlayerController。而客户端上的其他NPC(一定是在服务器创建的)是都没有owner的Actor,所以这些NPC都是没有连接的,他们的Role就为ROLE_SimulatedProxy。

         所以我们发现这些与客户端玩家控制息息相关的Actor才拥有所谓的连接。不过,进一步来讲,我们要这连接还有什么用?好吧,照搬官方文档。

         连接所有权是以下情形中的重要因素:

                   1.RPC需要确定哪个客户端将执行运行于客户端的 RPC

                   2.Actor复制与连接相关性

                   3.在涉及所有者时的 Actor 属性复制条件

         对于RPC,我们知道,UE4里面在Actor上调用RPC函数,可以实现类似在客户端与服务器之间发送可执行的函数的功能。最基本的,当我一个客户端拥有ROLE_AutonomousProxy权限的Actor在服务器代码里调用RPC函数(UFUNCTION(Reliable,Client))时,我怎么知道应该去众多的客户端的哪一个里面执行这个函数。(RPC的用法不细说,参考官方文档)答案就是通过这个Actor所包含的连接。关于RPC进一步的内容,下个问题里再详细描述。

         第二点,Actor本身是可以同步的,他的属性当然也是。这与连接所有权也是息息相关。因为有的东西我们只需要同步给特定的客户端,其他的客户端不需要知道,(比如我当前的摄像机相关内容)。

         对于第三点,其实就是Actor的属性是否同步可以进一步根据条件来做限制,有时候我们想限制某个属性只在拥有ROLE_AutonomousProxy的Actor使用,那么我们对这个Actor的属性ReplicatedMovement写成下面的格式就可以了。

           voidAActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty > &OutLifetimeProps )const
         {
              DOREPLIFETIME_CONDITION( AActor, ReplicatedMovement,COND_AutonomousOnly );
         }

     

         而经过前面的讨论我们知道ROLE_AutonomousProxy与所属连接是密不可分的。

         最后,这里留一个思考问题:如果我在客户端创建出一个Actor,然后把它的Owner设置为带连接的PlayerController,那么他也有连接么?这个问题在下面的一节中回答。

    附加:Actor的Role是ROLE_Authority就是服务端么?

             并不是,有了前面的讲述,我们已经可以理解,如果我在客户端创建一个独有的Actor(不能勾选bReplicate,参考第五条思考)。那么这个Actor的Role就是ROLE_Authority,所以这时候你就不能通过判断他的Role来确定当前调试的是客户端还是服务器。这时候最准确的办法是获取到NetDiver,然后通过NetDiver找到Connection。(事实上,GetNetMode()函数就是通过这个方法来判断当前是否是服务器的)对于服务器来说,他只有N个ClientConnections,对于客户端来说只有一个serverConnection。

            如何找到NetDriver呢?可以参考下面的图片,从Outer获取到当前的Level,然后通过Level找到World。World里面就有一个NetDiver。当然,方法不止这一个了,如果有Playercontroller的话,Playercontroller上面也有NetConnection,可以再通过NetConnection再获取到NetDiver。还可以通过堆栈,找到World。

     

    问题二:你真的会用RPC么?

         在看下面的图之前,先提出一个问题:
         对于一个形如UFUNCTION(Reliable,Client)的RPC函数,我们知道这个函数应该在服务器调用,在客户端执行。可是如果我在Standalone的端上执行该函数的时候会发生什么呢?
         答案是在服务器上执行。其实这个结果完全可以参考下面的这个官方图片。
         刚接触RPC的朋友可能只是简单的记住这个函数应该从哪里调用,然后在哪里执行。不过要知道,即使我声明一个在服务器调用的RPC我还是可以不按套路的在客户端去调用(有的时候并不是我们故意的,而是编写者没有理解透彻),其实这种不合理的情况UE早就帮我想到并且处理了。比如说你让自己客户端上的其他玩家去调用一个通知服务器来执行的RPC,这肯定是不合理的,因为这意味着你可以假装其他客户端随意给服务器发消息,这种操作与作弊没有区别~所以RPC机制就会果断丢弃这个操作。

     

          所以大家可以仔细去看看上面的这个图片,对照着理解一下各个情况的执行结果,无非就是三个变量:1、在哪个端调用2、当前执行RPC的Actor归属于哪个连接3、RPC的类型是什么。
         不过看到这里,再结合上一节结尾提到的问题,如果我在客户端创建一个Actor。把这个Actor的Owner设置为一个带连接PlayerController会怎么样呢?如果在这里调用RPC呢?
         我们确实可以通过下面这种方式在客户端给新生成的Actor指定一个Owner。

     

         好吧,关键时候还是得搬出来官方文档的内容。

         您必须满足一些要求才能充分发挥 RPC 的作用:

              1.      它们必须从 Actor 上调用。

             2.      Actor必须被复制。

              3.      如果 RPC 是从服务器调用并在客户端上执行,则只有实际拥有这个 Actor 的客户端才会执行函数。

              4.      如果 RPC 是从客户端调用并在服务器上执行,客户端就必须拥有调用 RPC 的 Actor。

              5.      多播 RPC 则是个例外:

                        o   如果它们是从服务器调用,服务器将在本地和所有已连接的客户端上执行它们。

                        o   如果它们是从客户端调用,则只在本地而非服务器上执行。

                        o    现在,我们有了一个简单的多播事件限制机制:在特定 Actor 的网络更新期内,多播函数将不会复制两次以上。按长期计划,我们会对此进行改善,同时更好的支持跨通道流量管理与限制。

          看完第二条,其实你就能理解了,你的Actor必须要被复制,也就是说必须是bReplicate属性为true,Actor是从服务器创建并同步给客户端的(客户端如果勾选了bReplicate就无法在客户端上正常创建,参考第四条问题)。所以,这时候调用RPC是失效的。我们不妨去思考一下,连接存在的意义本身就是一个客户端到服务器的关联,这个关联的主要目的就是为了执行同步。如果我只是在客户端创建一个给自己看的Actor,根本就不需要网络的连接信息(当然你也没有权限把它同步给服务器),所以就算他符合连接的条件,仍然是一个没有意义的连接。同时,我们可以进一步观察这个Actor的属性,除了Role以外,Actor身上还有一个RemoteRole来表示他的对应端(如果当前端是客户端,对应端就是服务器,当前端是服务器,对应端就是客户端)。你会发现这个在客户端创建的Actor,他的Role是ROLE_Authority(并不是ROLE_AutonomousProxy),而他的RemoteRole是ROLE_None。这也说明了,这个Actor只存在于当前的客户端内。


         下面我们讨论一下RPC与同步直接的关系,这里先提出一个问题,
         问题:服务器ActorA在创建一个新的ActorB的函数里同时执行自身的一个Client的RPC函数,RPC与ActorB的同步哪个先执行?

         答案是RPC先执行。你可以这样理解,我在创建一个Actor的同时立刻执行了RPC,那么RPC相关的操作会先封装到网络传输的包中,当这个函数执行完毕后,服务器再去调用同步函数并将相关信息封装到网络包中。所以RPC的消息是靠前的。
    那么这个问题会造成什么后果呢?
         1.  当你创建一个新的Actor的同时(比如在一个函数内),你将这个Actor作为RPC的参数传到客户端去执行,这时候你会发现客户端的RPC函数的参数为NULL。

         2.  你设置了一个bool类型属性A并用UProperty标记了一个回调函数OnRep_Use。你先在服务器里面修改了A为true,同时你调用了一个RPC函数让客户端把A置为true。结果就导致你的OnRep_Use函数没有执行。但实际上,这会导致你的OnRep_Use函数里面还有其他的操作没有执行。

         如果你觉得上面的情况从来没有出现过,那很好,说明暂时你的代码没有类似的问题,
         但是我觉得有必要提醒一下大家,因为UE4代码里面本身就有这样的问题,你以后也很有可能遇到。下面举例说明实际可能出现的问题:

         情况1:当我在服务器创建一个NPC的时候,我想让我的角色去骑在NPC上并控制这个NPC,所以我立刻就让我的Controller去Possess这个NPC。在这个过程中,PlayerController就会执行UFUNCTION(Reliable,Client) void ClientRestart (APawn*NewPawn)函数。当客户端收到这个RPC函数回调的时候就发现我的APlayerController::ClientRestart_Implementation (APawn* NewPawn)里面的参数为空~原因就是因为这个NPC刚在服务器创建还没有同步过来。

         情况2:对于Pawn里面的Controller成员声明如下
         UPROPERTY(replicatedUsing = OnRep_Controller)
         AController* Controller;
         OnRep_Controller回调函数里面回去执行Controller->SetPawnFromRep(this);进而执行
         Pawn = InPawn;

         OnRep_Pawn();

         下面重点来了,OnRep_Pawn函数里面会执行OldPawn->Controller=NULL;将客户端之前Controller控制的角色的Controller设置为空。到现在来看没有什么问题。那么现在结合上面第二个问题,如果一个RPC函数执行的时候在客户端的Controller同步前就修改为正确的Controller,那么OnRep_Controller回调函数就不会执行。所以客户端的原来Controller控制的OldPawn的Controller就不会置为空,导致的结果是客户端和服务器竟然不一样。
         实际上,确实存在这么一个函数,这个RPC函数就是ClientRestart。这看起来就很奇怪,因为ClientRestart如果没有正常执行的话,OnRep_Controller就会执行,进而导致客户端的oldPawn的Controller为空(与服务器不同,因为服务器并没有去设置OldPawn的Controller)。我不清楚这是不是UE4本身设计上的BUG。(不要妄想用AlwaysReplicate宏去解决,参考第八条有关AlwaysReplicate的使用)
         不管怎么说,你需要清楚的是RPC的执行与同步的执行是有先后关系的,而这种关系会影响到代码的逻辑,所以之后的代码有必要考虑到这一点。

         最后,对使用RPC的朋友做一个提醒,有些时候我们在使用UPROPERTY标记Server的函数时,可能是从客户端调用,也可能是从服务器调用。虽然结果都是在服务器执行,但是过程可完全不同。从客户端调用的在实际运行时是通过网络来处理的,一定会有延迟。而从服务器调用的则会立刻执行。

     附加:1.多播MultiCast RPC会发送给所有客户端么?

     看到这个问题,你可能想这还用说么?不发给所有客户端那要多播干什么?但事实上确实不一定。

    考虑到服务器上的一个NPC,在地图的最北面,有两个客户端玩家。一个玩家A在这个NPC附近,另一个玩家B在最南边看不到这个NPC(实际上就是由于距离太远,服务器没有把这个Actor同步到这个B玩家的客户端)。我们现在在这个NPC上调用多播RPC通知所有客户端上显示一个提示消失“NPC发现了宝藏”。这个消息会不会发送到B客户端上面?

    情况一:会。多播顾名思义就是通知所有客户端,不需要考虑发送到哪一个客户端,直接遍历所有的连接发送即可。

    情况二:不会。RPC本来就是基于Actor的,在客户端B上面连这个Actor都没有,我还可以使用RPC不会很奇怪?

     第一种情况强化了多播的概念,淡化了RPC基于Actor的机制,情况二则相反。所以看起来都有道理。实际上,UE4里面更偏向第二种情况,处理如下:

     如果一个多播标记为Reliable,那么他默认会给所有的客户端执行该多播事件,如果其标记的是unreliable,他就会检测该NPC与客户端B的网络相关性(即在客户端B上是否同步)。但实际上,UE还是认为开发者不应该声明一个Reliable的多播函数。下面给出UE针对这个问题的相关注释:(相关的细节在另一篇进一步探索UE网络同步的文档里面去分析)

    // Do relevancy check if unreliable.
    
    // Reliables will always go out. This is oddbehavior. On one hand we wish to garuntee "reliables always getthere". On the other
    
    // hand, replicating a reliable to something on theother side of the map that is non relevant seems weird.
    
    // Multicast reliables should probably never beused in gameplay code for actors that have relevancy checks. If they are, the
    
    // rpc will go through and the channel will be closedsoon after due to relevancy failing.

     

    问题三:COND_InitialOnly怎么用?

         前面提到过,Actor的属性同步可以通过这种方式来实现。

         声明一个属性并标记

         UPROPERTY(Replicated)
         uint8    bWeapon: 1;
         UPROPERTY(Replicated)
         uint8    bIsTargeting: 1;
         voidCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty > &OutLifetimeProps ) const
         {
              DOREPLIFETIME(Character,bWeapon );
              DOREPLIFETIME_CONDITION(Character, bIsTargeting,  COND_InitialOnly);
         }

     

         这里面的第一个属性一般的属性复制,第二个就是条件属性复制。条件属性复制无非就是告诉引擎,这个属性在哪些情况下同步,哪些情况下不同步。这些条件都是引擎事先提供好的。

         这里我想着重的提一下COND_InitialOnly这个条件宏,汉语的官方文档是这样描述的:该属性仅在初始数据组尝试发送。而英文是这样描述的:This property will only attempt to send on theinitial bunch。对比一下,果然还是英文看起来更直观一点。

         经过测试,这个条件的效果就是这个宏声明的属性只会在Actor初始化的时候同步一次,接下来的游戏过程中不会再同步。所以,我们大概能想到这个东西在有些时候确实用的到,比如同步玩家的姓名,是男还是女等,这些游戏开始到结束一般都不会改变的属性。那么在方舟里面,我还发现动物状态组件的同步状态上限ReplicatedGlobalMaxStatusValues是通过COND_InitialOnly条件来进行复制的。也就是说,上限一般调整的次数很少,如果真的有调整并需要同步,他会手动调用函数去同步该属性。这样就可以减少同步带来的压力。   然而,一旦你声明为COND_InitialOnly。你就要清楚,同步只会执行一次,客户端的OnRep回调函数就会执行一次。所以,当你在服务器创建了一个新的Actor的时候你需要第一时间把需要改变的值修改好,一旦你在下一帧(或是下一秒)去执行那么这个属性就无法正确的同步到客户端了。

     

    问题四:客户端与服务器一致么?

         我们已经知道UE4的客户端与服务器公用一套代码,那么我们在每次写代码的时候就有必要提醒一下自己。这段代码在哪个端执行,客户端与服务器执行与表现是否一致?

         虽然,我很早之前就知道这个问题,但是写代码的时候还是总是忽略这个问题,而且程序功能经常看起来运行的没什么问题。不过看起来正常不代表逻辑正常,有的时候同步机制帮你同步一些东西,有时候会删除一些东西,有时候又会生成一些东西,然而你可能一点都没发现。

         举个例子,我在一个ActorBeginPlay的时候给他创建一个粒子Emiter。代码大概如下:

         voidAGate::BeginPlay()
         {
               Super::BeginPlay();
              //单纯的在当前位置创建粒子发射器
              GetWorld()->SpawnActor<AEmitter>(SpawnEmitter,GetActorLocation(),UVictory
              Core::RTransform(SpawnEmitterRotationOffset,GetActorRotation()));
         }

     

         代码很简单,不过也值得我们分析一下。

         首先,服务器下,当Actor创建的时候就会执行BeginPlay,然后在服务器创建了一个粒子发射器。这一步在服务器(DedicateServer)创建的粒子其实就是不需要的,所以一般来说,这种纯客户端表现的内容我们不需要在专用服务器上创建。

         再来看一下客户端,当创建一个Gate的时候,服务器会同步到客户端一个Gate,然后客户端的Gate执行BeginPlay,创建粒子。这时候我们已经发现二者执行BeginPlay的时机不一样了。进一步测试,发现当玩家远离Gate的时候,由于UE的同步机制(只会同步一定范围内的Actor),客户端的Gate会被销毁,而粒子发射器也会销毁。而当玩家再次靠近的时候,Gate又被同步过来了,原来的粒子发射器也被同步过来。而因为客户端再次执行了BeginPlay,又创建了一个新的粒子,这样就会导致不断的创建新的粒子。

         你觉得上面的描述准确么?

         并不准确,因为上述逻辑的执行还需要一个前置条件——这个粒子的bReplicate属性是为false的。有的时候,我们可能一不小心就写出来上面这种代码,但是表现上确实正常的,为什么?因为SpawnActor是否成功是有条件限制的,在生成过程中有一个函数

         bool AActor::TemplateAllowActorSpawn(UWorld* World,const FVector& AtLocation, const FRotator& AtRotation, const structFActorSpawnParameters& SpawnParameters)
         {
             return !bReplicates || SpawnParameters.bRemoteOwned||World->GetNetMode() != NM_Client;
         }

     

         如果你是在客户端,且这个Actor勾选了bReplicate的话,TemplateAllowActorSpawn就会返回false,创建Actor就会失败。如果这个Actor没有勾选bReplicate的话,那么服务器只会创建一个,客户端就可能不断的创建,而且服务器上的这个Actor与客户端的Actor没有任何关系。

         另外,还有一种常见的错误。就是我们的代码执行是有条件的,然而这个条件在客户端与服务器是不一样的(没同步)。如,

         voidGate::CreateParticle(int32 ID)
         {
              if(GateID!= ID)
              {
                    FActorSpawnParameters SpawnInfo;
                   GetWorld()->SpawnActor<AEmitter>(SpawnEmitter, GetActorLocation(),  GetActorRotation(), SpawnInfo);
             }
         }

     

         这个GateID是我们在GateBeginPlay的时候随机初始化的,然而这个GateID只在服务器与客户端是不同的。所以需要服务器同步到客户端,才能按照我们理想的逻辑去执行

     

    问题五:属性同步的基本规则是?

         单纯的非休眠状态Actor的属性同步比较简单,但是一旦涉及到休眠状态,回调函数的执行,还是值得总结一下的。

         非休眠状态下的Actor的属性同步:只在服务器属性值发生改变的情况下执行
         回调函数执行条件:服务器同步过来的数值与客户端不同

         休眠的ACtor:不同步

         首先要认识到,同步操作触发是由服务器决定的,所以不管客户端是什么值,服务器觉得该同步就会把数据同步到客户端。而回调操作是客户端执行,所以客户端会判断与当前的值是否相同来决定是否产生回调。
        然后是属性同步,属性同步的基本原理就是服务器在创建同步通道的时候给每一个Actor对象创建一个属性变化表(这里面涉及到FObjectReplicator,FRepLayout,FRepState,FRepChangedPropertyTracker相关的类,有兴趣可以进一步了解,我也会在另一个博客里面去讲解),里面会记录一个当前默认的Actor属性值。之后,每次属性发生变化的时候,服务器都会判断新的值与当前属性变化表里面的值是否相同,如果不同就把数据同步到客户端并修改属性变化表里的数据。对于一个非休眠且保持连接的Actor,他的属性变化表是一直存在的,所以他的表现出来的同步规则也很简单,只要服务器变化就同步。

         动态数组TArray在网络中是可以正常同步的,系统会检测到你的数组长度是否发生了变化,并通知客户端改变。

     

    附加:结构体属性同步有什么特别的么?


         注意,UE里面UStruct类型的结构体在反射系统中对应的是UScriptStruct,他本身可以被标记Replicated并且结构体内的数据默认都会被同步,而且如果里面有还子结构体的话也仍然会递归的进行同步。如果不想同步的话,需要在对应的属性标记NotReplicated,而且这个标记只对UStruct有效,对UClass无效。另外,如果是Ustruct数组一定要在内部属性标记Uproperty,否在在数组同步的时候就会产生崩溃。
        有一点特别的是,Struct结构内的数据是不能标记Replicated的。如果你给Struct里面的属性标记replicated,UHT在编译的时候就会提醒你编译失败。

        最后,UE里面的UStruct不可以以成员指针的方式在类中声明。

     

     

    问题六:组件同步的基本规则是?

    组件在同步上分为两大类:静态组件与动态组件。

    对于静态组件:一旦一个Actor被标记为同步,那么这个Actor身上默认所挂载的组件也会随Actor一起同步到客户端(也需要序列化发送)。什么是默认挂载的组件?就是C++构造函数里面创建的默认组件或者在蓝图里面添加构建的组件。所以,这个过程与该组件是否标记为Replicate是没有关系的。

    对于动态组件:就是我们在游戏运行的时候,服务器创建或者删除的组件。比如,当玩家走进一个洞穴时,给洞穴里面的火把生成一个粒子特效组件,然后同步到客户端上,当玩家离开的时候再删除这个组件,玩家的客户端上也随之删除这个组件。

    对于动态组件,我们必须要设置他的Replicate属性为true,即通过函数 AActorComponent::SetIsReplicated(true)来操作。而对于静态组件,如果我们不想同步组件上面的属性,我们就没有必要设置Replicate属性。

    一旦我们执行了SetIsReplicated(true)。那么组件在属性同步以及RPC上与Actor的同步几乎没有区别,组件上也需要设置GetLifetimeReplicatedProps来执行属性同步,Actor同步的时候会遍历他的子组件查看是否标记Replicate以及是否有属性要同步。

    boolAActor::ReplicateSubobjects(UActorChannel *Channel, FOutBunch *Bunch, FReplicationFlags *RepFlags)
    
    {
            ......
    
             boolWroteSomething = false;
    
             for(UActorComponent* ActorComp : ReplicatedComponents)
    
             {
    
                       if(ActorComp && ActorComp->GetIsReplicated())
    
                       {
    
    //Lets the component add subobjects before replicating its own properties.
    
                                WroteSomething|= ActorComp->ReplicateSubobjects(Channel, Bunch,RepFlags);        
    
    //(this makes those subobjects 'supported', and from here on those objects mayhave reference replicated)     子对象(包括子组件)的同步,其实是在ActorChannel里进行
    
                       WroteSomething |= Channel->ReplicateSubobject(ActorComp,*Bunch,*RepFlags);
    
                       }
    
             }
    
             returnWroteSomething;
    
    }
    
     
    
    对于C++默认的组件,需要放在构造函数里面构造并设置同步,UE给出了一个例子:
    
    ACharacter::ACharacter()
    
    {
    
       // Etc...
    
       CharacterMovement = CreateDefaultSubobject<UMovementComp_Character>(TEXT("CharMoveComp");
    
       if (CharacterMovement)
    
       {
    
           CharacterMovement->UpdatedComponent = CapsuleComponent;
    
           CharacterMovement->GetNavAgentProperties()->bCanJump = true;
    
           CharacterMovement->GetNavAgentProperties()->bCanWalk = true;
    
           CharacterMovement->SetJumpAllowed(true);
    
             //Make DSO components net addressable 实际上如果设置了Replicate之后,这句代码就没有必要执行了
    
           CharacterMovement->SetNetAddressable();
    
              // Enable replication by default
    
           CharacterMovement->SetIsReplicated(true);
    
              
    
       }
    
    }
     
    

     

        如果想进一步的深入网络同步的相关细节,我会在下一篇博客里面进一步分析讲解。

     

    Tips:同步的一些小细节?

    1.当前新版的Server RPC好像要求必须加 reliable/unreliable ,以及WithValidation

    一旦加上WithValidation,还必须要添加一个验证函数。像下面这样,

    UFUNCTION(Server, unreliable, WithValidation)
    void ServerSpawnTestActor();

    virtual bool ServerSpawnTestActor_Validate();

     

    2.有属性同步我们知道必须要添加GetLifetimeReplicatedProps,但是同时要在.cpp里面添加头文件#include "Net/UnrealNetwork.h",否则找不到FLifetimeProperty

    void ALevelTestCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>&OutLifetimeProps) const
    {
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME(ALevelTestCharacter, TestRepActor);
    }

     

    3.看编译错误不要看VS的错误窗口,会看晕的,一定要看输出窗口的错误提示

     

    4.所有的Tick事件的注册都是在AActor::BeginPlay()里面完成的,所以重写各种Actor函数时一定别忘了加Super::XXXXX();

     

    原文链接(转载请标明):http://blog.csdn.net/u012999985/article/details/78244492

     

    展开全文
  • 网络同步技术理解

    千次阅读 2019-07-07 23:01:17
    记一下自己对网络同步的理解。 网络同步主要有二种,帧同步和状态同步。 帧同步 帧锁定同步算法。具体不介绍。 大致意思是客户端和客户端每一帧发送“cmd”,客户端收到所有的输入后,根据这些cmd模拟一帧。因此网络...

    记一下自己对网络同步的理解。

    网络同步主要有二种,帧同步和状态同步。

    帧同步

    帧锁定同步算法。具体不介绍。
    大致意思是客户端和客户端每一帧发送“cmd”,客户端收到所有的输入后,根据这些cmd模拟一帧。因此网络延迟与网络状况最不好的玩家有关。

    状态同步

    主要参考https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
    之前参与开发的一款FPS游戏就是基于这个架构。
    假设是一个Client/Sever架构。对于一个游戏,理想的情况下,服务器每一帧把客户端的输入收集并计算结果发给客户端,客户端只负责表现结果(即像看一段实时视屏,没有运行逻辑)。

    基本网络

    正常情况下,服务端会有一个运行帧率。比如15ms,即每秒66.6帧。服务器每一帧处理用户的cmd,运行物理模拟,运行游戏逻辑,更新entity的状态。模拟结束后,服务器选择对客户端发送快照(snapshot,当前游戏的状态)。如果加大运行帧率会提高模拟的表现,但是对服务器的cpu负担和客户端的带宽就会有更高的要求。一般客户端的带宽是有限的,如果服务器发送的高频率数据,客户端会有数据包丢失。同样客户端发送的cmd也会根据自己的带宽设置(每秒发送多少个cmd)。
    客户端每一帧向服务器发送指令(移动,开火等),服务器收到处理后返回游戏世界所有实体的快照,客户端收到这个快照(这个时间叫latency或者叫ping或者叫round trip time)。可以知道这个lantency越小,游戏的表现越好。
    因为网络延迟是不可避免的,那么使得游戏的表现更好呢?
    使用预测和延迟补偿技术就是为了提升游戏表现(减小高延迟玩家和低延迟玩家的不公平)。

    差值

    由于客户端收到的一个一个快照(正常是20个每秒),如果只是根据快照去更新的话,表现会非常卡,除非快照频率非常高(对服务器来说CPU和网络的压力会大大增加)。为了解决这个问题,客户端引入了快照缓存(snapshot history),根据网络状况和参数,计算一个延时值,比如当前时间的前0.1s(这个时间叫RenderTime),然后根据RenderTime计算在这个RenderTime二侧的SnapShot,根据时间进行差值然后渲染。这个时间一般是大于二个snapshot发送过来的时长,这样即使有一个snapshot由于网络原因丢失了,另外一个包还是可以进行差值。
    在这里插入图片描述
    按照上图为例,当前的时间是10.32s,最新收到的快照是344,假设差值延迟是0.1s,那么rendering time为10.22,当前的渲染帧数据可以根据340和342差值得到。如果342快照丢失了,我们还可以根据340和344快照来进行差值。如果342和344都丢失了,那么差值就会工作不正常,需要使用额外的差值(extrapolation, 一种简单的做法是复制当前帧)

    预测

    假设我们的网络延迟(latency)为150ms,我们在客户端按w,想要移动角色,客户端A把向前移动的cmd发送给服务器,服务器收到命令后,把客户端A的状态设置为向前,并同步给其他客户端,其他客户端看到A开始移动了。
    上面的流程,表现在客户端A为不流畅,操作迟钝。使用客户端预测可以去除这个延迟并使操作更加流畅。不等到服务器把A的位置更新,客户端预测执行命令的结果。这需要客户端和服务器需要对这个命令有共同的处理结果(即需要共用代码)。预测结束后A移动到了新位置,服务器上A的数据还是在旧位置。
    过了150ms,客户端收到了服务器的snapshot,snapshot包含了A在服务器上执行命令得到的新位置。客户端比较自己预测的位置和snapshot的位置。如果二种一样,嗯万幸预测成功了。如果不一致,说明预测出错了(可能A在服务器上计算时,被其他玩家冻结住,或者打击退了),说明客户端A在预测执行的时候并没有正确的其他玩家和环境的信息。服务器有绝对权,所以客户端需要纠正自己错误的位置,一种是直接拉回到服务器的位置,二是使用平滑差值到服务器发过来的位置。
    预测只对自己有效,因为你知道客户端的命令,但是不能预测其他玩家,你不能预测其他玩家的命令。

    延迟补偿

    假设我们在10.5对敌人B开了一枪,这个开枪的命令通过网络发送到服务器。在这个开枪命令发送到服务器之前,服务器还是在对世界进行模拟,当命令达到服务器时,B可能移动到一个不同的地方,如果命令在10.6到达,那么这个开枪不会被命中,即使在客户端A完全对准了玩家B。
    在这里插入图片描述
    补偿系统是保存了所有玩家在过去1s的信息(可以是服务器发送snapshot的历史,它包含了说有的玩家信息)。
    那么实际的击中模拟时间应该是
    comandExecuteTime = ServerCurrentTime - Lantency - ClientViewInterpolation

    服务器回到comandExecuteTime 这个时候的状态(只有玩家的位置和动作),计算开枪命中,计算结束后回到ServerCurrentTime的状态。
    上图是一个例子。服务器有200ms的latency。左边红色是客户端100ms+差值时间之前的hit。当射击命令到达服务器,玩家B已经到了左边的位置,服务器回溯当前世界,玩家B现在是蓝色包围盒的位置,计算击中计算。红色和蓝色不完全一样是因为时间的精度差异(latency在实际的网络也不是一个固定的值200)。对于快速移动的物体,这个差异会更加明显。

    实际使用

    同步原理

    在这里插入图片描述
    其中ServerTime表示服务器的当前时间。
    ClientTime表示客户端的时间。
    DeltaTime表示服务端和客户端的时间差值。
    RenderTime是当前渲染的时间。
    对于最上一行,服务器发送一个snapshot给客户端,其中包含时间戳T1,客户端收到该snapShot,记录本地时间T2,计算出Delta=T1-T2,即网络延迟。对于每一个周期(每一帧,每一个Update(), Tick()),客户端都会向前走,RenderTime根据如下公式:
    R e n d e r T i m e = C l i e n t T i m e + D e l t a − I n t e r p o l a t e I n t e r v a l − T i m e N u d g e RenderTime = ClientTime + Delta - InterpolateInterval - TimeNudge RenderTime=ClientTime+DeltaInterpolateIntervalTimeNudge

    服务端以固定的周期(比如20帧)计算游戏状态,并把场景中所有的对象的状态打成一个snapshot发给客户端。
    一个Snapshot包含如下信息:

    • ServerTime
    • Snapshot的序号
    • 对象列表
    • 最后计算的UserCmd
    • 当前玩家信息

    要求:客户端能够通过这些信息构建整个世界。

    每个玩家收到的snapshot都是不相同的,Snapshot包含了玩家自己和其他人的状态信息。

    回放

    在这里插入图片描述
    首先客户端对收到snapshot,按照snapshot中的serverTime进行排序,图中为S1,S2,S3,S4,S5。计算根据当前时间计算RenderTime,它会落在二个snapshot中间,根据时间对二个snapshot进行差值
    S c u r = a ∗ S l e f t + ( 1 − a ) S r i g h t S_{cur} = a*S_{left} + (1-a)S_{right} Scur=aSleft+(1a)Sright
    根据这个公式得到回放数据。
    举个例子:比如左边的snapshot玩家A的位置为1,右边的snapshot玩家A的位置为2,他们渲染时玩家A的位置会根据时间,进行位置差值。动画也是同样的道理,根据是动画当前播放的动画和normalizeTime,进行差值回放。
    ps:之前差值动画的会有一个问题。比如服务器帧率为20,那么每秒发送snapshot,每个snapshot采样的是当前的状态,比如这一次采样(snapshot)玩家P播放的动画A,normalizeTime为0.9,下一次采样(snapshot)播放的动画是B,normalizeTime为0.1,那么客户端进行差值的时候,就会有问题,客户端什么时候玩家P动画从A变到B?
    假设是A动画是正常结束,即normalize变为1时候结束,我们可以根据动画A的时长,计算剩余几秒动画A结束来进行差值(假设二个snapshot的时间时间差值就是动画运行时间)
    还有一种可能,动画A是被打断的,即在0.9-1.0之间就结束了。
    一种最简单的方法是这变化的那个时刻,把服务器的时间打上。

    预测

    在这里插入图片描述

    1. 客户端的每帧输入会打成一个包:UserCmd,这个UserCmd有个序号Seq
    2. 在每个服务器帧,服务端将累积未处理的UserCmd处理掉,并记录最新的Seq:LastCmd
    3. 客户端会保持一定数量的UserCmd,按照序号排序
      客户端的预测都是基于最新的Snapshot,并根据该Snapshot的LastCmd,将之后的UserCmd应用到该Snapshot上。

    上图为例子。首先服务器(运行帧率20)发送一个snapshot T1,id = 0,LastCmd = 0
    客户端收到后,根据这个snapshot构建世界,根据这个T1状态,执行cmd1,发送cmd1给服务器和cmd2,发送cmd2到服务器。
    服务器此时没有收到呵护短的cmd,发送snapshotT2,LastCmd=0,客户端收到后,比较LastCmd=0的snapShot,发现一致,预测成功。
    客户端执行cmd3和cmd4,cmd5…
    服务器又tick一次,这时收到了客户端的三个命令cmd1,cmd2,cmd3,它一次执行完这3个cmd,发送snapshot,id=2,Lastcmd=3。
    客户端收到后,比较预测的cmd=3的snapshot是否和服务器一致,如果一致就继续预测执行。如果不一致,根据服务器的snapshotT3设置为当前的状态,重新执行命令cmd4和cmd5,得到当前世界的结果。然后客户端执行cmd6,这时发送给服务器的cmd6丢失了,下一次服务器返回snapshot是T4,LastCmd=5。下一次服务器执行cmd7,没有收到cmd6,发送snapshot T5给客户端,客户端大概率会不一致,根据T5,一次执行cmd8,跳过了cmd6.
    可以看到,服务器即使没有收到cmd6,也可以照常执行,不会收到客户端网络的影响。

    补偿

    服务端保存一定历史记录的Snapshot。
    当UserCmd中有射击命令时,根据UserCmd的RenderTime查找Snapshot并插值,得出可能被击中人物的位置和状态,并判断击中的结果。

    总结

    这边预测执行回滚,补偿,是有一个假设,
    即世界是通过snapshot完全构建出来的+cmd来构建。
    如果预测回滚老是出现不一致
    有二种可能:

    1. 客户端和服务器执行的代码有一些不同(客户端和服务器公用同一套代码可以解决这个问题)
    2. snapshot里面的状态不能完整构建整个世界。比如你忘记同步一些变量,这些变量在执行cmd的过程中会影响snapshot里面的数据。

    参考

    http://t-machine.org/index.php/2008/03/13/entity-systems-are-the-future-of-mmos-part-4/
    https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
    https://www.cnblogs.com/yangrouchuan/p/7436389.html

    展开全文
  • 网络同步接口说明.网络同步接口说明.网络同步接口说明.网络同步接口说明.网络同步接口说明.网络同步接口说明.网络同步接口说明.网络同步接口说明.
  • 网络同步(帧同步)

    千次阅读 2018-02-08 22:12:36
    网络同步采用是帧同步技术。 帧同步的背景介绍:就是一个对于一个f(x) 函数,对于同一输入,输出结果一定相同。 拿格斗游戏举例,a挥刀,b下蹲作为输入的参数,x1,x2 ,函数f(x1,x2) 输出固定的一个结果(相对俩个...
  • UE4 网络同步Replicate 汇总

    千次阅读 2019-05-21 20:00:30
    一、 官方说明: ...二、 UE4网络模块分析 https://www.jianshu.com/p/b4f1a5412cc9 三、《Exploring in UE4》关于网络同步的理解与思考[概念理解] https://zhuanlan.zhihu.com/p/34721113 四、《Exploring in UE4》...
  • Google拼音 网络同步 网络错误

    千次阅读 2009-03-12 21:39:00
    问题:Google拼音网络同步辞典,网络同步,输入用户名,密码提示“网络...安装了杀毒软件并带防火墙,关闭防火墙再进行 “网络同步“3.安装了独立的防火墙,关闭防火墙再试试找了半天才发现是这个问题,贴到这里共享。
  • 网络游戏网络同步方案的选择

    万次阅读 多人点赞 2016-11-17 21:07:51
    甚至许多流行的端游也被开发者移植到手机上来,如今手游的类型多种多样,从单机到rpg,slg策略游戏,卡牌游戏弱交互游戏,以及rts,mmorpg,moba等重型多人在线游戏,其网络同步方案各有各的特点,到底如何选择游戏...
  • 网络同步与异步概念整理

    千次阅读 2019-07-10 15:05:41
    网络同步中,有两种同步方式,分别为同步与异步。 同步的操作指的是,当所有的操作请求都做完,才将结果返回给用户,用户才能进行下一个操作,这样就会让用户有一种卡顿的感觉,因为需要等待上一步操作的执行结果...
  • 手把手教你实现Unity网络同步

    千次阅读 2019-10-17 22:49:03
    [从零开始的Unity网络同步] 1.在Unity中搭建网络模块:https://www.jianshu.com/p/fa959d16eaed [从零开始的Unity网络同步] 2.服务端与客户端之间通信:https://www.jianshu.com/p/f8e9c3f49424 [从零开始的Unity...
  • 1588网络同步协议(中文版本完整版)
  • 《守望先锋》中的网络同步技术

    千次阅读 2016-11-27 23:12:50
    暴雪的网络程序员介绍《守望先锋》中的网络同步技术,以及如何去改善
  • UE4_网络同步原理深入

    千次阅读 2018-09-12 11:54:20
    UE4_网络同步原理深入 本文更多是对Exploring in UE4有关网络同步原理以及官方文档的一些自己理解和总结。 1. 通信的基本流程 1.1 UE4服务器与客户端的通信流程 UE4进程内部服务器Server与客户端Client的...
  • 同步是什么 所谓帧同步(lockstep),其实是同步操作,确保两个客户端逻辑一致,然后服务器接收每个客户端操作(cmd), 然后再下发下去,相同的逻辑,相同的操作,便会播放(DoAction)相同的结果。 如何...
  • ue4 蓝图网络同步、RPC总结

    万次阅读 多人点赞 2018-08-04 03:09:19
    对于ue4蓝图网络同步看了两篇博客,感觉挺简单的,没想到做项目的时候各种bug。 一、首先需要知道的基础概念 1、对于继承了Actor的类,在类默认值属性中勾选Replicates,服务器始终拥有该 Actor 的权威版本,而一...
  • okhttp网络同步请求,线程同步执行

    千次阅读 2018-05-15 12:02:57
    接口查询的时候开始时间和结束时间是不予许超过24小时的,所以当选择完时间之后就需要截取时间段,多次执行请求接口,然后把请求到的数据添加到数组,这就需要执行的时候线程和网络请求同步,围绕这个问题我做了下面...
  • 实时pvp(皇室战争)网络同步研究

    万次阅读 热门讨论 2016-05-06 13:13:25
    皇室战争实时pvp网络同步
  • IEEE1588精密网络同步时钟协议(PTP)-v2.0协议浅析 本文由安徽京准科技公司提供请勿转载! 1 引言  以太网技术由于其开放性好、价格低廉和使用方便等特点,已经广泛应用于电信级别的网络中,以太网的数据传输速度...
  • 游戏网络同步——MMO位置同步

    千次阅读 2014-08-22 11:55:16
    前提 1. client和server之间或多或少存在网络延迟,需要提前做好对时,并在网络环境发生变化时校正时差。国内的公网通信,非跨网的情况下,一般在120ms左右。本地的网络会好一些。...4. 控制位置同步
  • centos 6.5 时间网络同步

    千次阅读 2018-05-16 15:07:28
    设置定时任务同步时间 sudo crontab -e 00 12 * * * /usr/sbin/ntpdate cn .pool .ntp .org OK 啦! 感谢: https://blog.csdn.net/u011391839/article/details/62892020 ...
  • 说到网络同步,这真是一个网络游戏的重中之重,一个好的网络同步机制,可以让玩家的用户体验感飙升,至少,我玩过的魔兽争霸在网络同步方面做得非常好,即便是网络状况很不稳定,依然可以保证用户数据不会出现...
  • 使用OkHttp进行网络同步异步操作

    万次阅读 2016-11-06 10:59:23
    一、使用OkHttpOkHttp发送请求后,可以通过同步或异步地方式获取响应。下面就同步和异步两种方式进行介绍。1.1、同步方式发送请求后,就会进入阻塞状态,知道收到响应。下面看一个下载百度首页的例子:OkHttpClient...
  • Ubuntu时间与网络同步

    千次阅读 2018-10-15 08:49:57
    设置系统时间与网络时间同步 sudo ntpdate time.windows.com sudo ntpdate cn.pool.ntp.org 然后将时间更新到硬件上: sudo hwclock --localtime --systohc 注: Windows把系统硬件时间当作本地时间...
  • linux 设置与网络同步的时间

    千次阅读 2018-11-04 18:39:00
    #设置linux时间 与网络时间同步安装命令yum install -y ntp同步网络时间ntpdate ntp1.aliyun.com
  • centos 7 时间与网络同步

    万次阅读 2018-05-10 10:03:07
    设置系统时间与网络时间同步sudo ntpdate cn.pool.ntp.org13.将系统时间写入硬件时间 sudo hwclock --systohc14.查看系统时间timedatectl #得到 Local time: 四 2017-09-21 13:54:09 CST Univ...
  • 华中农业大学第四届程序设计大赛网络同步赛解题报告
  • VR中物理的网络同步

    千次阅读 2018-03-09 10:47:35
    之前做VR游戏时也是尝试了几种物理的同步方案, 最近看到Oculus Blog上也分享了一些, 经验, 做个笔记. 多人VR游戏中的物理交互, 有这么几个要求: 玩家对物体交互不能有延迟 物体能静止后稳定, 不...
  • CentOS 8时间网络同步设置

    千次阅读 2020-04-11 16:28:04
    添加时间同步服务器 vim /etc/chrony.conf # Use public servers from the pool.ntp.org project. # Please consider joining the pool (http://www.pool.ntp.org/join.html). pool 2.centos.pool.ntp.org ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 730,853
精华内容 292,341
关键字:

网络同步