第一部分从移动相关架构以及单机情况下移动的处理细节讲起 UE4移动组件详解(一)——移动框架与实现原理
而第二部分是关于移动组件同步解决方案的描述,里面有诸多细节来让移动的同步表现的更为流畅。关于移动网络同步这一块内容,博主还有一些地方还没有完全梳理清楚,会在之后的时间里慢慢完善。

四.移动同步解决方案

前面关于移动逻辑的细节处理都是在PerformMovement里面实现的,我们可以把函数PerformMovement当成一个完整的移动处理流程。这个流程无论是在客户端还是在服务器都必须要执行,或者作为一个单机游戏,这一个接口基本上可以满足我们的正常移动了。不过,在网络游戏中,为了让所有的玩家体验一个几乎相同的世界,需要保证一个具有绝对权威的服务器,这个服务器可以修正客户端的不正常移动行为,保证各个客户端的一致性。相关同步的操作都是基于UCharacterMovement组件实现的,所以我们的角色必须要使用这个移动组件。

移动组件的同步全都是基于RPC不可靠传输的,你会在UCharacterMovement头文件里面看到多个以Server或者Client开头的RPC函数。

关于移动组件的同步思路,建议选阅读一下官方文档的内容,https://docs.unrealengine.com/latest/CHN/Gameplay/Networking/CharacterMovementComponent/index.html 回头看可能更为清晰一点。现在我们把整个移动细节作为一个接口封装起来,宏观的研究移动组件的同步细节。

另外,如果还没有完全搞清ROLE_Authority,ROLE_AutonomousProxy,ROLE_SimulatedProxy的概念,请参考 UE4网络同步详解(一)——理解同步规则。这里举个例子,一个服务器上有一个玩家ServerA和一个NPC ServerB,客户端上拥有从服务器复制过来的这个玩家ClientA与NPC ClientB。由于ServerA与ServerB都是在服务器上生成的,所以他们两在服务器上的所有权Role都是ROLE_Authority。ClientA在客户端上由于被玩家控制,他的Role是ROLE_AutonomousProxy。ClientB在客户端是完全通过服务器同步来控制的,他的Role就是ROLE_SimulatedProxy。

4.1 服务器角色正常的移动流程

第三章节里面的图3-1就是单机或者ListenServer服务器执行的移动流程。作为一个本地控制的角色,他只需要认真的执行正常的移动(PerformMovement)逻辑处理即可,所以ListenServer服务器移动不再赘述。

但是对于DedicateServer,他的本地没有控制的角色,对移动的处理就有差异了。分为两种情况:

  1. 该角色在客户端是模拟(Simulate)角色,移动完全由服务器同步过去,如各类AI角色。这类移动一般是服务器上行为树主动触发的
  2. 该角色在客户端是拥有自治(Autonomous)权利的Character,如玩家控制的主角。这类移动一般是客户端接收玩家输入数据本地模拟后,再通过RPC发给服务器进行模拟的

从下面的代码可以了解到这两种情况的处理(注意注释):

