콘텐츠로 이동

UE5 Online Subsystem 기초

UE5의 Online Subsystem(OSS)은 Steam, Epic Online Services, Xbox Live, PlayStation Network 등 다양한 멀티플레이어 플랫폼 서비스를 통일된 인터페이스로 추상화합니다. 플랫폼을 교체해도 게임 코드는 변경하지 않아도 됩니다.


MyGame.Build.cs
PublicDependencyModuleNames.AddRange(new string[]
{
"OnlineSubsystem",
"OnlineSubsystemUtils"
});
DynamicallyLoadedModuleNames.Add("OnlineSubsystemSteam");
; DefaultEngine.ini
[OnlineSubsystem]
DefaultPlatformService=Steam
[OnlineSubsystemSteam]
bEnabled=true
SteamDevAppId=480 ; Spacewar 테스트 AppID

SessionManager.h
#pragma once
#include "OnlineSubsystem.h"
#include "Interfaces/OnlineSessionInterface.h"
#include "GameFramework/GameModeBase.h"
#include "SessionManager.generated.h"
UCLASS()
class ASessionManager : public AGameModeBase
{
GENERATED_BODY()
public:
void CreateSession(int32 MaxPlayers);
void FindSessions();
void JoinSession(int32 SessionIndex);
void DestroySession();
private:
IOnlineSessionPtr _sessionInterface;
TSharedPtr<FOnlineSessionSearch> _sessionSearch;
// 델리게이트 핸들
FDelegateHandle _createSessionHandle;
FDelegateHandle _findSessionsHandle;
FDelegateHandle _joinSessionHandle;
void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);
void OnFindSessionsComplete(bool bWasSuccessful);
void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);
};
SessionManager.cpp
#include "SessionManager.h"
#include "OnlineSubsystemUtils.h"
void ASessionManager::CreateSession(int32 MaxPlayers)
{
IOnlineSubsystem* OSS = IOnlineSubsystem::Get();
if (!OSS) return;
_sessionInterface = OSS->GetSessionInterface();
if (!_sessionInterface.IsValid()) return;
// 기존 세션 제거
if (_sessionInterface->GetNamedSession(NAME_GameSession))
_sessionInterface->DestroySession(NAME_GameSession);
// 세션 설정
FOnlineSessionSettings Settings;
Settings.NumPublicConnections = MaxPlayers;
Settings.bIsLANMatch = OSS->GetSubsystemName() == "NULL";
Settings.bUsesPresence = true;
Settings.bAllowJoinInProgress = false;
Settings.bShouldAdvertise = true;
Settings.bUseLobbiesIfAvailable = true;
// 커스텀 데이터 추가
Settings.Set(FName("MapName"),
FString("Desert_01"),
EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);
// 델리게이트 등록
_createSessionHandle = _sessionInterface->OnCreateSessionCompleteDelegates.AddUObject(
this, &ASessionManager::OnCreateSessionComplete);
_sessionInterface->CreateSession(0, NAME_GameSession, Settings);
}
void ASessionManager::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{
_sessionInterface->OnCreateSessionCompleteDelegates.Remove(_createSessionHandle);
if (bWasSuccessful)
{
UE_LOG(LogTemp, Log, TEXT("Session created: %s"), *SessionName.ToString());
// 맵 이동 (서버 트래블)
GetWorld()->ServerTravel("/Game/Maps/GameMap?listen");
}
else
{
UE_LOG(LogTemp, Error, TEXT("Failed to create session"));
}
}

void ASessionManager::FindSessions()
{
IOnlineSubsystem* OSS = IOnlineSubsystem::Get();
if (!OSS) return;
_sessionInterface = OSS->GetSessionInterface();
_sessionSearch = MakeShared<FOnlineSessionSearch>();
_sessionSearch->MaxSearchResults = 20;
_sessionSearch->bIsLanQuery = OSS->GetSubsystemName() == "NULL";
_sessionSearch->QuerySettings.Set(
SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);
_findSessionsHandle = _sessionInterface->OnFindSessionsCompleteDelegates.AddUObject(
this, &ASessionManager::OnFindSessionsComplete);
_sessionInterface->FindSessions(0, _sessionSearch.ToSharedRef());
}
void ASessionManager::OnFindSessionsComplete(bool bWasSuccessful)
{
_sessionInterface->OnFindSessionsCompleteDelegates.Remove(_findSessionsHandle);
if (!bWasSuccessful || !_sessionSearch.IsValid()) return;
const auto& Results = _sessionSearch->SearchResults;
UE_LOG(LogTemp, Log, TEXT("Found %d sessions"), Results.Num());
for (int32 i = 0; i < Results.Num(); ++i)
{
const auto& Result = Results[i];
FString SessionId = Result.GetSessionIdStr();
int32 Ping = Result.PingInMs;
int32 OpenSlots = Result.Session.NumOpenPublicConnections;
// 커스텀 데이터 읽기
FString MapName;
Result.Session.SessionSettings.Get(FName("MapName"), MapName);
UE_LOG(LogTemp, Log,
TEXT("[%d] ID:%s Ping:%dms Slots:%d Map:%s"),
i, *SessionId, Ping, OpenSlots, *MapName);
}
}

