멀티플레이어 환경에서 전체 게임의 진행 시간을 표시하는 타이머 HUD를 구현할 때, 서버 시간과 클라이언트 서버 시간 간에 괴리가 발생하는 것을 확인할 수 있었다.
HUD에 표시하는 시간을 GetWorld()->GetTimeSeconds()
함수를 이용해 구했다. 그러나 이 함수가 반환하는 결과값을 그대로 사용하기에 적절하지 않다. 클라이언트가 서버에 연결되는 시간만큼의 오차가 발생하기 때문이다.
게임은 서버에서 실행되고 있으므로 서버에서 GetWorld()->GetTimeSeconds()
을 실행하면 게임이 실행된 후의 시간을 정확히 구할 수 있다. 그러나 클라이언트에서 실행하면 '이 서버에 참여한 이후의 시간' 을 반환받는다.
서버의 시작과 거의 동시에 클라이언트가 접속했다면 1초 내외의 오차가 발생하겠지만, 만약 게임이 진행되는 중간에 난입할 수 있는 형태의 게임이라고 한다면 그 오차가 매우 커질 수 있다.
따라서 클라이언트와 서버의 통신을 이용해서 클라이언트에서 확실한 게임 시간을 구할 수 있도록 해야 한다.
통신에서 왕복 지연 또는 왕복 시간은 신호가 전송되는 데 걸리는 시간과 해당 신호가 수신되었음을 확인하는 데 걸리는 시간을 더한 것입니다.
여기서 Round Trip Time
(이하 RTT
) 이라는 개념을 알 필요가 있다.
RTT는 어떤 요청을 보냈을 때 그 요청에 대한 응답을 받을 때까지 걸리는 시간이다.
서버-클라이언트 구조에서 RTT는 필연적으로 발생하며, 시간을 정확하게 동기화해야 하는 타이머 UI의 경우에는 이 RTT까지를 고려해야 한다.
먼저 클라이언트에서 서버의 시간을 요청하는 RPC를 보낼 수 있다.
여기에서 유념해야 할 점은 RTT의 절반 만큼의 시간이 지나서야 서버에 도착한다는 점이다.
서버->클라이언트, 클라이언트->서버의 전송 속도에는 차이가 있을 수 있어서 단방향 전송 지연시간이 정확히 RTT/2라고 할 수는 없지만, RTT/2로 계산하여도 충분히 정확한 값을 얻을 수 있으므로 단방향 지연 시간은 RTT/2로 사용한다.
그리고 서버는 요청을 받은 즉시 현재 게임 시간을 구해서 다시 클라이언트에게 전달해 준다.
이때, 응답을 받은 클라이언트는 대뜸 시간 값만을 전달 받으면 안 된다. 클라이언트의 시간을 기록해 놓았다가 '언제 보낸 요청에 대한 서버의 시간인지' 를 알아야 이를 토대로 RTT를 구하고 시간을 동기화할 것이기 때문이다.
따라서 요청을 먼저 보낼 때, 클라이언트의 시간을 같이 파라미터로 담아서 보낼 수 있다.
그리고 요청을 받은 서버는 이 파라미터 값을 사용하지는 않지만, '클라이언트가 이 시간에 보낸 요청에 대한 응답이다' 라는 것을 알려주기 위해 그 값을 그대로 응답에 담아서 함께 보낸다.
서버 시작 후 3초 뒤에 클라이언트가 게임에 참여했다고 가정해 보자. 그리고 단방향 지연 시간을 0.1초로 가정한다.
client time 0.0
을 담아서 보냄client time 0.0
그리고 server time 3.1
을 함께 보낸다.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();
}