전체 글에 해당하는 글 69

플러그인 다듬기

개발 일지|2024. 12. 3. 22:11

MenuUI 클래스에 로비 경로를 추가해 레벨 이동이 하드코딩되있던 부분을 수정

	UFUNCTION(BlueprintCallable)
	void MenuSetup(int32 NumberOfPublicConnections = 4, FString TypeOfMatch = FString(TEXT("FreeForAll")), FString LobbyPath = FString(TEXT("/Game/FirstPerson/Maps/Lobby")));
    
    FString PathToLobby{TEXT("")};

간단하게 FString 변수를 추가하고, MenuSetup 선언부에 LobbyPath 추가

PathToLobby = FString::Printf(TEXT("%s?listen"), *LobbyPath);

UWorld* World = GetWorld();
if (World) {
	World->ServerTravel(PathToLobby);
}

MenuSetup 함수에 LobbyPath를 listen 옵션으로 지정해주고, CreateSession의 ServerTravel 로직을 변경해준다.

 

이렇게 하면 간단하게 변경 완료

 


 

다른 게임에 참여한 후, 호스트가 강제종료해 세션에서 나와진 후 일시적으로 Host가 되지 않는 버그

auto ExistingSession = SessionInterface->GetNamedSession(NAME_GameSession);
if (ExistingSession != nullptr) {
	SessionInterface->DestroySession(NAME_GameSession);
}
  • CreateSession 로직 맨 위의 이미 존재하는 세션을 검사해 세션을 닫는 부분이 있는데, 네트워크 응답에 시간이 필요하나 바로 CreateSession의 다음 로직으로 넘어가 발생하는 문제
  • DestroySession을 거쳐 세션이 이미 있는경우 세션을 없애고, 세션이 없다면 마지막에 저장된 변수를 사용해 세션을 생성하도록 변경
void UMultiplayerSessionsSubsystem::DestroySession()
{
	if (!SessionInterface.IsValid()) {
		MultiplayerOnDestroySessionComplete.Broadcast(false);
		return;
	}
	DestroySessionCompleteDelegateHandle = SessionInterface->AddOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteDelegate);

	if (!SessionInterface->DestroySession(NAME_GameSession)) {
		SessionInterface->ClearOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteDelegateHandle);
		MultiplayerOnDestroySessionComplete.Broadcast(false);
	}
}


void UMultiplayerSessionsSubsystem::OnDestroySessionComplete(FName SessionName, bool bWasSuccessful)
{
	if (SessionInterface) {
		SessionInterface->ClearOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteDelegateHandle);
	}
	if (bWasSuccessful && bCreateSessionOnDestroy) {
		bCreateSessionOnDestroy = false;
		CreateSession(LastNumPublicConnections, LastMatchType);
	}
	MultiplayerOnDestroySessionComplete.Broadcast(bWasSuccessful);
}
  • CreateSession에서는 세션 생성에 필요한 값을 먼저 저장해두고, DestroySession을 호출해 검사할 수 있도록 변경
auto ExistingSession = SessionInterface->GetNamedSession(NAME_GameSession);
if (ExistingSession != nullptr) {
	bCreateSessionOnDestroy = true;
	LastNumPublicConnections = NumPublicConnections;
	LastMatchType = MatchType;

	DestroySession();
}

 


Quit 버튼

  • 그냥 간단하게 메뉴 widget BP에 버튼 하나 만들어주고, OnClicked 이벤트에 Quit 노드 연결

메뉴 버튼 활성화 및 비활성화

  • 여러번 Host 버튼을 누르는걸 막아 중복 세션 생성을 막기 위함
  • 모종의 이유로 Host에 실패했거나, 세션 참가에 실패했을 때 버튼을 다시 활성화
  • SetIsEnabled 옵션을 true나 false로 두고 적절한 위치에 배치.

 

 

'개발 일지' 카테고리의 다른 글

Tracking Player  (0) 2024.12.03
플러그인 UI - 2  (0) 2024.12.02
플러그인 UI - 1  (0) 2024.12.01
플러그인 등록  (0) 2024.11.30
Join Session  (0) 2024.11.29

댓글()

Tracking Player

개발 일지|2024. 12. 3. 12:02

GameState

  • GameMode
    • 게임 내의 모든 규칙을 가지고 있음
    • 플레이어의 레벨 이동이나 스폰 장소 선택 등 관리
    • PostLogin(APlayerController* NewPlayer) 라는 가상 함수를 상속받아 게임에 참여하는 playercontroller에 접근할 수 있음
    • Logout(AController* Exiting) 함수로 게임을 나가는 플레이어들의 playercontroller에 접근할 수 있음
  • GameState
    • GameMode보다 좀 더 게임 상태에 대한 정보를 가지고 있도록 디자인됨
    • 클라이언트가 GameState 클래스에 접근해 정보를 얻을 수 있음
    • 플레이어의 점수나 승리 횟수같은 개별 유저의 정보에만 국한되지 않고 게임 상태에 대한 정보를 담도록 디자인되었음
    • 플레이어 상태에 대한 배열을 가지고 있음. 얼마나 많은 플레이어가 들어와있는지도 이 배열의 수를 통해 파악할 수 있음

