* 게임플레이 레벨에 게임을 잠시 중지하는 UI와 게임 결과를 띄우는 UI를 추가한다.

* 사용할 위젯에서 사용하는 버튼의 이름은 다음과 같다.
* btnResume : 현재 진행 중인 게음으로 돌아간다
* btnReturnToTitle : 타이틀 레벨로 돌아간다
* btnRetryGame : 게임에 재도전한다

* GamePause라는 이름의 액션 매핑 M키을 추가한다.

* 추가 후 플레이어 컨트롤러에서 해당 매핑을 처리하면 빙의한 폰에 관계없이 입력을 처리할 수 있으므로 게임시스템에 관련된 입력을 처리할 때는 플레이어 컨트롤러에서 구현하는 것이 적합하다.

//ABPlayerController.h
{
protected:
	virtual void SetupInputComponent() override;
private:
	void OnGamePause();
}    

//ABPlayerController.cpp
void AABPlayerController::SetupInputComponent()
{
	Super::SetupInputComponent();
	InputComponent->BindAction(TEXT("GamePause"), EInputEvent::IE_Pressed, this, &AABPlayerController::OnGamePause);

}

void AABPlayerController::OnGamePause() 
{

}

 

* 플레이어 컨트롤러의 코드를 완성하면 UI가 공용으로 사용할 기본 클래스를 ABGameplayWidget이라는 이름으로 생성한다. 이전과 동일 하게 UserWidget을 부모로 하는 클래스를 생성한다.

* UI_MENU 애셋의 그래프 탭으로 가서 부모 클래스를 ABGameplayWidget로 변경한다

 

* 이제 UI 위젯을 초기화하는 시점에서 발생하는 NativeConstruct 함수에서 이름으로 버튼을 찾고, 해당 이름의 버튼이 존재하면 바인딩하도록 로직을 구현한다.

#include "ArenaBattle.h"
//ABGameplayWidget.h
#include "Blueprint/UserWidget.h"
#include "ABGameplayWidget.generated.h"

UCLASS()
class ARENABATTLE_API UABGameplayWidget : public UUserWidget
{
	GENERATED_BODY()
	
protected:
	virtual void NativeConstruct() override;

	UFUNCTION()
		void OnResumeClicked();

	UFUNCTION()
		void OnReturnToTitleClicked();

	UFUNCTION()
		void OnRetryGameClicked();

private:
	UPROPERTY()
		class UButton* ResumeButton;

	UPROPERTY()
		class UButton* ReturnToTitleButton;

	UPROPERTY()
		class UButton* RetryGameButton;
};


//ABGameplayWidget.cpp
#include "ABGameplayWidget.h"
#include "Components/Button.h"

void UABGameplayWidget::NativeConstruct()
{
	Super::NativeConstruct();
	
	ResumeButton = Cast<UButton>(GetWidgetFromName(TEXT("btnResume")));
	if (nullptr != ResumeButton)
	{
		ResumeButton->OnClicked.AddDynamic(this, &UABGameplayWidget::OnResumeClicked);
	}

	ReturnToTitleButton = Cast<UButton>(GetWidgetFromName(TEXT("btnReturnToTitle")));
	if (nullptr != ReturnToTitleButton)
	{
		ReturnToTitleButton->OnClicked.AddDynamic(this, &UABGameplayWidget::OnReturnToTitleClicked);
	}

	RetryGameButton = Cast<UButton>(GetWidgetFromName(TEXT("btnRetryGame")));
	if (nullptr != RetryGameButton)
	{
		RetryGameButton->OnClicked.AddDynamic(this, &UABGameplayWidget::OnRetryGameClicked);
	}
}

void UABGameplayWidget::OnResumeClicked()
{

}

void UABGameplayWidget::OnReturnToTitleClicked()
{

}

void UABGameplayWidget::OnRetryGameClicked()
{

}



 

* 위젯의 기본 뼈대가 완성됐다면 M 버튼을 눌렀을 떄 메뉴UI가 나타나도록 기능을 추가한다. 메뉴 UI가 나오면 고려할 사항들은 다음과 같다.

1. 게임플레이의중지
2. 버튼을 클릭할 수 있도록 마우스 커서를 보여주기
3. 입력이 게임에 전달되지 않고  UI에만 전달되도록 제어

