UE实现获取客户端与服务器的延迟

发布时间 2024-01-11 14:54:33作者: ViKyanite

思路简述

主要是采用NTP的思想:
image-20240111135752355

图中的时间点参数:

  1. Client's Send Time (t1): 客户端发送请求时的时间戳
  2. Server's Receive Time (t2): 服务器收到请求的时间戳
  3. Server's Transmit Time (t3): 服务器发送回应的时间戳
  4. Client's Receive Time (t4): 客户端收到服务器回应的时间戳

因为在UE中t2t3太过于接近,于是就将两个时间合并仅有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是符合这个要求。

RPC-要求和注意事项