Login & Logout

  • GameModeBase에 구현
  • Login 함수와 Logout 함수를 override받아 사용
  • GameState 관련 로직 사용을 위해 두개의 전처리 include
#include "GameFramework/GameStateBase.h"
#include "GameFramework/PlayerState.h"
  • GameState가 가지고 있는 TArray plaeyer 배열을 사용해 접속하고 종료하는 플레이어 수, 이름을 가져옴
void ALobbyGameMode::PostLogin(APlayerController* NewPlayer)
{
	Super::PostLogin(NewPlayer);

	if (GameState) {
		int32 NumOfPlayer = GameState.Get()->PlayerArray.Num();

		if (GEngine) {
			GEngine->AddOnScreenDebugMessage(
				1,
				60.f,
				FColor::Yellow,
				FString::Printf(TEXT("Players in game: %d"), NumOfPlayer)
			);

			APlayerState* PlayerState = NewPlayer->GetPlayerState<APlayerState>();
			if (PlayerState) {
				FString PlayerName = PlayerState->GetPlayerName();
				GEngine->AddOnScreenDebugMessage(
					-1,
					60.f,
					FColor::Cyan,
					FString::Printf(TEXT("%s has joined the game!"), *PlayerName)
				);
			}
		}
	}
}

void ALobbyGameMode::Logout(AController* Exiting)
{
	Super::Logout(Exiting);

	APlayerState* PlayerState = Exiting->GetPlayerState<APlayerState>();

	if (PlayerState) {
		int32 NumOfPlayer = GameState.Get()->PlayerArray.Num();

		if (GEngine) {
			GEngine->AddOnScreenDebugMessage(
				1,
				60.f,
				FColor::Yellow,
				FString::Printf(TEXT("Players in game: %d"), NumOfPlayer - 1)
			);
		}

		FString PlayerName = PlayerState->GetPlayerName();
		GEngine->AddOnScreenDebugMessage(
			-1,
			60.f,
			FColor::Cyan,
			FString::Printf(TEXT("%s has exited the game!"), *PlayerName)
		);
	}
}
  • 이후 LobbyGameMode를 상속받은 BP_LobbyGameMode 클래스를 만들어 DefaultPawn을 firstplayerCharacter로 변경, LobbyMap의 defaultGameMode를 BP_LobbyGameMode로 변경

'개발 일지' 카테고리의 다른 글

플러그인 다듬기  (0) 2024.12.03
플러그인 UI - 2  (0) 2024.12.02
플러그인 UI - 1  (0) 2024.12.01
플러그인 등록  (0) 2024.11.30
Join Session  (0) 2024.11.29

댓글()

플러그인 UI - 2

개발 일지|2024. 12. 2. 22:50

Custom Delegate 제작

  • Subsystem 플러그인에서 커스텀 델리게이트를 만들어 플러그인을 사용하는 user widget에 콜백 함수를 바인딩한다.
  • 이 방법으로 MultiplayerSessionSubsystem에서 플러그인을 사용하는 클래스로 연결
  • UserWidget -> Plugin -> SessoinInterface 클래스로 이어지는 One-Way dependency 설계를 델리게이트-콜백함수로 연결하며 플러그인의 호환성 유지
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMultiplayerOnCreateSessionComplete, bool, bWasSuccessful);

// Own Custom delegates for the menu class to bind callbacks
FMultiplayerOnCreateSessionComplete MultiplayerOnCreateSessionComplete;
  • 이를위해 MultiplayerSessionSubsystem에 다이나믹 델리게이트 선언
  • UserWidget 클래스인 MenuUI의 MenuSetup 함수에 콜백함수 바인딩
if (MultiplayerSessionsSubsystem) {
	MultiplayerSessionsSubsystem->MultiplayerOnCreateSessionComplete.AddDynamic(this, &ThisClass::OnCreateSession);
}
  • UserWidget 클래스에 OnCreateSession 함수 구현
void UMenuUI::OnCreateSession(bool bWasSuccessful)
{
	if (bWasSuccessful) {
		if (GEngine) {
			GEngine->AddOnScreenDebugMessage(
				-1,
				15.f,
				FColor::Yellow,
				FString(TEXT("Session created successfully!"))
			);
		}
	}
}
  • MultiplayerSessionSubsystem에 콜백함수 구현. 여기서 델리게이트 리스트 제거 및 MenuUI의 델리게이트 broadcast