* 플레이어 컨트롤러의 SetPause 함수를 사용하면 플레이를 일시 중지할 수 있다.
* 마우스 커서는 플레이어 컨트롤러의 bShowMouseCursor 속성을 true로 설정하면 보이며,
* UI에만 입력을 전달하도록  SetInputMode 함수에 FInputMideUIOnly 클래스를 인자로 넣어준다.
* 관련기능은 앞으로도 재사용할 예정이므로,  이 기능을 묶은 함수를 별도로 제작한다

//ABPlayerController.h
{
public:
	void ChangeInputMode(bool bGameMode = true);

protected:
	UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = UI)
	TSubclassOf<class UABGameplayWidget> MenuWidgetClass;

private:
	UPROPERTY()
	class UABGameplayWidget* MenuWidget;

	FInputModeGameOnly GameInputMode;
	FInputModeUIOnly   UIInputMode;
};


//ABPlayerController.cpp
#include "ABGameplayWidget.h"

AABPlayerController::AABPlayerController()
{
.....
		static ConstructorHelpers::FClassFinder<UABGameplayWidget> UI_MENU_C(TEXT("/Game/Book/UI/UI_Menu.UI_Menu_C"));
	if (UI_MENU_C.Succeeded())
	{
		MenuWidgetClass = UI_MENU_C.Class;
	}
}


void AABPlayerController::BeginPlay()
{
	Super::BeginPlay();

	ChangeInputMode(true);

	HUDWidget = CreateWidget<UABHUDWidget>(this, HUDWidgetClass);
	ABCHECK(nullptr != HUDWidget);
	HUDWidget->AddToViewport(1);

.....
}

void AABPlayerController::ChangeInputMode(bool bGameMode)
{
	if (bGameMode)
	{
		SetInputMode(GameInputMode);
		bShowMouseCursor = false;
	}
	else
	{
		SetInputMode(UIInputMode);
		bShowMouseCursor = true;
	}
}

void AABPlayerController::OnGamePause()
{
	MenuWidget = CreateWidget<UABGameplayWidget>(this, MenuWidgetClass);
	ABCHECK(nullptr != MenuWidget);
	MenuWidget->AddToViewport(3);

	SetPause(true);
	ChangeInputMode(false);
}


 

M 키를 누른 화면


 

* 이번에는 메뉴의 버튼을 눌렀을 때의 행동을 구현한다.

* UI 시스템은 RemoveFromParent함수를 사용해 현재 뷰포트에 띄워진 자신을 제거할 수 있다.
* UI는 GetOwningPlayer 함수를 사용해 현재 자신을 생성하고 관리하는 플레이어 컨트롤러의 정보를 가져올 수 있다.

* 이를 사용해 입력과 게임의 진행을 원래대로 되돌려 놓는다.

//ABGameplayWidget.cpp
#include "ABPlayerController.h"

void UABGameplayWidget::OnResumeClicked()
{
	auto ABPlayerController = Cast<AABPlayerController>(GetOwningPlayer());
	ABCHECK(nullptr != ABPlayerController);

	RemoveFromParent();
	ABPlayerController->ChangeInputMode(true);
	ABPlayerController->SetPause(false);
}

void UABGameplayWidget::OnReturnToTitleClicked()
{
	UGameplayStatics::OpenLevel(GetWorld(), TEXT("Title"));
}

* 게임으로 돌아가기 버튼을 눌러 원래 게임플레이 상황으로 돌아가는지 확인한다.

 



* 이어서 결과 UI를 표시하는 기능을 구현해본다

* 결과 화면 UI를 표시하기 위해 앞서 제작한 ABGameplayerWidget을 상속받은 ABGameplayResultWidget이라는 새로운 클래스를 생성한다.

* 해당 UI에는 게임을 다시 시작하는 btnRetryGame 버튼과 결과를 보여주는 txtResult 텍스트블록, 그리고 획득한 점수를 보여주는 txtTotalScore라는 세 개의 위젯 컨트롤이 있으며, 앞서 btnRetryGame 버튼의 행동은 부모 클래스인 ABGameplayWidget에서 처리하도록 설계했다.

 

//ABGameplayResultWidget.h
#include "ArenaBattle.h"
#include "ABGameplayWidget.h"
#include "ABGameplayResultWidget.generated.h"

UCLASS()
class ARENABATTLE_API UABGameplayResultWidget : public UABGameplayWidget
{
	GENERATED_BODY()

protected:
	virtual void NativeConstruct() override;
};

