기존에 만들었던 아이템 수집 게임의 기본 골격 위에 새로운 게임 플레이와 UI/UX를 덧씌우는 리팩토링 및 확장 작업을 진행했다. 이번 과제의 핵심은 "멀티 웨이브 시스템"을 도입하여 게임의 깊이를 더하고, 기존의 투박했던 UI를 "전면 리뉴얼"하여 시각적 완성도를 높이는 것이었다. 기존 코드를 최대한 재사용하면서도 확장성을 고려하여 재설계하는 과정에서 많은 것을 배울 수 있었다. 🚀
기존에는 '레벨 시작 -> 코인 수집 -> 레벨 종료'의 단순한 흐름이었다. 하지만 멀티 웨이브 시스템을 도입하면서 GameState가 관리해야 할 상태가 훨씬 복잡해졌다.
EWaveState (enum class): 게임의 현재 상태를 명확히 구분하기 위해 열거형 클래스를 도입했다. 예를 들어 EWaveState::WaitingToStart (웨이브 시작 대기), EWaveState::WaveInProgress (웨이브 진행 중), EWaveState::WaveComplete (웨이브 성공) 등으로 상태를 나누었다.GameState 내에 SetWaveState(EWaveState NewState) 같은 함수를 만들어 상태를 변경하고, 상태가 변경될 때마다 필요한 로직(타이머 시작, 아이템 스폰, UI 업데이트 등)이 호출되도록 설계했다. 이렇게 하니 복잡한 흐름 속에서도 코드가 꼬이지 않고 명확해졌다.단순히 정보를 표시하는 것을 넘어, 보기 좋고 사용하기 편한 UI를 만드는 방법을 학습했다.
CanvasPanel 위에 VerticalBox나 HorizontalBox를 배치하여 위젯들을 정렬하고, Border로 배경이나 테두리를 감싸는 등 체계적인 구조를 만들었다. 이렇게 하면 해상도가 바뀌어도 레이아웃이 쉽게 깨지지 않는다.Normal(평상시), Hovered(마우스 올렸을 때), Pressed(클릭했을 때) 상태에 따라 다른 이미지나 색상을 지정할 수 있는 ButtonStyle 에셋을 사용했다. 이를 통해 코드 변경 없이 디자인만 쉽게 교체할 수 있었다.GameState의 변수에 바인딩하여 자동으로 업데이트되게 했다. 버튼 클릭 같은 일회성 상호작용은 이벤트(OnClicked)를 C++ 함수에 연결하여 처리했다.// 멀티 웨이브 시스템을 관리하도록 확장된 GameState
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameState.h"
#include "SpartaGameState.generated.h"
// 현재 웨이브 상태를 나타내는 열거형
UENUM(BlueprintType)
enum class EWaveState : uint8
{
WaitingToStart,
WaveInProgress,
WaveComplete,
GameOver
};
UCLASS()
class SPARTAPROJECT_API ASpartaGameState : public AGameState
{
GENERATED_BODY()
protected:
// 현재 웨이브 번호
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Wave")
int32 WaveCount;
// 현재 웨이브의 남은 시간
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Wave")
float RemainingTime;
// 현재 게임의 웨이브 상태
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Wave")
EWaveState WaveState;
// 다음 웨이브를 시작하는 함수
void StartWave();
// 웨이브 타이머가 종료되었을 때 호출되는 함수
void WaveTimeUp();
public:
// 외부에서 웨이브 상태를 변경하기 위한 함수
void SetWaveState(EWaveState NewState);
};
// 웨이브에 따라 스폰할 아이템 개수를 조절할 수 있도록 확장된 SpawnVolume
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SpawnVolume.generated.h"
UCLASS()
class ASpawnVolume : public AActor
{
GENERATED_BODY()
public:
// 지정된 개수만큼 아이템을 스폰하는 함수
UFUNCTION(BlueprintCallable, Category = "Spawning")
void SpawnItems(int32 NumberOfItems);
protected:
// 아이템 스폰 확률을 담고 있는 데이터 테이블
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawning")
class UDataTable* ItemDataTable;
};
// 리뉴얼된 메뉴 UI의 버튼 인터랙션을 처리하는 부분
#include "Blueprint/UserWidget.h"
#include "Components/Button.h"
void ASpartaPlayerController::SetupMainMenuInteractions()
{
if (MainMenuWidgetInstance)
{
// 위젯 블루프린트에서 'StartButton'이라는 이름의 버튼을 찾아옴
UButton* StartButton = Cast<UButton>(MainMenuWidgetInstance->GetWidgetFromName("StartButton"));
if (StartButton)
{
// 버튼의 OnClicked 이벤트에 StartGame 함수를 바인딩(연결)
StartButton->OnClicked.AddDynamic(this, &ASpartaPlayerController::StartGame);
}
}
}
GetWorldTimerManager()의 SetTimer, ClearTimer, PauseTimer 함수를 상황에 맞게 정확히 사용하지 않으면 타이머가 겹치거나 멈추지 않는 버그가 발생했다.TextBlock이나 Button에 접근하려면, 해당 위젯이 블루프린트에서 "Is Variable" 체크가 되어 있어야 했다. 이걸 체크하지 않아서 GetWidgetFromName()이 계속 nullptr를 반환해 한참을 헤맸다. 😅SetInputMode(FInputModeUIOnly())를 설정해야 했다. 게임 시작 시 다시 SetInputMode(FInputModeGameOnly())로 돌려주는 것을 잊어서, 게임이 시작됐는데도 캐릭터가 움직이지 않는 황당한 상황을 겪었다.| 개념 | 설명 | 비고 |
|---|---|---|
Enum을 이용한 상태 관리 | 게임의 복잡한 흐름을 Waiting, InProgress, Complete 등 명확한 상태로 나누어 관리한다. | Switch 문과 함께 사용하면 상태별 로직을 깔끔하게 분리할 수 있다. |
| 데이터 기반 웨이브 설계 | 웨이브별 아이템 개수, 제한 시간 등의 데이터를 DataTable이나 배열로 관리하여 코드 수정 없이 밸런스를 조절할 수 있다. | GameState에 TArray<FWaveData> 같은 구조체 배열을 두는 방식도 좋다. |
| UMG 위젯 계층 구조 | CanvasPanel, Box, Border 등을 조합하여 반응형이고 유지보수하기 좋은 UI 레이아웃을 설계한다. | 디자이너 툴에서 위젯을 부모-자식 관계로 구성하는 것이 핵심이다. |
| UI 인터랙션 구현 | 버튼의 OnClicked, OnHovered 같은 이벤트를 활용하여 사용자 경험을 향상시키는 동적인 UI를 만든다. | ButtonStyle 에셋을 사용하면 시각적 변화를 쉽게 적용할 수 있다. |