简介
本文主要用于整理虚幻引擎中移动相关的机制。
内容可能摘录自各个文章、论坛、文档等,仅用作记录。
基础简介
移动是一个非常重要并且较为复杂的模块,UnrealEngine
对于移动的实现使用了组合模式,在描述Actor
的移动时,含义是具有移动组件的Actor
可以移动。
移动组件的基类为UMovementComponent
,提供基本的移动功能,有多个不同功能的子类,一些子类移动组件专门用于服务一种特殊的Actor
,比如通常代表玩家的ACharacter
,对应的移动组件UCharacterMovementComponent
。
Actor
虽然代表在World
中的一个实体,但是本身是没有位置概念的。Actor
的位置由其具有的USceneComponent
赋予。因此移动的本质,就是改变Actor
上的某个USceneComponent
的位置,通常指的是Actor
的RootComponent
。
UMovementComponent
上有个属性UpdatedComponent
,就是用于设置移动组件所修改的USceneComponent
。暴露给蓝图有个参数bAutoRegisterUpdatedComponent
,如果设置为true
,则会在组件初始化时,读取Owner
的RootComponent
,调用SetUpdatedComponent
设置UpdatedComponent
。
移动流程
网络同步下的角色移动至少需要考虑以下几个方面:
- 玩家客户端操作无延迟
- 需要直接响应移动输入并上报给服务器
- 位置以服务器为准
- 防止外挂
- 同步到的移动需要表现丝滑
- 不可能每帧都收到位置更新,需要适当进行插值
自主实现
先思考如果自己实现的情况下可能会怎么做。
- 本地接收到输入之后,在本地先操作移动,同时把移动的操作发送给服务器
- 可能包含的信息是:当前位置、目标位置等
- 服务器接收到移动的操作,在服务器上执行移动,并把玩家位移的信息同步给所有客户端
- 信息同样包含:当前位置、目标位置、速度等
- 其他客户端收到了移动的信息后,修改角色的移动目标,根据插值调整角色位置
引擎实现
虚幻引擎网络同步情况下的移动流程与此类似,大概为:
-
每一帧执行TickComponent时,计算这一帧的加速度和转向,之后对于主控的Character,调用ReplicateMoveToServer把移动同步给服务器
-
ReplicateMoveToServer会把移动保存到列表,然后执行PerformMovement在本地预执行移动操作。
-
然后会调用ServerMove把移动同步给服务器,告知移动的参数、客户端自己移动的位置,以及时间戳
-
ServerMove在服务器上执行,根据客户端声明的位置,与服务器的位置做对比,如果差异过大,则调用ClientAdjustPosition在主控端校正位置
-
客户端如果收到ClientAdjustPosition,会把客户端角色位置设置为服务器上的位置,并把bUpdatePosition标记为true,这将会影响到后续的移动更新
-
当客户端再次调用TickComponent时,如果存在bUpdatePosition,则会调用ClientUpdatePositionAfterServerUpdate来重演在服务器上调整移动之后发生的所有移动。
在引擎源码CharacterMovementComponent.h中可以找到对移动同步流程的描述:
Here’s how player movement prediction, replication and correction works in network games:
Every tick, the TickComponent() function is called. It figures out the acceleration and rotation change for the frame, and then calls PerformMovement() (for locally controlled Characters), or ReplicateMoveToServer() (if it’s a network client).
ReplicateMoveToServer() saves the move (in the PendingMove list), calls PerformMovement(), and then replicates the move to the server by calling the replicated function ServerMove() - passing the movement parameters, the client’s resultant position, and a timestamp.
ServerMove() is executed on the server. It decodes the movement parameters and causes the appropriate movement to occur. It then looks at the resulting position and if enough time has passed since the last response, or the position error is significant enough, the server calls ClientAdjustPosition(), a replicated function.
ClientAdjustPosition() is executed on the client. The client sets its position to the servers version of position, and sets the bUpdatePosition flag to true.
When TickComponent() is called on the client again, if bUpdatePosition is true, the client will call ClientUpdatePosition() before calling PerformMovement(). ClientUpdatePosition() replays all the moves in the pending move list which occurred after the timestamp of the move the server was adjusting.
玩家输入
管理玩家输入的也是一个组件UInputComponent
,通常可以调用BindAxis
来注册事件响应。
一般最终会调用到UPawnMovementComponent::AddInputVector
来处理移动。
主控角色移动
在UE的网络框架中,角色主要分为三种:ROLE_Authority、ROLE_AutonomousProxy、ROLE_SimulatedProxy。
在客户端主控角色也即Autonomous角色会接受控制,然后把移动数据发往服务器。
本地的每次移动都会生成FSavedMove_Character,并维护一个TArray<FSavedMovePtr> SavedMoves
的数组,保存了当前玩家本地已经做的移动,这些移动还没经过服务器检查。
如果服务器认可了一些移动,就可以把这些移动删掉,如果检查不通过,就可以据此执行异常处理。
协议选择
在UE中,默认使用UDP作为传输协议,这可以使得数据包尽快送达。
UDP不保证可达和有序,但是应用层面可以通过设计来在需要的地方避免这些问题。
Server同步移动给客户端
Actor基本同步方案
Actor自身就支持移动同步,打开ReplicateMovement开关后,当Actor的RootComponent位置、朝向等数据发生变化时,就会把数据同步给Simulate客户端。
当Simulate的客户端收到同步之后,会简单的设置自己的位置和朝向。移动数据的同步有间隔,因此这种实现会导致Actor发生闪现。
Character移动同步
针对Actor基本同步模式的不足,CharacterMovementComponent针对性的做了表现平滑处理,让Simulate角色移动尽可能平滑自然。
Character主要有两个组件,Capsule和Mesh,Capsule是Chara
关键概念
UCharacterMovement
角色移动组件是最为复杂的一个子类,需要重点进行分析。
classDiagram class UCharacterMovementComponent { +IRVOAvoidanceInterface +INetworkPredictionInterface } UActorComponent <|-- UMovementComponent UMovementComponent <|-- UNavMovementComponent UNavMovementComponent <|-- UPawnMovementComponent UPawnMovementComponent <|-- UCharacterMovementComponent
FSavedMove_Character
用于描述玩家的一次移动,可以认为是一次移动的快照。
主要属性有:
属性 | 描述 |
---|---|
TimeStamp | 这次移动发生的时间 |
DeltaTime | 这次移动使用的时间 |
CustomTimeDilation | 时间膨胀系数,可以用于快进和慢放 |
StartPackedMovementMode | 移动发生前的MovementMode |
StartLocation | 移动发生前的位置 |
StartVelocity | 移动发生前的速度 |
EndPackedMovementMode | 移动发生后的MovementMode |
SavedLocation | 移动发生后的位置 |
SavedVelocity | 移动发生后的速度 |
Acceleration | 移动所用加速度 |
理论上只要有这些数据,就能复盘整个移动过程,也可用作回放功能。
ReplicateMoveToServer
首先会从SavedMoves里找到最早发生的一个ImportantMove(通过IsImportantMove判断),也就是最新被服务器确认的有显著差异的Move。
之后创建一个FSavedMove_Character并初始化。然后执行PerformMovement,对角色计算操作后的属性,设置上相关信息。
根据能否被合并,进行处理。
延迟发送Move
一个Move有可能可以被延迟一会,与后面的Move合并后再发给服务器。因此一个新建的Move被发往服务器前会先判断是否可以延迟发送。
首先判断是否开启了NetEnableMoveCombining,如果没开也不会延迟发送。
同时还会判断当前的Move是否能被延迟发送,会检查该Move前后MovementMode是否改变,如果改变也需要即使变化。也就是说,如果此次Move没有显著改变,那么则可以延后发送,理论上服务器根据之前的信息推算,结果应该是一样的。
然后会计算当前预期的移动更新时间间隔,根据当前网速、玩家数量等信息,在基准值ClientNetSendMoveDeltaTime上做调整,得到最终间隔,如果Tick时还没达到更新间隔,就会延迟发送Move,把它储存在PendingMove中,留着以后处理。
CallServerMove
函数接受两个参数,一个是刚创建的Move,另一个是之前获取的ImportantMove(ImportantMove可能为空)。不需要把整个Move都发往服务器,只需要位置、旋转、加速度等关键信息,并且这些信息会经过压缩。
压缩的过程简单来说,会尝试牺牲精度,把一些字段合并在一个数据结构中。
之后还会调用ServerMoveOld,把ImportantMove中的一些信息发送到服务器,可以简单理解为一种冗余的保险。
如果存在PendingMove,说明存在未合并的Move,需要调用ServerMoveDual一次发送两个连续的Move。否则,说明发送间隔较大,或者PendingMove已经被合并,就调用ServerMode发送这个Move。
ServerMoveOld
ServerMoveOld主要作为一种冗余措施,防止服务器新收到一个移动数据时,因为网络丢包而落后太多,导致移动判断不通过,进而纠正客户端位置。ServerMoveOld可以让服务器使用传递的加速度,粗略的从旧位置快速移动到新位置,不校验移动结果。
TODO:安全性如何保证?
TODO
-
具有物理模拟下的移动
-
移动如何通过RPC发送的