![[Unreal Engine C++] Online Subsystem을 활용한 멀티플레이 구현](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoU16v%2FbtsGMVDW6Xf%2Fyk4bIWGt98Ka2gekRwRtn0%2Fimg.png)
언리얼에는 온라인 서브시스템 Steam API를 통해 만든 게임을 밸브의 스팀 플랫폼에 출시하는 것이 가능하다.
그 기능을 이용해서 멀티 기능을 만들어보자
시작하기 전 준비
Online Subsystem을 사용하기 전, 프로젝트를 하나 생성해준다.
필자는 3인칭 게임에 적용하고자 했기 때문에 3인칭 게임으로 생성해주었다.
편집 - 플러그인에서 "OnlineSubsystem Steam"을 추가해준다.
그 뒤, 만들어진 (ProjectName).Build.cs에 "OnlineSubsystem" , "OnlineSubsystemSteam"을 추가해준다.
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "EnhancedInput", "OnlineSubsystem", "OnlineSubsystemSteam" });
(Unreal Projects) / (ProjectName) / Config / DefaultEngine.ini 파일을 열어 다음과 같은 내용을 추가해준다.
[/Script/Engine.GameEngine]
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
[OnlineSubsystem]
DefaultPlatformService=Steam
[OnlineSubsystemSteam]
bEnabled=true
SteamDevAppId=480
bInitServerOnClient=true
[/Script/OnlineSubsystemSteam.SteamNetDriver]
NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection"
각 줄에 대한 설명은 다음과 같다.
[/Script/Engine.GameEngine]
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
// 스팀을 통한 네트워킹 기능 활성화를 위해 스팀 넷 드라이버 사용.
[OnlineSubsystem]
DefaultPlatformService=Steam
// 기본 온라인 기능을 스팀을 통해 제공.
[OnlineSubsystemSteam]
bEnabled=true
SteamDevAppId=480
bInitServerOnClient=true
// 온라인 서브시스템을 활성화.
// DevAppId = 480은 스팀에서 제공하는 개발자 테스트 ID.
// 클라이언트에서 서버를 초기화 할 수 있도록 허용.
[/Script/OnlineSubsystemSteam.SteamNetDriver]
NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection"
// 스팀을 통한 네트워크 연결 구현.
이후 (ProjectName) 폴더 내에 있는 Biraries, Intermediate, Saved 파일을 삭제한 후, 프로젝트 파일을 재생성하고 다시 빌드까지 해주면 기본 준비는 끝났다.
Online Subsystem 기본 요소 생성
멀티플레이 게임 세션의 생성, 검색, 참가, 관리 등을 위해 언리얼에서 제공하는 인터페이스인 OnlineSessionInterface를 먼저 불러와야 한다.
// Multi.h
protected:
void GetOnlineSubsystem();
// MultiPlayCharacter.cpp
void AMultiPlayCharacter::BeginPlay()
{
...
GetOnlineSubsystem();
}
void AMultiPlayCharacter::GetOnlineSubsystem()
{
IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get();
if (OnlineSubsystem)
{
OnlineSessionInterface = OnlineSubsystem->GetSessionInterface();
if (GEngine)
GEngine->AddOnScreenDebugMessage(-1, 15.f, FColor::Purple, FString::Printf(TEXT("Subsystem Name : % s"), *OnlineSubsystem->GetSubsystemName().ToString()));
}
}
온라인 세션 인터페이스를 받아 이름을 출력하여 제대로 불러왔는지 확인할 수 있다.
Online Subsystem Steam으로 세션 만들기
이제 본격적으로 여러 플레이어가 함께 플레이할 수 있는 환경인 Session을 만들어보자.
// MultiPlayCharacter.h
protected:
UFUNCTION()
void CreateGameSession();
void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);
private:
FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate;
// MultiPlayCharacter.cpp
AMultiPlayCharacter::AMultiPlayCharacter()
: CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete))
{
...
}
...
void AMultiPlayCharacter::CreateGameSession()
{
if (!OnlineSessionInterface.IsValid())
{
if (GEngine)
GEngine->AddOnScreenDebugMessage(-1, 15.f, FColor::Red, FString(TEXT("Game Session Interface is invailed")));
return;
}
auto ExistingSession = OnlineSessionInterface->GetNamedSession(NAME_GameSession);
if (ExistingSession != nullptr)
{
OnlineSessionInterface->DestroySession(NAME_GameSession);
if (GEngine)
GEngine->AddOnScreenDebugMessage(-1, 15.f, FColor::Black, FString::Printf(TEXT("Destroy session : %s"), NAME_GameSession));
}
OnlineSessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate);
TSharedPtr<FOnlineSessionSettings> SessionSettings = MakeShareable(new FOnlineSessionSettings());
SessionSettings->bIsLANMatch = false;
SessionSettings->NumPublicConnections = 4;
SessionSettings->bAllowJoinInProgress = true;
SessionSettings->bAllowJoinViaPresence = true;
SessionSettings->bShouldAdvertise = true;
SessionSettings->bUsesPresence = true;
SessionSettings->bUseLobbiesIfAvailable = true;
SessionSettings->Set(FName("MatchType"), FString("FreeForAll"), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);
const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
OnlineSessionInterface->CreateSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *SessionSettings);
}
인터페이스가 유효한지와 세션이 존재한지를 확인한 후 처리를 해준다.
세션이 생성 완료되었을 때 호출될 Delegate인 CreateSessionCompleteDelegate를 추가해주고, 세션을 셋팅한다.
각각의 설명은 다음과 같다.
SessionSettings->bIsLANMatch = false; // LAN 연결이 가능여부
SessionSettings->NumPublicConnections = 4; // 최대 접속 가능 수
SessionSettings->bAllowJoinInProgress = true; // Session 진행중에 접속 허용
SessionSettings->bAllowJoinViaPresence = true; // 세션 참가 지역을 현재 지역으로 제한 (스팀의 presence 사용)
SessionSettings->bShouldAdvertise = true; // 현재 세션을 광고할지 (스팀의 다른 플레이어에게 세션 홍보 여부)
SessionSettings->bUsesPresence = true; // 현재 지역에 세션 표시
SessionSettings->bUseLobbiesIfAvailable = true; // 플랫폼이 지원하는 경우 로비 API 사용
SessionSettings->Set(FName("MatchType"), FString("FreeForAll"), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing); // 세션의 MatchType을 FreeForAll로 지정
세션이 생성 완료되면 실행될 함수는 다음과 같다.
// MultiPlayCharacter.cpp
void AMultiPlayCharacter::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{
if (bWasSuccessful)
{
if (GEngine)
GEngine->AddOnScreenDebugMessage(-1, 15.f, FColor::Blue, FString::Printf(TEXT("Created session : %s"), *SessionName.ToString()));
UWorld* World = GetWorld();
if (World)
World->ServerTravel(FString("/Game/ThirdPerson/Maps/ThirdPersonMap?listen"));
}
else
{
if (GEngine)
GEngine->AddOnScreenDebugMessage(-1, 15.f, FColor::Red, FString(TEXT("Failed to create session!")));
}
}
세션이 성공적으로 생성된다면 ServerTravel을 이용해 같이 플레이할 맵을 listen으로 열어준다.
이렇게 된다면 다른 플레이어가 이 세션이 접속한다면 같은 맵에서 서로 볼 수 있게 되고, 그 맵을 이용할 수 있게 된다.
세션을 검색하고 세션에 참여하기
세션이 만들어졌기 때문에 다른 플레이어가 그 세션을 검색하여 참여할 수 있게 해야한다.
// MultiPlayCharacter.h
private:
TSharedPtr<FOnlineSessionSearch> SessionSearch;
// MultiPlayCharacter.cpp
void AMultiPlayCharacter::JoinGameSession()
{
if (!OnlineSessionInterface.IsValid())
{
if (GEngine)
GEngine->AddOnScreenDebugMessage(-1, 15.f, FColor::Red, FString(TEXT("Game Session Interface is invailed")));
return;
}
OnlineSessionInterface->AddOnFindSessionsCompleteDelegate_Handle(FindSessionCompleteDelegate);
SessionSearch = MakeShareable(new FOnlineSessionSearch());
SessionSearch->MaxSearchResults = 10000;
SessionSearch->bIsLanQuery = false;
SessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);
const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
OnlineSessionInterface->FindSessions(*LocalPlayer->GetPreferredUniqueNetId(), SessionSearch.ToSharedRef());
}
세션의 최대 검색 수 10000, LAN 사용 여부, 세션의 쿼리를 현재로 설정하여 검색을 한다.
검색이 완료되면 그 세션에 접속할 수 있게 Delegate와 그에 따른 함수를 구현한다.
AMultiPlayCharacter::AMultiPlayCharacter()
: CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete))
, FindSessionCompleteDelegate(FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionComplete))
{
...
}
void AMultiPlayCharacter::OnFindSessionComplete(bool bWasSuccessful)
{
if (!OnlineSessionInterface.IsValid() || !bWasSuccessful)
return;
for (auto Result : SessionSearch->SearchResults)
{
FString Id = Result.GetSessionIdStr();
FString User = Result.Session.OwningUserName;
FString MatchType;
Result.Session.SessionSettings.Get(FName("MatchType"), MatchType);
if (GEngine)
GEngine->AddOnScreenDebugMessage(-1, 15.f, FColor::Cyan, FString::Printf(TEXT("Session ID : %s / Owner : %s"), *Id, *User));
if (MatchType == FString("FreeForAll"))
{
if (GEngine)
GEngine->AddOnScreenDebugMessage(-1, 15.f, FColor::Cyan, FString::Printf(TEXT("Joining Match Type : %s"), *MatchType));
OnlineSessionInterface->AddOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegate);
const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
OnlineSessionInterface->JoinSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, Result);
}
}
}
세션을 검색한 결과를 SessionSearch가 가지고 있고, 그 결과에 값을 for문을 이용하여 확인한다.
위에서 Session 타입을 "FreeForAll"로 지정해두었고, Join할 때 그와 같으면 접속할 수 있게 하였다.
검색이 완료되서 그 세션에 접속한다고 정해졌을 때 실행되는 함수는 다음과 같다.
// MultiPlayCharacter.cpp
AMultiPlayCharacter::AMultiPlayCharacter()
: CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete))
, FindSessionCompleteDelegate(FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionComplete))
, JoinSessionCompleteDelegate(FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplate))
{
...
}
void AMultiPlayCharacter::OnJoinSessionComplate(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
if (!OnlineSessionInterface.IsValid())
return;
FString Address;
if (OnlineSessionInterface->GetResolvedConnectString(NAME_GameSession, Address))
{
if (GEngine)
GEngine->AddOnScreenDebugMessage(-1, 15.f, FColor::Yellow, FString::Printf(TEXT("Connect String : %s"), *Address));
APlayerController* PlayerController = GetGameInstance()->GetFirstLocalPlayerController();
if (PlayerController)
PlayerController->ClientTravel(Address, ETravelType::TRAVEL_Absolute);
}
}
Delegate로 연결된 OnJoinSessionComplate가 실행되면 IP주소를 얻어와 그 서버에 ClientTravel을 이용해 접속하게 된다.
정리
총 정리하자면 다음과 같다.
1. GetOnlineSubsystem()으로 온라인 서브시스템 인터페이스 불러오기
2. CreateGameSession()으로 세션만들기. 성공하면 CreateSessionCompleteDelegate를 이용하여 OnCreateSessionComplete() 함수 실행
3. OnCreateSessionComplete()에서 ServerTravel로 지정된 맵으로 이동하게 되고, ?listen을 이용하여 다른 플레이어가 들어올 수 있도록 설정한다.
4. JoinGameSession()으로 들어올 수 있는 세션을 검색한다. 검색이 끝나면 FindSessionCompleteDelegate으로 OnFindSessionComplete() 함수를 실행한다.
5. OnFindSessionComplete() 에서 Type이 맞는 세션을 찾게 된다면 JoinSessionCompleteDelegate를 이용하여 OnJoinSessionComplate() 함수를 실행한다.
6. OnJoinSessionComplate()에서 ClientTravel를 이용해 IP주소로 접속한다.
이를 이용해 멀티플레이를 구현할 수 있다.
참고자료
'UnrealEngine > 공부' 카테고리의 다른 글
[GitLab] SourceTree를 활용해 팀 프로젝트 진행해보기 (0) | 2024.08.04 |
---|---|
[Unreal Engine C++] C++에서 Niagara System 스폰 (1) | 2024.05.29 |
[Unreal Engine C++] 청크 개념을 이용한 랜덤 맵 생성 알고리즘 (0) | 2024.03.30 |
[Unreal Engine C++] 랜덤 맵 생성에 관한 아이디어들 (0) | 2024.03.29 |
[Unreal Engine C++] Timeline을 사용하여 암전 효과 만들기 (0) | 2024.03.02 |
CSE & GAME 개발 블로그
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 부탁드립니다!