Deeper Learning

[UE5] Subsystem, UserWidget 의존성 관리, Delegate 활용 (Session Interface를 사용한 멀티플레이 Plugin 제작) 본문

Game Development/Unreal Engine

[UE5] Subsystem, UserWidget 의존성 관리, Delegate 활용 (Session Interface를 사용한 멀티플레이 Plugin 제작)

Dlaiml 2024. 7. 15. 22:06

 

 

[UE5] Session 기반 멀티플레이 Plugin 제작 - OnlineSubsystemSteam 환경에서 ServerTravel 디버깅

이전 포스팅에서 OnlineSubsystem의 Session Interface를 사용하여 멀리 떨어진 플레이어도 같이 게임을 즐길 수 있도록 간단하게 멀티플레이 기능을 만들어 보았다.  [UE5] 클라이언트-서버 모델언리얼

dlaiml.tistory.com

 

이번 포스팅도 이전 포스팅에 이어서 Steam Online Subsystem의 Session Interface를 활용한 멀티플레이 플러그인 개발기이다.

쉽게 적용해 보고 테스트할 수 있도록 UserWidget과 테스트 레벨을 추가하였다. 

 

이번 포스팅에서는 플러그인을 만들면서 Steam Online Subsystem, 플러그인 Subsystem, UserWidget 버튼에 연결된 함수까지 흐름을 Delegate로 처리한 방식에 대해 정리해보려 한다.

 

이하 본문에서는 편의상 아래와 같이 부르려 한다.

 

Online Subsystem Steam  -> 온라인 서브시스템

제작 중인 플러그인의 GameInstanceSubsystem에서 파생한 서브시스템 -> 플러그인 서브시스템

UserWidget -> 테스트 UI or UserWidget

 

*(의존관계) UserWidget -> 플러그인 서브시스템 -> 온라인 서브시스템

Dependency

현재 제작중인 플러그인은 온라인 서브시스템의 세션 인터페이스를 활용하고 있으며 플러그인 서브시스템에 주요 기능들이 구현되어 있으며 이를 사용자에게 제공하기 위한 테스트 UI도 있다.

 

테스트 레벨의 UI에서 세션생성 버튼을 누르면 ->플러그인 서브시스템에서 아래 코드를 포함하는 함수를 호출 -> 온라인 서브시스템의 세션 인터페이스의 CreateSession이 호출되어 Steam에서 세션을 생성하는 과정으로 세션 생성이 이루어진다.

// ...
const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
if (LocalPlayer != nullptr)
{
    if (!SessionInterface->CreateSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *LastSessionSettings))
    {
        SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegateHandle);

        MultiPlayerOnCreateSessionComplete.Broadcast(false);		
    }
}
// ...

 


언리얼 엔진의 Online Subsystem은 는 당연히 내가 직접 만든 GameInstanceSubsystem에 의존하지 않는다.

 

그렇다면 세션 생성 이후에 호출하고 싶은 플러그인 서브시스템의 함수가 있다면 이를 어떻게 처리해야 될까?

 

온라인 서브시스템에서 플러그인 서브시스템에 접근해서 멤버함수를 호출하는 코드를 세션 생성 함수 아래줄에 삽입하면 가능하다. 하지만 이는 low-level 구현에 의존하게 되어 객체지향의 원칙에도 위배되고 온라인 서브시스템을 사용하는 사람들이 모두 서브시스템의 코드를 알아야 하고 수정해야 하는 서브시스템이 되어버린다.

 

Delgate를 활용하면 이를 해결할 수 있다.

CreateSession 이후 온라인 서브시스템은 OnCreateSessionCompleteDelegate를 Broadcast 하도록 되어있다.

플러그인 서브시스템은 CreateSession 이전에 온라인 서브시스템의 OnCreateSessionCompleteDelegate에 세션 생성 이후 원하는 함수를 Bind 하면 세션 생성 이후 Delegate에 Bind 된 함수들이 호출되면서 세션 생성 이후 원하는 코드를 실행할 수 있게 된다.

 

세션 생성을 CreateSession을 예시로 실행순서에 따라 코드를 타고 올라가면서 각 레이어에서의 구현과 Delegate를 살펴보자.

 

UserWidget

 

플러그인 사용자가 쉽게 기능을 파악할 수 있도록 돕는 테스트 Level을 만들었다.

 

테스트 Level 레벨은 Host, Join, Quit 3개의 버튼으로 구성된 간단한 UserWidget인 Menu를 생성하고 Menu의 SetupMenu라는 함수를 실행하여 뷰포트에 위젯을 추가한다.

Host 버튼을 누르면 세션을 생성하고 다른 플레이어를 기다리는 Level인 Lobby 레벨로 이동한다.

 

// UMenu.cpp

void UMenu::HostButtonClicked()
{
	HostButton->SetIsEnabled(false);
	if (MultiPlayerSessionSubsystem != nullptr)
	{
		MultiPlayerSessionSubsystem->CreateSession(NumPublicConnections, MatchType);
	}
}


