TrickyDroneDelivery: UI 구현
이전에는 GameMode/GameState를 이용해서 간단한 게임 룰을 만들어봤다. 하지만 단순 로그로 게임 진행상황을 표시하는 정도로는 게임이 아쉬울 것이다. 이번엔 UI를 추가하여 더 게임스럽게 만들어보자.
게임 전환 변경점
제출 과제로서의 요건을 맞추기 위해 게임 진행에 있어 몇몇 변경점이 생겼다.
- 웨이브가 필요하다는 조건이 있었기에, 레벨마다 웨이브가 3개씩 있도록 바꾸었다.
- 그에 따라, 레벨 시작시 모든 상자가 스폰되던 것에서 3개의 웨이브에 따라 순차적으로 소환되는 것으로 바꾸었다.
- 게임 진행에 쓰이는
HUD 생성
UMG(Unreal Motion Graphic)을 이용하면 손쉽게 UI를 구성할 수 있다. HUD(Head-Up Display)로 남은 목숨(부서져도 되는 상자), 남은 상자, 남은 시간을 표시하도록 해보자. 먼저 HUD클래스의 기능을 선언할 C++ 클래스를 만들어 보겠다. 어떤 UI 요소들을 가지고, 어떤 함수를 통해 업데이트할지 기초적인 함수를 정의해줬다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//TDDHUD.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "TDDHUD.generated.h"
/**
*
*/
UCLASS()
class TRICKYDRONEDELIVERY_API UTDDHUD : public UUserWidget
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, AllowPrivateAccess = "true"))
class UTextBlock* StageInfoText;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, AllowPrivateAccess = "true"))
class UTextBlock* TimeText;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, AllowPrivateAccess = "true"))
class UTextBlock* LifeText;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, AllowPrivateAccess = "true"))
class UTextBlock* DeliveryText;
UFUNCTION()
void UpdateInfo(int32 Stage, int32 Wave, float Time, int32 Life, int32 Remains);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//TDDHUD.cpp
#include "TDDHUD.h"
#include "Components/TextBlock.h"
void UTDDHUD::UpdateInfo(int32 Stage, int32 Wave, float Time, int32 Life, int32 Remains)
{
FString StageInfoString = FString::Printf(TEXT("Stage %d - Wave %d"), Stage, Wave);;
StageInfoText->SetText(FText::FromString(StageInfoString));
UE_LOG(LogTemp, Warning, TEXT("time: %f"), Time);
FString TimeString = FString::Printf(TEXT("Time: %d"), FMath::FloorToInt(Time));;
TimeText->SetText(FText::FromString(TimeString));
FString LifeString = FString::Printf(TEXT("Life: %d"), Life);;
LifeText->SetText(FText::FromString(LifeString));
FString DeliveryString = FString::Printf(TEXT("Remaining Deliveries: %d"), Remains);;
DeliveryText->SetText(FText::FromString(DeliveryString));
}
각 정보에 해당하는 텍스트 블록들을 4개 가지고, UpdateInfo()함수를 이용해 이를 바꿀 수 있도록 만들어 두었다. 이제 이 C++클래스를 상속받는 위젯 블루프린트 클래스를 만들어 텍스트 위치와 폰트등을 조절해주자. 
왼쪽 위에는 레벨/웨이브와 점수, 오른쪽 위에는 남은 시간와 목숨을 표시한다. 단순 텍스트만 띄우는 것으론 의미가 없을 것이다. 이제 HUD를 실제로 띄우고 이전에 게임스테이트에 추가해놓은 델리게이트를 이용해 일정시간마다 UI를 업데이트 하도록 해보자. 매 틱마다 업데이트한다면 지금 수준의 간단한 게임은 상관 없겠지만 성능에 꽤나 부하를 줄 수 있을 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//DronePlayerController.h
//...
UCLASS()
class TRICKYDRONEDELIVERY_API ADronePlayerController : public APlayerController
{
GENERATED_BODY()
public:
//...
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = UI)
TSubclassOf<UUserWidget> HUD;
virtual void BeginPlay() override;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//DronePlayerController.cpp
#include "DronePlayerController.h"
#include "TDDHUD.h"
//...
void ADronePlayerController::BeginPlay()
{
Super::BeginPlay();
if (ULocalPlayer* LocalPlayer = GetLocalPlayer()) {
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>()) {
if (IMC_Drone != nullptr) {
Subsystem->AddMappingContext(IMC_Drone, 0);
}
}
}
if (IsValid(HUDClass)) {
UTDDHUD* HUDWidget = CreateWidget<UTDDHUD>(this,HUDClass);
if (IsValid(HUDWidget)) {
ATDDGameState* GS = GetWorld()->GetGameState<ATDDGameState>();
if (IsValid(GS)) {
GS->OnTimeChange.AddDynamic(HUDWidget, &UTDDHUD::UpdateInfo);
}
HUDWidget->AddToViewport();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//TDDGameState.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameState.h"
#include "TDDGameState.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FiveParams(
FOnTimeChange,
int32, Stage,
int32, Wave,
float, Time,
int32, Life,
int32, Remains);
class ATDDGameMode;
class UTDDGameInstance;
UCLASS()
class TRICKYDRONEDELIVERY_API ATDDGameState : public AGameState
{
GENERATED_BODY()
public:
ATDDGameState();
virtual void BeginPlay() override;
UFUNCTION(Exec)
void DeliverPackage();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = TTDGameState)
int32 MaxBrokenBoxes;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = TTDGameState)
int32 TimePerPackage;
int32 RemainingLife;
int32 RemainingDeliveries;
FTimerHandle DeliveryTimerHandle;
FTimerHandle UpdateTimerHandle;
FOnTimeChange OnTimeChange;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = TDDGameInstance)
int32 CurrentWave;
void EndTimer();
void BreakBox();
protected:
ATDDGameMode* GM;
UTDDGameInstance* GI;
void UpdateRemainingDeliveries();
void UpdateHUD();
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
//TDDGameState.cpp
#include "TDDGameState.h"
#include "TDDGameMode.h"
#include "TDDGameInstance.h"
#include "DronePlayerController.h"
ATDDGameState::ATDDGameState()
{
}
void ATDDGameState::BeginPlay()
{
Super::BeginPlay();
GM = GetWorld()->GetAuthGameMode<ATDDGameMode>();
GI = GetGameInstance<UTDDGameInstance>();
RemainingLife = MaxBrokenBoxes;
CurrentWave = 1;
UpdateRemainingDeliveries();
}
void ATDDGameState::DeliverPackage() {
RemainingDeliveries--;
UE_LOG(LogTemp, Warning, TEXT("Package Dissappeared! Remaining %d Deliveries!"), RemainingDeliveries);
if (RemainingDeliveries <= 0) {
if (CurrentWave < 3) {
UE_LOG(LogTemp, Warning, TEXT("wave %d clear!"), CurrentWave);
CurrentWave++;
UpdateRemainingDeliveries();
}
else {
GM->StageClear();
}
}
else {
UpdateHUD();
}
}
void ATDDGameState::EndTimer()
{
UE_LOG(LogTemp, Warning, TEXT("TimerEnded!"));
GM->GameOver();
}
void ATDDGameState::BreakBox()
{
RemainingLife--;
UE_LOG(LogTemp, Warning, TEXT("BoxBroken! life: %d"), RemainingLife);
if (RemainingLife <= 0) {
GM->GameOver();
}
DeliverPackage();
UpdateHUD();
}
void ATDDGameState::UpdateRemainingDeliveries()
{
UE_LOG(LogTemp, Warning, TEXT("Reset for new wave"));
APlayerController* PC = GetWorld()->GetFirstPlayerController();
if (IsValid(PC)) {
GM->RespawnPlayer(PC);
}
if (IsValid(GM) && IsValid(GI)) {
RemainingDeliveries = GI->CurrentLevel + CurrentWave - 1;
UE_LOG(LogTemp, Warning, TEXT("RemainingDeliveries renewed: %d"), RemainingDeliveries);
GM->SpawnPackages(RemainingDeliveries);
}
else {
UE_LOG(LogTemp, Warning, TEXT("unalbe to renew RemainingDeliveries"));
RemainingDeliveries = 0;
}
//Set Timer
if (GetWorld()->GetTimerManager().IsTimerActive(DeliveryTimerHandle)) {
GetWorld()->GetTimerManager().ClearTimer(DeliveryTimerHandle);
}
GetWorld()->GetTimerManager().SetTimer(DeliveryTimerHandle, this, &ATDDGameState::EndTimer, (TimePerPackage) * RemainingDeliveries, false);
if (GetWorld()->GetTimerManager().IsTimerActive(UpdateTimerHandle)) {
GetWorld()->GetTimerManager().ClearTimer(UpdateTimerHandle);
}
GetWorld()->GetTimerManager().SetTimer(UpdateTimerHandle, this, &ATDDGameState::UpdateHUD, 1.0, true);
UpdateHUD();
}
void ATDDGameState::UpdateHUD()
{
OnTimeChange.Broadcast(GI->CurrentLevel, CurrentWave, GetWorld()->GetTimerManager().GetTimerRemaining(DeliveryTimerHandle), RemainingLife, RemainingDeliveries);
}
PlayerController에 UserWidget클래스를 저장할 수 있는 변수를 UPROPERTY()로 선언하고 에디터에서 등록해줬다. 그리고 게임모드에서는 OnTimeChange 델리게이트에 UpdateInfo() 함수를 등록, 0.1초마다 Broadcast되어 시간과 점수등 정보가 업데이트되도록 만들었다. 상자가 부서지는등의 상황에는 타이머와 별개로 HUD를 업데이트한다. 플레이해보면 진행에 따라 숫자가 잘 바뀌는 것을 볼 수 있다. 
메뉴, 이미지 띄우기
게임을 진행하기 전후로 상황에 알맞는 위젯을 띄우는 기능도 만들어보자. 게임 스테이지에 들어가기 전, 게임에서 패배했을 때, 승리했을 때까지 총 3개의 상황을 만들었다. 각 상황에 맞는 위젯 블루프린트를 만들어주고, 해당 블루프린트를 띄워줄 게임모드와 레벨을 하나씩 만든다. 각 위젯 블루프린트에서의 버튼은 상황에 맞게 정해진 레벨을 호출할 것이다. 지금까지 만든 게임의 실제 진행 스테이지인 GameStage에선 게임의 승패여부에 따라 ClearStage, OverStage를 호출하도록 만들었다.




