Online Subsystem
플랫폼 독립적인 온라인 서비스 통합 - 세션 관리, 인증, 크로스플레이
학습 목표
- Online Subsystem 아키텍처와 인터페이스 이해
- 세션 생성, 검색, 참가 구현
- Epic Online Services(EOS) 통합
- Steam 및 크로스플랫폼 지원
1. Online Subsystem 개요
1.1 Online Subsystem 아키텍처
Online Subsystem은 플랫폼별 온라인 서비스를 추상화하는 언리얼의 핵심 시스템입니다.
┌─────────────────────────────────────────────────────────────────┐
│ 게임 코드 │
│ (플랫폼 독립적 API 호출) │
├─────────────────────────────────────────────────────────────────┤
│ Online Subsystem Interface │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Session │ │ Identity │ │ Friends │ │ Leaderb │ │
│ │Interface │ │Interface │ │Interface │ │ Interface│ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Platform Implementations │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Steam │ │ EOS │ │ Null │ │ Custom │ │
│ │Subsystem │ │Subsystem │ │Subsystem │ │Subsystem │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────┘
1.2 주요 인터페이스
// Online Subsystem의 주요 인터페이스들
void ExploreOnlineSubsystem()
{
// Online Subsystem 가져오기
IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
if (!OnlineSub)
{
UE_LOG(LogOnline, Warning, TEXT("No Online Subsystem available"));
return;
}
// 현재 사용 중인 서브시스템 이름
FName SubsystemName = OnlineSub->GetSubsystemName();
UE_LOG(LogOnline, Log, TEXT("Using Online Subsystem: %s"), *SubsystemName.ToString());
// ===== 주요 인터페이스 =====
// 1. Session Interface - 게임 세션 관리
IOnlineSessionPtr SessionInterface = OnlineSub->GetSessionInterface();
// 2. Identity Interface - 사용자 인증
IOnlineIdentityPtr IdentityInterface = OnlineSub->GetIdentityInterface();
// 3. Friends Interface - 친구 목록
IOnlineFriendsPtr FriendsInterface = OnlineSub->GetFriendsInterface();
// 4. Leaderboards Interface - 리더보드
IOnlineLeaderboardsPtr LeaderboardsInterface = OnlineSub->GetLeaderboardsInterface();
// 5. Achievements Interface - 업적
IOnlineAchievementsPtr AchievementsInterface = OnlineSub->GetAchievementsInterface();
// 6. Presence Interface - 온라인 상태
IOnlinePresencePtr PresenceInterface = OnlineSub->GetPresenceInterface();
}
1.3 프로젝트 설정
// MyGame.Build.cs
public class MyGame : ModuleRules
{
public MyGame(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"InputCore",
"OnlineSubsystem", // Online Subsystem 핵심
"OnlineSubsystemUtils" // 유틸리티 함수
});
// EOS 사용 시
if (Target.Platform == UnrealTargetPlatform.Win64)
{
PublicDependencyModuleNames.Add("OnlineSubsystemEOS");
PublicDependencyModuleNames.Add("EOSShared");
}
// Steam 사용 시
PublicDependencyModuleNames.Add("OnlineSubsystemSteam");
}
}
2. 세션 관리 시스템
2.1 Session Manager Subsystem
// SessionManager.h
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "Interfaces/OnlineSessionInterface.h"
#include "OnlineSessionSettings.h"
#include "SessionManager.generated.h"
// 세션 검색 결과 데이터
USTRUCT(BlueprintType)
struct FSessionSearchResultData
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly)
FString SessionId;
UPROPERTY(BlueprintReadOnly)
FString HostName;
UPROPERTY(BlueprintReadOnly)
int32 CurrentPlayers;
UPROPERTY(BlueprintReadOnly)
int32 MaxPlayers;
UPROPERTY(BlueprintReadOnly)
int32 PingInMs;
UPROPERTY(BlueprintReadOnly)
FString MapName;
UPROPERTY(BlueprintReadOnly)
FString GameMode;
};
// 세션 이벤트 델리게이트
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSessionCreated, bool, bWasSuccessful);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSessionsFound, const TArray&, Results);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSessionJoined, bool, bWasSuccessful, const FString&, ErrorMessage);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSessionDestroyed);
UCLASS()
class MYGAME_API USessionManager : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
// ===== 세션 생성 =====
UFUNCTION(BlueprintCallable, Category = "Session")
void CreateSession(int32 MaxPlayers, bool bIsLAN, const FString& MapName);
// ===== 세션 검색 =====
UFUNCTION(BlueprintCallable, Category = "Session")
void FindSessions(bool bIsLAN = false);
// ===== 세션 참가 =====
UFUNCTION(BlueprintCallable, Category = "Session")
void JoinSession(int32 SessionIndex);
UFUNCTION(BlueprintCallable, Category = "Session")
void JoinSessionById(const FString& SessionId);
// ===== 세션 종료 =====
UFUNCTION(BlueprintCallable, Category = "Session")
void DestroySession();
// ===== 세션 정보 =====
UFUNCTION(BlueprintPure, Category = "Session")
bool IsInSession() const;
UFUNCTION(BlueprintPure, Category = "Session")
bool IsSessionHost() const;
// ===== 이벤트 =====
UPROPERTY(BlueprintAssignable, Category = "Session|Events")
FOnSessionCreated OnSessionCreated;
UPROPERTY(BlueprintAssignable, Category = "Session|Events")
FOnSessionsFound OnSessionsFound;
UPROPERTY(BlueprintAssignable, Category = "Session|Events")
FOnSessionJoined OnSessionJoined;
UPROPERTY(BlueprintAssignable, Category = "Session|Events")
FOnSessionDestroyed OnSessionDestroyed;
protected:
// 델리게이트 핸들
FDelegateHandle CreateSessionDelegateHandle;
FDelegateHandle FindSessionsDelegateHandle;
FDelegateHandle JoinSessionDelegateHandle;
FDelegateHandle DestroySessionDelegateHandle;
// 검색 결과 캐시
TSharedPtr SessionSearch;
// 콜백 함수
void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);
void OnFindSessionsComplete(bool bWasSuccessful);
void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);
void OnDestroySessionComplete(FName SessionName, bool bWasSuccessful);
// 헬퍼 함수
IOnlineSessionPtr GetSessionInterface() const;
FUniqueNetIdPtr GetLocalPlayerNetId() const;
};
2.2 Session Manager 구현
// SessionManager.cpp
#include "SessionManager.h"
#include "OnlineSubsystem.h"
#include "OnlineSessionSettings.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/LocalPlayer.h"
#include "GameFramework/PlayerController.h"
void USessionManager::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
UE_LOG(LogOnline, Log, TEXT("SessionManager initialized"));
}
void USessionManager::Deinitialize()
{
// 세션이 있으면 정리
if (IsInSession())
{
DestroySession();
}
Super::Deinitialize();
}
IOnlineSessionPtr USessionManager::GetSessionInterface() const
{
IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
if (OnlineSub)
{
return OnlineSub->GetSessionInterface();
}
return nullptr;
}
FUniqueNetIdPtr USessionManager::GetLocalPlayerNetId() const
{
UWorld* World = GetWorld();
if (!World) return nullptr;
const ULocalPlayer* LocalPlayer = World->GetFirstLocalPlayerFromController();
if (LocalPlayer)
{
return LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId();
}
return nullptr;
}
// ===== 세션 생성 =====
void USessionManager::CreateSession(int32 MaxPlayers, bool bIsLAN, const FString& MapName)
{
IOnlineSessionPtr Sessions = GetSessionInterface();
if (!Sessions.IsValid())
{
UE_LOG(LogOnline, Error, TEXT("Session interface not available"));
OnSessionCreated.Broadcast(false);
return;
}
FUniqueNetIdPtr LocalPlayerId = GetLocalPlayerNetId();
if (!LocalPlayerId.IsValid())
{
UE_LOG(LogOnline, Error, TEXT("Local player not found"));
OnSessionCreated.Broadcast(false);
return;
}
// 기존 세션 정리
auto ExistingSession = Sessions->GetNamedSession(NAME_GameSession);
if (ExistingSession)
{
Sessions->DestroySession(NAME_GameSession);
}
// 델리게이트 바인딩
CreateSessionDelegateHandle = Sessions->AddOnCreateSessionCompleteDelegate_Handle(
FOnCreateSessionCompleteDelegate::CreateUObject(
this, &USessionManager::OnCreateSessionComplete));
// 세션 설정
FOnlineSessionSettings SessionSettings;
SessionSettings.bIsLANMatch = bIsLAN;
SessionSettings.bUsesPresence = true;
SessionSettings.NumPublicConnections = MaxPlayers;
SessionSettings.NumPrivateConnections = 0;
SessionSettings.bAllowInvites = true;
SessionSettings.bAllowJoinInProgress = true;
SessionSettings.bAllowJoinViaPresence = true;
SessionSettings.bShouldAdvertise = true;
SessionSettings.bUseLobbiesIfAvailable = true;
// 커스텀 설정 추가
SessionSettings.Set(
SETTING_MAPNAME,
MapName,
EOnlineDataAdvertisementType::ViaOnlineService
);
SessionSettings.Set(
FName("GameMode"),
FString("CoOp"),
EOnlineDataAdvertisementType::ViaOnlineService
);
SessionSettings.Set(
FName("BuildVersion"),
FString("1.0.0"),
EOnlineDataAdvertisementType::ViaOnlineService
);
// 세션 생성
Sessions->CreateSession(*LocalPlayerId, NAME_GameSession, SessionSettings);
UE_LOG(LogOnline, Log, TEXT("Creating session - MaxPlayers: %d, LAN: %s"),
MaxPlayers, bIsLAN ? TEXT("Yes") : TEXT("No"));
}
void USessionManager::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{
IOnlineSessionPtr Sessions = GetSessionInterface();
if (Sessions.IsValid())
{
Sessions->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionDelegateHandle);
}
if (bWasSuccessful)
{
UE_LOG(LogOnline, Log, TEXT("Session created successfully: %s"), *SessionName.ToString());
// Listen 서버로 맵 열기
UWorld* World = GetWorld();
if (World)
{
// 세션 설정에서 맵 이름 가져오기
FString MapName = TEXT("MainLevel");
if (Sessions.IsValid())
{
FOnlineSessionSettings* Settings = Sessions->GetSessionSettings(NAME_GameSession);
if (Settings)
{
Settings->Get(SETTING_MAPNAME, MapName);
}
}
// listen 옵션으로 맵 로드
UGameplayStatics::OpenLevel(World, FName(*MapName), true, TEXT("listen"));
}
}
else
{
UE_LOG(LogOnline, Error, TEXT("Failed to create session"));
}
OnSessionCreated.Broadcast(bWasSuccessful);
}
// ===== 세션 검색 =====
void USessionManager::FindSessions(bool bIsLAN)
{
IOnlineSessionPtr Sessions = GetSessionInterface();
if (!Sessions.IsValid())
{
UE_LOG(LogOnline, Error, TEXT("Session interface not available"));
OnSessionsFound.Broadcast(TArray());
return;
}
FUniqueNetIdPtr LocalPlayerId = GetLocalPlayerNetId();
if (!LocalPlayerId.IsValid())
{
UE_LOG(LogOnline, Error, TEXT("Local player not found"));
OnSessionsFound.Broadcast(TArray());
return;
}
// 델리게이트 바인딩
FindSessionsDelegateHandle = Sessions->AddOnFindSessionsCompleteDelegate_Handle(
FOnFindSessionsCompleteDelegate::CreateUObject(
this, &USessionManager::OnFindSessionsComplete));
// 검색 설정
SessionSearch = MakeShareable(new FOnlineSessionSearch());
SessionSearch->MaxSearchResults = 20;
SessionSearch->bIsLanQuery = bIsLAN;
SessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);
// 빌드 버전 필터 (선택사항)
// SessionSearch->QuerySettings.Set(
// FName("BuildVersion"),
// FString("1.0.0"),
// EOnlineComparisonOp::Equals);
// 검색 시작
Sessions->FindSessions(*LocalPlayerId, SessionSearch.ToSharedRef());
UE_LOG(LogOnline, Log, TEXT("Searching for sessions - LAN: %s"),
bIsLAN ? TEXT("Yes") : TEXT("No"));
}
void USessionManager::OnFindSessionsComplete(bool bWasSuccessful)
{
IOnlineSessionPtr Sessions = GetSessionInterface();
if (Sessions.IsValid())
{
Sessions->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionsDelegateHandle);
}
TArray Results;
if (bWasSuccessful && SessionSearch.IsValid())
{
UE_LOG(LogOnline, Log, TEXT("Found %d sessions"),
SessionSearch->SearchResults.Num());
for (int32 i = 0; i < SessionSearch->SearchResults.Num(); i++)
{
const FOnlineSessionSearchResult& SearchResult = SessionSearch->SearchResults[i];
const FOnlineSession& Session = SearchResult.Session;
FSessionSearchResultData ResultData;
ResultData.SessionId = Session.GetSessionIdStr();
ResultData.CurrentPlayers = Session.SessionSettings.NumPublicConnections -
Session.NumOpenPublicConnections;
ResultData.MaxPlayers = Session.SessionSettings.NumPublicConnections;
ResultData.PingInMs = SearchResult.PingInMs;
// 호스트 이름
ResultData.HostName = Session.OwningUserName;
// 커스텀 설정 가져오기
Session.SessionSettings.Get(SETTING_MAPNAME, ResultData.MapName);
Session.SessionSettings.Get(FName("GameMode"), ResultData.GameMode);
Results.Add(ResultData);
UE_LOG(LogOnline, Log, TEXT(" [%d] %s - %s (%d/%d) Ping: %dms"),
i, *ResultData.HostName, *ResultData.MapName,
ResultData.CurrentPlayers, ResultData.MaxPlayers,
ResultData.PingInMs);
}
}
else
{
UE_LOG(LogOnline, Warning, TEXT("Session search failed or returned no results"));
}
OnSessionsFound.Broadcast(Results);
}
// ===== 세션 참가 =====
void USessionManager::JoinSession(int32 SessionIndex)
{
if (!SessionSearch.IsValid() ||
SessionIndex < 0 ||
SessionIndex >= SessionSearch->SearchResults.Num())
{
UE_LOG(LogOnline, Error, TEXT("Invalid session index: %d"), SessionIndex);
OnSessionJoined.Broadcast(false, TEXT("Invalid session index"));
return;
}
IOnlineSessionPtr Sessions = GetSessionInterface();
if (!Sessions.IsValid())
{
OnSessionJoined.Broadcast(false, TEXT("Session interface not available"));
return;
}
FUniqueNetIdPtr LocalPlayerId = GetLocalPlayerNetId();
if (!LocalPlayerId.IsValid())
{
OnSessionJoined.Broadcast(false, TEXT("Local player not found"));
return;
}
// 델리게이트 바인딩
JoinSessionDelegateHandle = Sessions->AddOnJoinSessionCompleteDelegate_Handle(
FOnJoinSessionCompleteDelegate::CreateUObject(
this, &USessionManager::OnJoinSessionComplete));
// 세션 참가
Sessions->JoinSession(
*LocalPlayerId,
NAME_GameSession,
SessionSearch->SearchResults[SessionIndex]
);
UE_LOG(LogOnline, Log, TEXT("Joining session at index: %d"), SessionIndex);
}
void USessionManager::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
IOnlineSessionPtr Sessions = GetSessionInterface();
if (Sessions.IsValid())
{
Sessions->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionDelegateHandle);
}
FString ErrorMessage;
bool bSuccess = (Result == EOnJoinSessionCompleteResult::Success);
if (bSuccess)
{
UE_LOG(LogOnline, Log, TEXT("Successfully joined session: %s"), *SessionName.ToString());
// 연결 문자열 가져오기
FString ConnectString;
if (Sessions->GetResolvedConnectString(SessionName, ConnectString))
{
UE_LOG(LogOnline, Log, TEXT("Connecting to: %s"), *ConnectString);
// 서버에 연결
APlayerController* PC = GetWorld()->GetFirstPlayerController();
if (PC)
{
PC->ClientTravel(ConnectString, TRAVEL_Absolute);
}
}
else
{
bSuccess = false;
ErrorMessage = TEXT("Failed to get connect string");
UE_LOG(LogOnline, Error, TEXT("%s"), *ErrorMessage);
}
}
else
{
switch (Result)
{
case EOnJoinSessionCompleteResult::SessionIsFull:
ErrorMessage = TEXT("Session is full");
break;
case EOnJoinSessionCompleteResult::SessionDoesNotExist:
ErrorMessage = TEXT("Session does not exist");
break;
case EOnJoinSessionCompleteResult::CouldNotRetrieveAddress:
ErrorMessage = TEXT("Could not retrieve address");
break;
case EOnJoinSessionCompleteResult::AlreadyInSession:
ErrorMessage = TEXT("Already in session");
break;
default:
ErrorMessage = TEXT("Unknown error");
break;
}
UE_LOG(LogOnline, Error, TEXT("Failed to join session: %s"), *ErrorMessage);
}
OnSessionJoined.Broadcast(bSuccess, ErrorMessage);
}
// ===== 세션 종료 =====
void USessionManager::DestroySession()
{
IOnlineSessionPtr Sessions = GetSessionInterface();
if (!Sessions.IsValid())
{
return;
}
auto ExistingSession = Sessions->GetNamedSession(NAME_GameSession);
if (!ExistingSession)
{
return;
}
DestroySessionDelegateHandle = Sessions->AddOnDestroySessionCompleteDelegate_Handle(
FOnDestroySessionCompleteDelegate::CreateUObject(
this, &USessionManager::OnDestroySessionComplete));
Sessions->DestroySession(NAME_GameSession);
UE_LOG(LogOnline, Log, TEXT("Destroying session"));
}
void USessionManager::OnDestroySessionComplete(FName SessionName, bool bWasSuccessful)
{
IOnlineSessionPtr Sessions = GetSessionInterface();
if (Sessions.IsValid())
{
Sessions->ClearOnDestroySessionCompleteDelegate_Handle(DestroySessionDelegateHandle);
}
UE_LOG(LogOnline, Log, TEXT("Session destroyed: %s"),
bWasSuccessful ? TEXT("Success") : TEXT("Failed"));
OnSessionDestroyed.Broadcast();
}
// ===== 세션 정보 =====
bool USessionManager::IsInSession() const
{
IOnlineSessionPtr Sessions = GetSessionInterface();
if (Sessions.IsValid())
{
return Sessions->GetNamedSession(NAME_GameSession) != nullptr;
}
return false;
}
bool USessionManager::IsSessionHost() const
{
IOnlineSessionPtr Sessions = GetSessionInterface();
if (Sessions.IsValid())
{
FNamedOnlineSession* Session = Sessions->GetNamedSession(NAME_GameSession);
if (Session)
{
return Session->bHosting;
}
}
return false;
}
3. Epic Online Services (EOS) 통합
3.1 EOS 설정
; DefaultEngine.ini - EOS 기본 설정
[OnlineSubsystem]
DefaultPlatformService=EOS
[OnlineSubsystemEOS]
bEnabled=true
[/Script/EOSShared.EOSDeveloperSettings]
; Epic Developer Portal에서 가져온 값들
ProductName=MyOpenWorldRPG
ProductVersion=1.0.0
ProductId=YOUR_PRODUCT_ID
SandboxId=YOUR_SANDBOX_ID
DeploymentId=YOUR_DEPLOYMENT_ID
ClientId=YOUR_CLIENT_ID
ClientSecret=YOUR_CLIENT_SECRET
; 캐시 설정
CacheDir=Saved/EOS
[/Script/Engine.Engine]
!NetDriverDefinitions=ClearArray
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="/Script/SocketSubsystemEOS.NetDriverEOSBase",DriverClassNameFallback="/Script/OnlineSubsystemUtils.IpNetDriver")
3.2 EOS 인증 매니저
// EOSAuthManager.h
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "Interfaces/OnlineIdentityInterface.h"
#include "EOSAuthManager.generated.h"
// 로그인 유형
UENUM(BlueprintType)
enum class EEOSLoginType : uint8
{
// Epic Games 계정 (오버레이 표시)
AccountPortal,
// 개발 인증 도구 (개발용)
Developer,
// 디바이스 ID (익명 - 모바일)
DeviceId,
// 외부 인증 (Steam, PSN 등)
ExternalAuth
};
// 로그인 결과 델리게이트
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnEOSLoginComplete, bool, bWasSuccessful, const FString&, ErrorMessage);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnEOSLogoutComplete);
UCLASS()
class MYGAME_API UEOSAuthManager : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
// ===== 로그인 =====
// Epic 계정 포털 로그인
UFUNCTION(BlueprintCallable, Category = "EOS|Auth")
void LoginWithAccountPortal();
// 개발자 인증 (테스트용)
UFUNCTION(BlueprintCallable, Category = "EOS|Auth")
void LoginWithDeveloperTool(const FString& Host, const FString& Credentials);
// 디바이스 ID 로그인 (익명)
UFUNCTION(BlueprintCallable, Category = "EOS|Auth")
void LoginWithDeviceId();
// ===== 로그아웃 =====
UFUNCTION(BlueprintCallable, Category = "EOS|Auth")
void Logout();
// ===== 상태 확인 =====
UFUNCTION(BlueprintPure, Category = "EOS|Auth")
bool IsLoggedIn() const;
UFUNCTION(BlueprintPure, Category = "EOS|Auth")
FString GetDisplayName() const;
UFUNCTION(BlueprintPure, Category = "EOS|Auth")
FString GetProductUserId() const;
// ===== 이벤트 =====
UPROPERTY(BlueprintAssignable, Category = "EOS|Events")
FOnEOSLoginComplete OnLoginComplete;
UPROPERTY(BlueprintAssignable, Category = "EOS|Events")
FOnEOSLogoutComplete OnLogoutComplete;
protected:
void LoginInternal(const FOnlineAccountCredentials& Credentials);
void OnLoginCompleteInternal(int32 LocalUserNum, bool bWasSuccessful,
const FUniqueNetId& UserId, const FString& Error);
void OnLogoutCompleteInternal(int32 LocalUserNum, bool bWasSuccessful);
IOnlineIdentityPtr GetIdentityInterface() const;
};
// EOSAuthManager.cpp
#include "EOSAuthManager.h"
#include "OnlineSubsystem.h"
void UEOSAuthManager::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
// 로그인 델리게이트 바인딩
IOnlineIdentityPtr Identity = GetIdentityInterface();
if (Identity.IsValid())
{
Identity->AddOnLoginCompleteDelegate_Handle(
0,
FOnLoginCompleteDelegate::CreateUObject(
this, &UEOSAuthManager::OnLoginCompleteInternal));
Identity->AddOnLogoutCompleteDelegate_Handle(
0,
FOnLogoutCompleteDelegate::CreateUObject(
this, &UEOSAuthManager::OnLogoutCompleteInternal));
}
}
IOnlineIdentityPtr UEOSAuthManager::GetIdentityInterface() const
{
IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
if (OnlineSub)
{
return OnlineSub->GetIdentityInterface();
}
return nullptr;
}
void UEOSAuthManager::LoginWithAccountPortal()
{
FOnlineAccountCredentials Credentials;
Credentials.Type = TEXT("accountportal");
LoginInternal(Credentials);
}
void UEOSAuthManager::LoginWithDeveloperTool(const FString& Host, const FString& CredentialsString)
{
// 개발 인증 도구 사용 (테스트용)
// Host: localhost:6547 (기본값)
// Credentials: DevAuthTool에서 생성한 인증 이름
FOnlineAccountCredentials Credentials;
Credentials.Type = TEXT("developer");
Credentials.Id = Host.IsEmpty() ? TEXT("localhost:6547") : Host;
Credentials.Token = CredentialsString;
LoginInternal(Credentials);
}
void UEOSAuthManager::LoginWithDeviceId()
{
// 익명 로그인 (모바일/기기 ID 기반)
FOnlineAccountCredentials Credentials;
Credentials.Type = TEXT("deviceid");
LoginInternal(Credentials);
}
void UEOSAuthManager::LoginInternal(const FOnlineAccountCredentials& Credentials)
{
IOnlineIdentityPtr Identity = GetIdentityInterface();
if (!Identity.IsValid())
{
UE_LOG(LogOnline, Error, TEXT("Identity interface not available"));
OnLoginComplete.Broadcast(false, TEXT("Identity interface not available"));
return;
}
// 이미 로그인 중이면 스킵
if (Identity->GetLoginStatus(0) == ELoginStatus::LoggedIn)
{
UE_LOG(LogOnline, Log, TEXT("Already logged in"));
OnLoginComplete.Broadcast(true, TEXT(""));
return;
}
UE_LOG(LogOnline, Log, TEXT("Attempting login with type: %s"), *Credentials.Type);
Identity->Login(0, Credentials);
}
void UEOSAuthManager::OnLoginCompleteInternal(int32 LocalUserNum, bool bWasSuccessful,
const FUniqueNetId& UserId, const FString& Error)
{
if (bWasSuccessful)
{
UE_LOG(LogOnline, Log, TEXT("Login successful - User: %s"), *GetDisplayName());
}
else
{
UE_LOG(LogOnline, Error, TEXT("Login failed: %s"), *Error);
}
OnLoginComplete.Broadcast(bWasSuccessful, Error);
}
void UEOSAuthManager::Logout()
{
IOnlineIdentityPtr Identity = GetIdentityInterface();
if (Identity.IsValid())
{
Identity->Logout(0);
}
}
void UEOSAuthManager::OnLogoutCompleteInternal(int32 LocalUserNum, bool bWasSuccessful)
{
UE_LOG(LogOnline, Log, TEXT("Logout complete: %s"),
bWasSuccessful ? TEXT("Success") : TEXT("Failed"));
OnLogoutComplete.Broadcast();
}
bool UEOSAuthManager::IsLoggedIn() const
{
IOnlineIdentityPtr Identity = GetIdentityInterface();
if (Identity.IsValid())
{
return Identity->GetLoginStatus(0) == ELoginStatus::LoggedIn;
}
return false;
}
FString UEOSAuthManager::GetDisplayName() const
{
IOnlineIdentityPtr Identity = GetIdentityInterface();
if (Identity.IsValid())
{
TSharedPtr UserId = Identity->GetUniquePlayerId(0);
if (UserId.IsValid())
{
return Identity->GetPlayerNickname(0);
}
}
return TEXT("");
}
FString UEOSAuthManager::GetProductUserId() const
{
IOnlineIdentityPtr Identity = GetIdentityInterface();
if (Identity.IsValid())
{
TSharedPtr UserId = Identity->GetUniquePlayerId(0);
if (UserId.IsValid())
{
return UserId->ToString();
}
}
return TEXT("");
}
4. 크로스플랫폼 지원
4.1 EOS Plus - Steam 통합
; DefaultEngine.ini - EOS + Steam 크로스플레이 설정
[OnlineSubsystemEOSPlus]
bEnabled=true
[OnlineSubsystem]
; EOS Plus를 기본으로 사용
DefaultPlatformService=EOSPlus
; Steam을 네이티브 플랫폼으로
NativePlatformService=Steam
[OnlineSubsystemEOS]
bUseEAS=true
bUseEOSConnect=true
; Steam 계정을 EOS Connect에 연동
bMirrorStatsToEOS=true
bMirrorAchievementsToEOS=true
bMirrorPresenceToEOS=true
[OnlineSubsystemSteam]
bEnabled=true
SteamDevAppId=480 ; 테스트용 AppID (실제 배포 시 변경)
4.2 크로스플레이 세션 매니저
// CrossPlaySessionManager.h
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "CrossPlaySessionManager.generated.h"
// 플랫폼 유형
UENUM(BlueprintType)
enum class EGamePlatform : uint8
{
Unknown,
Steam,
Epic,
PlayStation,
Xbox,
Switch
};
UCLASS()
class MYGAME_API UCrossPlaySessionManager : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
// 현재 플랫폼 가져오기
UFUNCTION(BlueprintPure, Category = "CrossPlay")
EGamePlatform GetCurrentPlatform() const;
// 크로스플레이 가능 여부
UFUNCTION(BlueprintPure, Category = "CrossPlay")
bool IsCrossPlayEnabled() const;
// 크로스플레이 세션 생성
UFUNCTION(BlueprintCallable, Category = "CrossPlay")
void CreateCrossPlaySession(int32 MaxPlayers, const FString& MapName);
// 크로스플레이 세션 검색
UFUNCTION(BlueprintCallable, Category = "CrossPlay")
void FindCrossPlaySessions();
// 플랫폼별 친구 목록 통합
UFUNCTION(BlueprintCallable, Category = "CrossPlay")
void GetCombinedFriendsList();
protected:
// EOS와 네이티브 플랫폼 친구 목록 병합
void MergeFriendsLists(
const TArray>& EOSFriends,
const TArray>& NativeFriends
);
};
// CrossPlaySessionManager.cpp
#include "CrossPlaySessionManager.h"
#include "OnlineSubsystem.h"
EGamePlatform UCrossPlaySessionManager::GetCurrentPlatform() const
{
// 네이티브 플랫폼 서브시스템 확인
IOnlineSubsystem* NativeSub = IOnlineSubsystem::GetByPlatform();
if (NativeSub)
{
FName SubName = NativeSub->GetSubsystemName();
if (SubName == TEXT("Steam"))
{
return EGamePlatform::Steam;
}
else if (SubName == TEXT("EOS"))
{
return EGamePlatform::Epic;
}
// 추가 플랫폼...
}
return EGamePlatform::Unknown;
}
bool UCrossPlaySessionManager::IsCrossPlayEnabled() const
{
// EOS Plus 사용 여부 확인
IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
if (OnlineSub)
{
return OnlineSub->GetSubsystemName() == TEXT("EOSPlus");
}
return false;
}
void UCrossPlaySessionManager::CreateCrossPlaySession(int32 MaxPlayers, const FString& MapName)
{
IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
if (!OnlineSub)
{
UE_LOG(LogOnline, Error, TEXT("Online Subsystem not available"));
return;
}
IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
if (!Sessions.IsValid())
{
return;
}
// 크로스플레이 세션 설정
FOnlineSessionSettings SessionSettings;
SessionSettings.bUsesPresence = true;
SessionSettings.NumPublicConnections = MaxPlayers;
SessionSettings.bAllowJoinInProgress = true;
SessionSettings.bAllowJoinViaPresence = true;
SessionSettings.bShouldAdvertise = true;
// EOS 로비 사용 (크로스플랫폼 필수)
SessionSettings.bUseLobbiesIfAvailable = true;
// 크로스플레이 플래그
SessionSettings.Set(
FName("CROSSPLAY_ENABLED"),
true,
EOnlineDataAdvertisementType::ViaOnlineService
);
SessionSettings.Set(
SETTING_MAPNAME,
MapName,
EOnlineDataAdvertisementType::ViaOnlineService
);
const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
if (LocalPlayer)
{
Sessions->CreateSession(
*LocalPlayer->GetPreferredUniqueNetId(),
NAME_GameSession,
SessionSettings
);
}
}
void UCrossPlaySessionManager::FindCrossPlaySessions()
{
IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
if (!OnlineSub) return;
IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
if (!Sessions.IsValid()) return;
// 크로스플레이 세션만 검색
TSharedRef SearchRef = MakeShared();
SearchRef->MaxSearchResults = 20;
SearchRef->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);
SearchRef->QuerySettings.Set(
FName("CROSSPLAY_ENABLED"),
true,
EOnlineComparisonOp::Equals
);
const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
if (LocalPlayer)
{
Sessions->FindSessions(*LocalPlayer->GetPreferredUniqueNetId(), SearchRef);
}
}
void UCrossPlaySessionManager::GetCombinedFriendsList()
{
// EOS 친구 목록 가져오기
IOnlineSubsystem* EOSSub = IOnlineSubsystem::Get(TEXT("EOS"));
IOnlineSubsystem* NativeSub = IOnlineSubsystem::GetByPlatform();
TArray> EOSFriends;
TArray> NativeFriends;
// EOS 친구
if (EOSSub)
{
IOnlineFriendsPtr EOSFriendsInterface = EOSSub->GetFriendsInterface();
if (EOSFriendsInterface.IsValid())
{
EOSFriendsInterface->ReadFriendsList(
0,
TEXT(""),
FOnReadFriendsListComplete::CreateLambda(
[this, &EOSFriends, EOSFriendsInterface](int32 LocalUserNum, bool bWasSuccessful, const FString& ListName, const FString& ErrorStr)
{
if (bWasSuccessful)
{
EOSFriendsInterface->GetFriendsList(LocalUserNum, ListName, EOSFriends);
}
}
)
);
}
}
// 네이티브 플랫폼 친구 (Steam 등)
if (NativeSub && NativeSub != EOSSub)
{
IOnlineFriendsPtr NativeFriendsInterface = NativeSub->GetFriendsInterface();
if (NativeFriendsInterface.IsValid())
{
NativeFriendsInterface->ReadFriendsList(
0,
TEXT(""),
FOnReadFriendsListComplete::CreateLambda(
[this, &NativeFriends, NativeFriendsInterface](int32 LocalUserNum, bool bWasSuccessful, const FString& ListName, const FString& ErrorStr)
{
if (bWasSuccessful)
{
NativeFriendsInterface->GetFriendsList(LocalUserNum, ListName, NativeFriends);
}
}
)
);
}
}
// 목록 병합
MergeFriendsLists(EOSFriends, NativeFriends);
}
void UCrossPlaySessionManager::MergeFriendsLists(
const TArray>& EOSFriends,
const TArray>& NativeFriends)
{
// 중복 제거 및 병합 로직
TSet SeenIds;
TArray> MergedFriends;
// EOS 친구 추가
for (const auto& Friend : EOSFriends)
{
FString FriendId = Friend->GetUserId()->ToString();
if (!SeenIds.Contains(FriendId))
{
SeenIds.Add(FriendId);
MergedFriends.Add(Friend);
}
}
// 네이티브 플랫폼 친구 추가 (EOS에 없는 경우)
for (const auto& Friend : NativeFriends)
{
FString FriendId = Friend->GetUserId()->ToString();
if (!SeenIds.Contains(FriendId))
{
SeenIds.Add(FriendId);
MergedFriends.Add(Friend);
}
}
UE_LOG(LogOnline, Log, TEXT("Combined friends list: %d friends"), MergedFriends.Num());
}
5. 친구 초대 시스템
5.1 초대 매니저
// InviteManager.h
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "Interfaces/OnlineFriendsInterface.h"
#include "InviteManager.generated.h"
// 친구 정보 구조체
USTRUCT(BlueprintType)
struct FFriendInfo
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly)
FString UserId;
UPROPERTY(BlueprintReadOnly)
FString DisplayName;
UPROPERTY(BlueprintReadOnly)
bool bIsOnline;
UPROPERTY(BlueprintReadOnly)
bool bIsPlayingThisGame;
UPROPERTY(BlueprintReadOnly)
bool bCanInvite;
};
// 초대 관련 델리게이트
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFriendsListReady, const TArray&, Friends);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInviteSent, bool, bWasSuccessful, const FString&, FriendName);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInviteReceived, const FString&, FromUser, const FString&, SessionId);
UCLASS()
class MYGAME_API UInviteManager : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
// 친구 목록 요청
UFUNCTION(BlueprintCallable, Category = "Invite")
void RequestFriendsList();
// 친구 초대
UFUNCTION(BlueprintCallable, Category = "Invite")
void InviteFriend(const FString& FriendUserId);
// 초대 수락
UFUNCTION(BlueprintCallable, Category = "Invite")
void AcceptInvite(const FString& SessionId);
// 초대 거절
UFUNCTION(BlueprintCallable, Category = "Invite")
void DeclineInvite(const FString& SessionId);
// 이벤트
UPROPERTY(BlueprintAssignable)
FOnFriendsListReady OnFriendsListReady;
UPROPERTY(BlueprintAssignable)
FOnInviteSent OnInviteSent;
UPROPERTY(BlueprintAssignable)
FOnInviteReceived OnInviteReceived;
protected:
void OnReadFriendsListComplete(int32 LocalUserNum, bool bWasSuccessful,
const FString& ListName, const FString& ErrorStr);
void OnSessionInviteReceived(const FUniqueNetId& UserId, const FUniqueNetId& FromId,
const FString& AppId, const FOnlineSessionSearchResult& InviteResult);
};
// InviteManager.cpp
#include "InviteManager.h"
#include "OnlineSubsystem.h"
void UInviteManager::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
// 초대 수신 델리게이트 등록
IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
if (OnlineSub)
{
IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
if (Sessions.IsValid())
{
Sessions->AddOnSessionInviteReceivedDelegate_Handle(
FOnSessionInviteReceivedDelegate::CreateUObject(
this, &UInviteManager::OnSessionInviteReceived));
}
}
}
void UInviteManager::RequestFriendsList()
{
IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
if (!OnlineSub) return;
IOnlineFriendsPtr Friends = OnlineSub->GetFriendsInterface();
if (!Friends.IsValid()) return;
Friends->ReadFriendsList(
0,
EFriendsLists::ToString(EFriendsLists::Default),
FOnReadFriendsListComplete::CreateUObject(
this, &UInviteManager::OnReadFriendsListComplete)
);
}
void UInviteManager::OnReadFriendsListComplete(int32 LocalUserNum, bool bWasSuccessful,
const FString& ListName, const FString& ErrorStr)
{
TArray FriendInfoList;
if (bWasSuccessful)
{
IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
if (OnlineSub)
{
IOnlineFriendsPtr FriendsInterface = OnlineSub->GetFriendsInterface();
if (FriendsInterface.IsValid())
{
TArray> FriendsList;
FriendsInterface->GetFriendsList(LocalUserNum, ListName, FriendsList);
for (const auto& Friend : FriendsList)
{
FFriendInfo Info;
Info.UserId = Friend->GetUserId()->ToString();
Info.DisplayName = Friend->GetDisplayName();
// 온라인 상태
FOnlineUserPresence Presence;
Friend->GetUserAttribute(TEXT("Presence"), Presence.Status.StatusStr);
Info.bIsOnline = Friend->GetPresence().bIsOnline;
Info.bIsPlayingThisGame = Friend->GetPresence().bIsPlayingThisGame;
// 초대 가능 여부 (온라인이고 같은 게임 플레이 중이 아닐 때)
Info.bCanInvite = Info.bIsOnline && !Info.bIsPlayingThisGame;
FriendInfoList.Add(Info);
}
}
}
}
// 온라인 친구 먼저 정렬
FriendInfoList.Sort([](const FFriendInfo& A, const FFriendInfo& B)
{
if (A.bIsOnline != B.bIsOnline)
{
return A.bIsOnline > B.bIsOnline;
}
return A.DisplayName < B.DisplayName;
});
OnFriendsListReady.Broadcast(FriendInfoList);
}
void UInviteManager::InviteFriend(const FString& FriendUserId)
{
IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
if (!OnlineSub) return;
IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
if (!Sessions.IsValid()) return;
// 현재 세션이 있는지 확인
FNamedOnlineSession* CurrentSession = Sessions->GetNamedSession(NAME_GameSession);
if (!CurrentSession)
{
UE_LOG(LogOnline, Warning, TEXT("No active session to invite to"));
OnInviteSent.Broadcast(false, TEXT(""));
return;
}
// UniqueNetId 생성
IOnlineIdentityPtr Identity = OnlineSub->GetIdentityInterface();
if (!Identity.IsValid()) return;
TSharedPtr FriendNetId = Identity->CreateUniquePlayerId(FriendUserId);
if (!FriendNetId.IsValid())
{
OnInviteSent.Broadcast(false, TEXT("Invalid friend ID"));
return;
}
// 초대 전송
bool bSuccess = Sessions->SendSessionInviteToFriend(0, NAME_GameSession, *FriendNetId);
// 친구 이름 가져오기 (로그용)
FString FriendName = FriendUserId;
IOnlineFriendsPtr Friends = OnlineSub->GetFriendsInterface();
if (Friends.IsValid())
{
TSharedPtr Friend = Friends->GetFriend(0, *FriendNetId,
EFriendsLists::ToString(EFriendsLists::Default));
if (Friend.IsValid())
{
FriendName = Friend->GetDisplayName();
}
}
UE_LOG(LogOnline, Log, TEXT("Invite sent to %s: %s"),
*FriendName, bSuccess ? TEXT("Success") : TEXT("Failed"));
OnInviteSent.Broadcast(bSuccess, FriendName);
}
void UInviteManager::OnSessionInviteReceived(const FUniqueNetId& UserId, const FUniqueNetId& FromId,
const FString& AppId, const FOnlineSessionSearchResult& InviteResult)
{
// 초대자 이름 가져오기
FString FromUserName = FromId.ToString();
IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
if (OnlineSub)
{
IOnlineFriendsPtr Friends = OnlineSub->GetFriendsInterface();
if (Friends.IsValid())
{
TSharedPtr Friend = Friends->GetFriend(0, FromId,
EFriendsLists::ToString(EFriendsLists::Default));
if (Friend.IsValid())
{
FromUserName = Friend->GetDisplayName();
}
}
}
FString SessionId = InviteResult.Session.GetSessionIdStr();
UE_LOG(LogOnline, Log, TEXT("Received invite from %s to session %s"),
*FromUserName, *SessionId);
OnInviteReceived.Broadcast(FromUserName, SessionId);
}
void UInviteManager::AcceptInvite(const FString& SessionId)
{
// SessionManager를 통해 세션 참가
USessionManager* SessionManager = GetGameInstance()->GetSubsystem();
if (SessionManager)
{
SessionManager->JoinSessionById(SessionId);
}
}
void UInviteManager::DeclineInvite(const FString& SessionId)
{
UE_LOG(LogOnline, Log, TEXT("Declined invite to session: %s"), *SessionId);
// 필요시 거절 알림 전송
}
6. 실습 과제
과제 1: 세션 브라우저 UI
세션 검색 결과를 표시하고 참가할 수 있는 UI 위젯을 구현하세요.
- 세션 목록 스크롤 뷰
- 호스트 이름, 플레이어 수, 핑 표시
- 새로고침 버튼
- 참가 버튼
과제 2: 친구 초대 시스템
온라인 친구 목록을 표시하고 초대를 보낼 수 있는 시스템을 구현하세요.
- 친구 목록 UI (온라인/오프라인 구분)
- 초대 버튼
- 초대 수신 팝업
- 수락/거절 버튼
과제 3: 자동 재연결
연결이 끊어졌을 때 자동으로 재연결을 시도하는 시스템을 구현하세요.
- 연결 끊김 감지
- 재연결 시도 (최대 3회)
- 재연결 진행 UI
- 실패 시 메인 메뉴 이동
핵심 요약
- Online Subsystem: 플랫폼별 온라인 서비스를 추상화하는 인터페이스
- Session Interface: 세션 생성, 검색, 참가, 종료 관리
- EOS: Epic의 크로스플랫폼 온라인 서비스
- EOS Plus: EOS와 네이티브 플랫폼(Steam 등) 통합
- 델리게이트 패턴: 비동기 작업 완료 처리
PRACTICE
도전 과제
배운 내용을 직접 실습해보세요
실습 1: Online Subsystem NULL 설정
DefaultEngine.ini에서 OnlineSubsystem=NULL을 설정하고, IOnlineSubsystem::Get()으로 서브시스템에 접근하세요. 세션 생성(CreateSession)과 검색(FindSessions)의 기본 플로우를 구현하세요.
실습 2: 세션 기반 매치메이킹
FOnlineSessionSettings를 설정하여 RPG Co-op 세션을 생성하세요. bUsesPresence, NumPublicConnections, 커스텀 세션 키(MapName, Difficulty)를 설정하고, 다른 플레이어가 검색/참가하는 플로우를 구현하세요.
심화 과제: Steam Online Subsystem 통합
OnlineSubsystemSteam 플러그인을 연동하고, Steam의 로비/매치메이킹 API를 통해 실제 온라인 세션을 구현하세요. Steam App ID 설정, 인증 플로우, 프렌드 초대 기능을 통합하세요.