[UE5] 멀티플레이어 세션 플러그인 구현 #1

kkado·2024년 7월 31일
0

UE5

목록 보기
50/61
post-thumbnail

멀티플레이어 게임을 구현하기 위한 환경을 구축하는 플러그인을 직접 만들어 사용하고자 한다.

Online Subsystem

멀리 떨어진 사람과 인터넷상에서 연결되어 함께 플레이할 수 있는 멀티플레이어 게임을 경험한 적이 있을 것이다. 다른 플레이어와 연결되기 위해서는 그 플레이어의 IP 주소와 같은 정보를 알아야 직접 연결을 할 수 있을 테지만, 우리는 실제로 게임플레이 상에서 그런 작업 없이도 다른 사람과 연결될 수 있고, 매치메이킹이 가능하다.

이것이 가능한 이유는 '서버' 에 연결되어 서버가 매치메이킹을 주관하고, 플레이어를 서로 연결시켜 주기 때문이다. 언리얼 엔진은 이 과정에서 온라인 서브시스템(Online Subsystem)을 제공하며, 언리얼 엔진으로 개발한 게임은 다른 유저들을 연결해주는 서비스에 접속할 수 있게 해준다. 예컨대 스팀, Xbox와 같은 플랫폼을 뜻한다.

만약 하나의 게임을 여러 플랫폼으로 출시하고 싶다면, 그 각각의 플랫폼에 맞는 멀티플레이어 환경을 직접 구현해야 할까? 언리얼 엔진에서 제공하는 온라인 서브시스템은 추상화된 기능을 제공한다. 즉 하나의 코드 베이스를 이용하더라도 플랫폼 세부 사항을 처리할 수 있다.

이제 온라인 서브시스템을 기반으로, 온라인 상의 유저들과 매치메이킹 할 수 있는 기능을 하나의 플러그인으로 패키징하여 임포트할 수 있게 하는 것이 목표이다.


Session Interface

세션 인터페이스는 '게임 세션' 을 관리할 수 있게 한다. 세션을 생성 및 삭제하고, 세션을 탐색하고, 관리하는 모든 일련의 기능들을 제공한다. 여기서 '세션' 이란, 서버에서 동작하는 게임 인스턴스를 의미한다.

각 세션에는 여러 프로퍼티들이 Session Setting으로서 존재하며, 세션의 속성을 설정할 수 있다.

일반적인 게임 세션의 기본 라이프 사이클은 다음과 같다.

  • 세션 생성
  • 세션에 다른 사용자가 참여, 및 등록
  • 세션 시작
  • 게임 진행
  • 세션 종료
  • 사용자 등록 해제, 세션 종료
  • 세션 파괴

이 중에서 우리는 5개의 주요 기능만 다룬다. 세션 생성, 세션 탐색, 세션 참여, 세션 시작, 세션 파괴가 그것이다.


구현 계획

리슨 서버를 열어 세션을 만드는 '호스트' 플레이어와, 단순히 'Join' 버튼만 클릭하여 현재 참여할 수 있는 세션들 중 하나에 참여할 수 있는 '게스트' 플레이어가 있는 환경을 만들고자 한다. 즉 각각의 역할의 플로우는 다음과 같다.

  • 호스트 : 'HOST' 버튼 클릭 -> 세션 세팅을 명시하여 세션 생성 -> 플레이어가 참여 가능하고 대기할 수 있는 로비 공간 생성 완료
  • 게스트 : 'JOIN' 버튼 클릭 -> 세션 세팅을 명시하여 세션 탐색 -> 참여 가능한(Valid한) 세션 탐색 완료 -> 그 세션의 주소값을 얻은 후 그 세션에 참여

스팀 환경 설정

https://dev.epicgames.com/documentation/en-us/unreal-engine/online-subsystem-steam-interface-in-unreal-engine?application_version=5.1

스팀 환경에서 멀티플레이어를 구현할 것이기 때문에 .ini 파일을 스팀 환경에 맞게 수정해야 한다. 위 링크를 참조해 보면

DefaultEngine.ini 파일에 위와 같은 내용을 추가하라고 안내되어 있다.