//ABGameplayResultWidget.cpp
#include "ABGameplayResultWidget.h"
#include "Components/TextBlock.h"

void UABGameplayResultWidget::NativeConstruct()
{
	Super::NativeConstruct();

	auto Result = Cast<UTextBlock>(GetWidgetFromName(TEXT("txtResult")));
	ABCHECK(nullptr != Result);

	auto TotalScore = Cast<UTextBlock>(GetWidgetFromName(TEXT("txtTotalScore")));
	ABCHECK(nullptr != TotalScore);
}

* 이를 UI_Result의 부모를 ABGameplayResultWidget 클래스로 설정한다

 


 

* 결과 UI는 게임이 종료되면 마지막으로 한 번만 띄워진다.
* UI의 인스턴스는 시작할 떄 미리 만들어뒀다가 게임이 종료될 떄 만들어진 인스턴스를 뷰포트에 띄우도록 로직을 구성했다

//ABPlayerController.h
{
public:
    void ShowResultUI();
pritected:
	UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = UI)
	TSubclassOf<class UABGameplayResultWidget> ResultWidgetClass;
private:
	UPROPERTY()
	class UABGameplayResultWidget* ResultWidget;
}    

//ABPlayerController.cpp
#include "ABGameplayResultWidget.h"


AABPlayerController::AABPlayerController()
{
...
	static ConstructorHelpers::FClassFinder<UABGameplayResultWidget> UI_RESULT_C(TEXT("/Game/Book/UI/UI_Result.UI_Result_C"));
	if (UI_RESULT_C.Succeeded())
	{
		ResultWidgetClass = UI_RESULT_C.Class;
	}
}

void AABPlayerController::BeginPlay()
{
	Super::BeginPlay();

	ChangeInputMode(true);

	HUDWidget = CreateWidget<UABHUDWidget>(this, HUDWidgetClass);
	ABCHECK(nullptr != HUDWidget);
	HUDWidget->AddToViewport(1);

	ResultWidget = CreateWidget<UABGameplayResultWidget>(this, ResultWidgetClass);
	ABCHECK(nullptr != ResultWidget);
....
}

void AABPlayerController::ShowResultUI()
{
	ResultWidget->AddToViewport();
	ChangeInputMode(false);
}

* 이번에는 ㄱ임 플레이가 종료되는 시점을 지정해야 한다.    

* 게임플레이가 종료되는 시점은 두 가지로, 플레이어가 죽을 떄와 목표를 달성했을 떄다.
* 전자는 게임의 미션을 클리어하지 못한 상황이고, 후자는 게임의 미션이 클리어 된 상황을 의미한다.

* 게임의 미션은 테스트하기 편하도록 우선 두 개의 섹션을 COMPLETE 스테이트로 만들면 달성하는 것으로 정해본다

* 게임의 미션을 달성했는지 여부는 게임의 정보이므로 GameState에 bGameCleared라는 속성을 추가한다. 그리고 * GameMode에서 게임의 미션이 달성되면 현재 게임에서 동작하는 모든 폰을 멈추고 bGameCleared 속성을 true로 설정하는 기능을 구현한다.
* 그리고 미션을 달성하거나 플레이어가 죽으면 플레이어 컨트롤러의 ShowResultUI 함수를 호출해 결과 UI를 띄운다.

//ABGameState.h
{
public:
...
	void SetGameCleared();
	bool IsGameCleared() const;

private:
...
	UPROPERTY(Transient)
		bool bGameCleared;
}
//ABGameState.cpp

AABGameState::AABGameState()
{
	TotalGameScore = 0;
	bGameCleared = false;
}

void AABGameState::SetGameCleared()
{
	bGameCleared = true;
}

bool AABGameState::IsGameCleared() const
{
	return bGameCleared;
}

 

//ABGameMode.h
{
private:
	UPROPERTY()
	int32 ScoreToClear;
}


//ABGameMode.cpp
AABGameMode::AABGameMode()
{
...
	ScoreToClear = 2;
}

