Class GameDev* SheepAdult

[Unreal Engine 5] Server와 Client의 게임 시간 맞추기(Listen Server) 본문

Unreal Engine

[Unreal Engine 5] Server와 Client의 게임 시간 맞추기(Listen Server)

SheepAdult 2023. 6. 23. 19:37

 대부분의 게임엔 게임 플레이 타임이 표시된다. 하지만 컴퓨터마다 접속 시간이 달라 동시에 게임에 입장한다고 하더라도 컴퓨터마다의 속도가 다르기 때문에 GetWorld()->GetTimeSeconds()로 시간을 받아온다면 서버와 클라이언트마다의 시간이 모두 달라 게임에 악영향을 끼칠 수 있다. 그래서 서버 컴퓨터의 시간으로 클라이언트에 맞춰줄 필요가 있다. 그렇다면 서버와 클라이언트 사이의 시간 차를 구하여 클라이언트 컴퓨터에 연산하는 방향으로 일을 처리해야 한다.

 

 과정은 아래와 같다. 먼저 클라이언트의 전송 시각과 서버가 수신 후 전송 시각을 전송한 클라이언트가 이를 수신하면, 수신한 데이터와 수신한 시각을 바탕으로 서버와 클라이언트 사이의 시간차를 구한다.

서버와 클라이언트 사이의 시간 차 구하는 과정

 C++코드로 구현을 해보았다. 이는 HUD에 시간을 표기를 하므로 PlayerController 클래스에서 이루어진다.

00:00 포맷으로 시간을 표기하므로 int32(언리얼 기본 int 자료형) 자료형을 사용할 것이다.

 

 시간은 매 프레임 계산되므로 Tick함수에서 시간 계산 함수를 호출한다. Tick함수에서는 먼저 서버와 클라이언트 사이에 동기화를 해주기 위한 함수인 SetSyncBetweenServerAndClient 함수를 작성했다. SetTimeInHUD는 나중에 작성한다.

void AMainPlayerController::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	// HUD에 시간 표기
	SetTimeInHUD();
	// 서버와 클라이언트 사이의 시간 동기화
	SetSyncBetweenServerAndClient(DeltaTime);
}

void AMainPlayerController::SetSyncBetweenServerAndClient(float DeltaTime)
{
	// 매프레임 RPC를 호출하기엔 부담스럽기도 하고, 굳이 그렇게 할
    // 필요가 없기에 일정 주기를 두고 호출한다.
	RunningTimeForSync += DeltaTime;
    // 주기인 3.f는 하드 코딩으로 되어있지만 원하는 주기로 작성
    // IsLocalController()는 클라이언트에서 제어되는 로컬 컨트롤러면 true를 반환한다.
	if (IsLocalController() && 3.f < RunningTimeForSync)
	{
    	// Server에서 호출되는 Server RPC로 위의 그림 1에 해당한다.
        // Client에서 호출하고 Server에서 실행한다.
        // 인자가 a이다.
		ServerRequestServerTime(GetWorld()->GetTimeSeconds());
		TimeSyncRunningTime = 0.f;
	}
}

ServerRequestServerTime이 호출되면 서버는 필요한 시간들을 담아 다시 클라이언트에 전송해야하므로 Client RPC를 호출한다.

void AMainPlayerController::ServerRequestServerTime_Implementation(float TimeOfClientRequest)
{
	// 전송하는 서버 시각
	float ServerTimeOfReceipt = GetWorld()->GetTimeSeconds();
    // 클라이언트로 전송 (Client RPC)
	ClientReportServerTime(TimeOfClientRequest, ServerTimeOfReceipt);
}

void AMainPlayerController::ClientReportServerTime_Implementation(float TimeOfClientRequest, float TimeServerReceivedClientRequest)
{
	// 전송 후 다시 수신받는 데 까지의 시간 = RoundTrip time
	float RoundTripTime = GetWorld()->GetTimeSeconds() - TimeOfClientRequest;
    // RountTrip의 절반에 서버 시간을 더하면 대략적으로 현재 서버 시간을 짐작할 수 있다.
	float CurrentServerTime = TimeServerReceivedClientRequest + (0.5f * RoundTripTime);
    // 클라이언트와 서버 사이의 시간 차이
	ClientServerDelta = CurrentServerTime - GetWorld()->GetTimeSeconds();
}

서버와 클라이언트 사이의 시간차이를 구했으므로 이제 HUD에 표기해 보자. 우리는 00:00 포맷을 사용할 것이다.

 

void AMainPlayerController::SetGameTimeInHUD(float Time)
{
	// null check
	PlayerHUD = PlayerHUD == nullptr ? Cast<APlayerHUD>(GetHUD()) : PlayerHUD;
	bool bHUDValid = PlayerHUD &&
		PlayerHUD->CharacterOverlay &&
        // 위젯과 바인딩 된 텍스트
		PlayerHUD->CharacterOverlay->MatchCountdownText;
	if (bHUDValid)
	{
		int32 Minutes = FMath::FloorToInt(Time / 60.f);
		int32 Seconds = Time - Minutes * 60;
		FString TimeText = FString::Printf(TEXT("%02d:%02d"), Minutes, Seconds);
		PlayerHUD->CharacterOverlay->PlayTimeText->SetText(FText::FromString(TimeText));
	}
}

void AMainPlayerController::SetTimeInHUD()
{
	uint32 SecondsLeft = FMath::CeilToInt(TotalTime - 서버시간);
    // int형의 시간을 표시할 것이므로 1초 단위로만 표기하면 된다. 이에 내림을 사용한다.
	if (CountdownInt != SecondsLeft)
	{
    	// HUD에 남은 Game Time을 업데이트한다.
		SetGameTimeInHUD(MatchTime - 서버시간);
	}
	CountdownInt = SecondsLeft;
}

// 서버시간은 아래와 같이 구한다.
float AMainPlayerController::GetServerTime()
{
	// HasAuthority() = 서버의 (쉽게 말해)Pawn들에서 호출됨
	// 서버에서 구동되는 Pawn들은 서버 시간을 반환하면 되므로 그냥 지난 시간을 반환한다.
	if (HasAuthority()) return GetWorld()->GetTimeSeconds();
    // 클라이언트에서 구동되는 Pawn들은 클라이언트 시간에 (클라 - 서버)시간을 더해준다.
	return GetWorld()->GetTimeSeconds() + ClientServerDelta;
}

마지막으로 컨트롤러가 네트워크에 연결됐을 때, 가장 빠른 시간내에 시간을 설정하고 싶기 때문에 ReceivedPlayer() 함수를 상속받아 사용해 주면 된다. 해당 함수를 호출하지 않는다면 우리가 설정해 준 업데이트 주기가 되기 전까지(게임에 접속한 직후) 서버 시간과 차이가 있을 가능성이 크다. 아래와 같다.

// 유효한 네트워크 연결이 있는 가장 빠른 시점 네트워크 시각 요청을 추가
void AMainPlayerController::ReceivedPlayer()
{
	Super::ReceivedPlayer();
	// 이 또한 서버에서는 필요 없는 작업이며, 각각의 컨트롤러들만 설정되면 되므로
    // IsLocalController()하에 호출한다.
	if (IsLocalController())
	{
		ServerRequestServerTime(GetWorld()->GetTimeSeconds());
	}
}

아래는 최종 그림이다.

아래는 결과 영상이다. 로그 찍히는 것을 보면 Local controller 일 경우 호출하므로 서로 다른 시간대에 주고 받고 한다.

https://www.youtube.com/watch?v=n-iihphmo48