void UMultiplayerSessionsSubsystem::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{
	if (SessionInterface) {
		SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegateHandle);
	}
	MultiplayerOnCreateSessionComplete.Broadcast(bWasSuccessful);
}
  • 그리고 HostButtonClicked 함수에 있던 ServerTravel 로직을 OnCreateSession 콜백 함수로 이동
  • 버튼을 누르자마자 바로 레벨이동이 발생하지 않고 세션이 생성될때까지 기다릴 수 있도록 하기위함
void UMenuUI::OnCreateSession(bool bWasSuccessful)
{
	if (bWasSuccessful) {
		if (GEngine) {
			GEngine->AddOnScreenDebugMessage(
				-1,
				15.f,
				FColor::Yellow,
				FString(TEXT("Session created successfully!"))
			);
		}
		UWorld* World = GetWorld();
		if (World) {
			World->ServerTravel("/Game/FirstPerson/Maps/Lobby?listen");
		}
	}
}

 

 


Find & JoinSession 구현

  • Character 클래스에서 했던것과 크게 다르지는 않다. 델리게이트와 콜백함수를 사용해 의존성을 줄인 방식으로 구현된다.
  • 먼저 MultiplayerSessionsSubsystem에서 SessionSearch 관련 로직을 짠다.
  • 마지막에 MenuUI 클래스에서 델리게이트를 통해 찾은 함수와 성공 여부를 보낼 수 있도록 한다.
void UMultiplayerSessionsSubsystem::FindSessions(int32 MaxSearchResults)
{
	if (!SessionInterface.IsValid()) {
		return;
	}
	FindSessionsCompleteDelegateHandle = SessionInterface->AddOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegate);

	LastSessionSearch = MakeShareable(new FOnlineSessionSearch());
	LastSessionSearch->MaxSearchResults = MaxSearchResults;
	LastSessionSearch->bIsLanQuery = IOnlineSubsystem::Get()->GetSubsystemName() == "NULL" ? true : false;

	LastSessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);

	const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
	if (!SessionInterface->FindSessions(*LocalPlayer->GetPreferredUniqueNetId(), LastSessionSearch.ToSharedRef())) {
		SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegateHandle);

		MultiplayerOnFindSessionsComplete.Broadcast(TArray<FOnlineSessionSearchResult>(), false);
	}
}
void UMultiplayerSessionsSubsystem::OnFindSessionsComplete(bool bWasSuccessful)
{
	if (SessionInterface) {
		SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegateHandle);
	}

	if (LastSessionSearch->SearchResults.Num() <= 0) {
		// Fail to find any Sessions
		MultiplayerOnFindSessionsComplete.Broadcast(TArray<FOnlineSessionSearchResult>(), false);
		return;
	}

	MultiplayerOnFindSessionsComplete.Broadcast(LastSessionSearch->SearchResults, bWasSuccessful);
}
  • 그러면 MenuUI에서 받은 결과를 바탕으로 반복문을 돌려 결과를 확인하고 우리가 세팅한 조건과 일치하다면 MultiplayerSessionsSubsystem의 JoinSession을 다시 호출한다.
void UMenuUI::OnFindSessions(const TArray<FOnlineSessionSearchResult>& SessionResults, bool bWasSuccessful)
{
	if (MultiplayerSessionsSubsystem == nullptr) {
		return;
	}
	for (auto Result : SessionResults) {
		FString SettingsValue;
		Result.Session.SessionSettings.Get(FName("MatchType"), SettingsValue);
		if (SettingsValue == MatchType) {
			MultiplayerSessionsSubsystem->JoinSession(Result);
			return;
		}
	}
}
  • 그러면 다시 MultiplayerSessionsSubsystem의 JoinSession에서 MenuUI의 JoinSession으로 넘어가도록 호출한다.
void UMultiplayerSessionsSubsystem::JoinSession(const FOnlineSessionSearchResult& SessionResult)
{
	if (!SessionInterface.IsValid()) {
		MultiplayerOnJoinSessionComplete.Broadcast(EOnJoinSessionCompleteResult::UnknownError);
		return;
	}

	JoinSessionCompleteDelegateHandle = SessionInterface->AddOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegate);
	
	const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
	if (!SessionInterface->JoinSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, SessionResult)) {
		SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegateHandle);
		MultiplayerOnJoinSessionComplete.Broadcast(EOnJoinSessionCompleteResult::UnknownError);
	}

}

void UMultiplayerSessionsSubsystem::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
	if (SessionInterface) {
		SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegateHandle);
	}
	MultiplayerOnJoinSessionComplete.Broadcast(Result);
}
  • 최종적으로 MenuUI는 EOnJoinSessionCompleteResult 타입의 결과값을 받아 ClientTravel에 필요한 값들을 저장하고 호출하여 세션에 유저가 참가하도록 한다.
