멀티플레이어 게임을 구현하기 위한 환경을 구축하는 플러그인을 직접 만들어 사용하고자 한다.
멀리 떨어진 사람과 인터넷상에서 연결되어 함께 플레이할 수 있는 멀티플레이어 게임을 경험한 적이 있을 것이다. 다른 플레이어와 연결되기 위해서는 그 플레이어의 IP 주소와 같은 정보를 알아야 직접 연결을 할 수 있을 테지만, 우리는 실제로 게임플레이 상에서 그런 작업 없이도 다른 사람과 연결될 수 있고, 매치메이킹이 가능하다.
이것이 가능한 이유는 '서버' 에 연결되어 서버가 매치메이킹을 주관하고, 플레이어를 서로 연결시켜 주기 때문이다. 언리얼 엔진은 이 과정에서 온라인 서브시스템(Online Subsystem)을 제공하며, 언리얼 엔진으로 개발한 게임은 다른 유저들을 연결해주는 서비스에 접속할 수 있게 해준다. 예컨대 스팀, Xbox와 같은 플랫폼을 뜻한다.
만약 하나의 게임을 여러 플랫폼으로 출시하고 싶다면, 그 각각의 플랫폼에 맞는 멀티플레이어 환경을 직접 구현해야 할까? 언리얼 엔진에서 제공하는 온라인 서브시스템은 추상화된 기능을 제공한다. 즉 하나의 코드 베이스를 이용하더라도 플랫폼 세부 사항을 처리할 수 있다.
이제 온라인 서브시스템을 기반으로, 온라인 상의 유저들과 매치메이킹 할 수 있는 기능을 하나의 플러그인으로 패키징하여 임포트할 수 있게 하는 것이 목표이다.
세션 인터페이스는 '게임 세션' 을 관리할 수 있게 한다. 세션을 생성 및 삭제하고, 세션을 탐색하고, 관리하는 모든 일련의 기능들을 제공한다. 여기서 '세션' 이란, 서버에서 동작하는 게임 인스턴스를 의미한다.
각 세션에는 여러 프로퍼티들이 Session Setting으로서 존재하며, 세션의 속성을 설정할 수 있다.
일반적인 게임 세션의 기본 라이프 사이클은 다음과 같다.
이 중에서 우리는 5개의 주요 기능만 다룬다. 세션 생성, 세션 탐색, 세션 참여, 세션 시작, 세션 파괴가 그것이다.
리슨 서버를 열어 세션을 만드는 '호스트' 플레이어와, 단순히 'Join' 버튼만 클릭하여 현재 참여할 수 있는 세션들 중 하나에 참여할 수 있는 '게스트' 플레이어가 있는 환경을 만들고자 한다. 즉 각각의 역할의 플로우는 다음과 같다.
스팀 환경에서 멀티플레이어를 구현할 것이기 때문에 .ini
파일을 스팀 환경에 맞게 수정해야 한다. 위 링크를 참조해 보면
DefaultEngine.ini
파일에 위와 같은 내용을 추가하라고 안내되어 있다.
붙여넣기 해 주고 ;
로 주석처리 되어 있는 부분을 해제하여 bInitServerOnClient
속성도 true
로 설정한다.
먼저 '플러그인' (Plugin) 이 무엇인지부터 알 필요가 있다.
플러그인이란 '특정 목적을 위해 설계된 코드의 모음' 이라고 정의할 수 있다.
개발자의 편의에 맞게 프로젝트에 추가하여 사용할 수 있는 '모듈' 이라고 생각하면 좋을 것이다. 즉 어떠한 기능을 '플러그인으로 만든다' 라고 함은, 고유한 기능을 캡슐화하여 관리 측면에서의 용이함을 가져가기 위함이라고 볼 수 있다. 다른 프로젝트를 만들 때 비슷한 기능이 필요하다면 그대로 이식하기도 편하다.
플러그인은 엔진 내부의 플러그인 에디터에서 확인할 수 있다.
좌상단의 'Add' 버튼을 클릭하여 새 플러그인을 간편하게 만들 수 있다.
생성한 후 IDE를 열어 보면 'Plugins' 폴더가 새롭게 생성되어 있고 그 밑에 만든 플러그인이 들어 있는 것을 볼 수 있다.
플러그인은 다른 플러그인에 종속성을 가질 수 있다. 그리고 .uplugin
파일을 열어 그 의존성을 명시해 주어야 한다.
"Modules"
항목 아래에 새로운 "Plugins"
을 만들어 OnlineSubsystem
과 OnlineSubsystemSteam
을 명시해 주었다. 이 글에서는 스팀 플랫폼을 타겟으로 하기 때문이다.
이 플러그인이 빌드될 때 어떤 모듈을 함께 컴파일해야 하는지를 명시해 준다.
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"OnlineSubsystem",
"OnlineSubsystemSteam",
// ... add other public dependencies that you statically link with here ...
}
);
OnlineSubsystem과 OnlineSubsystemSteam을 추가했다.
Game Instance 클래스는 게임 세션을 다루기 좋은 클래스이다. 게임의 생성 시 함께 생성되며, 게임이 종료될 때까지 파괴되지 않고, 또한 레벨의 이동에도 여전히 동일한 인스턴스를 유지할 수 있다.
그러나 GameInstance 클래스는 멀티플레이어 게임 전반의 많은 기능들을 함께 가지고 있기 때문에 '세션 관리' 만을 위한 클래스라고는 보기 힘들다. 따라서 우리는 이 GameInstance와 비슷하게 동작하는 하위 시스템에서 세션 관리만을 담당하는 클래스에 기능들을 구현하면 좋을 것이며, 마침 언리얼 엔진에는 '서브시스템' 이라는 클래스가 존재한다.
언리얼 엔진 내부 기능들을 수정하거나 오버라이딩하는 복잡성을 피할 수 있도록 함과 동시에 블루프린트나 파이썬, C++에는 노출시킴으로써 개발자로 하여금 확장성을 구현할 수 있도록 하는 개념이다.
GameInstance와 함께 동작하는 GameInstanceSubsystem을 부모 클래스로써 사용하도록 하고, 클래스를 생성한다.
우측의 드롭다운을 이용해서 이 클래스를 생성하는 위치가 메인 프로젝트인지, 아니면 플러그인인지를 명시할 수 있고, 여기서 플러그인으로 지정했는지를 확인한다.
먼저 델리게이트가 무엇인지부터 알 필요가 있는데, 델리게이트는 쉽게 말해 '함수의 레퍼런스를 갖고 있는 오브젝트' 라고 할 수 있다. 어떤 함수를 델리게이트에 바인딩
할 수 있다. 델리게이트가 fire
될 때 (또는 broadcast
된다고 한다), 해당 델리게이트에 바인딩 된 함수들을 실행시킨다. 이렇게 실행되는 함수들을 Callback
또는 Callback 함수
라고 부른다.
'세션 인터페이스' 란 우리가 세션을 관리할 수 있도록 (예컨대 CreateSession()
과 같은) 함수를 제공하는 인터페이스이다.
이 인터페이스는 델리게이트의 리스트를 가지고 있다. 이 리스트에 델리게이트를 등록하면 그 델리게이트를 fire할 수 있다. 간단히 그림으로 나타내면 다음과 같다.
그럼 우리는 아래와 같은 작업을 수행해야 한다.
이 과정을 거침으로써 인터페이스가 특정 작업을 수행했을 때 실행될 기능들을 정의 및 구현할 수 있다.
사용자의 화면에 표시되어 서브시스템의 기능들에 접근할 수 있도록 하는 유저 위젯을 먼저 만든다.
유저 위젯 블루프린트를 만들었고 간단하게 캔버스패널 아래에 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을 구해서 멤버 포인터에 할당해 놓는다.
다음 글에서는 본격적인 함수들을 만들고 메뉴에서 이 함수들을 호출, 사용할 수 있도록 한다.