붙여넣기 해 주고 ; 로 주석처리 되어 있는 부분을 해제하여 bInitServerOnClient 속성도 true 로 설정한다.


플러그인 생성

먼저 '플러그인' (Plugin) 이 무엇인지부터 알 필요가 있다.

플러그인이란 '특정 목적을 위해 설계된 코드의 모음' 이라고 정의할 수 있다.
개발자의 편의에 맞게 프로젝트에 추가하여 사용할 수 있는 '모듈' 이라고 생각하면 좋을 것이다. 즉 어떠한 기능을 '플러그인으로 만든다' 라고 함은, 고유한 기능을 캡슐화하여 관리 측면에서의 용이함을 가져가기 위함이라고 볼 수 있다. 다른 프로젝트를 만들 때 비슷한 기능이 필요하다면 그대로 이식하기도 편하다.

플러그인 생성

플러그인은 엔진 내부의 플러그인 에디터에서 확인할 수 있다.

좌상단의 'Add' 버튼을 클릭하여 새 플러그인을 간편하게 만들 수 있다.

생성한 후 IDE를 열어 보면 'Plugins' 폴더가 새롭게 생성되어 있고 그 밑에 만든 플러그인이 들어 있는 것을 볼 수 있다.

.uplugin 파일 수정

플러그인은 다른 플러그인에 종속성을 가질 수 있다. 그리고 .uplugin 파일을 열어 그 의존성을 명시해 주어야 한다.

"Modules" 항목 아래에 새로운 "Plugins"을 만들어 OnlineSubsystemOnlineSubsystemSteam 을 명시해 주었다. 이 글에서는 스팀 플랫폼을 타겟으로 하기 때문이다.

Build.cs 파일 수정

이 플러그인이 빌드될 때 어떤 모듈을 함께 컴파일해야 하는지를 명시해 준다.

PublicDependencyModuleNames.AddRange(
	new string[]
	{
		"Core",
		"OnlineSubsystem",
		"OnlineSubsystemSteam",
		// ... add other public dependencies that you statically link with here ...
	}
	);

OnlineSubsystem과 OnlineSubsystemSteam을 추가했다.


Game Instance Subsystem 생성

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/programming-subsystems-in-unreal-engine?application_version=5.1

Game Instance 클래스는 게임 세션을 다루기 좋은 클래스이다. 게임의 생성 시 함께 생성되며, 게임이 종료될 때까지 파괴되지 않고, 또한 레벨의 이동에도 여전히 동일한 인스턴스를 유지할 수 있다.

그러나 GameInstance 클래스는 멀티플레이어 게임 전반의 많은 기능들을 함께 가지고 있기 때문에 '세션 관리' 만을 위한 클래스라고는 보기 힘들다. 따라서 우리는 이 GameInstance와 비슷하게 동작하는 하위 시스템에서 세션 관리만을 담당하는 클래스에 기능들을 구현하면 좋을 것이며, 마침 언리얼 엔진에는 '서브시스템' 이라는 클래스가 존재한다.

언리얼 엔진 내부 기능들을 수정하거나 오버라이딩하는 복잡성을 피할 수 있도록 함과 동시에 블루프린트나 파이썬, C++에는 노출시킴으로써 개발자로 하여금 확장성을 구현할 수 있도록 하는 개념이다.

GameInstance와 함께 동작하는 GameInstanceSubsystem을 부모 클래스로써 사용하도록 하고, 클래스를 생성한다.

우측의 드롭다운을 이용해서 이 클래스를 생성하는 위치가 메인 프로젝트인지, 아니면 플러그인인지를 명시할 수 있고, 여기서 플러그인으로 지정했는지를 확인한다.


Delegate

먼저 델리게이트가 무엇인지부터 알 필요가 있는데, 델리게이트는 쉽게 말해 '함수의 레퍼런스를 갖고 있는 오브젝트' 라고 할 수 있다. 어떤 함수를 델리게이트에 바인딩 할 수 있다. 델리게이트가 fire 될 때 (또는 broadcast 된다고 한다), 해당 델리게이트에 바인딩 된 함수들을 실행시킨다. 이렇게 실행되는 함수들을 Callback 또는 Callback 함수 라고 부른다.