void UMenuUI::OnJoinSession(EOnJoinSessionCompleteResult::Type Result)
{
	IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get();
	if (Subsystem) {
		IOnlineSessionPtr SessionInterface = Subsystem->GetSessionInterface();
		if (SessionInterface.IsValid()) {
			FString Address;
			SessionInterface->GetResolvedConnectString(NAME_GameSession, Address);

			APlayerController* PlayerController = GetGameInstance()->GetFirstLocalPlayerController();
			if (PlayerController) {
				PlayerController->ClientTravel(Address, ETravelType::TRAVEL_Absolute);
			}
		}
	}
}

 

이렇게 보면 과정이 복잡한것 같지만, 플러그인을 통해 대부분의 로직이 돌아가면서 여러 프로젝트에서 돌려가며 쓸 수 있는 의존성이 최소화된 구조가 완성되었다.

'개발 일지' 카테고리의 다른 글

플러그인 다듬기  (0) 2024.12.03
Tracking Player  (0) 2024.12.03
플러그인 UI - 1  (0) 2024.12.01
플러그인 등록  (0) 2024.11.30
Join Session  (0) 2024.11.29

댓글()

플러그인 UI - 1

개발 일지|2024. 12. 1. 21:16

플러그인 UI 동작 구현

  • HostButton과 JoinButton을 UserWidget 클래스파일의 헤더로 가져온다
  • UPROPERTY()의 meta=bindwidget 속성을 주면 변수의 이름을 UI 버튼 변수명과 완전히 일치하게 설정 -> UI의 버튼에 접근할 수 있게 된다
  • 각 버튼의 콜백 함수 작성
protected:
	virtual bool Initialize() override;

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

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

	UFUNCTION()
	void HostButtonClicked();

	UFUNCTION()
	void JoinButtonClicked();

 

  • 이후 cpp 파일에서 버튼 클릭 콜백 함수 테스트용 디버그 로직과 initialize 함수에 AddDynamic으로 콜백함수 바인딩
bool UMenuUI::Initialize()
{
	if (!Super::Initialize()) {
		return false;
	}

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

	return true;
}

void UMenuUI::HostButtonClicked()
{
	if (GEngine) {
		GEngine->AddOnScreenDebugMessage(
			-1,
			15.f,
			FColor::Yellow,
			FString(TEXT("Host Button Clicked"))
		);
	}
}

void UMenuUI::JoinButtonClicked()
{
	if (GEngine) {
		GEngine->AddOnScreenDebugMessage(
			-1,
			15.f,
			FColor::Yellow,
			FString(TEXT("Join Button Clicked"))
		);
	}
}
  • Initialize 함수의 로직을 생성자에 작성하면, 위젯이 생성되기 전 너무 이른 시점에 로직이 작동하기 때문에 오류가 발생하므로 Initialize 함수에 작성 -> Initialize 함수는 위젯이 생성된 후 작동

MultiplayerSessionSubsystem 함수 구현

  • 이제 UI에 매핑될 함수 내부를 구현한다.
  • 지난번 함수 선언만 해뒀던 MultiplayerSessionSubsystem으로 가 cpp에 함수 기능 구현

CreateSession

  • SessionSettings의 bIsLanMatch 옵션을 삼항연산자로 true와 false 중 상황에 따라 적용되도록 수정
    • NULL일때 true, Steam일때 false
  • 세션 생성에 실패시 등록된 델리게이트 제거
void UMultiplayerSessionsSubsystem::CreateSession(int32 NumPublicConnections, FString MatchType)
{
	if (!SessionInterface.IsValid()) {
		return;
	}

	auto ExistingSession = SessionInterface->GetNamedSession(NAME_GameSession);
	if (ExistingSession != nullptr) {
		SessionInterface->DestroySession(NAME_GameSession);
	}

	// Store the Delegate in a FDelegateHandle so we can later remove it from the delegate list
	CreateSessionCompleteDelegateHandle = SessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate);

	LastSessionSettings = MakeShareable(new FOnlineSessionSettings());
	// Subsystem == Steam -> true, NULL -> false
	LastSessionSettings->bIsLANMatch = IOnlineSubsystem::Get()->GetSubsystemName() == "NULL" ? true : false;
	LastSessionSettings->NumPublicConnections = NumPublicConnections;
	LastSessionSettings->bAllowJoinInProgress = true;
	LastSessionSettings->bAllowJoinViaPresence = true;
	LastSessionSettings->bShouldAdvertise = true;
	LastSessionSettings->bUsesPresence = true;
	LastSessionSettings->Set(FName("MatchType"), MatchType, EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);

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

}
  • MenuUI에서 Host 버튼 클릭시 최종적으로 Listen 옵션으로 Lobby맵으로 이동하도록 구현
