思路简述
主要是采用NTP的思想:
图中的时间点参数:
- Client's Send Time (t1): 客户端发送请求时的时间戳
- Server's Receive Time (t2): 服务器收到请求的时间戳
- Server's Transmit Time (t3): 服务器发送回应的时间戳
- Client's Receive Time (t4): 客户端收到服务器回应的时间戳
因为在UE中t2与t3太过于接近,于是就将两个时间合并仅有t3。
实现
以下是参考代码,采用UE自带的RPC实现。
AMyPlayerController.h:
class AMyPlayerController : public APlayerController
{
GENERATED_BODY()
// ... other things
// TIME SYNC
public:
void StartSynchronization();
private:
// RPC
UFUNCTION(Server, Reliable)
void Server_HandleTimeSyncRequest(float ClientTimestamp);
UFUNCTION(Client, Reliable)
void Client_ReceiveServerTime(float ClientTimestamp, float ServerTimestamp);
void CalculateTimeOffset(float ClientTimestamp, float ServerTimestamp);
// 为了求NumberOfSyncs的平均时延
TArray<float> TimeOffsets;
int32 NumberOfSyncs;
int32 CurrentSyncIndex;
void SendNextSyncRequest();
void CalculateAverageOffset();
};
AMyPlayerController.cpp:
AMyPlayerController::AMyPlayerController()
{
// ....
// other init
NumberOfSyncs = 10; // Number of sync requests
CurrentSyncIndex = 0;
}
void AMyPlayerController::BeginPlay()
{
// Call the base class
Super::BeginPlay();
// ...
// other code
// only client calls
if (!HasAuthority())
{
StartSynchronization();
}
}
void AMyPlayerController::StartSynchronization()
{
TimeOffsets.Empty();
SendNextSyncRequest();
}
void AMyPlayerController::SendNextSyncRequest()
{
if (CurrentSyncIndex < NumberOfSyncs)
{
if (HasAuthority())
{
// Server does nothing here
}
else
{
float ClientTimestamp = GetWorld()->GetTimeSeconds();
Server_HandleTimeSyncRequest(ClientTimestamp);
}
}
else
{
CalculateAverageOffset();
}
}
void AMyPlayerController::Server_HandleTimeSyncRequest_Implementation(float ClientTimestamp)
{
float ServerTimestamp = GetWorld()->GetTimeSeconds();
Client_ReceiveServerTime(ClientTimestamp, ServerTimestamp);
}
void AMyPlayerController::Client_ReceiveServerTime_Implementation(float ClientTimestamp, float ServerTimestamp)
{
CalculateTimeOffset(ClientTimestamp, ServerTimestamp);
CurrentSyncIndex++;
SendNextSyncRequest(); // Send next request
}
void AMyPlayerController::CalculateTimeOffset(float ClientTimestamp, float ServerTimestamp)
{
float CurrentClientTime = GetWorld()->GetTimeSeconds();
float RoundTripTime = CurrentClientTime - ClientTimestamp;
float Offset = ServerTimestamp - ClientTimestamp - RoundTripTime / 2.0f;
TimeOffsets.Add(Offset);
AverTimeOffset = Offset;
}
void AMyPlayerController::CalculateAverageOffset()
{
float SumOffsets = 0.0f;
for (float Offset : TimeOffsets)
{
SumOffsets += Offset;
}
AverTimeOffset = SumOffsets / TimeOffsets.Num();
}
基本上的调用流程就是:
-
Client调用
SendNextSyncRequest()
, -
这个函数中调用了Server的RPC即
Server_HandleTimeSyncRequest_Implementation(t0)
,把客户端自己的时间传过去 -
Server的这个RPC中又调用了Client的RPC即
Client_ReceiveServerTime_Implementation(t0, t1)
, -
客户端在调用
Client_ReceiveServerTime_Implementation(t0, t1)
时会调用t3 = GetWorld()->GetTimeSeconds()
, -
t0, t1, t3三个时间点凑齐,开始计算时延
最后还暴露了一个参数NumberOfSyncs
,用于调整获取NumberOfSyncs
次时延的平均值。
疑问
Q1: 为什么放在APlayerController中?
因为RPC的调用有要求,需要客户端实际拥有这个Actor,显然APlayerController是符合这个要求。
标签:ClientTimestamp,void,float,AMyPlayerController,Client,UE,服务器,Server,客户端 From: https://www.cnblogs.com/Vikyanite/p/17958568