[UE5] 클라이언트-서버 간 Game Timer 동기화

kkado·2024년 7월 3일
0

UE5

목록 보기
46/61
post-thumbnail

멀티플레이어 환경에서 전체 게임의 진행 시간을 표시하는 타이머 HUD를 구현할 때, 서버 시간과 클라이언트 서버 시간 간에 괴리가 발생하는 것을 확인할 수 있었다.

HUD에 표시하는 시간을 GetWorld()->GetTimeSeconds() 함수를 이용해 구했다. 그러나 이 함수가 반환하는 결과값을 그대로 사용하기에 적절하지 않다. 클라이언트가 서버에 연결되는 시간만큼의 오차가 발생하기 때문이다.

게임은 서버에서 실행되고 있으므로 서버에서 GetWorld()->GetTimeSeconds() 을 실행하면 게임이 실행된 후의 시간을 정확히 구할 수 있다. 그러나 클라이언트에서 실행하면 '이 서버에 참여한 이후의 시간' 을 반환받는다.

서버의 시작과 거의 동시에 클라이언트가 접속했다면 1초 내외의 오차가 발생하겠지만, 만약 게임이 진행되는 중간에 난입할 수 있는 형태의 게임이라고 한다면 그 오차가 매우 커질 수 있다.

따라서 클라이언트와 서버의 통신을 이용해서 클라이언트에서 확실한 게임 시간을 구할 수 있도록 해야 한다.


Round Trip Time

통신에서 왕복 지연 또는 왕복 시간은 신호가 전송되는 데 걸리는 시간과 해당 신호가 수신되었음을 확인하는 데 걸리는 시간을 더한 것입니다.

여기서 Round Trip Time(이하 RTT) 이라는 개념을 알 필요가 있다.
RTT는 어떤 요청을 보냈을 때 그 요청에 대한 응답을 받을 때까지 걸리는 시간이다.

서버-클라이언트 구조에서 RTT는 필연적으로 발생하며, 시간을 정확하게 동기화해야 하는 타이머 UI의 경우에는 이 RTT까지를 고려해야 한다.


계산

먼저 클라이언트에서 서버의 시간을 요청하는 RPC를 보낼 수 있다.
여기에서 유념해야 할 점은 RTT의 절반 만큼의 시간이 지나서야 서버에 도착한다는 점이다.

서버->클라이언트, 클라이언트->서버의 전송 속도에는 차이가 있을 수 있어서 단방향 전송 지연시간이 정확히 RTT/2라고 할 수는 없지만, RTT/2로 계산하여도 충분히 정확한 값을 얻을 수 있으므로 단방향 지연 시간은 RTT/2로 사용한다.

그리고 서버는 요청을 받은 즉시 현재 게임 시간을 구해서 다시 클라이언트에게 전달해 준다.

이때, 응답을 받은 클라이언트는 대뜸 시간 값만을 전달 받으면 안 된다. 클라이언트의 시간을 기록해 놓았다가 '언제 보낸 요청에 대한 서버의 시간인지' 를 알아야 이를 토대로 RTT를 구하고 시간을 동기화할 것이기 때문이다.

따라서 요청을 먼저 보낼 때, 클라이언트의 시간을 같이 파라미터로 담아서 보낼 수 있다.

그리고 요청을 받은 서버는 이 파라미터 값을 사용하지는 않지만, '클라이언트가 이 시간에 보낸 요청에 대한 응답이다' 라는 것을 알려주기 위해 그 값을 그대로 응답에 담아서 함께 보낸다.

예시

서버 시작 후 3초 뒤에 클라이언트가 게임에 참여했다고 가정해 보자. 그리고 단방향 지연 시간을 0.1초로 가정한다.

  • 클라이언트 0.0초(서버 3.0초)에 서버에게 client time 0.0 을 담아서 보냄
  • 이 요청은 클라이언트 기준 0.1초 (서버 3.1초)에 서버에 도착한다.
  • 서버는 즉시 응답을 전송하며, client time 0.0 그리고 server time 3.1 을 함께 보낸다.
  • 이 응답은 클라이언트 기준 0.2초 (서버 3.2초)에 클라이언트에 도착한다.
  • 클라이언트는 client time 0.0과 현재 시간 0.2를 비교해 RTT는 0.2 라는 것을 확인할 수 있다.
  • 그리고 받은 server time 3.1초가 실제 서버와의 차이 + 지연시간 이므로, 단방향 지연시간이 0.1초라는 점을 이용해서 server time에서 RTT/2를 뺌으로써 (3.1 - 0.1) 실제 서버와 클라이언트는 3초의 괴리가 있다는 것을 알아낼 수 있다.

구현

void ABlasterPlayerController::ServerRequestServerTime_Implementation(float TimeOfClientRequest)
{
	float ServerTimeOfReceipt = GetWorld()->GetTimeSeconds();
	ClientReportServerTime(TimeOfClientRequest, ServerTimeOfReceipt);
}

void ABlasterPlayerController::ClientReportServerTime_Implementation(float TimeOfClientRequest, float TimeServerReceivedClientRequest)
{
	float RTT = GetWorld()->GetTimeSeconds() - TimeOfClientRequest;
	float CurrentServerTime = TimeServerReceivedClientRequest - RTT / 2.f;
	ClientServerDelta = CurrentServerTime - GetWorld()->GetTimeSeconds();
}
profile
울면안돼 쫄면안돼 냉면됩니다

0개의 댓글