void UMenuUI::HostButtonClicked()
{
	if (GEngine) {
		GEngine->AddOnScreenDebugMessage(
			-1,
			15.f,
			FColor::Yellow,
			FString(TEXT("Host Button Clicked"))
		);
	}
	if (MultiplayerSessionsSubsystem) {
		MultiplayerSessionsSubsystem->CreateSession(4, FString("FreeForAll"));
		UWorld* World = GetWorld();
		if (World) {
			World->ServerTravel("/Game/FirstPerson/Maps/Lobby?listen");
		}
	}
}
  • 이후 UI 전용으로 설정되었던 InputMode를 GameOnly로 변경하는 함수 구현
  • MenuTearDown() 함수를 레벨 이동 시점에 호출하도록 NativeDestruct() 함수 override

void UMenuUI::NativeDestruct()
{
	MenuTearDown();

	Super::NativeDestruct();
}

void UMenuUI::MenuTearDown()
{
	RemoveFromParent();
	UWorld* World = GetWorld();
	if (World) {
		APlayerController* PlayerController = World->GetFirstPlayerController();
		if (PlayerController) {
			FInputModeGameOnly InputModeData;
			PlayerController->SetInputMode(InputModeData);
			PlayerController->SetShowMouseCursor(false);
		}
	}
}
  • 이후 테스트 해보면 정상적으로 세션 생성이 완료되는것 확인 가능

'개발 일지' 카테고리의 다른 글

Tracking Player  (0) 2024.12.03
플러그인 UI - 2  (0) 2024.12.02
플러그인 등록  (0) 2024.11.30
Join Session  (0) 2024.11.29
OnlineSubsystem Steam 연결 및 Create Session  (0) 2024.11.27

댓글()

플러그인 등록

개발 일지|2024. 11. 30. 22:46

플러그인

  • 특정 목적을 위해 디자인된 코드와 데이터의 집합
  • 플러그인으로 런타임이나 게임플레이, 에디터에 관련된 기능을 추가할 수 있다.
  • 하나 이상의 모듈로 만들어진다.
    • 모듈은 C++코드로 만들어진 별개의 유닛
    • 모듈은 별개의 build.cs 파일을 포함한다.
    • 개별 작업에 대한 고유한 기능을 캡슐화한 형태
    • Uasset(Mesh, Texture)을 포함하지 않는 코드로만 이루어짐
    • 프로젝트 자체도 모듈이 될 수 있다
  • 만드는법 자체도 우선 굉장히 간단한데, edit->plugin에서 Add->Blank(옵션 선택)선택하고 원하는 대로 정보 작성하면 바로 생성된다.

프로젝트에 추가된 모습


Setup Dependencies

  • 여기서 uplugin 파일로 들어가면 플러그인에 대한 여러 정보들을 확인할 수 있다.
  • 해당 파일에서 기능에 필요한 다른 플러그인도 추가할 수 있다.
"Modules": [
	{
		"Name": "MultiplayerSessions",
		"Type": "Runtime",
		"LoadingPhase": "Default"
	}
],
"Plugins": [
	{
		"Name": "OnlineSubsystem",
		"Enabled": true
	},
	{
		"Name": "OnlineSubsystemSteam",
		"Enabled": true
	}
]
  • 그리고 플러그인의 Build.cs 파일에 DependencyModule을 추가해준다.
PublicDependencyModuleNames.AddRange(
	new string[]
	{
		"Core",
		"OnlineSubsystem",
		"OnlineSubsystemSteam",
		// ... add other public dependencies that you statically link with here ...
	}
	);
  • 그리고 빌드하면 끝

Session 관리 로직을 구현할 Parent Class

  • Game Instance
    • 게임 생성시 같이 생성됨
    • 게임이 꺼질때까지 없어지지 않음
    • 레벨 이동시에도 유지되어 항상 같은 Game Instance를 사용 가능
  • Game Instance Subsystem
    • 게임 인스턴스 단위로 유지됨
    • 자동으로 엔진에 의해 생성되며, 엔진이 수명주기를 관리
    • 어느 시점에 초기화되고 종료될지는 엔진의 구조에 따라 결정됨 (별도로 메모리 해제 생각하지 않아도 됨)
    • 게임 인스턴스가 생성된 후 생성되며, 게임 인스턴스가 없어질 때 없어지므로 세션 관련 로직이 들어가기 알맞음

Subsystem 생성

  • 에디터의 Contents browser -> C++ 클래스에 새롭게 하나 추가
  • GameInstanceSubsystem 클래스를 상속받아 만들고, 타겟 모듈을 우리가 추가한 플러그인으로 설정
  • 생성한 후, 헤더와 cpp파일에 Sessioninterface의 5가지 함수를 추가하고 델리게이트 및 콜백함수까지 선언
  • 이후 내부 로직 구현

