본문으로 건너뛰기
Unreal Engine 멀티플레이LESSON 28

세션 생성과 참가 흐름 이해하기

난이도입문 → 초급
예상 시간120분
선수지식27강의 ClientTravel·ServerTravel 구분

28강. 세션 생성과 참가 흐름 이해하기

1. 이번 강의에서 해결할 문제​

세션 생성 성공을 곧바로 상대와 네트워크 연결된 상태로 착각하거나, Join 성공 뒤 Connect String을 사용하지 않아 로비로 이동하지 않습니다. Online Subsystem 작업은 비동기이며 Create → Advertise → Find → Join → Resolve Address → ClientTravel 단계를 각각 확인해야 합니다.

2. 학습 목표​

3. 핵심 개념​

세션은 검색 가능한 매치 메타데이터와 참가 수명을 표현합니다. 실제 네트워크 이동은 Join 결과에서 플랫폼별 Connect String을 얻고 로컬 PlayerController가 ClientTravel을 실행해야 합니다. 서버는 세션 생성 뒤 /Game/Maps/Lobby?listen으로 리슨 World를 엽니다.

Online Subsystem은 플랫폼 서비스와 비동기 통신하므로 호출 반환값은 “작업을 시작했는가”이고 완료 Delegate가 최종 결과입니다. Delegate Handle을 작업마다 보관하고 완료·실패·Subsystem 종료에서 해제합니다.

4. 단계별 실습​

CaptureArena.Build.cs 의존성

CaptureArena.Build.cs
PublicDependencyModuleNames.AddRange(new[]
{
"Core", "CoreUObject", "Engine", "InputCore",
"OnlineSubsystem", "OnlineSubsystemUtils"
});

파일 경로: Source/CaptureArena/Online/CaptureSessionSubsystem.h

CaptureSessionSubsystem.h
#pragma once

#include "CoreMinimal.h"
#include "Interfaces/OnlineSessionInterface.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "CaptureSessionSubsystem.generated.h"

UCLASS()
class CAPTUREARENA_API UCaptureSessionSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()

public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;

void CreateSession(bool bLan);
void FindSessions(bool bLan);
void JoinFirstSession();

private:
void HandleCreateSessionComplete(FName Name, bool bSuccess);
void HandleFindSessionsComplete(bool bSuccess);
void HandleJoinSessionComplete(FName Name, EOnJoinSessionCompleteResult::Type Result);

IOnlineSessionPtr SessionInterface;
TSharedPtr<FOnlineSessionSearch> SessionSearch;
FDelegateHandle CreateHandle;
FDelegateHandle FindHandle;
FDelegateHandle JoinHandle;
};
CaptureSessionSubsystem.cpp
#include "Online/CaptureSessionSubsystem.h"

#include "OnlineSessionSettings.h"
#include "OnlineSubsystem.h"

void UCaptureSessionSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
IOnlineSubsystem* Online = IOnlineSubsystem::Get();
SessionInterface = Online ? Online->GetSessionInterface() : nullptr;
}

void UCaptureSessionSubsystem::CreateSession(bool bLan)
{
if (!SessionInterface.IsValid()) return;

FOnlineSessionSettings Settings;
Settings.bIsLANMatch = bLan;
Settings.NumPublicConnections = 2;
Settings.bShouldAdvertise = true;
Settings.bAllowJoinInProgress = true;
Settings.bUsesPresence = true;
Settings.Set(FName("MODE"), FString("CAPTURE"),
EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);

CreateHandle = SessionInterface->AddOnCreateSessionCompleteDelegate_Handle(
FOnCreateSessionCompleteDelegate::CreateUObject(
this, &ThisClass::HandleCreateSessionComplete));

if (!SessionInterface->CreateSession(0, NAME_GameSession, Settings))
{
SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateHandle);
}
}

void UCaptureSessionSubsystem::HandleCreateSessionComplete(
FName Name, bool bSuccess)
{
SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateHandle);
if (!bSuccess || !GetWorld()) return;

