UE5的移动预测系统与GAS(Gameplay Ability System)有相似之处,但实现机制有所不同。本文将从底层解析UE5中客户端移动是如何预测并同步到服务器的。
基础架构:Character Movement Component
UE5的移动预测核心是
UCharacterMovementComponent
,它实现了客户端预测、服务器权威和回滚纠正的整套机制。
// UCharacterMovementComponent内部关键结构
struct FNetworkPredictionData_Client_Character
{
// 预测键相关
uint32 ClientUpdateNumber; // 客户端移动更新计数
TArray<FSavedMovePtr> SavedMoves; // 已发送但未确认的移动
FSavedMovePtr PendingMove; // 待发送的移动
FSavedMovePtr LastAckedMove; // 服务器最后确认的移动
// ...
};
客户端移动同步到服务器的流程
1. 输入收集与移动创建
每帧客户端处理输入并创建FSavedMove_Character
对象:
void UCharacterMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
// 1. 收集当前输入状态
// 2. 创建并填充FSavedMove对象
FSavedMove_Character* NewMove = AllocateNewMove();
NewMove->SetMoveFor(CharacterOwner, DeltaTime, Acceleration, NewRotation);
// 3. 执行预测移动
PerformMovement(DeltaTime);
// 4. 保存移动到预测数据中
ClientData->PendingMove = NewMove;
}
2. 网络RPCs发送移动数据
UE使用特殊的RPC机制发送移动信息:
void UCharacterMovementComponent::CallServerMove()
{
FSavedMovePtr LastClientMove = ClientData->SavedMoves.Last();
// 每个服务器移动请求包含:
// - 时间戳
// - 预测键(ClientUpdateNumber)
// - 加速度、旋转等移动参数
// - 位置(用于验证)
if (CanSendLastMove())
{
ServerMove(
LastClientMove->TimeStamp,
LastClientMove->Acceleration,
LastClientMove->GetCompressedFlags(),
LastClientMove->ClientUpdateNumber // 这是预测键
);
}
}
3. 服务器验证与权威处理
服务器接收到移动请求后验证和应用:
void UCharacterMovementComponent::ServerMove_Implementation(float TimeStamp, FVector_NetQuantize10 InAccel, uint8 CompressedMoveFlags, uint32 ClientUpdateNumber)
{
// 验证时间戳防止作弊
if (!IsValidTimeStamp(TimeStamp))
{
return;
}
// 应用移动
CharacterOwner->MoveAutonomous(TimeStamp, InAccel, CompressedMoveFlags);
// 记录最后确认的移动请求编号
ServerData->LastClientUpdateNumber = ClientUpdateNumber;
// 如必要,发送纠正信息
if (NeedsClientCorrection())
{
ClientAdjustPosition(CurrentServerTimeStamp, GetActorLocation(), GetActorRotation());
}
}
4. 客户端纠正机制
当客户端预测与服务器权威计算不匹配时,执行纠正:
void UCharacterMovementComponent::ClientAdjustPosition_Implementation(float TimeStamp, FVector NewLocation, FVector NewVelocity, UPrimitiveComponent* NewBase)
{
// 1. 查找匹配的预测移动
// 2. 计算错误量
// 3. 应用纠正并重新应用后续所有待确认移动
// 错误超过阈值时,直接纠正
if (ClientError > AllowedError)
{
// 丢弃所有待确认移动并直接采用服务器状态
UpdateComponentVelocity();
// 平滑过渡到正确位置
SmoothCorrection(NewLocation);
}
else
{
// 重新应用所有待确认移动
ForcePositionUpdate(TimeStamp);
ReplayMoves(ClientData);
}
}
与GAS预测系统的区别与联系
虽然角色移动和GAS都使用预测键(prediction key)概念,但有几个关键差异:
细粒度不同:
移动预测通常每帧或固定间隔发送
GAS预测通常基于能力激活事件触发
数据结构不同:
移动使用
FSavedMove_Character
保存状态GAS使用
FPredictionKey
和FGameplayAbilitySpec
网络优化:
移动预测专门设计了压缩算法减少带宽
移动预测有批量处理机制(
ServerMoveBatch
)减少RPC调用
底层源码关键点
在引擎源码层面,以下是几个核心实现点:
预测键生成:
// 在客户端生成唯一递增的预测键
ClientData->ClientUpdateNumber++;
移动数据压缩:
// 使用量化和标志位压缩移动数据
uint8 CompressedFlags = 0;
if (bPressedJump) CompressedFlags |= FLAG_Jump;
if (bWantsToCrouch) CompressedFlags |= FLAG_Crouch;
// FVector_NetQuantize10用于位置压缩
网络带宽优化:
// 仅当必要时发送完整数据
if (PendingMove->GetCompressedFlags() != LastAckedMove->GetCompressedFlags())
{
// 发送更完整的状态
ServerMoveFull();
}
else
{
// 发送增量更新
ServerMoveMinimal();
}
物理状态同步:
当使用物理模拟时,底层还会同步FRigidBodyState
确保物理状态一致。
深入定制移动预测
如果需要扩展默认预测系统,可以:
- 派生自己的
FSavedMove
类:
class FMySavedMove : public FSavedMove_Character
{
// 添加自定义状态
bool bMyCustomFlag;
virtual void SetMoveFor(...) override
{
Super::SetMoveFor(...);
// 保存自定义状态
bMyCustomFlag = MyCharacter->IsCustomFlagActive();
}
virtual void PrepareForReplay() override
{
Super::PrepareForReplay();
// 设置回放状态
MyCharacter->SetCustomFlag(bMyCustomFlag);
}
};
- 扩展
UCharacterMovementComponent
:
UMyMovementComponent::AllocateNewMove()
{
return new FMySavedMove();
}
高级网络优化考虑
客户端权威与服务器权威平衡:
可以通过修改ShouldUsePackedMovementRPCs()
和ClientAuthorativePosition
调整。移动补偿(Lag Compensation):
在UWorld::GetTimeSeconds()
基础上,UE提供FRewindData
支持服务器回滚验证。智能合并与优先级:
可定制ProcessQueuedMoves()
控制移动请求的优先级和合并策略。