함수 동작 테스트를 위한 메뉴 구성

  • 플러그인 추가시 메뉴까지 같이 호환되도록 플러그인의 public 폴더에 UserWidget 생성
void UMenuUI::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);
		}
	}
}
  • 생성된 MenuUI 클래스에 기초 로직 세팅
  • 간단하게 마우스 커서 옵션만 설정 후 widget blueprint 추가
  • 테스트용으로 간단하게 Host버튼, Join버튼 추가
  • 메인 레벨의 블루프린트를 열어 BeginPlay노드로 위젯 생성

'개발 일지' 카테고리의 다른 글

플러그인 UI - 2  (0) 2024.12.02
플러그인 UI - 1  (0) 2024.12.01
Join Session  (0) 2024.11.29
OnlineSubsystem Steam 연결 및 Create Session  (0) 2024.11.27
Listen Server와 Dedicated Server  (0) 2024.11.26

댓글()

Join Session

개발 일지|2024. 11. 29. 18:10

대망의 JoinSession 어떻게 하는지 보자.

 

Create Sesssion때처럼 헤더파일에 JoinSession 함수 선언, 델리게이트와 콜백함수도 작성한다.

	UFUNCTION(BlueprintCallable)
	void JoinGameSession();

	void OnFindSessionsComplete(bool bWasSuccessful);

private:
	FOnFindSessionsCompleteDelegate FindSessionsCompleteDelegate;

생성자에 델리게이트를 바인딩 해주고, JoinGameSession 함수 구현

void AMenuCharacter::JoinGameSession()
{
	// Find game sessions
	if (!OnlineSessionInterface.IsValid()) {
		return;
	}

	OnlineSessionInterface->AddOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegate);

	TSharedPtr<FOnlineSessionSearch> SessionSearch = MakeShareable(new FOnlineSessionSearch());
	SessionSearch->MaxSearchResults = 10000;
	SessionSearch->bIsLanQuery = false;

	const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
	OnlineSessionInterface->FindSessions(*LocalPlayer->GetPreferredUniqueNetId(), SessionSearch.ToSharedRef());
}

 

그리고 위에서 선언한 "TSharedPtr<FOnlineSessionSearch> SessionSearch" 변수에서 SearchResults를 가져와야하기 때문에 로컬 변수로 선언하지 말고 헤더로 빼준다.

그리고 SessionSearch 옵션 중 "SessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);"

설정을 통해 현재 존재하는 세션만 검색하도록 설정해준다.


이후 콜백함수에 SearchResult를 순회하며 결과를 보도록 작성

void AMenuCharacter::OnFindSessionsComplete(bool bWasSuccessful)
{
	for (auto Result : SessionSearch->SearchResults) {
		FString Id = Result.GetSessionIdStr();
		FString User = Result.Session.OwningUserName;
		if (GEngine) {
			GEngine->AddOnScreenDebugMessage(
				-1,
				15.f,
				FColor::Cyan,
				FString::Printf(TEXT("Id: %s, User: %s"), *Id, *User)
			);
		}
	}
	

}

 


그다음 다음과 같은 구문을 CreateSession에 추가

SessionSettings->Set(FName("MatchType"), FString("FreeForAll"), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);

MatchType이라는 Key의 Value값을 FreeForAll로 정하고, 세션 설정 데이터가 온라인 서비스와 핑 두가지 모두를 이용하여 전달될 수 있도록 설정 -> 플레이어가 세션을 검색할때 두가지 정보를 모두 이용 가능

 

OnFindSessionsComplete 콜백 함수에도 해당 타입을 구분하도록 추가

FString MatchType = "FreeForAll";
Result.Session.SessionSettings.Get(FName("MatchType"), MatchType);
if (MatchType == FString("FreeForAll")) {
	GEngine->AddOnScreenDebugMessage(
		-1,
		15.f,
		FColor::Cyan,
		FString::Printf(TEXT("Joining Match Type: %s"), *MatchType)
	);
}

 

그다음 JoinSessionCompleteDelegate 헤더파일에 추가 -> 실질적인 ClientTravel이 일어날 부분

void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);
FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate;

EOnJoinSessionCompleteResult::Type Result -> 이 파라미터는 세션 참가 시도 후 결과값을 나타내는 Enum 변수.

AlreadyInSession, SessionDestroy같은 값들이 결과값으로 도출된다.

 

cpp 파일의 생성자에도 델리게이트 및 콜백함수 등록

AMenuCharacter::AMenuCharacter():
	CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete)),
	FindSessionsCompleteDelegate(FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionsComplete)),
	JoinSessionCompleteDelegate(FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplete))

