[UE5] Subsystem (Session 기반 멀티플레이 Plugin 제작)
저번 포스팅에 이어 OnlineSubsystem의 SessionInterface를 활용한 멀티플레이 플러그인 제작과 관련된 포스팅이다.
언리얼 엔진 문서를 찾아보니 플러그인 제작에는 Subsystem을 사용하는 것을 추천하고 있었다.
이전 Client-Server 모델과 OnlineSubsystem을 학습하면서 작성한 멀티플레이를 위한 세션 관리 함수들이 있는데, 이를 플러그인으로 제작하려다 보니 고민이 생겼다.
void CreateSession(int32 NumPublicConnections, FString MatchType);
void FindSessions(int32 MaxSearchResults);
void JoinSession(const FOnlineSessionSearchResult& SessionResult);
void StartSession();
void DestroySession();
PlayerController를 상속받는 직접 만든 클래스에서 이를 관리하였는데 이를 플러그인으로 만들때에도 동일한 방식으로 이를 PlayerController에 구현하면 범용성이 매우 떨어지며 다른 PlayerController를 사용하는 것도 불가능하다.
GameInstance, GameMode에 이를 구현하는것이 적합한데, 여러 Level에서 공통적으로 사용하도록 제작하여야 하기 때문에 GameInstance에 이를 구현하기로 하였다.
하지만 앞서 언급한 문제는 여전히 존재한다.
플러그인을 사용할 때 직접 정의한 GameInstance을 사용하려면 다중상속이 불가피한데 이는 복잡도가 증가하고 유지보수 또한 어려워지며 충돌문제도 잦다.
또한 GameInstance의 Level이 달라져도 유지된다는 Lifetime 때문에 GameInstance에 이를 직접 모두 구현하는 것은 GameInstance의 핵심 기능이 아닌 다른 기능들을 대거 추가하는 형태가 되어버린다.
Engine 클래스들의 [Override, 복잡성 증가, 본 목적과 다른 코드 추가, 기능추가를 위한 수많은 파생]을 최대한 피하면서 같은 기능을 담고 있는 하나의 모듈을 엔진 클래스의 Lifetime을 따라 사용하고 싶다면 언리얼 엔진의 Subsystem을 사용하는 것이 적합하다.
Subsystem
언리얼 엔진 공식문서를 보면 사용가능한 Subsystem과 Parent class를 확인할 수 있다.
Engine, Editor, GameInstance, LocalPlayer 총 4개의 종류가 있는데 클래스의 lifetime을 고려하여 적절한 Subsystem을 선택하여야 한다.
각 Subsystem은 아래 코드로 접근이 가능하다
// Engine Subsystem
UMyEngineSubsystem* MySubsystem = GEngine->GetEngineSubsystem<UMyEngineSubsystem>();
// Editor Subsystem
UMyEditorSubsystem* MySubsystem = GEditor->GetEditorSubsystem<UMyEditorSubsystem>();
// GameInstance Subsystem
UGameInstance* GameInstance = ...;
UMyGameSubsystem* MySubsystem = GameInstance->GetSubsystem<UMyGameSubsystem>();
// LocalPlayer Subsystem
ULocalPlayer* LocalPlayer = ...;
UMyPlayerSubsystem * MySubsystem = LocalPlayer->GetSubsystem<UMyPlayerSubsystem>();
UGameInstance가 생성될 때, UGameInstanceSubsystem도 생성되는 방식으로 Initialize, DeIntialize 등 lifetime을 같이한다.
게임시작 시점부터 게임이 끝날 때까지 유지되기를 원하고, Level, Player에 따른 큰 변화없이 Host, Join 등 기능을 제공하기를 원하므로 UGameInstanceSubsystem을 상속받기로 결정하였다.
로직의 구현은 UGameInstanceSubsystem이 아닌 PlayerController나 Character에 구현할 때와 차이가 없다. (CreateSession, ServerTravel 등 주요 함수들이 PlayerController, Character에 의존성이 없었기 때문에)
//////////////////////////////////
//UMultiPlayerSessionSubsystem.h//
//////////////////////////////////
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "Interfaces/OnlineSessionInterface.h"
#include "MultiPlayerSessionSubsystem.generated.h"
/**
*
*/
UCLASS()
class MULTIPLAYERSESSIONPLUGIN_API UMultiPlayerSessionSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
UMultiPlayerSessionSubsystem();
// Handle Session Functionality
void CreateSession(int32 NumPublicConnections, FString MatchType);
void FindSessions(int32 MaxSearchResults);
void JoinSession(const FOnlineSessionSearchResult& SessionResult);
void StartSession();
void DestroySession();
protected:
// Callbacks for Member Delegates
void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);
void OnFindSessionsComplete(bool bWasSuccessful);
void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);
void OnStartSessionComplete(FName SessionName, bool bWasSuccessful);
void OnDestroySessionComplete(FName SessionName, bool bWasSuccessful);
private:
IOnlineSessionPtr SessionInterface;
TSharedPtr<FOnlineSessionSettings> LastSessionSettings;
// Add to Online Session Interface Delegate List
FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate;
FDelegateHandle CreateSessionCompleteDelegateHandle;
FOnFindSessionsCompleteDelegate FindSessionsCompleteDelegate;
FDelegateHandle FindSessionsCompleteDelegateHandle;
FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate;
FDelegateHandle JoinSessionCompleteDelegateHandle;
FOnStartSessionCompleteDelegate StartSessionCompleteDelegate;
FDelegateHandle StartSessionCompleteDelegateHandle;
FOnDestroySessionCompleteDelegate DestroySessionCompleteDelegate;
FDelegateHandle DestroySessionCompleteDelegateHandle;
};
(cpp파일에서는 아직 직접 정의한 Delegate를 SessionInterface의 Delegate에 연결하는 부분, 콜백함수를 바인딩 하는 부분만 완료되어 코드를 옮기진 않겠다.)
또 Subsystem은 자동으로 블루프린트에 노출되어 UFUNCTION()을 추가하기만 해도 쉽게 블루프린트에서 Subsystem의 함수를 활용할 수 있게된다.
Subsystem에 대해 알지 못하였을 때, 병행하고 있는 프로젝트에서 Save/Load, 사용자 설정과 관련된 기능을 모두 GameInstance를 파생한 클래스에 함수를 모두 추가시켰었다.
Subsystem을 사용하는 방식이 더 적합하기 때문에 후에 리팩토링을 진행할 예정이다.
Reference[0]의 공식문서에 Subsystem을 사용하는 이유, 예시가 간단하게 잘 나와있어서 읽어보기를 추천한다.
Reference
[0] https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/Subsystems/