void UMenu::SetupMenu()
{
	AddToViewport();
	// ...
	UGameInstance* GameInstance = GetGameInstance();
	if (GameInstance != nullptr)
	{
		MultiPlayerSessionSubsystem = GameInstance->GetSubsystem<UMultiPlayerSessionSubsystem>();
	}
    // ...
}

 

SetupMenu를 보면 GameInstace를 가져와 플러그인의 서브시스템인 MultiPlayerSessionSubsystem을 멤버 변수에 할당하는 것을 볼 수 있다.

 

HostButton이 눌리면 UserWidget플러그인 서브시스템의 CreateSession을 호출한다.

 

UserWidget인 Menu는 플러그인 서브시스템인 MultiPlayerSessionSubsystem에 의존하고 있다.

하지만

플러그인 서브시스템 MultiPlayerSessionSubsystem은 UserWidget인 Menu에 의존해서는 안된다.

 

사용자는 자신이 직접 만든 게임의 UI와 Level에서 멀티플레이 기능을 활용해야 하는데, 세션 생성 및 멀티플레이 기능이 특정 레벨, 특정 UserWidget에서만 실행이 된다면 플러그인이라고 부를 수 없다.

 

플러그인 서브시스템

 

 

[UE5] Subsystem (Session 기반 멀티플레이 Plugin 제작)

저번 포스팅에 이어 OnlineSubsystem의 SessionInterface를 활용한 멀티플레이 플러그인 제작과 관련된 포스팅이다.  [UE5] Session 기반 멀티플레이 Plugin 제작 - OnlineSubsystemSteam 환경에서 ServerTravel 디버깅

dlaiml.tistory.com

(자세한 내용은 이전 포스팅 참고)

 

플러그인 서브시스템의 헤더파일과 UserWidget에서 호출한 CreateSession 함수와 생성자의 코드 스니펫을 확인해 보자.

///////////////////////////////////
// MultiPlayerSessionSubsystem.h //
///////////////////////////////////

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMultiPlayerOnCreateSessionComplete, bool, bWasSuccessful);

class MULTIPLAYERSESSIONPLUGIN_API UMultiPlayerSessionSubsystem : public UGameInstanceSubsystem
{
// ...
	FMultiPlayerOnCreateSessionComplete MultiPlayerOnCreateSessionComplete;
// ...

	void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);
// ...
	FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate;
	FDelegateHandle CreateSessionCompleteDelegateHandle;
// ...
}

 

Dynamic Multicast Delegate인 FMultiPlayerOnCreateSessionComplete를 선언하는 것을 볼 수 있으며

선언한 Delegate를 멤버 변수인 MultiPlayerOnCreateSessionComplete로 선언하여 사용하고 있다.

 

다음으로는 콜백함수로 사용할 OnCreateSessionComplete를 선언하였다.

 

그리고 온라인 서브시스템의 Delegate인 OnCreateSessionCompleteDelegate와 같은 형태의 Delegate인 CreateSessionCompleteDelegate도 멤버로 선언하였고, DelegateHandle도 같이 선언하였다.

 

이를 어떻게 사용하는지 아래 cpp 파일에서 확인해 보자. 

 

void UMultiPlayerSessionSubsystem::CreateSession(int32 NumPublicConnections, FString MatchType)
{
	// ...
	CreateSessionCompleteDelegateHandle = SessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate)
	const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
	if (LocalPlayer != nullptr)
	{
		if (!SessionInterface->CreateSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *LastSessionSettings))
		{
			SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegateHandle);
		
			MultiPlayerOnCreateSessionComplete.Broadcast(false);		
		}
	}
	// ...
}

// ...
UMultiPlayerSessionSubsystem::UMultiPlayerSessionSubsystem()
	: CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete))
// ...

 

먼저 생성자의 초기화 부분을 보면 온라인 서브시스템의 Delegate인 OnCreateSessionCompleteDelegate와 같은 Signature를 가지는 CreateSessionCompleteDelegate에 콜백함수로 OnCreateSessionComplete를 Bind 하고 있다.

 

하지만 아직 직접 만든 Delegate인 CreateSessionCompleteDelegate를 호출하는 부분이 없다.

 

세션을 직접 생성하지 않는 플러그인 서브시스템에서 세션생성 완료 Delegate를 정의한 형태인데, 온라인 서브시스템OnCreateSessionCompleteDelegate와 같이 세션 생성시점에 CreateSessionCompleteDelegate 호출하도록 하는 Delegate를 아래 코드에서 등록한다.

CreateSessionCompleteDelegateHandle = SessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate)

 

SessionInterface->CreateSession 에서 온라인 서브시스템의 세션 생성을 호출하는 것을 확인할 수 있다.

 

Online Subsystem

 

[UE5] Online Subsystem (Session Interface를 사용한 Steam 멀티플레이 구현)

[UE5] 클라이언트-서버 모델언리얼 엔진에서 싱글 플레이 게임에 대해서만 학습하다 보니 멀티플레이 게임, 네트워크에 대해 알고 싶어져 멀티플레이에 대해 학습을 시작하였다. 이번 포스팅에