그리고 클래스 명을 ThisClass로 바꿔서 테스트 후 본 프로젝트에 이식하기 용이하도록 변경

그다음 OnJoinSessionComplete 함수 구현

void AMenuCharacter::OnJoinSessionComplete(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);
		}
	}
}

여기서 ClientTravel의 파라미터는

  • Address: 이동할 레벨. IP주소나 도메인, 로컬맵 등등이 들어갈 수 있다.
  • ETravelType::Travel_Absolute: Address를 절대경로로 처리한다. 이전 레벨이나 세션 상태와 관계없이 지정된 맵이나 서버로 이동한다.
    • Travel Type은 Relative와 Partial도 있다.

이후 테스트를 해보면 세션 생성 및 탐색은 잘 동작하며, 세션 참가 자체는 dev키가 테스트를 위한 공용키라 지금처럼 하드코딩 된 상태로는 작동하지 않는것을 확인할 수 있었다.

'개발 일지' 카테고리의 다른 글

플러그인 UI - 1  (0) 2024.12.01
플러그인 등록  (0) 2024.11.30
OnlineSubsystem Steam 연결 및 Create Session  (0) 2024.11.27
Listen Server와 Dedicated Server  (0) 2024.11.26
계속되는 세션 오류  (0) 2024.11.22

댓글()

OnlineSubsystem Steam 연결 및 Create Session

개발 일지|2024. 11. 27. 19:01

OnlineSubsystem Steam에 연결해보자

 

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

먼저 언리얼 공식 문서에 검색해보면 다음의 구문을 Config/Engine.ini 파일에 추가하라고 되어있다.

[/Script/Engine.GameEngine]
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
 
[OnlineSubsystem]
DefaultPlatformService=Steam
 
[OnlineSubsystemSteam]
bEnabled=true
SteamDevAppId=480
 
; If using Sessions
; bInitServerOnClient=true
 
[/Script/OnlineSubsystemSteam.SteamNetDriver]
NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection"

중간에 세션을 사용할거면 bInitServerOnClient=true 문장을 추가하라고 되있다.

아마 세션 멀티플레이 기능을 사용하지 않고 도전과제, 친구 등의 기능만 사용하는 개발자들을 위해 저부분을 주석처리 한것 같다.

우리는 세션 기능을 사용할것이기 때문에 주석을 풀어준다.

 

그리고 uproject 파일을 우클릭해 Generate project file을 해준다.


다음으로 할일은 Character 클래스에 세션 로직을 추가해 Steam에 실제로 연결이 되는지 확인하는 것이다.

1인칭 기본 템플릿으로 프로젝트를 생성한다.

plugin을 열어 OnlineSubsystem Steam을 체크해주면 에디터를 재시작하라고 뜨는데 순순히 따라준다.

이러면 플러그인은 추가됬고, 코드로 가서 로직을 작성해보자.

 

캐릭터 클래스에 세션 관리 로직을 추가한다.

멀티플레이 게임이기 때문에 각각의 유저가 세션 생성, 세션 참가 등의 활동을 수행할것이다.

따라서 캐릭터 클래스에 로직을 추가해야 각각의 플레이어가 문제없이 세션 관련 활동을 진행할 수 있을것이다.

 

캐릭터의 생성자에 OnlineSubsystem을 Get 함수로 받아오고 헤더파일에 선언한 OlnineSessionInterface 변수에 SessionInterface를 저장해준다.

// MenuCharacter.h

public:
	// Pointer to the online session Interface
	IOnlineSessionPtr OnlineSessionInterface;
    
// MenuCharacter.cpp

// Session Section
IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get();
if (OnlineSubsystem) {
	OnlineSessionInterface = OnlineSubsystem->GetSessionInterface();
}

이러고 빌드를 하면 에러가 뜬다.

키워드는 C3646

IOnlineSessionPtr 타입의 변수로 선언한 OnlineSessionInterface가 unknown override specifier라는 에러이다.

보통 포인터 변수를 선언할 때 전방선언을 해준다. 하지만 IOnline session pointer 타입으로 선언한 OnlineSessionInterface 변수는 그렇게 할 수 없는데, 변수가 ESPMode의 thread safe으로 지정된 SharedPtr 변수이며 Thread Safe를 보장받는 스마트 포인터이기 때문이다.

 

두가지 해결방법이 있는데, 하나는 헤더에 OnlineSubsystem을 추가하는 것이고 다른 하나는 TSharedPtr로 스마트포인터를 감싸는것이다.

이번엔 후자의 방법을 간단히 사용하겠다.

public:
	// Pointer to the online session Interface
	TSharedPtr<class IOnlineSession, ESPMode::ThreadSafe> OnlineSessionInterface;

 

