게임을 실행하였는데 특정 경우 Match Count가 움직이지 않는 버그가 발생했다.

본래 MatchCount가 4->0으로 이동하면 게임이 시작되는 구조인데, 해당 Count가 정상적으로 작동하지 않았다. 여러군데 로그를 찍어본 결과, 결과적으로 MatchState의 문제였다.
void ACDGameMode::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
FName CurrentMatchState = GetMatchState();
UE_LOG(LogTemp, Display, TEXT("ACDGameMode::Tick - Current MatchState: %s"), *CurrentMatchState.ToString());
...
}
이런식으로 해당 MatchState를 계속해서 추적하였는데 돌리자마자 아래의 결과가 나왔다.
ACDGameMode::Tick - Current MatchState: Aborted
당연히 내가 지정한 적 없는 MatchState였고, 무언가 중단되었음을 알리는 다소 불안한 메시지였다.

언리얼의 공식문서에는 복구할 수 없는 오류가 발생되었을 때 나타난다고 적혀있다.
다른 사람들의 후기를 살펴보면 보통 RestartMatch이후에 MatchState를 잘못 넘겨줘서 저런일이 발생한다는데, 나의 경우는 Restart를 호출하기도 전, 처음으로 진입하는 MatchState가 Aborted였다.
이것저것 찾다가, 로그를 뒤적거린 끝에 원인을 찾을 수 있었다.
Log LogNet Join request: /Game/Maps/StartUpMap?game=/Game/BP/GameMode/BP_LobbyGameMode.BP_LobbyGameMode_C
Display LogTemp ACDGameMode::Tick - Current MatchState: Aborted
기존에 Lobby->CDGameMap으로 전환하는 과정에서 Server는 정상적으로 GameMap으로 이동하였다. 위의 로그를 살펴보면 LoadMap까지도 정상적으로 성공하였었다. 그러나 클라이언트가 /Game/Maps/StartUpMap에서 Join을 요청하는 과정중에 일어났다.
즉 서버는 게임을 진행해야 하는 본맵(CDGameMap)을 중단하고, 이 과정 속에서 MatchState가 Aborted로 넘어가게 된다. 포인트는 의도하지 않은 맵 전환(Join 요청)이 걸려서 발생했다는 것이다.
이는 언리얼엔진 네트워크 구조상에서 기인하였는데, 클라이언트는 자신이 완전히 다른 맵으로 이동하기 전, 그러니까 확정되기 전까지는 이전 맵(StartUpMap)을 기준으로 행동한다. 나의 경우 아래의 코드처럼 PostLogin시 본 맵으로 넘어가는 구조를 갖고 있다.
void ALobbyGameMode::PostLogin(APlayerController* NewPlayer)
{
Super::PostLogin(NewPlayer);
...
...
if (NumberOfPlayers == 2)
{
UWorld* World = GetWorld();
if (World)
{
bUseSeamlessTravel = true;
World->ServerTravel(FString("/Game/Maps/CDGameMap?listen"));
}
}
}
이 PostLogin이라는 함수는 Client가 해당 맵을 인식하기 전, 컨트롤러가 넘어오게 되면 호출하게 된다. 즉 Client가 완전히 안정되기 전이 아닌, 컨트롤러가 넘어온것을 확인하면 PostLogin이 호출된다는 것이다. 때문에 맵을 확인하기 전까지, 클라이언트가 완벽한 준비상태에 들어서야 해당 게임을 시작해야 하는 조건을 가져야만 한다.
이에 대해 두가지 방법을 사용하였다.
하나는 클라이언트가 안정화될 시간을 벌어다줄 일정시간의 Timer도입이다.
void ALobbyGameMode::PostLogin(APlayerController* NewPlayer)
{
Super::PostLogin(NewPlayer);
int32 NumberOfPlayers = GameState.Get()->PlayerArray.Num();
if (NumberOfPlayers == 2)
{
GetWorld()->GetTimerManager().SetTimerForNextTick(this, &ALobbyGameMode::StartGame);
}
}
틱만 넘겨줘도 크게 오류는 발생하지 않는다. 그러나 안정된 코드를 원한다면 HasClientLoadedCurrentWorld()함수를 이용하는 편을 권장한다.
이름 그대로 클라이언트가 월드의 로딩이 완료됐을때를 확인하는 함수이다.
void ALobbyGameMode::CheckPlayersReadyAndStart()
{
for (APlayerState* PlayerState : GameState.Get()->PlayerArray)
{
APlayerController* PC = Cast<APlayerController>(PlayerState->GetOwner());
if (PC && !PC->HasClientLoadedCurrentWorld())
{
bAllPlayersReady = false;
break;
}
}
if (bAllPlayersReady)
{
StartGame();
}
}
PC에서 사용하는 함수인데, 이를 이용하여 각 클라이언트가 현재의 맵 로딩을 성공하였는지에 대한 여부를 확인할 수 있다. 플래그를 더하여 모든 플레이어가 준비되었을때만, Start Game()을 실행하는 방식으로 구현할 수 있다.