void ASessionManager::JoinSession(int32 SessionIndex)
{
if (!_sessionInterface.IsValid() || !_sessionSearch.IsValid()) return;
const auto& Results = _sessionSearch->SearchResults;
if (!Results.IsValidIndex(SessionIndex)) return;
_joinSessionHandle = _sessionInterface->OnJoinSessionCompleteDelegates.AddUObject(
this, &ASessionManager::OnJoinSessionComplete);
_sessionInterface->JoinSession(0, NAME_GameSession, Results[SessionIndex]);
}
void ASessionManager::OnJoinSessionComplete(
FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
_sessionInterface->OnJoinSessionCompleteDelegates.Remove(_joinSessionHandle);
if (Result == EOnJoinSessionCompleteResult::Success)
{
// 서버 주소 가져와서 클라이언트 트래블
FString TravelURL;
if (_sessionInterface->GetResolvedConnectString(NAME_GameSession, TravelURL))
{
APlayerController* PC = GetWorld()->GetFirstPlayerController();
if (PC)
{
PC->ClientTravel(TravelURL, ETravelType::TRAVEL_Absolute);
}
}
}
else
{
FString ErrorMsg;
switch (Result)
{
case EOnJoinSessionCompleteResult::SessionIsFull:
ErrorMsg = "세션이 가득 찼습니다"; break;
case EOnJoinSessionCompleteResult::SessionDoesNotExist:
ErrorMsg = "세션이 존재하지 않습니다"; break;
default:
ErrorMsg = "참가 실패"; break;
}
UE_LOG(LogTemp, Error, TEXT("JoinSession failed: %s"), *ErrorMsg);
}
}

void ASessionManager::GetFriendsList()
{
IOnlineSubsystem* OSS = IOnlineSubsystem::Get();
if (!OSS) return;
IOnlineFriendsPtr FriendsInterface = OSS->GetFriendsInterface();
if (!FriendsInterface.IsValid()) return;
// 친구 목록 읽기 요청
FriendsInterface->ReadFriendsList(
0,
EFriendsLists::ToString(EFriendsLists::Default),
FOnReadFriendsListComplete::CreateLambda(
[FriendsInterface](int32 LocalUserNum, bool bWasSuccessful,
const FString& ListName, const FString& Error)
{
if (!bWasSuccessful) return;
TArray<TSharedRef<FOnlineFriend>> Friends;
FriendsInterface->GetFriendsList(LocalUserNum, ListName, Friends);
for (const auto& Friend : Friends)
{
FString Name = Friend->GetDisplayName();
bool Online = Friend->GetPresence().bIsOnline;
FString Status = Friend->GetPresence().StatusStr;
UE_LOG(LogTemp, Log, TEXT("Friend: %s [%s] %s"),
*Name, Online ? TEXT("Online") : TEXT("Offline"), *Status);
}
}
)
);
}

// 로컬 플레이어 UniqueId 가져오기
void GetLocalPlayerId()
{
IOnlineSubsystem* OSS = IOnlineSubsystem::Get();
if (!OSS) return;
IOnlineIdentityPtr IdentityInterface = OSS->GetIdentityInterface();
if (!IdentityInterface.IsValid()) return;
// 로그인 상태 확인
ELoginStatus::Type Status = IdentityInterface->GetLoginStatus(0);
if (Status != ELoginStatus::LoggedIn)
{
UE_LOG(LogTemp, Warning, TEXT("Player not logged in"));
return;
}
TSharedPtr<const FUniqueNetId> UserId = IdentityInterface->GetUniquePlayerId(0);
if (UserId.IsValid())
{
FString UserIdStr = UserId->ToString();
FString DisplayName = IdentityInterface->GetPlayerNickname(0);
UE_LOG(LogTemp, Log, TEXT("Player: %s (%s)"), *DisplayName, *UserIdStr);
}
}

인터페이스역할
IOnlineSession세션 생성·검색·참가·삭제
IOnlineFriends친구 목록, 친구 초대
IOnlineIdentity로그인, 플레이어 ID
IOnlineLeaderboards리더보드 읽기·쓰기
IOnlineAchievements업적 조회·해제

OSS의 모든 작업은 비동기 델리게이트 기반입니다. 델리게이트 핸들을 저장했다가 콜백 완료 후 반드시 Remove해야 메모리 누수를 방지할 수 있습니다.