SpartaGame의 레벨을 순차적으로 클리어하는 방식에서 벗어나, 하나의 레벨 안에서 여러 웨이브(Wave)가 진행되는 구조로 게임 루프를 재설계했다. 이를 위해 GameState에 웨이브 정보와 타이머를 관리하는 로직을 집중시켰다. 또한, 플레이 경험을 향상시키기 위해 기존의 HUD와 메뉴 UI를 전면 리뉴얼하고, PlayerController를 통해 게임 일시정지, 입력 모드 전환 등 UI와 상호작용하는 기능을 C++ 코드로 구현했다. 이번 과제를 통해 게임의 핵심 상태 관리와 UI/UX 설계의 중요성을 깊이 이해할 수 있었다. 🚀
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameStateBase.h"
#include "SpartaGameState.generated.h"
UCLASS()
class SPARTAN_API ASpartaGameState : public AGameStateBase
{
GENERATED_BODY()
public:
// Wave 관리
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Wave")
int32 CurrentWaveIndex; // 현재 진행 중인 웨이브 번호
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Wave")
int32 MaxWaves; // 이번 레벨의 최대 웨이브 수
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Wave")
float WaveDuration; // 각 웨이브의 제한 시간 (초)
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Wave")
TArray<int32> ItemsToSpawnPerWave; // 웨이브마다 스폰할 아이템 개수를 담는 배열
};
#include "SpartaGameState.h"
#include "TimerManager.h"
#include "Engine/Engine.h"
#include "Blueprint/UserWidget.h"
#include "Components/TextBlock.h"
#include "Spartan/Volumes/SpawnVolume.h"
#include "Spartan/Items/CoinItem.h"
#include "Spartan/Player/SpartaPlayerController.h"
// 새로운 웨이브를 시작하는 함수
void ASpartaGameState::StartWave()
{
// 코인 카운트 초기화
SpawnedCoinCount = 0;
CollectedCoinCount = 0;
// 이전 Wave 아이템 제거
for (AActor* Item : CurrentWaveItems)
{
if (Item && Item->IsValidLowLevelFast())
{
Item->Destroy();
}
}
CurrentWaveItems.Empty();
// 이번 Wave에 스폰할 아이템 개수 결정
int32 ItemToSpawn = (ItemsToSpawnPerWave.IsValidIndex(CurrentWaveIndex)) ? ItemsToSpawnPerWave[CurrentWaveIndex] : 20;
// SpawnVolume을 이용해 아이템 스폰
if (ASpawnVolume* SpawnVolume = GetSpawnVolume())
{
for (int32 i = 0; i < ItemToSpawn; i++)
{
if (AActor* SpawnedActor = SpawnVolume->SpawnRandomItem())
{
if (SpawnedActor->IsA(ACoinItem::StaticClass()))
{
SpawnedCoinCount++;
}
CurrentWaveItems.Add(SpawnedActor);
}
}
}
// Wave별 환경 변화
if (CurrentWaveIndex == 1)
{
EnableWave2();
}
else if (CurrentWaveIndex == 2)
{
EnableWave3();
}
// HUD 위젯에 웨이브 시작 알림 표시
if (ASpartaPlayerController* SpartaPlayerController = GetSpartaPlayerController())
{
if (UUserWidget* HUDWidget = SpartaPlayerController->GetHUDWidget())
{
// 위젯 블루프린트에 있는 애니메이션 실행
UFunction* PlayAnimFunc = HUDWidget->FindFunction(FName("PlayShowWaveNotifyAnim"));
if (PlayAnimFunc)
{
HUDWidget->ProcessEvent(PlayAnimFunc, nullptr);
}
// 위젯의 텍스트 블록을 찾아 현재 웨이브 번호로 업데이트
if (UTextBlock* WaveNotifyText = Cast<UTextBlock>(HUDWidget->GetWidgetFromName("WaveNotifyText")))
{
WaveNotifyText->SetText(FText::FromString(
FString::Printf(TEXT("Wave %d 발생!"), CurrentWaveIndex + 1)));
}
}
}
// Wave 타이머 시작 (WaveDuration 후 OnWaveTimeUp 호출)
GetWorldTimerManager().SetTimer(
WaveTimerHandle,
this,
&ThisClass::OnWaveTimeUp,
WaveDuration,
false
);
}
// Wave 2 시작 시 호출되는 디버그 메시지 함수
void ASpartaGameState::EnableWave2()
{
const FString Msg = TEXT("Wave 2: Spawning and activating 5 Spike Traps!");
UE_LOG(LogTemp, Warning, TEXT("%s"), *Msg); // 에디터의 출력 로그에 메시지 표시
if (GEngine)
{
// 게임 화면 좌측 상단에 3초간 디버그 메시지 표시
GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Red, Msg);
}
}
// Wave 3 시작 시 호출되는 디버그 메시지 함수
void ASpartaGameState::EnableWave3()
{
const FString Msg = TEXT("Wave 3: Spawning coins that orbit around!");
UE_LOG(LogTemp, Warning, TEXT("%s"), *Msg); // 에디터의 출력 로그에 메시지 표시
if (GEngine)
{
// 게임 화면 좌측 상단에 3초간 디버그 메시지 표시
GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Red, Msg);
}
}
#include "SpartaPlayerController.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Blueprint/UserWidget.h"
// 일시정지(Pause) 메뉴를 띄우는 함수
void ASpartaPlayerController::PauseGame()
{
// Game을 일시정지하면, 일반 타이머/틱이 멈춤
SetPause(true);
// 마우스 커서 보이도록, UI모드로 전환
bShowMouseCursor = true;
SetInputMode(FInputModeUIOnly());
// Menu Widget 띄우기
ShowMainMenu(false);
}
// 게임을 종료하는 함수
void ASpartaPlayerController::QuitGame()
{
// 게임을 완전히 종료
UKismetSystemLibrary::QuitGame(
GetWorld(),
this,
EQuitPreference::Quit,
false
);
}
PauseGame 함수에서 SetInputMode(FInputModeUIOnly())로 UI만 조작 가능하도록 변경한 뒤, 게임으로 복귀(Resume)할 때 다시 SetInputMode(FInputModeGameOnly())로 되돌리는 로직을 빠뜨리는 실수를 했다. 이 때문에 메뉴를 닫아도 캐릭터가 움직이지 않고 마우스 커서가 계속 보이는 버그가 발생했다. UI를 띄울 때와 닫을 때의 입력 모드 설정은 반드시 한 쌍으로 관리해야 한다는 것을 배웠다.SetTimer로 웨이브 타이머를 설정했는데, 특정 조건(예: 플레이어 사망)으로 웨이브가 비정상적으로 종료될 때 타이머를 해제(ClearTimer)하지 않았다. 그 결과, 이미 종료된 웨이브의 타이머가 뒤늦게 만료되면서 다음 웨이브 로직을 호출해 게임 흐름이 엉키는 문제가 발생했다. 타이머는 생성만큼이나 소멸 관리도 중요하다는 점을 깨달았다.| 개념 | 설명 | 비고 |
|---|---|---|
| GameState | 게임의 핵심 상태(점수, 남은 시간, 현재 웨이브 등)를 모든 클라이언트에게 복제하고 관리하는 중앙 관리자 역할. | 플레이어의 상태가 아닌, '게임 판' 자체의 상태를 저장하기에 적합하다. |
| FTimerManager | SetTimer 함수를 통해 지정된 시간 후에 특정 함수를 호출하도록 예약하는 시스템. | WaveDuration 같은 제한 시간 기능이나 주기적인 이벤트 발생에 매우 유용하다. |
| PlayerController | 플레이어의 입력을 받아 처리하고, HUD나 메뉴 같은 UI와의 상호작용을 담당하는 클래스. | 게임 일시정지, 마우스 커서 표시, 입력 모드 변경 등은 PlayerController에서 처리하는 것이 일반적이다. |
| Input Mode | 사용자의 입력(키보드, 마우스)이 게임 월드에 적용될지, UI에 적용될지를 결정하는 모드. | FInputModeGameOnly, FInputModeUIOnly, FInputModeGameAndUI 등이 있다. |