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만큼 지연을 추가하여 공정성을 보장하세요.