컨텐츠 검색
[UE C++] 메인 메뉴(종료, 메인메뉴로 돌아가기) 기능 추가, 멀티 웨이브 구현

2026. 1. 29. 20:43Unreal Engine/개념

1. 효율적인 UI 재사용: ExitButton 하나로 종료와 복귀를 동시에

메인 메뉴의 'EXIT' 버튼과 게임 오버 시의 'MAIN MENU' 버튼은 서로 다른 기능을 수행하지만, UI 구조는 동일합니다. 이를 위해 bIsExit 변수를 활용해 상황에 맞는 기능을 수행하도록 설계했습니다.

  • 상태 제어: HighScorePlayerController에서 bIsExit 변수를 관리하며, 게임 오버 시에만 이 값을 false로 변경하여 버튼의 역할을 스위칭합니다.
  • 텍스트 바인딩: 위젯 블루프린트에서 bIsExit 값에 따라 버튼 텍스트가 'EXIT' 또는 'MAIN MENU'로 출력되도록 구성했습니다.
// HighScorePlayerController.cpp
#include "Kismet/KismetSystemLibrary.h"

void AHighScorePlayerController::ExitGame()
{
    if (bIsExit)
    {
        // 평상시엔 게임 종료
        UKismetSystemLibrary::QuitGame(GetWorld(), this, EQuitPreference::Quit, false);
    }
    else
    {
        // 죽었을 때는 메인 메뉴로 이동
        UGameplayStatics::OpenLevel(GetWorld(), TEXT("L_MenuLevel"));
        ShowMainMenu(false);
    }
}

게임 시작 화면 게임 오버 화면


2. 3x3 멀티 스테이지 및 난이도 시스템

한 레벨 내에서 3번의 웨이브가 진행되고, 총 3개의 레벨을 클리어해야 하는 구조입니다.

  • 난이도 곡선: 웨이브가 올라갈수록 시간은 15초씩 감소하고, 아이템 스폰량은 10개씩 감소하게 설계했습니다.
  • 데이터 기반 설계: MaxLevelDuration, MultipleTime, ItemToSpawn 등을 UPROPERTY로 선언하여 에디터에서 자유롭게 밸런싱이 가능하도록 했습니다.
// HighScoreGameState.h
int32 LevelWave;

UPROPERTY(Category = "Level", EditAnywhere, BlueprintReadOnly)
int32 ItemToSpawn;

UPROPERTY(Category = "Level", EditAnywhere, BlueprintReadOnly)
int32 MultipleTime;

UPROPERTY(Category = "Level", EditAnywhere, BlueprintReadOnly)
int32 MultipleItem;

UPROPERTY(Category = "Level", EditAnywhere, BlueprintReadOnly)
float MaxLevelDuration;
// HighScoreGameState.cpp
LevelDuration = MaxLevelDuration - ((LevelWave - 1) * MultipleTime);    

int32 CurrentWaveItemCount = ItemToSpawn - ((LevelWave - 1) * MultipleItem);


3. 디버깅 케이스: 타이머가 -1.0에서 멈춘 이유 (음수 시간 버그)

멀티 웨이브를 테스트하던 중, 웨이브 2로 넘어가면 타이머가 작동하지 않고 UI에 -1.0이 뜨는 현상이 발생했습니다.

  • 발견된 원인: 빠른 테스트를 위해 초기 시간(MaxLevelDuration)을 10초로 줄였으나, 감소치인 MultipleTime은 15초 그대로였습니다. 결과적으로 10 - (1 * 15) = -5가 되어 타이머에 음수 값이 전달되었고, 시스템이 정상적으로 등록되지 않았습니다.
  • 해결책: StartWave() 함수에서 타이머를 설정하기 전 반드시 기존 타이머를 ClearTimer 하고, 계산된 시간이 양수인지 확인하는 로직을 추가했습니다. 그리고 원래 의도했던 시간인 60초로 되돌렸더니 정상 동작했습니다.

수정 전 수정 후


4. 게임 흐름 제어: StartWave & OnLevelTimeUp

로직을 모듈화하여 StartLevel에서 StartWave를 분리 호출함으로써 안정성을 높였습니다.

  • StartWave: 모든 코인 카운트를 초기화하고 새로운 난이도를 적용한 뒤 타이머를 재가동합니다.
  • OnLevelTimeUp: 현재 웨이브가 3단계 미만이면 웨이브를 올리고, 3단계를 마쳤다면 다음 레벨(맵)로 이동시킵니다.
  • OnCoinCollected: 제한 시간 내에 모든 코인을 수집하면 즉시 다음 단계로 넘어가도록 처리했습니다.
// HighScoreGameState.cpp
void AHighScoreGameState::StartWave()
{
    SpawnedCoinCount = 0;
    CollectedCoinCount = 0;

    GetWorldTimerManager().ClearTimer(LevelTimerHandle);

    LevelDuration = MaxLevelDuration - ((LevelWave - 1) * MultipleTime);    

    int32 CurrentWaveItemCount = ItemToSpawn - ((LevelWave - 1) * MultipleItem);

    TArray<AActor*> FoundVolumes;
    UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASpawnVolume::StaticClass(), FoundVolumes);

    if (FoundVolumes.Num() > 0)
    {
        ASpawnVolume* SpawnVolume = Cast<ASpawnVolume>(FoundVolumes[0]);
        if (SpawnVolume)
        {
            for (int32 i = 0; i < CurrentWaveItemCount; i++)
            {
                AActor* SpawnedActor = SpawnVolume->SpawnRandomItem();
                if (SpawnedActor && SpawnedActor->IsA(ACoinItem::StaticClass()))
                {
                    SpawnedCoinCount++;
                }
            }
        }
    }

    GetWorldTimerManager().SetTimer(
        LevelTimerHandle,
        this,
        &AHighScoreGameState::OnLevelTimeUp,
        LevelDuration,
        false
    );
}
// HighScoreGameState.cpp
void AHighScoreGameState::OnLevelTimeUp()
{
    if (LevelWave < 3)
    {
        // 다음 웨이브 진행
        LevelWave++;
        StartWave();
        UpdateHUD();
    }
    else
    {
        // 3웨이브까지 끝났다면 다음 레벨로 이동
        LevelWave = 1; 
        EndLevel();
    }
}
// HighScoreGameState.cpp
void AHighScoreGameState::OnCoinCollected()
{
    CollectedCoinCount++;

    if (SpawnedCoinCount > 0 && CollectedCoinCount >= SpawnedCoinCount)
    {
        // 타이머를 멈추고 강제로 OnLevelTimeUp을 호출하여 다음 단계로 넘김
        GetWorldTimerManager().ClearTimer(LevelTimerHandle);
        OnLevelTimeUp();
    }
}