Puzzle Platform(UI System)

정재훈·2023년 5월 29일
0

Unreal Multiplay

목록 보기
2/7
  • PuzzlePlatformGameInstance.h
public:
	UFUNCTION(BlueprintCallable)
		void LoadMenu();

Blueprint callable 함수를 만듭니다.

  • PuzzlePlatformGameInstance.cpp
UPuzzlePlatformGameInstance::UPuzzlePlatformGameInstance(const FObjectInitializer& ObjectInitializer)
{
	UE_LOG(LogTemp, Warning, TEXT("GameInstance Constructor"));
	ConstructorHelpers::FClassFinder<UUserWidget> MenuBPClass(TEXT("/Game/ThirdPerson/Blueprints/UI/WBP_MainMenu"));
	if (!ensure(MenuBPClass.Class != nullptr)) return;

	MenuClass = MenuBPClass.Class;
}

void UPuzzlePlatformGameInstance::LoadMenu()
{
	if (!ensure(MenuClass != nullptr)) return;

	UUserWidget* Menu = CreateWidget(GetWorld(), MenuClass);
	if (!ensure(Menu != nullptr)) return;
	UE_LOG(LogTemp, Warning, TEXT("Widget Init"));
	Menu->AddToViewport();

	APlayerController* PlayerController = GetFirstLocalPlayerController();
	if (!ensure(PlayerController != nullptr)) return;

	//input mode struct
	FInputModeUIOnly InputModeData;
	InputModeData.SetWidgetToFocus(Menu->TakeWidget());
	InputModeData.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);

	PlayerController->SetInputMode(InputModeData);

	PlayerController->bShowMouseCursor = true;
}

game instance의 생성자에서 FClassFinder를 통해 만들어둔 블루프린트 ui위젯을 받아옵니다.
LoadMenu 함수가 호출되면 ui를 화면에 띄우고 플래이어 컨트롤러의 input mode를 set해줘 마우스가 화면에 나타나고 화면을 벗어날수 있으며 메뉴에 버튼을 클릭할수 있도록 합니다.

다음으로는 menu화면을 띄워줄 새로운 empty 레벨을 만들어 level blueprint에 beginplay하면 LoadMenu를 호출하도록 했습니다.

blueprint c++ 연결

위에 사용한 main menu 위젯은 c++ 클래스 없이 블루프린트로 구현했었다. 하지만 이렇게 되면 위젯에서 구현한 함수를 다른 c++ 클래스에서 사용하기에 불편함이 있다.

UserWidget c++클래스를 새로 생성하고 이미 구현한 블루프린트의 parent class를 이 c++ 클래스로 reparent 해주면 c++에서 구현이 가능하다.

  • MainMenu.h
private:
	UPROPERTY(meta = (BindWidget))
		class UButton* Host;

	UPROPERTY(meta = (BindWidget))
		class UButton* Join;
  • MainMenu.cpp
bool UMainMenu::Initialize()
{
	bool Success = Super::Initialize();
	if (!Success) return false;

	if (!ensure(Host != nullptr)) return false;
	Host->OnClicked.AddDynamic(this, &UMainMenu::HostServer);

	return true;
}

void UMainMenu::HostServer()
{
	if (MenuInterface != nullptr)
	{
		MenuInterface->Host();
	}
}

다음으로는 기존 블루프린트 위젯에 있던 버튼들을 바인딩해주고
onclick에 함수를 호출해 기능을 구현해줄수 있다.

종속성 반전

Menu의 Host 버튼을 눌러 game instance에 Host함수를 호출하는 방식으로 구현하게 되면 menu가 gameinstance에 의존하게 되어 game instance가 변경되면 Menu의 함수도 고쳐야 되는 문제가 있다. menu를 독립된 플러그인으로 구현하기 위해서 종속성을 반대로 구현할 필요가 있다.

Compile time에는 gameinstance가 menu system에 의존하되 runtime에는 상호의존적으로 구현하기위해서 인터페이스를 사용한다. menu는 interface를 inclue하고 사용하며 game instance는 interface를 include 하고 구현한다. 그러면 menu에서 game instance를 interface를 통해 call 할수 있다.

  • MenuInterface.h
#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "MenuInterface.generated.h"

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UMenuInterface : public UInterface
{
	GENERATED_BODY()
};

/**
 * 
 */
class MULTIPLAYER_API IMenuInterface
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
	//have empty implementation(implement)
	virtual void Host() =0;
};

interface의 헤더를 보면 두개의 class를 가지고 있다. 하나는 uinterface를 inherit하며 하나는 아무것도 inherit하지 않는다.
첫번째 class는 건들지 않고 두번째 클래스에 구현할것이다. 두가지 클래스로 나뉘어진 이유는
1. interface는 uobject를 상속하지 않으면 unreal이 인식하지 못한다는 문제가 있어 첫번째 class가 있다.
2. 여러개의 uobject를 상속하면 안된다는 다중상속 문제를 해결하기 위해서두번째 class가 있다.

