PART 9 - 강의 1/3

Listen Server 구현

코옵 게임을 위한 P2P 호스팅 시스템

01

Listen Server 개요

호스트 플레이어가 서버 역할

아키텍처 ┌─────────────────────────────────────────────────────────────┐ │ Listen Server 구조 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ [Host Player Machine] │ │ ┌───────────────────────────────────────┐ │ │ │ [Server Logic] [Client Logic] │ │ │ │ │ │ │ │ │ │ └────────┬───────────┘ │ │ │ │ │ │ │ │ │ [Local Player] │ │ │ └───────────────────────────────────────┘ │ │ │ │ │ │ Network │ │ ┌────────────┼────────────┐ │ │ v v v │ │ [Client 1] [Client 2] [Client 3] │ │ │ │ 장점: 서버 비용 없음, 빠른 세션 생성 │ │ 단점: 호스트 이탈 시 문제, 호스트 유리 (0ms 핑) │ │ │ └─────────────────────────────────────────────────────────────┘
사용 사례

Listen Server는 2-8명 규모의 코옵 게임, 프라이빗 매치, LAN 파티 등에 적합합니다. 경쟁 게임에서는 호스트 어드밴티지 때문에 권장하지 않습니다.

02

Listen Server 생성

세션 호스팅 시작

C++ // Listen Server 시작 (호스트) void UMySessionSubsystem::HostGame(int32 MaxPlayers) { IOnlineSubsystem* OSS = IOnlineSubsystem::Get(); IOnlineSessionPtr Sessions = OSS->GetSessionInterface(); // 세션 설정 FOnlineSessionSettings SessionSettings; SessionSettings.NumPublicConnections = MaxPlayers; SessionSettings.bIsLANMatch = false; SessionSettings.bUsesPresence = true; SessionSettings.bAllowJoinInProgress = true; SessionSettings.bAllowInvites = true; SessionSettings.bShouldAdvertise = true; // 게임 정보 SessionSettings.Set("GameMode", "Coop", EOnlineDataAdvertisementType::ViaOnlineService); SessionSettings.Set("MapName", CurrentMapName, EOnlineDataAdvertisementType::ViaOnlineService); // 델리게이트 바인딩 CreateSessionDelegateHandle = Sessions->OnCreateSessionCompleteDelegates .AddUObject(this, &UMySessionSubsystem::OnCreateSessionComplete); // 세션 생성 Sessions->CreateSession(0, NAME_GameSession, SessionSettings); } void UMySessionSubsystem::OnCreateSessionComplete( FName SessionName, bool bSuccess) { IOnlineSessionPtr Sessions = IOnlineSubsystem::Get()->GetSessionInterface(); Sessions->OnCreateSessionCompleteDelegates .Remove(CreateSessionDelegateHandle); if (bSuccess) { // Listen Server로 맵 로드 UGameplayStatics::OpenLevel( this, "/Game/Maps/CoopMap", true, "listen"); // "listen" 옵션이 핵심! } }
03

클라이언트 참여

세션 검색 및 참여

C++ // 세션 검색 void UMySessionSubsystem::FindGames() { IOnlineSessionPtr Sessions = IOnlineSubsystem::Get()->GetSessionInterface(); SessionSearch = MakeShareable(new FOnlineSessionSearch()); SessionSearch->MaxSearchResults = 20; SessionSearch->bIsLanQuery = false; SessionSearch->QuerySettings.Set( SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals); FindSessionsDelegateHandle = Sessions->OnFindSessionsCompleteDelegates .AddUObject(this, &UMySessionSubsystem::OnFindSessionsComplete); Sessions->FindSessions(0, SessionSearch.ToSharedRef()); } // 세션 참여 void UMySessionSubsystem::JoinGame(int32 SessionIndex) { if (!SessionSearch.IsValid() || SessionIndex >= SessionSearch->SearchResults.Num()) return; IOnlineSessionPtr Sessions = IOnlineSubsystem::Get()->GetSessionInterface(); JoinSessionDelegateHandle = Sessions->OnJoinSessionCompleteDelegates .AddUObject(this, &UMySessionSubsystem::OnJoinSessionComplete); Sessions->JoinSession( 0, NAME_GameSession, SessionSearch->SearchResults[SessionIndex]); } void UMySessionSubsystem::OnJoinSessionComplete( FName SessionName, EOnJoinSessionCompleteResult::Type Result) { if (Result == EOnJoinSessionCompleteResult::Success) { IOnlineSessionPtr Sessions = IOnlineSubsystem::Get()->GetSessionInterface(); // 호스트 주소 획득 FString ConnectString; Sessions->GetResolvedConnectString(SessionName, ConnectString); // 호스트에 접속 APlayerController* PC = GetWorld()->GetFirstPlayerController(); if (PC) { PC->ClientTravel(ConnectString, TRAVEL_Absolute); } } }
04

호스트/클라이언트 분기

역할별 로직 처리

C++ // 네트워크 역할 확인 void AMyActor::DoNetworkAwareLogic() { // 방법 1: NetMode 확인 ENetMode NetMode = GetNetMode(); switch (NetMode) { case NM_Standalone: // 싱글플레이어 break; case NM_DedicatedServer: // 데디케이티드 서버 (화면 없음) break; case NM_ListenServer: // 리슨 서버 (호스트 플레이어) // 서버 로직 + 클라이언트 로직 모두 실행 break; case NM_Client: // 순수 클라이언트 break; } // 방법 2: Authority 확인 if (HasAuthority()) { // 서버 (Dedicated/Listen 모두) } else { // 클라이언트 } // 방법 3: 로컬 제어 확인 if (IsLocallyControlled()) { // 이 머신에서 조종하는 캐릭터 } } // Listen Server에서 호스트 플레이어 특별 처리 void AMyPlayerController::BeginPlay() { Super::BeginPlay(); if (GetNetMode() == NM_ListenServer && IsLocalController()) { // 호스트 플레이어 - 서버 권한 + 로컬 제어 bIsHost = true; // 호스트 전용 UI 표시 ShowHostControls(); } }
SUMMARY

핵심 요약

  • Listen Server - 호스트 플레이어가 서버 역할, 무료 호스팅
  • "listen" 옵션 - OpenLevel에 "listen"으로 서버 시작
  • GetResolvedConnectString - 호스트 주소 획득 후 ClientTravel
  • NM_ListenServer - 호스트에서 서버+클라이언트 로직 모두 실행
PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: Listen Server 세션 생성

CreateSession으로 Listen Server 세션을 생성하고 bIsLANMatch, NumPublicConnections을 설정하세요. 다른 클라이언트에서 FindSessions와 JoinSession으로 접속하세요.

실습 2: 호스트 특수 처리

NM_ListenServer에서 호스트 플레이어의 IsLocalController()가 true인 점을 활용하여 호스트 전용 UI(킥 버튼, 서버 설정)를 구현하세요. 호스트에서만 RepNotify가 호출되지 않는 문제를 REPNOTIFY_Always로 해결하세요.

심화 과제

Listen Server의 호스트 어드밴티지(0ms 레이턴시)를 완화하는 인위적 지연 시스템을 구현하세요. 호스트의 입력 처리에 평균 클라이언트 RTT/2만큼 지연을 추가하여 공정성을 보장하세요.