세션 생성과 참가 흐름 이해하기
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 의존성
PublicDependencyModuleNames.AddRange(new[]
{
"Core", "CoreUObject", "Engine", "InputCore",
"OnlineSubsystem", "OnlineSubsystemUtils"
});
파일 경로: Source/CaptureArena/Online/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;
};
#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. 핵심 요약
세션 생성과 참가 흐름 이해하기 미니 퀴즈
선택 즉시 정답과 해설을 확인할 수 있습니다. 결과는 이 브라우저에만 저장됩니다.
학습을 마쳤나요?
직접 실습과 점검 질문까지 확인한 뒤 완료로 표시하세요.