이렇게 해서 PIE로 테스트 해보면, 전부 Subsystem Null이 잡힌다.

Subsystem NULL은 NULL값이 아니라, 플랫폼이 NULL인것.

패키징해서 실행해보면 Steam 연결이 잘 되는걸 확인할 수 있다.


Delegates

  • 언리얼 엔진의 함수 reference를 가지고 있는 오브젝트
  • 함수를 바인딩해 시그널을 broadcasting시켜 각 함수가 실행되도록 하는 역할
  • 콜백 함수를 델리게이트에 바인딩 한다.
  • 게임에서 특정 이벤트가 일어나면 델리게이트는 fire되거나 broadcast를 실행하고 그 응답으로 콜백 함수가 실행된다.
  • 온라인 세션 인터페이스는 델리게이트들을 이용하는데, 세션을 생성하거나 참가하는걸 인터넷을 통해 정보를 수신받아야 할 수 있기 때문. 인터넷 속도에 따라 시간이 소요되는것도 당연

대충 이런 구조로 돌아간다


CreateSession

  • 헤더에 CreateSession함수, 델리게이트, 콜백함수를 선언해준다.
	UPROPERTY(BlueprintCallable)
	void CreateGameSession();

	void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);

private:
	FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate;
  • 그리고 cpp파일의 캐릭터 생성자에 델리게이트를 바인딩한다. 
    • 생성자에 델리게이트 바인딩? -> 객체 생성 시점에 해당 이벤트를 처리할 준비를 마치기 위해.
    • 기본 구조: FDelegateType::CreateUObject(UObjectInstance, &ClassName::FunctionName);
    • CreateUObject: 델리게이트에 현재 객체와 멤버함수를 바인딩, UObjectInstance: 델리게이트를 호출할 때 사용할 UObjectInstance, &ClassName::FunctionName: 호출할 함수의 포인터.
AMenuCharacter::AMenuCharacter():
	CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &AMenuCharacter::OnCreateSessionComplete))
  • 그리고 가장 먼저 할 일은, 세션이 이미 있는지 확인 후 없애는것.
  • 이후 세션 세팅을 설정해주고, CreateSession으로 세션 생성을 마친다.
void AMenuCharacter::CreateGameSession()
{
	// Called when pressing the 1 key
	if (!OnlineSessionInterface.IsValid()) {
		return;
	}
	auto ExistingSession = OnlineSessionInterface->GetNamedSession(NAME_GameSession);
	if (ExistingSession != nullptr) {
		OnlineSessionInterface->DestroySession(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;

	const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();

	OnlineSessionInterface->CreateSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *SessionSettings);
	
}

void AMenuCharacter::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{
	if (bWasSuccessful) {
		if (GEngine) {
			GEngine->AddOnScreenDebugMessage(
				-1,
				15.f,
				FColor::Blue,
				FString::Printf(TEXT("Created session: %s"), *SessionName.ToString())
			);
		}
	}
	else {
		if (GEngine) {
			GEngine->AddOnScreenDebugMessage(
				-1,
				15.f,
				FColor::Red,
				FString(TEXT("Faild to Create Session!"))
			);
		}
	}
}
  • 그다음 window 버전으로 패키징 후 실행시켜보면, 스팀 오버레이가 우하단에 출력되며 올바르게 연결된것을 확인할 수 있다.

'개발 일지' 카테고리의 다른 글

플러그인 등록  (0) 2024.11.30
Join Session  (0) 2024.11.29
Listen Server와 Dedicated Server  (0) 2024.11.26
계속되는 세션 오류  (0) 2024.11.22
세션 관리 이동  (0) 2024.11.20

댓글()

Listen Server와 Dedicated Server

개발 일지|2024. 11. 26. 16:48

Listen Server로 구현하기로 했다.

 

Dedicated Server로 구현하려고 했는데, 문제가 너무 많다.

일반 가정집에서 Dedicated Server를 사용하려면, 방화벽과 포트포워딩 문제뿐만 아니라

동적으로 변하는 IP를 사용하는 문제, 클라우드 서버 금액문제 등 너무 번거롭다.

 

현재 프로젝트는 테스트하기 용이한 Listen Server로 구현한다.

제대로된 서버 맛보기 -> IOCP 윈도우 서버를 직접 구현해 MMO 게임 개발

이걸 다음 프로젝트의 목표로 삼고, 이번 프로젝트를 끝내면 개발해보자.

'개발 일지' 카테고리의 다른 글

Join Session  (0) 2024.11.29
OnlineSubsystem Steam 연결 및 Create Session  (0) 2024.11.27
계속되는 세션 오류  (0) 2024.11.22
세션 관리 이동  (0) 2024.11.20
세션 생성 시도시 Fatal Error  (0) 2024.11.19

댓글()