网络时钟对齐解决方案
由于网络延迟等问题,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; }
|