Post

TrickyDroneDelivery: UI 구현

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++클래스를 상속받는 위젯 블루프린트 클래스를 만들어 텍스트 위치와 폰트등을 조절해주자. image

왼쪽 위에는 레벨/웨이브와 점수, 오른쪽 위에는 남은 시간와 목숨을 표시한다. 단순 텍스트만 띄우는 것으론 의미가 없을 것이다. 이제 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를 업데이트한다. 플레이해보면 진행에 따라 숫자가 잘 바뀌는 것을 볼 수 있다. image

메뉴, 이미지 띄우기

게임을 진행하기 전후로 상황에 알맞는 위젯을 띄우는 기능도 만들어보자. 게임 스테이지에 들어가기 전, 게임에서 패배했을 때, 승리했을 때까지 총 3개의 상황을 만들었다. 각 상황에 맞는 위젯 블루프린트를 만들어주고, 해당 블루프린트를 띄워줄 게임모드와 레벨을 하나씩 만든다. 각 위젯 블루프린트에서의 버튼은 상황에 맞게 정해진 레벨을 호출할 것이다. 지금까지 만든 게임의 실제 진행 스테이지인 GameStage에선 게임의 승패여부에 따라 ClearStage, OverStage를 호출하도록 만들었다.

게임 시작 화면, Start버튼을 가진다.
버튼들은 연결될 레벨들을 각각 설정해줬다.
각각의 위젯에 대응되는 게임모드들은 해당 위젯을 화면에 띄운다.
게임 시작위젯과 유사하게 만든 클리어 위젯
게임 패배시 나오는 위젯
This post is licensed under CC BY 4.0 by the author.