* 게임플레이 레벨에 게임을 잠시 중지하는 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);
}
* 이번에는 메뉴의 버튼을 눌렀을 때의 행동을 구현한다.
* 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가 뜬다
'Unreal > Game 1 (C++)' 카테고리의 다른 글
23. 게임 실행 (0) | 2019.05.16 |
---|---|
22. 게임의 완성 3 (타이틀 화면의 제작 ) (0) | 2019.05.12 |
22. 게임의 완성 2 (전투 시스템의 설계) (0) | 2019.05.10 |
22. 게임의 완성 1 (게임 데이터의 저장과 로딩) (0) | 2019.05.10 |
21. 게임플레이 제작 3 (게임 데이터의 관리) (0) | 2019.05.09 |