'세션 인터페이스' 란 우리가 세션을 관리할 수 있도록 (예컨대 CreateSession() 과 같은) 함수를 제공하는 인터페이스이다.

이 인터페이스는 델리게이트의 리스트를 가지고 있다. 이 리스트에 델리게이트를 등록하면 그 델리게이트를 fire할 수 있다. 간단히 그림으로 나타내면 다음과 같다.

그럼 우리는 아래와 같은 작업을 수행해야 한다.

  • 세션 인터페이스 구하기
  • 델리게이트 생성
  • 콜백 함수 생성하여 델리게이트에 바인딩
  • 인터페이스의 델리게이트 리스트에 델리게이트 추가

이 과정을 거침으로써 인터페이스가 특정 작업을 수행했을 때 실행될 기능들을 정의 및 구현할 수 있다.


User Widget 생성

사용자의 화면에 표시되어 서브시스템의 기능들에 접근할 수 있도록 하는 유저 위젯을 먼저 만든다.

업로드중..

유저 위젯 블루프린트를 만들었고 간단하게 캔버스패널 아래에 Host 그리고 Join 두 개의 버튼을 만들었고 각각의 이름을 HostButton, JoinButton 으로 설정했다. 이 이름으로 C++ 클래스에서 함수를 바인딩할 것이다.

메뉴 클래스를 초기 설정하는 MenuSetup 함수를 선언하고

void UMenu::MenuSetup()
{
	AddToViewport();
	SetVisibility(ESlateVisibility::Visible);
	bIsFocusable = true;

	UWorld* World = GetWorld();
	if (World)
	{
		APlayerController* PlayerController = World->GetFirstPlayerController();
		if (PlayerController)
		{
			FInputModeUIOnly InputModeData;
			InputModeData.SetWidgetToFocus(TakeWidget());
			InputModeData.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
			PlayerController->SetInputMode(InputModeData);
			PlayerController->SetShowMouseCursor(true);
		}
	}
}

위와 같이 정의한다. 포커스를 설정하고 인풋모드를 변경, 마우스 커서를 보이게 하는 등의 설정을 진행한다.

private:
	UPROPERTY(meta = (BindWidget))
	class UButton* HostButton;

	UPROPERTY(meta = (BindWidget))
	UButton* JoinButton;

	UFUNCTION()
	void HostButtonClicked();

	UFUNCTION()
	void JoinButtonClicked();

메뉴 클래스에 다음과 같이 버튼 클래스와 바인딩할 콜백 함수를 선언하고 (여기서 함수에는 UFUNCTION() 이, 버튼 클래스에는 meta = (BindWidget) 이 꼭 필요함)

bool UMenu::Initialize()
{
	if (!Super::Initialize())
	{
		return false;
	}

	if (HostButton)
	{
		HostButton->OnClicked.AddDynamic(this, &ThisClass::HostButtonClicked);
	}
	if (JoinButton)
	{
		JoinButton->OnClicked.AddDynamic(this, &ThisClass::JoinButtonClicked);
	}

	return true;
}

Initialize에서 바인딩한다.

바인딩한 HostButtonClicked, JoinButtonClicked 함수는 서브시스템에서 관련 기능을 모두 만든 후에 다시 구현 할 예정.

그리고 세션 함수들을 호출할 GameInstance Subsystem을 private 세션에 선언한다.

class UMultiplayerSessionsSubsystem* MultiplayerSessionsSubsystem;
UGameInstance* GameInstance = GetGameInstance();
if (GameInstance)
{
	MultiplayerSessionsSubsystem = GameInstance->GetSubsystem<UMultiplayerSessionsSubsystem>();
}

GetGameInstance() 를 통해 게임 인스턴스를 구하고, 그 하위 클래스인 Game Instance Subsystem을 구해서 멤버 포인터에 할당해 놓는다.


다음 글에서는 본격적인 함수들을 만들고 메뉴에서 이 함수들을 호출, 사용할 수 있도록 한다.

profile
울면안돼 쫄면안돼 냉면됩니다

0개의 댓글