다같이 모여서 게임을 해봤는데 로비에 5명이 모여 게임을 시작하면 게임 맵에서 술래/숨는 사람 역할이 배정될 때 가끔 한두 명이 역할도 없고 캐릭터도 없이 덩그러니 떨어져 있음

혼자 PIE로 돌릴 땐 멀쩡했는데 팀원들과 EOS로 붙어서 테스트하면 랜덤하게 터짐

// FDLobbyGameMode::TryStartMatch
if (GameState->PlayerArray.Num() < MinPlayersToStart) return;  // 3명 미만이면 무시
GetWorld()->ServerTravel(NextLevelName);

 

ServerTravel은 서버가 접속자 전원을 데리고 다음 레벨로 이동하는 엔진 함수

여기까지는 문제없음 모두가 데려가짐

 

현재 로비 게임모드는 bUseSeamlessTravel을 켜지 않았어서 로비 -> 게임은 하드 트래블

하드 트래블에서는 각 클라이언트가 접속을 완전히 끊었다가 새 맵을 로드한 뒤 서버에 다시 붙는다

즉 클라들은 한 명씩, 제각각의 속도로 재접속하는 것

 

한마디로 로비 -> 게임 맵으로 ServerTravel 후 서버가 클라 재접속을 기다리지 않고 고정 타이머로 역할 배정을 해버려서 느리게 접속한 플레이어가 역할 없이 게임에 남음

 

서버는 새 맵이 뜨자마자 StartPlay가 돈다

// FDGameMode::StartPlay
GetWorldTimerManager().SetTimer(PhaseTimerHandle, this, &AFDGameMode::AssignRoles, 5.f, false);

 

맵 뜨고 5초 뒤에 역할 배정

 

누군가의 인터넷이 느리거나 EOS 릴레이 핸드셰이크가 삐끗해서 5초 안에 재접속을 못 끝내면 그 순간 AssignRoles가 도는데 PlayerArray에는 그 사람이 없는 것

// AssignRoles는 지금 접속해 있는 사람만 순회
TArray<APlayerState*> Players = GameState->PlayerArray;   // ← 재접속 늦은 사람은 빠져 있음
const int32 TaggerIndex = FMath::RandRange(0, Players.Num() - 1);

 

 

게임 맵이 정확히 몇 명이 다 와야 하는지를 알아야 함 

그래야 "3명 있는데 2명은 오는 중"과 "3명이 전부"를 구분할 수 있음

 

GameMode, GameState, PlayerState는 레벨이 바뀌면 전부 파괴되고 새로 생성됨

레벨을 건너 살아남는 객체는 GameInstance 하나뿐

 

전체 흐름

  1. 로비: 시작이 확정되는 순간(ServerTravel 직전)의 PlayerArray.Num()을 GameInstance에 실어둔다
  2. 게임 맵: StartPlay에서 그 값을 꺼내 그 인원이 다 모일 때까지 폴링으로 기다린 뒤 역할 배정에 들어간다
    단, 한 명이 계속 안 와도 멈추지 않도록 최대 대기시간(타임아웃) 안전장치를 둔다
  3. : 매치가 시작(로스터 확정)된 뒤 접속하려는 난입자는 PreLogin에서 거부

 

코드

1. GameInstance

// FDGameInstance.h
// GameInstance는 레벨이 바뀌어도 안 죽으니 이 값이 그대로 게임 맵까지 건너감
int32 ExpectedPlayerCount = 0;

 

2. 로비 : ServerTravel 직전에 인원 확정

// FDLobbyGameMode::TryStartMatch, ServerTravel 바로 위
const int32 StartCount = GameState->PlayerArray.Num();
if (UFDGameInstance* GI = GetGameInstance<UFDGameInstance>())  // 월드의 GameInstance를 타입 캐스트해 꺼냄
{
    GI->ExpectedPlayerCount = StartCount;
}
GetWorld()->ServerTravel(NextLevelName);

 

3. 게임 맵 : 고정 타이머를 전원 대기 폴링으로 교체

void AFDGameMode::StartPlay()
{
    Super::StartPlay();

    if (const UFDGameInstance* GI = GetGameInstance<UFDGameInstance>())
    {
        ExpectedPlayerCount = GI->ExpectedPlayerCount;   // 로비가 실어보낸 목표 인원 회수
    }

    PlayerWaitElapsed = 0.f;

    // 5초 고정 대신 전원 모일 때까지 0.5초 간격 반복 폴링 (SetTimer 마지막 인자 true = 반복)
    GetWorldTimerManager().SetTimer(
        WaitTimerHandle, this, &AFDGameMode::WaitForPlayers, PlayerWaitInterval, true);
}

void AFDGameMode::WaitForPlayers()
{
    PlayerWaitElapsed += PlayerWaitInterval;

    const int32 Current = GameState->PlayerArray.Num();
    const bool bEveryoneHere = (ExpectedPlayerCount > 0) && (Current >= ExpectedPlayerCount);
    const bool bTimedOut     = (PlayerWaitElapsed >= MaxPlayerWaitTime);

    // 아직 덜 모였고 타임아웃도 아니면 다음 폴링까지 대기
    if (!bEveryoneHere && !bTimedOut)
    {
        return;
    }


    // 시작 처리
    bRosterLocked = true;
    GetWorldTimerManager().ClearTimer(WaitTimerHandle);
    AssignRoles();
}

 

4. 난입 차단 : PreLogin

void AFDGameMode::PreLogin(const FString& Options, const FString& Address,
                           const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage)
{
    // PreLogin은 PostLogin보다 먼저 불리는 입장 심사대
    // ErrorMessage에 값을 채우면 엔진이 그 접속을 승인하지 않음
    if (bRosterLocked)
    {
        ErrorMessage = TEXT("Match already in progress");
        return;
    }
    
    Super::PreLogin(Options, Address, UniqueId, ErrorMessage);
}

 

락이 걸린 뒤(=매치 시작 후)의 접속만 튕겨냄

+ Recent posts