interface에 순수 가상함수 host를 구현한다.

  • PuzzlePlatformGameInstance.h
UCLASS()
class MULTIPLAYER_API UPuzzlePlatformGameInstance : public UGameInstance, public IMenuInterface
{
	GENERATED_BODY()
.
.
.

private:
	TSubclassOf<class UUserWidget> MenuClass;
};

game instance는 UGameInstance, IMenuInterface 를 다중 상속 받으며

  • PuzzlePlatformGameInstance.cpp
void UPuzzlePlatformGameInstance::LoadMenu()
{
	if (!ensure(MenuClass != nullptr)) return;

	//UUserWidget* Menu = CreateWidget(GetWorld(), MenuClass);
	UMainMenu* Menu = CreateWidget<UMainMenu>(this, MenuClass);
	.
    .
    .
	Menu->SetMenuInterface(this);
}

menu를 불러와 interface를 set 해준다.

  • MainMenu.h
#include "MenuInterface.h"

IMenuInterface* MenuInterface;

menu의 헤더에 포인터 변수를 만들어주고

void UMainMenu::SetMenuInterface(IMenuInterface* GetMenuInterface)
{
	this->MenuInterface = GetMenuInterface;
}

void UMainMenu::HostServer()
{
	if (MenuInterface != nullptr)
	{
		MenuInterface->Host();
	}
}

gmae instance로부터 interface를 받아와 interface에 순수가상함수 host를 호출하면 받아온 interface를 상속한 game instance에 구현된 host를 호출할수 있다.

기능구현

블루프린트에서 UI를 구현한 부분은 따로 다루지 않겠다.

  • Main Menu

Host 버튼을 누르면 Hosting을해 게임서버를 실행하며 게임화면으로 넘어가고
Join버튼을 누르면 JoinMenu화면으로 넘어간다.

void UMainMenu::OpenJoinMenu()
{
	if (!ensure(MenuSwitcher != nullptr)) return;
	if (!ensure(JoinMenu != nullptr)) return;
	MenuSwitcher->SetActiveWidget(JoinMenu);
}

widgetswitcher을 이용해 2페이지의 메뉴를 하나의 위젯 블루프린트에서 구현하였고 SetActiveWidget을 통해 페이지를 옮겨다닐수 있다.

  • JoinMenu

Cancel을 누르면 main menu로 돌아가며 IP Adress를 적고 Join버튼을 누르면 그 IP에서 Hosting한 게임에 참여할수 있다.

  • MainMenu.cpp
void UMainMenu::JoinServer()
{
	if (MenuInterface != nullptr)
	{
		if (!ensure(IPAddressField != nullptr)) return;
		const FString& Address = IPAddressField->GetText().ToString();
		MenuInterface->Join(Address);
	}
}

Hosting과 마찬가지로 인터페이스를 통해 종속성문제를 해결하고 Address를 FString으로 받아 인자로 넘겨준다.

  • PuzzlePlaformGameInstance.cpp
void UPuzzlePlatformGameInstance::Join(const FString& Address)
{
	if (Menu != nullptr)
	{
		Menu->Teardown();
	}
    .
    .
    .
  • MenuWidget.cpp
void UMenuWidget::Teardown()
{
	this->RemoveFromViewport();

	UWorld* World = GetWorld();
	if (!ensure(World != nullptr)) return;

	APlayerController* PlayerController = World->GetFirstPlayerController();
	if (!ensure(PlayerController != nullptr)) return;

	FInputModeGameOnly InputModeData;
	PlayerController->SetInputMode(InputModeData);

	PlayerController->bShowMouseCursor = false;
}

Join함수는 Menu UI가 닫히고 게임화면에서 캐릭터를 컨트롤해야하기 때문에 위에 구현한 함수에서 UI를 띄울때 Setup함수에서 마우스등 InputMode를 셋팅한것을 다시 반대로 해제하는 Teardown함수를 추가해주었다. Host함수또한 마찬가지다.

  • MainMenu.h
class MULTIPLAYER_API UMainMenu : public UMenuWidget

또한 Setup, Teardown, SetMenuInterface등 앞으로 메뉴화면이 추가되면 똑같이 공유하게될 함수를 따로 MenuWidget class를 만들어 빼주고 menu class의 parent class로 설정해 주었다.

  • In Game Menu

    게임 화면에서의 메뉴로 m key를 누르면 화면에 나타나도록 구현했다.

InGameMenu.cpp

void UInGameMenu::CancelPressed()
{
	Teardown();
}


void UInGameMenu::QuitPressed()
{
	if (MenuInterface != nullptr) {
		Teardown();
		MenuInterface->LoadMainMenu();
	}
}

cancel 버튼을 누르면 다시 게임화면으로 돌아가고 Quit 버튼을 누르면 다시 Main menu로 돌아가게 된다.
위에 구현한 MenuWidget class의 child class이기때문에 Setup, Teardown, SetMenuInterface 함수를 따로 구현하지 않아도 된다.

profile
게임 개발 공부중!

0개의 댓글