void AABGameMode::AddScore(class AABPlayerController * ScoredPlayer)
{
	for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
	{
		const auto ABPlayerController = Cast<AABPlayerController>(It->Get());
		if ((nullptr != ABPlayerController) && (ScoredPlayer == ABPlayerController))
		{
			ABPlayerController->AddGameScore();
			break;
		}
	}

	ABGameState->AddGameScore();


	if (GetScore() >= ScoreToClear);
	{
		ABGameState->SetGameCleared();

		for (FConstPawnIterator It = GetWorld()->GetPawnIterator(); It; ++It)
		{
			(*It)->TurnOff();
		}

		for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
		{
			const auto ABPlayerController = Cast<AABPlayerController>(It->Get());
			if (nullptr != ABPlayerController)
			{
				ABPlayerController->ShowResultUI();
			}
		}
	}
}
//ABCharacter.cpp
void AABCharacter::SetCharacterState(ECharacterState NewState)
{
	ABCHECK(CurrentState != NewState);
	CurrentState = NewState;

	switch (NewState)
	{
.........
	case ECharacterState::DEAD:
	{
		SetActorEnableCollision(false);
		GetMesh()->SetHiddenInGame(false);
		HPBarWidget->SetHiddenInGame(true);
		ABAnim->SetDeadAnim();
		bCanBeDamaged = false;

		if (bIsPlayer)
		{
			DisableInput(ABPlayerController);
		}
		else
		{
			ABAIController->StopAI();
		}

		GetWorld()->GetTimerManager().SetTimer(DeadTimerHandle, FTimerDelegate::CreateLambda([this]() -> void {
			if (bIsPlayer)
			{
				//ABPlayerController->RestartLevel(); //제거
				ABPlayerController->ShowResultUI();   //추가됨
			}
			else
			{
				Destroy();
			}
		}),DeadTimer,false);

		break;
	}
	}
}

 

* UI 위젯의 NativeCOnstruct 함수는 AddToViewport 함수가 외부에서 호출될 떄 UI 위젯이 초기화 되면서 호출된다는 특징을 가진다.

* 그래서 플레이어 컨트롤러의 SㅗowResultUI 함수에서 AddToViewport 함수를 호출하기 전에 미리 UI 위젯이 게임스테이트의 정보를 읽어들일 수 있도록 바인딩을 설정하고,
* ABGameplayWidget 클래스에서 아직 구현하지 못한 btnRetryGame 버튼의 기능을 구현해 마무리한다.

//ABGameplayResultWudget.h
#include "ArenaBattle.h"
#include "ABGameplayWidget.h"
#include "ABGameplayResultWidget.generated.h"


UCLASS()
class ARENABATTLE_API UABGameplayResultWidget : public UABGameplayWidget
{
	GENERATED_BODY()

public:
	void BindGameState(class AABGameState* GameState);

protected:
	virtual void NativeConstruct() override;
	
private:
	TWeakObjectPtr<class AABGameState> CurrentGameState;
};


//ABGameplayResultWudget.cpp
#include "ABGameplayResultWidget.h"
#include "Components/TextBlock.h"
#include "ABGameState.h"


void UABGameplayResultWidget::NativeConstruct()
{
	Super::NativeConstruct();

	ABCHECK(CurrentGameState.IsValid());

	auto Result = Cast<UTextBlock>(GetWidgetFromName(TEXT("txtResult")));
	ABCHECK(nullptr != Result);
	Result->SetText(FText::FromString(CurrentGameState->IsGameCleared() ?
		TEXT("Mission Complte") : TEXT("Mission Failed")));


	auto TotalScore = Cast<UTextBlock>(GetWidgetFromName(TEXT("txtTotalScore")));
	ABCHECK(nullptr != TotalScore);
	TotalScore->SetText(FText::FromString(FString::FromInt(CurrentGameState->GetTotalGameScore)));
}

void UABGameplayResultWidget::BindGameState(class AABGameState* GameState)
{
	ABCHECK(nullptr != GameState);
	CurrentGameState = GameState;
}

 

//ABGameplayWidget.cpp
void UABGameplayWidget::OnRetryGameClicked()
{
	auto ABPlayerController = Cast<AABPlayerController>(GetOwningPlayer());
	ABCHECK(nullptr != ABPlayerController);
	ABPlayerController->RestartLevel();
}
//ABPlayerController.cpp
#include "ABGameState.h"

void AABPlayerController::ShowResultUI()
{
	auto ABGameState = Cast<AABGameState>(UGameplayStatics::GetGameState(this));
	ABCHECK(nullptr != ABGameState);
	ResultWidget->BindGameState(ABGameState);

	ResultWidget->AddToViewport();
	ChangeInputMode(false);
}

* 이제 플레이어가 죽거나 클리어 조건을 만족하면 결과 UI가 뜬다

 

+ Recent posts