// UCharacterMovementComponent:: TickComponent
// simulate的角色在服务器执行IsLocallyControlled也会返回true
// Allow root motion to move characters that have no controller.
if( CharacterOwner->IsLocallyControlled() || (!CharacterOwner->Controller && bRunPhysicsWithNoController) || (!CharacterOwner->Controller && CharacterOwner->IsPlayingRootMotion()) )
{{SCOPE_CYCLE_COUNTER(STAT_CharUpdateAcceleration);// We need to check the jump state before adjusting input acceleration, to minimize latency// and to make sure acceleration respects our potentially new falling state.CharacterOwner->CheckJumpInput(DeltaTime);// apply input to accelerationAcceleration = ScaleInputAcceleration(ConstrainInputAcceleration(InputVector));AnalogInputModifier = ComputeAnalogInputModifier();}if (CharacterOwner->Role == ROLE_Authority){// 单机或者DedicateServer控制simulate角色移动PerformMovement(DeltaTime);}else if (bIsClient){ReplicateMoveToServer(DeltaTime, Acceleration);}
}
else if (CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy)
{//DedicateServer控制自治客户端角色移动// Server ticking for remote client.// Between net updates from the client we need to update position if based on another object,// otherwise the object will move on intermediate frames and we won't follow it.MaybeUpdateBasedMovement(DeltaTime);MaybeSaveBaseLocation();// Smooth on listen server for local view of remote clients. We may receive updates at a rate different than our own tick rate.if (CharacterMovementCVars::NetEnableListenServerSmoothing && !bNetworkSmoothingComplete && IsNetMode(NM_ListenServer)){SmoothClientPosition(DeltaTime);}
}

这两种情况详细的流程我们在下面两个小结分析。

4.2 Autonomous角色

一个客户端的角色是完全通过服务器同步过来的,他身上的移动组件也一样是被同步过来的,所以游戏一开始客户端的角色与服务器的数据是完全相同的。对于Autonomous角色,大致的实现思路如下:

客户端通过接收玩家的Input输入,开始进行本地的移动模拟流程,移动前首先创建一个移动预测数据结构FNetworkPredictionData_Client_Character,执行PerformMovement移动,随后保存当前的移动数据(速度,旋转,时间戳以及移动结束后的位置等信息)到前面的FNetworkPredictionData里面的SavedMoves列表里面,并通过RPC将当前的Move数据发送该数据到服务器。然后继续进行TickComponent操作,重复这个流程。

客户端在发送给服务器RPC消息的同时,本地还会不断的执行移动模拟。SavedMoves列表里面的数据也就越来越多。如果这时候收到了一个ClientAckGoodMove调用,那么表示服务器接收了对应时间戳的客户端移动,客户端就将这个时间戳之前的SavedMoves全部移除。如果客户端收到了ClientAdjustPosition调用,那么表示对应这个时间戳的移动有问题,客户端需要修改成服务器传过来的位置,并重新播放那些还没被确认的SaveMoves列表里面的移动。

图4-1

整个流程如下图所示:

图4-2 Autonomous角色移动流程图

4.2.1 SavedMoves与移动合并

仔细阅读源码的朋友对上面给出的流程可能并不是很满意,因为除了ServerMove你可能还看到了ServerMoveDual以及ServerMoveOld等函数接口。而且除了SavedMoves列表,还有PendingMove,FreeMove这些移动列表。他们都是做什么的?

简单来讲,这属于移动带宽优化的一个方式,将没有意义的移动合并,减少消息的发送量。

当客户端执行完本次移动后,都会把当前的移动数据以一个结构体保存到SavedMove列表,然后会判断当前的这个移动是否可以被延迟发送(CanDelaySendingMove(),默认为true),如果可以就会继续判断当前的客户端网络速度如何。如果当前的速度有一点慢或者上次更新的时间很短,移动组件就会将当前的移动赋值给PendingMove(表示将要执行的移动)并取消本次给服务器消息的发送。

const bool bCanDelayMove = (CharacterMovementCVars::NetEnableMoveCombining != 0) && CanDelaySendingMove(NewMove);if (bCanDelayMove && ClientData->PendingMove.IsValid() == false)
{// Decide whether to hold off on move// send moves more frequently in small games where server isn't likely to be saturatedfloat NetMoveDelta;UPlayer* Player = (PC ? PC->Player : nullptr);AGameStateBase const* const GameState = GetWorld()->GetGameState();if (Player && (Player->CurrentNetSpeed > 10000) && (GameState != nullptr) && (GameState->PlayerArray.Num() <= 10)){NetMoveDelta = 0.011f;}else if (Player && CharacterOwner->GetWorldSettings()->GameNetworkManagerClass) {//这里会根据网络管理的配置以及客户端网络速度来决定是否延迟发送NetMoveDelta = FMath::Max(0.0222f,2 * GetDefault<AGameNetworkManager>(CharacterOwner->GetWorldSettings()->GameNetworkManagerClass)->MoveRepSize/Player->CurrentNetSpeed);}else{NetMoveDelta = 0.011f;}if ((GetWorld()->TimeSeconds - ClientData->ClientUpdateTime) * CharacterOwner->GetWorldSettings()->GetEffectiveTimeDilation() < NetMoveDelta){// Delay sending this move.ClientData->PendingMove = NewMove;return;}
}

当客户端进去下次Tick的时候,就会判断当前的新的移动是否能与上次保存的PendingMove合并。如果可以,就可以减少一次消息的发送。如果不能合并,那么在本次移动结束后给服务器发送一个两次移动(ServerMoveDual),就是单纯的执行两次ServerMove。

服务器在受到两次移动的时候对第一次移动不进行任何校验,只对第二个移动进行正常的校验,判断是否是第一次的标准就是ClientPosition是不是FVector(1.f,2.f,3.f)。通过下面的代码就可以了解了

void UCharacterMovementComponent::ServerMoveDual_Implementation(float TimeStamp0,FVector_NetQuantize10 InAccel0,uint8 PendingFlags,uint32 View0,float TimeStamp,FVector_NetQuantize10 InAccel,FVector_NetQuantize100 ClientLoc,uint8 NewFlags,uint8 ClientRoll,uint32 View,UPrimitiveComponent* ClientMovementBase,FName ClientBaseBone,uint8 ClientMovementMode)
{ServerMove_Implementation(TimeStamp0, InAccel0, FVector(1.f,2.f,3.f), PendingFlags, ClientRoll, View0, ClientMovementBase, ClientBaseBone, ClientMovementMode);ServerMove_Implementation(TimeStamp, InAccel, ClientLoc, NewFlags, ClientRoll, View, ClientMovementBase, ClientBaseBone, ClientMovementMode);
}

其实,UE的思想就是,将所有的移动的关键信息都数据化,这样移动就可以自由的存储和回放。为了节省带宽,提高效率,我们也就可以想出各种办法来减少发送不必要的消息,对于一个没有移动过的玩家,理论上我们甚至都可以不去同步他的移动信息。

图4-3 移动预测及保存的数据结构示意图

4.3 Simulate角色

首先看一下官方文档对Simulate角色移动的描述:

对于那些不由人类控制的人物,其动作往往会通过正常的 PerformMovement() 代码在服务器(此时充当了主控者)上进行更新。Actor 的状态,如方位、旋转、速率和其他一些选定的人物特有状态(如跳跃)都会通过正常的复制机制复制到其他机器,因此,它们不必在每一帧都经由网络传送。为了在远程客户端上针对这些人物提供更流畅的视觉呈现,该客户端机器将在每一帧为模拟代理执行一次模拟更新,直到新的数据(由服务器主控)到来。本地客户端查看其他远程人类玩家时也是如此;远程玩家将其更新发送给服务器,后者为该玩家执行一次完整的动作更新,然后定期复制数据给所有其他玩家。
这个更新的作用是根据复制的状态来模拟预期的动作结果,以便在下一次更新前“填补空缺”。所以,客户端并没有在新的位置放置由服务器发送的代理,然后将它们保留到下次更新到来(可能是几个后续帧),而是通过应用速率和移动规则,在每一帧模拟出一次更新。在另一次更新到来时,客户端将重置本地模拟并开始新一次模拟。

简单来说,Simulate角色的在服务器上的移动就是正常的PerformMovement流程。而在客户端上,该角色的移动分成两个步骤来处理——收到服务器的同步数据时就直接进行设置。在没有收到服务器消息的时候根据上一次服务器传过来的数据(包括速度与旋转等)在本地执行Simulate模拟,等着下一个同步数据到来。Simulate角色采用这样的机制,本质上是为了减小同步带来的开销。下面代码展示了所有Character的同步属性

    void ACharacter::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const{Super::GetLifetimeReplicatedProps( OutLifetimeProps );DOREPLIFETIME_CONDITION( ACharacter, RepRootMotion,COND_SimulatedOnlyNoReplay);DOREPLIFETIME_CONDITION( ACharacter, ReplicatedBasedMovement,   COND_SimulatedOnly );DOREPLIFETIME_CONDITION( ACharacter, ReplicatedServerLastTransformUpdateTimeStamp, COND_SimulatedOnlyNoReplay);DOREPLIFETIME_CONDITION( ACharacter, ReplicatedMovementMode,    COND_SimulatedOnly );DOREPLIFETIME_CONDITION( ACharacter, bIsCrouched,           COND_SimulatedOnly );// Change the condition of the replicated movement property to not replicate in replays since we handle this specifically via saving this out in external replay dataDOREPLIFETIME_CHANGE_CONDITION(AActor,ReplicatedMovement,COND_SimulatedOrPhysicsNoReplay);}

ReplicatedMovement记录了当前Character的位置旋转,速度等重要的移动数据,这个成员(包括其他属性)在Simulate或者开启物理模拟的客户端才执行(可以先忽略NoReplay,这个和回放功能有关)。同时,我们可以看到Character大部分的同步属性都是与移动同步有关,而且基本都是SimulatedOnly,这表示这些属性只在模拟客户端才会进行同步。除了ReplicatedMovement属性以外,ReplicatedMovementMode同步了当前的移动模式,ReplicatedBasedMovement同步了角色所站在的Component的相关数据,ReplicatedServerLastTransformUpdateTimeStamp同步了最新的服务器移动更新帧,也就相当于最后一次服务器更新移动的时间(在ACharacter::PreReplication里会将服务器当前的移动数据赋值给ReplicatedServerLastTransformUpdateTimeStamp然后进行同步)。

了解了这些同步的数据后,我们开始分析其移动流程。流程如下图所示(RootMotion的情况我在上一章节已经描述,这里不再赘述)。其实其基本思路与普通的移动处理相似,只不过是调用SimulateTick去根据当前的速度等条件模拟客户端移动,但是有一点非常重要的差异就是Simulate的角色的胶囊体移动与Mesh移动是分开进行的。这么做的原因是什么呢?我们稍后再解释。

图4-4 Simulate角色移动流程图

客户端的模拟我们大致了解了流程,那么接收服务器数据并修正是在哪里处理的呢?答案是AActor::OnRep_ReplicatedMovement。客户端在接收到服务器同步的ReplicatedMovement时,会产生回调函数触发SmoothCorrection的执行,从当前客户端的位置平滑的过度到服务器同步的位置。

前面提到了胶囊体与Mesh的移动是分开处理的,其目的就是提高代理模拟的流畅度。其实在官方文档上有简单的例子,

比如这种情况,一个 replicated 的状态显示当前的角色在时间为 t=0 的时刻以速度 (100, 0, 0) 移动,那么当时间更新到 t=1 的时候,这个模拟的代理将会在 X 方向移动 100 个单位,然后如果这时候服务端的角色在发送了那个 (100, 0, 0) 的 replcated 信息后立刻不动了,那么这个 replcated 信息则会使到服务端角色的位置和客户端的模拟位置处于不同的点上。

为了避免这种“突变”情况,UE采用了Mesh网格的平滑操作。胶囊体的移动正常进行,但是其对应的Mesh网格不随胶囊体移动,而要通过SmoothClientPosition处理,在SmoothNetUpdateTime时间内完成移动,这样玩家在视觉上就不会觉得代理角色的位置突变。通过FScopedPreventAttachedComponentMove类可以限制某个组件暂时不跟随父类组件移动。

对于Smooth平滑,UE定义了下面几种情况,默认我们采用Exponential(指数增长,越远移动越快):

    /** Smoothing approach used by network interpolation for Characters. */UENUM(BlueprintType)enum class ENetworkSmoothingMode : uint8{/** No smoothing, only change position as network position updates are received. */Disabled     UMETA(DisplayName="Disabled"),/** Linear interpolation from source to target. */Linear           UMETA(DisplayName="Linear"),/** Exponential. Faster as you are further from target. */Exponential      UMETA(DisplayName="Exponential"),/** Special linear interpolation designed specifically for replays. Not intended as a selectable mode in-editor. */Replay           UMETA(Hidden, DisplayName="Replay"),};

4.4 关于物理托管后的移动

一般情况下我们是通过移动组件来控制角色的移动,不过如果给玩家角色的胶囊体(一般Mesh也是)勾选了SimulatePhysics,那么角色就会进入物理托管而不受移动组件影响,组件的同步自然也是无效了,常见的应用就是玩家结合布娃娃系统,角色死亡后表现比较自然的摔倒效果。相关代码如下:

// // UCharacterMovementComponent::TickComponent
// We don't update if simulating physics (eg ragdolls).
if (bIsSimulatingPhysics)
{// Update camera to ensure client gets updates even when physics move him far away from point where simulation startedif (CharacterOwner->Role == ROLE_AutonomousProxy && IsNetMode(NM_Client)){APlayerController* PC = Cast<APlayerController>(CharacterOwner->GetController());APlayerCameraManager* PlayerCameraManager = (PC ? PC->PlayerCameraManager : NULL);if (PlayerCameraManager != NULL && PlayerCameraManager->bUseClientSideCameraUpdates){PlayerCameraManager->bShouldSendClientSideCameraUpdate = true;}}return;
}

对于开启物理的Character,Simulate的客户端也是采取移动数据靠服务器同步的机制,只不过移动的数据不是服务器PerformMovement算出来的,而是从根组件的物理对象BodyInstance获取的,代码如下。

void AActor::GatherCurrentMovement()
{AttachmentReplication.AttachParent = nullptr;UPrimitiveComponent* RootPrimComp = Cast<UPrimitiveComponent>(GetRootComponent());if (RootPrimComp && RootPrimComp->IsSimulatingPhysics()){FRigidBodyState RBState;RootPrimComp->GetRigidBodyState(RBState);ReplicatedMovement.FillFrom(RBState, this);ReplicatedMovement.bRepPhysics = true;}
}

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

UE4移动组件详解(二)——移动同步机制相关推荐

  1. UE4移动组件详解(三)——RootMotion与特殊移动模式的实现思路

    更多相关内容参考 UE4移动组件详解(一)--移动框架与实现原理 UE4移动组件详解(二)--移动同步机制 五.特殊移动模式的实现思路 这一章节不是详细的实现教程,只是给大家提供常见游戏玩法的一些设计 ...

  2. tomcat服务组件详解(二)

    Tomcat的架构: 顶级组件: 位于配置层次的顶级,并且彼此间有着严格的对应关系 连接器: 连接客户端(可以是浏览器或Web服务器)请求至Servlet容器 容器: 包含一组其它组件 被嵌套的组件: ...

  3. cwinthread*线程指针怎么销毁结束_最新版Web服务器项目详解 01 线程同步机制封装类...

    点 击 关 注 上 方"两猿社" 设 为"置 顶 或 星 标",干 货 第 一 时 间 送 达. 互 联 网 猿 | 两 猿 社 基础知识 RAII RAII全称是"Resource Acq ...

  4. ue4移动到一定距离_UE4移动组件详解(一)——移动框架与实现原理

    原文链接(转载请标明):UE4移动组件详解(一)--移动框架与实现原理_Jerish的博客-CSDN博客​blog.csdn.net 前言 关于UE4的移动组件,我写了一篇非常详细的分析文档.由于篇幅 ...

  5. 【Unity3D-UGUI系列】(十二)ScrollView 滚动视图组件详解

    推荐阅读 CSDN主页 GitHub开源地址 Unity3D插件分享 简书地址 我的个人博客 QQ群:1040082875 大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有 ...

  6. Streamsets组件详解

    Streamsets优化详解 一.Origin类组件详解 二.Processor类组件详解 三.Destination类组件详解 四.Executor类组件使用详解 一.Origin类组件详解 Ama ...

  7. 【Unity3D-UGUI系列】(一)Canvas 画布组件详解

    推荐阅读 CSDN主页 GitHub开源地址 Unity3D插件分享 简书地址 我的个人博客 QQ群:1040082875   大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉 ...

  8. Android笔记——四大组件详解与总结

    android四大组件分别为activity.service.content provider.broadcast receiver. -------------------------------- ...

  9. Android Lifecycle 生命周期组件详解

    转载请标明出处:https://blog.csdn.net/zhaoyanjun6/article/details/99695779 本文出自[赵彦军的博客] 一.Lifecycle简介 为什么要引进 ...

最新文章

  1. 乌托邦畅想:众筹开源城市
  2. linux清除邮件队列
  3. pytorch神经网络插件或可以提高所有网络的准确率(提高权重的利用率)
  4. WinCE 自由拼音输入法的测试
  5. 我是如何学习写一个操作系统(一):开篇
  6. FlashCC学习札记
  7. iOS 两种方法实现左右滑动出现侧边菜单栏 slide view
  8. 挖孔屏设计!Moto G8高清渲染图曝光:“奥利奥”摄像头消失
  9. shell 并发脚本
  10. EntityFramework6.X 之 Operation
  11. html5怎么设置黑色背景及亮度,网页背景怎么设置为纯黑色css样式
  12. windows实用工具集
  13. python控制步进电机驱动器_怎样用树莓派和L298N电机驱动器模块控制步进电机
  14. 弹性网卡支持私网多IP
  15. OUC2021秋-数值分析-期末(回忆版)
  16. Luat 功能开发教程(十八) 阿里云
  17. 外汇天眼:央行人民币降息意味着什么?有什么影响?
  18. Linux内核版本和发行版本
  19. 一文全搞定:应届生offer,三方,劳动合同区别与注意事项
  20. 【LeetCode】﹝并查集ி﹞连通分量个数(套用模板一直爽)

热门文章

  1. LCD12864驱动(Proteus中用51单片机驱动AMPIRE128X64)
  2. What is outlier?
  3. CLIST 数组的用法 CListCtrl m_list 用法
  4. Linux 终端命令 --常用命令一
  5. html怎么多行超出省略号,css+js 如何实现多行文字超出显示省略号(需要同时兼容ie chrome等浏览器)...
  6. MinGW和MSYS简介
  7. iOS小工具合集-(合一Kit)
  8. linux创建分区大小命令,Linux使用fdisk创建分区详解
  9. 编程式路由导航连续跳转出现NavigationDuplicated报错的问题
  10. 小米手机miui12稳定版蓝牙时断不稳定的解决办法。