网络时钟对齐解决方案
由于网络延迟等问题,Client 时间与 DS 时间可能不同。
对于 Client 需要有一个接近正确的 GetServerWorldTime 来获取当前 DS 的时间戳,尽可能保证在 DS 与各个不同的 Client 中,同一时刻该值唯一。
GameInstance
通过 GameInstance 来存储一些时间数据记录,最后保证所有读取数据从这里访问:
1 2 3
| int64 StartTicks = 0; float WorldSeconds = 0; float ServerWorldSeconds = 0;
|
首先在 UGameInstance::Init() 时,记录 StartTicks = FDateTime::Now().GetTicks();
后续在 GeServerWorldTimeSeconds 时,对 ServerWorldSeconds、WorldSeconds 进行更新;
ServerTimeSynchronizer
Register
在 Client 的 PlayerController 成功 Received 时,进行对时校验的注册,每隔 SyncServerTime_TimeInterval = 5.0f 进行一次对时;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| void APlayerController::ReceivedPlayer() { if (IsLocalController()) { ClientStartSyncTime(); } }
void APlayerController::ClientStartSyncTime() { if (GetNetMode() != NM_Client) return; GetWorld()->GetTimerManager().SetTimer(SyncServerTime_TimeHandle, [WeakSelfPtr = TWeakObjectPtr<APlayerController>(this)]() { if (!WeakSelfPtr.IsValid() || WeakSelfPtr->GetNetMode() != NM_Client) return; if (!WeakSelfPtr->ServerTimeSynchronizer.IsTimeSynced()) { WeakSelfPtr->C2S_ReqReportTime_Reliable(FDateTime::Now().GetTicks()); } else { WeakSelfPtr->C2S_ReqReportTime_Unreliable(FDateTime::Now().GetTicks()); } }, SyncServerTime_TimeInterval, true); }
|
Req & Res
通过 Client 定时发起对时请求 C2S_ReqReportTime,DS 接收到请求后进行回复 S2C_ResReportTime(如果从未进行过对时,则需要 Reliable);
在 Client 收到 Res 之后,可以根据发包、收包的时间差,不断校准 ServerTime;
1 2 3 4 5 6 7 8 9 10
| UFUNCTION(Reliable, Server) void C2S_ReqReportTime_Reliable(int64 ClientTime); UFUNCTION(Unreliable, Server) void C2S_ReqReportTime_Unreliable(int64 ClientTime);
void OnReceivedServerTime(int64 ClientTime, int64 ServerTime); UFUNCTION(Reliable, Client) void S2C_ResReportTime_Reliable(int64 ClientTime, int64 ServerTime); UFUNCTION(Unreliable, Client) void S2C_ResReportTime_Unreliable(int64 ClientTime, int64 ServerTime);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| void APlayerController::C2S_ReqReportTime_Reliable_Implementation(int64 ClientTime) { UGameInstance* GameInstance = GetGameInstance(); if (GameInstance == nullptr) return;
if (GameInstance->IsDedicatedServerInstance() && GameInstance->StartTicks > 0) { S2C_ResReportTime_Reliable(ClientTime, FDateTime::Now().GetTicks() - GameInstance->StartTicks); } }
void APlayerController::C2S_ReqReportTime_Unreliable_Implementation(int64 ClientTime) { UGameInstance* GameInstance = GetGameInstance(); if (GameInstance == nullptr) return;
if (GameInstance->IsDedicatedServerInstance() && GameInstance->StartTicks > 0) { S2C_ResReportTime_Unreliable(ClientTime, FDateTime::Now().GetTicks() - GameInstance->StartTicks); } }
void APlayerController::OnReceivedServerTime(int64 ClientTime, int64 ServerTime) { int64 ClientNow = FDateTime::Now().GetTicks(); int64 RTT = ClientNow - ClientTime; bool bUpdated = ServerTimeSynchronizer.UpdateServerTime(ServerTime, ClientNow, RTT); int64 EstimatedServerTime = ServerTimeSynchronizer.CurrentServerTime(ClientNow);
if (!ServerTimeSynchronizer.IsTimeSynced()) { C2S_ReqReportTime_Reliable(FDateTime::Now().GetTicks()); } }
void APlayerController::S2C_ResReportTime_Reliable_Implementation(int64 ClientTime, int64 ServerTime) { OnReceivedServerTime(ClientTime, ServerTime); }
void APlayerController::S2C_ResReportTime_Unreliable_Implementation(int64 ClientTime, int64 ServerTime) { OnReceivedServerTime(ClientTime, ServerTime); }
|
Calculate
通过 FServerTimeSynchronizer 记录时间并辅助对时;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class ENGINE_API FServerTimeSynchronizer { public: FServerTimeSynchronizer() = default;
public: int64 CurrentServerTime(int64 TimeNow = 0) const; bool UpdateServerTime(int64 ServerTime, int64 CurrentTime, int64 LatestRTT); bool IsTimeSynced() const { return bTimeSynced; }
private: int32 CalculateApproximatingDeltaTime(int64 TimeDelta) const; private: static constexpr int MAX_VALID_RTT = 500; static constexpr float APPROXIMATING_RATE = 0.33;
private: int64 ClientTime = 0; int64 EstimatedServerTime = 0; int LatestRTT = INT_MAX; float DeltaTimeClientToServer = 0.0; bool bTimeSynced = false; };
|
flowchart LR
Client_Req--->|RTT|DS
DS--->|RTT|Client_Res
每次收到包进行 UpdateServerTime,根据时间差计算出 RTT (收包发包时间差 / 2) 加上当时准确的 ServerTime(RPC 带下来的),可以计算出此时客户端对应的 EstimatedServerTime(此时预估的 ServerTime);同时记录下这次的 ClientTime;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| bool FServerTimeSynchronizer::UpdateServerTime(int64 ServerTime, int64 CurrentTime, int64 RTT) { if (RTT > MAX_VALID_RTT * ETimespan::TicksPerMillisecond) return false;
if (RTT > LatestRTT) return false;
int64 LastEstimatedServerTime = CurrentServerTime(CurrentTime); ClientTime = CurrentTime; EstimatedServerTime = ServerTime + RTT / 2; LatestRTT = RTT;
if ( !bTimeSynced || APPROXIMATING_RATE <= 0) { DeltaTimeClientToServer = 0.0; } else { DeltaTimeClientToServer = (float)(LastEstimatedServerTime - EstimatedServerTime); } return bTimeSynced = true; }
|
这样就可以在后续任何一次查询时,根据查询时的 ClientTime ,与本次对时的预估 ServerTime、 ClientTime 计算出期望的 ServerTime;
1 2 3 4 5 6
| int64 FServerTimeSynchronizer::CurrentServerTime(int64 TimeNow) const { if (!bTimeSynced) return 0; int64 TimeDelta = FMath::Max( TimeNow - ClientTime, 0ll ); return EstimatedServerTime + TimeDelta + CalculateApproximatingDeltaTime(TimeDelta); }
|
同时,这里引入一个 APPROXIMATING_RATE,进行一定的时间预测逼近,在 DeltaTime 足够小的时候,根据 DeltaTimeClientToServer(客户端与服务器的预测时间差值)进行一定的时间外推 / 内收,让结果尽可能准确。这里 DeltaTimeClientToServer < 0 说明客户端时间比服务器慢,则需要一定的加快。
1 2 3 4 5 6 7 8 9 10 11
| int32 FServerTimeSynchronizer::CalculateApproximatingDeltaTime(int64 TimeDelta) const { if (APPROXIMATING_RATE <= 0) return 0;
float fEls = float(TimeDelta) * APPROXIMATING_RATE; if (fEls >= FMath::Abs(DeltaTimeClientToServer)) return 0; return DeltaTimeClientToServer < 0 ? ceil(DeltaTimeClientToServer + fEls) : ceil(DeltaTimeClientToServer - fEls); }
|
GetServerTimeTicks
最后在 PlayerController 暴露 GetServerTimeTicks 给外部访问:
1 2 3 4
| int64 APlayerController::GetServerTimeTicks() { return ServerTimeSynchronizer.CurrentServerTime(FDateTime::Now().GetTicks()); }
|
GetServerWorldTimeSeconds
对 AGameStateBase::GetServerWorldTimeSeconds 进行重载,最后统一通过 WorldGameState 进行时间访问;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| double AGameStateBase::GetServerWorldTimeSeconds() const { UWorld* World = GetWorld(); if (World == nullptr) return 0.; if (UGameInstance* GameInstance = GetGameInstance()) { float NowSeconds = GetWorld()->TimeSeconds;
if (NowSeconds != GameInstance->WorldSeconds) { GameInstance->WorldSeconds = NowSeconds;
int64 NowServerTicks = 0; if (GameInstance->IsDedicatedServerInstance() || HasAuthority()) { NowServerTicks = FDateTime::Now().GetTicks() - GameInstance->StartTicks; } else if (APlayerController* PC = GetGameInstance()->GetFirstLocalPlayerController(GetWorld())) { NowServerTicks = PC->GetServerTimeTicks(); }
GameInstance->ServerWorldSeconds = NowServerTicks / ETimespan::TicksPerMillisecond / 1000.f; }
return GameInstance->ServerWorldSeconds; } return World->GetTimeSeconds() + ServerWorldTimeSecondsDelta; }
|