dlaiml.tistory.com

이전 포스팅에서 다룬 적이 있는데, 온라인 서브시스템은 여러 Interface로 구현되어 있다.  Steam, Playstation, Apple, XBOX 등 많은 플랫폼에서 사용할 수 있도록 하기 위함이다. (자세한 내용은 위 포스팅 참고)

 

현재 Steam 온라인 서브시스템을 사용하고 있기 때문에 Steam의 세션을 생성하는 코드가 온라인 서브시스템의 순수 가상함수인 SessionInterface->CreateSession을 Override 하였다.

 

이제 세션이 생성되었다.

 

Delegate & Callback Invocation

 

세션이 생성되면 앞서 등록한 플러그인 서브시스템의 CreateSessionCompleteDelegate이 Broadcast 되어 Bind 된 콜백 함수인  OnCreateSessionComplete 호출된다.

 

온라인 서브시스템의 세션 생성 완료 Delegate는 SessionName과 bWasSuccessful을 Parameter로 하기 때문에 동일하게 두 Parameter를 받아오고 있다.

 

void UMultiPlayerSessionSubsystem::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{
	if (SessionInterface.IsValid())
	{
		SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegateHandle);
	}
	MultiPlayerOnCreateSessionComplete.Broadcast(bWasSuccessful);
}

 

앞서 온라인 서브시스템의 세션 생성 완료에 추가한 Delegate를 ClearOnCreateSessionCompleteDelegate_Handle로 제거한다.

 

그리고 직접 정의한 Delegate인 MultiPlayerOnCreateSessionComplete를 세션 생성 성공 여부 Bool을 담아 Broadcast 한다.

 

이제 다시 UserWidget의 코드를 보자.

void UMenu::MenuSetup(int32 _NumPublicConnections, FString _MatchType, FString _LobbyPath)
{
//...
    MultiPlayerSessionSubsystem->MultiPlayerOnCreateSessionComplete.AddDynamic(this, &ThisClass::OnCreateSession);
// ...
}


void UMenu::OnCreateSession(bool bWasSuccessful)
{
    UWorld* World = GetWorld();
    if (World)
    {
        World->GetAuthGameMode()->bUseSeamlessTravel = true;
        World->ServerTravel(LobbyPath);
    }
}

 

플러그인 서브시스템의 MultiPlayerOnCreateSessionComplete에 UserWidget의 콜백 함수인 OnCreateSession를 Setup 단계에서 Binding 한다.

 

 OnCreateSession에서는 Lobby Level의 경로로 ServerTravel 한다.

 

 

 UserWidget의 Host 버튼 클릭부터 ServerTravel까지 순서대로 정리하면 아래와 같다.

 

  1. UserWidget의 버튼 클릭
  2. 플러그인 서브시스템의 CreateSession 
  3. 온라인 서브시스템의 CreateSession
  4. Steam Online Subsystem에서 세션 생성 (Steam 관련 구현)
  5. 등록해 두었던 플러그인 서브시스템의 OnCreateSessionDelegate Broadcast
  6. 플러그인 서브시스템의 콜백 함수 OnCreateSessionComplete 호출
  7. 플러그인 서브시스템에서 선언한 Delegate MultiPlayerOnCreateSessionComplete Broadcast
  8. UserWidget의 콜백 함수 OnCreateSession 호출
  9. ServerTravel로 레벨 이동

온라인 서브시스템 <- 플러그인 서브시스템 <- UserWidget의 의존관계가 한 방향으로 정리되면서

 

온라인 서브시스템플러그인 서브시스템을 알지 못하여 구애받지 않고

플러그인 서브시스템 역시 UserWidget을 알지 못하고 구애받지 않는다.

 

플러그인 사용자는 플러그인 서브시스템의 MultiPlayerOnCreateSessionComplete에 자신이 원하는 함수를 바인딩하여 세션 생성 성공 이후 원하는 함수를 실행할 수 있게 되었다.

 

플러그인 개발 후기

Delegate를 십분 활용하여 플러그인 개발을 마쳤으며 플러그인 폴더를 옮겨서 다른 프로젝트에서 테스트 해보았는데 정상적으로 잘 작동하였다.

 

파이썬으로 인공지능을 개발할 때는 비교적 덜 사용하였던 디자인패턴이라 Delegate에 대해 여러번 정리하면서 익숙해지려 한다.

 

Reference

[0] https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/Delegates/Multicast/

 

Multi-cast Delegates

Delegates that can be bound to multiple functions and execute them all at once.

docs.unrealengine.com

[1] https://dlaiml.tistory.com/entry/UE5-Online-Subsystem

 

[UE5] Online Subsystem (Session Interface를 사용한 Steam 멀티플레이 구현)

[UE5] 클라이언트-서버 모델언리얼 엔진에서 싱글 플레이 게임에 대해서만 학습하다 보니 멀티플레이 게임, 네트워크에 대해 알고 싶어져 멀티플레이에 대해 학습을 시작하였다. 이번 포스팅에

dlaiml.tistory.com

[2]  Udemy Intermediate C++ Network

Comments