GetWorld()->ServerTravel(TEXT("/Game/Maps/Lobby?listen"));
}

void UCaptureSessionSubsystem::FindSessions(bool bLan)
{
if (!SessionInterface.IsValid()) return;

SessionSearch = MakeShared<FOnlineSessionSearch>();
SessionSearch->bIsLanQuery = bLan;
SessionSearch->MaxSearchResults = 50;
SessionSearch->QuerySettings.Set(
SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);

FindHandle = SessionInterface->AddOnFindSessionsCompleteDelegate_Handle(
FOnFindSessionsCompleteDelegate::CreateUObject(
this, &ThisClass::HandleFindSessionsComplete));

if (!SessionInterface->FindSessions(0, SessionSearch.ToSharedRef()))
{
SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(FindHandle);
}
}

void UCaptureSessionSubsystem::HandleFindSessionsComplete(bool bSuccess)
{
SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(FindHandle);
UE_LOG(LogTemp, Display, TEXT("Find success=%d Results=%d"),
bSuccess, SessionSearch.IsValid() ? SessionSearch->SearchResults.Num() : 0);
}

void UCaptureSessionSubsystem::JoinFirstSession()
{
if (!SessionInterface.IsValid() || !SessionSearch.IsValid()
|| SessionSearch->SearchResults.IsEmpty()) return;

JoinHandle = SessionInterface->AddOnJoinSessionCompleteDelegate_Handle(
FOnJoinSessionCompleteDelegate::CreateUObject(
this, &ThisClass::HandleJoinSessionComplete));

if (!SessionInterface->JoinSession(
0, NAME_GameSession, SessionSearch->SearchResults[0]))
{
SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinHandle);
}
}

void UCaptureSessionSubsystem::HandleJoinSessionComplete(
FName Name, EOnJoinSessionCompleteResult::Type Result)
{
SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinHandle);
if (Result != EOnJoinSessionCompleteResult::Success) return;

FString ConnectString;
if (!SessionInterface->GetResolvedConnectString(Name, ConnectString)) return;

if (APlayerController* PC = GetGameInstance()->GetFirstLocalPlayerController())
{
PC->ClientTravel(ConnectString, TRAVEL_Absolute);
}
}

Deinitialize에서는 유효한 Handle을 모두 Clear하고 검색 객체를 Reset합니다. 이미 세션이 있으면 새 Create 전에 DestroySession 완료를 기다리는 상태 머신을 추가해야 합니다. UI 버튼은 작업 중 비활성화해 중복 Delegate와 요청을 막습니다.

5. 코드가 동작하는 이유​

GameInstanceSubsystem은 맵 Travel보다 오래 살아 메뉴·로비 사이의 비동기 세션 작업을 관리하기 적합합니다. 호스트는 세션 광고 성공 뒤 listen Lobby를 열고, 참가자는 검색 결과를 Join한 뒤 Online Subsystem이 제공하는 주소로 ClientTravel합니다. GameMode는 서버 경기 규칙, SessionSubsystem은 프로세스의 온라인 접속 흐름을 맡습니다.

6. 자주 하는 실수와 해결법​

7. 직접 실습​

8. 이해 점검 질문 3개​

9. 핵심 요약​

MINI QUIZ

세션 생성과 참가 흐름 이해하기 미니 퀴즈

선택 즉시 정답과 해설을 확인할 수 있습니다. 결과는 이 브라우저에만 저장됩니다.

0 / 2
  1. 문제 1“세션 생성과 참가 흐름 이해하기” 실습 결과를 해석하는 올바른 기준은 무엇인가요?
  2. 문제 2“세션 생성과 참가 흐름 이해하기”에서 ‘Join 성공 뒤 Travel 누락’ 문제가 생겼습니다. 가장 알맞은 진단 또는 대응은 무엇인가요?
LESSON STATUS

학습을 마쳤나요?

직접 실습과 점검 질문까지 확인한 뒤 완료로 표시하세요.

28강. 세션 생성과 참가 흐름 이해하기 미완료 상태