포스트

UE 레벨 전환·HUD 4페이지·게임오버 구현

바인딩 전에 나간 Broadcast는 그냥 사라진다

기능이 다 돌아가도 메뉴→본편→엔딩 루프가 닫히지 않으면 게임이 아니다. 레벨 셋을 잇는 전환과 탈출 타이머, 위젯 하나로 네 페이지를 처리하는 HUD, 레벨을 갈지 않는 게임오버.

UE 레벨 전환·HUD 4페이지·게임오버 구현

기능이 다 돌아가도 게임은 아직 게임이 아니다. 메뉴에서 시작해 본편으로 들어가고, 죽거나 탈출하면 끝 화면으로 이어지는 루프가 닫혀야 비로소 플레이가 된다. 이 글에서는 그 루프를 붙인 과정을 이야기하려 한다 — 구현은 레벨 세 개를 잇는 전환 흐름과 10분 탈출 타이머, 위젯 하나로 네 페이지를 처리하는 HUD, 그리고 레벨 전환 없이 화면만 바꾸는 게임오버다. 트러블슈팅은 바인딩 전에 지나간 델리게이트 Broadcast가 사라져 UI가 빈 채로 뜨는 문제다.

기술 구현 1 — 레벨 전환과 탈출 타이머

HandleEscapeSuccess 본 구현

AVOIDVehicle::TryStartEngine cpp:38 의 TODO 자리를 GameMode 위임으로 교체.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// VOIDGameMode.cpp
void AVOIDGameMode::HandleEscapeSuccess(AActor* Driver)
{
    UE_LOG(LogTemp, Warning, TEXT("[VOID] HandleEscapeSuccess by %s — OpenLevel(%s)"),
        *GetNameSafe(Driver), *EscapeLevelName.ToString());

    // OpenLevel 이전에 World cleanup 시 Driver 가 무효화될 수 있어
    // 추가 캐싱 없이 로그 + ClearTimer 만 처리.
    GetWorldTimerManager().ClearTimer(WaveTimerHandle);
    CurrentPhase = EVOIDWavePhase::Completed;

    if (UWorld* World = GetWorld())
    {
        UGameplayStatics::OpenLevel(World, EscapeLevelName);
    }
}
1
2
3
4
5
6
7
8
9
10
// VOIDVehicle.cpp
bool AVOIDVehicle::TryStartEngine(AActor* Driver)
{
    if (!bRepairComplete) return false;
    if (auto* GM = Cast<AVOIDGameMode>(UGameplayStatics::GetGameMode(this)))
    {
        GM->HandleEscapeSuccess(Driver);
    }
    return true;
}

새 레벨 3개 신설

레벨역할GameMode
Lv_Main메인 메뉴AVOIDGameMode (HUD 의 MainMenu 모드 사용)
Lv_VoidProto본편 (기존)AVOIDGameMode
Lv_Escape탈출 엔딩AVOIDGameMode (HUD 의 GameClear 모드 사용)

Project Default Map = Lv_Main. 본편은 Lv_VoidProto 로 통일.

10분 탈출 타이머

EscapeTimeLimit=600. GameMode BeginPlay 에서 1초 Tick 으로 AVOIDGameState::SetRemainingTime(N) Broadcast → HUD TimeText “MM:SS” 갱신. 만료 시 HandleGameOver.

GameStateClass 생성자 강제

PIE 시작 직후 TimeText 가 디폴트 텍스트 그대로 머무는 회귀가 있었다. 원인: 어떤 BP 에 캐싱된 GameStateBase 가 남아 있어서 AVOIDGameState 가 사용 안됨 → 델리게이트 Broadcast 자체가 안 일어남.

1
2
3
4
AVOIDGameMode::AVOIDGameMode()
{
    GameStateClass = AVOIDGameState::StaticClass();
}

설계 결정: BP 측 GameState 지정에 의존하지 말고 C++ 생성자에서 강제. “BP 에 옛날 값 캐싱” 회귀를 원천 차단.



기술 구현 2 — 위젯 하나로 만든 HUD 4페이지

8번 과제 발제는 메인 메뉴 / 인게임 / 게임오버 / 게임클리어 4 화면을 요구한다. 위젯을 4개 따로 만들면 PlayerController 가 매번 위젯 swap 을 해야 해서 번거로움 → 하나의 WBP_HUD 안에 두 컨테이너 (Lv_1 + Lv_2) 로 통합.

모드 enum 과 페이지 매핑

모드Lv_1 (오버레이)Lv_2 (인게임 HUD)마우스InputMode
MainMenuStartButton + ExitButtonhidden보임UI Only
InGamehiddenScore/Wave/Time/Health/Repair…숨김Game Only
GameOverGameOverText + ReStartButton + ExitButtonhidden보임UI Only
GameClear“ESCAPED” 텍스트 + ExitButtonhidden보임UI Only
1
2
3
4
5
6
7
UENUM(BlueprintType)
enum class EVOIDHUDMode : uint8
{
    MainMenu, InGame, GameOver, GameClear
};

void UVOIDHUDWidget::SetHUDMode(EVOIDHUDMode NewMode);

자동 모드 결정 (NativeConstruct)

레벨명을 보고 자동으로 모드 분기. PlayerController 측에서 추가 설정 필요 없음.

1
2
3
4
5
6
7
8
9
10
11
void UVOIDHUDWidget::NativeConstruct()
{
    Super::NativeConstruct();
    const FString LevelName = UGameplayStatics::GetCurrentLevelName(this);
    if      (LevelName == TEXT("Lv_Main"))   SetHUDMode(EVOIDHUDMode::MainMenu);
    else if (LevelName == TEXT("Lv_Escape")) SetHUDMode(EVOIDHUDMode::GameClear);
    else                                     SetHUDMode(EVOIDHUDMode::InGame);

    BindButtons();
    BindGameStateDelegates();
}

GameOver 는 레벨 전환 없이 모드만 토글

1
2
3
4
5
6
7
8
9
10
void AVOIDGameMode::HandleGameOver()
{
    if (auto* PC = UGameplayStatics::GetPlayerController(this, 0))
    {
        if (auto* HUD = Cast<UVOIDHUDWidget>(PC->GetHUDWidgetInstance()))
        {
            HUD->SetHUDMode(EVOIDHUDMode::GameOver);
        }
    }
}

설계 결정: GameOver 때 OpenLevel 하지 않는다. 같은 레벨에서 SetHUDMode(GameOver) 만 호출 → 시체/플레이 상태가 배경에 남아 몰입감 유지. ReStart 버튼은 그제서야 OpenLevel(현재레벨) 로 리셋.

버튼 자동 바인딩 — AddUniqueDynamic

BP 그래프에서 OnClicked 노드를 따로 그리지 않아도 되도록 C++ 측에서 자동 바인딩.

1
2
3
4
5
6
void UVOIDHUDWidget::BindButtons()
{
    if (StartButton)   StartButton->OnClicked.AddUniqueDynamic(this, &UVOIDHUDWidget::HandleStartClicked);
    if (ReStartButton) ReStartButton->OnClicked.AddUniqueDynamic(this, &UVOIDHUDWidget::HandleRestartClicked);
    if (ExitButton)    ExitButton->OnClicked.AddUniqueDynamic(this, &UVOIDHUDWidget::HandleExitClicked);
}

AddUniqueDynamic 은 동일 핸들러 재바인딩을 자동 회피 → NativeConstruct 두 번 호출돼도 안전.

Exit 버튼 — 레벨별 분기

1
2
3
4
5
6
7
8
9
10
11
12
void UVOIDHUDWidget::HandleExitClicked()
{
    const FString L = UGameplayStatics::GetCurrentLevelName(this);
    if (L == TEXT("Lv_Main") || L == TEXT("Lv_Escape"))
    {
        UKismetSystemLibrary::QuitGame(this, GetOwningPlayer(), EQuitPreference::Quit, false);
    }
    else
    {
        UGameplayStatics::OpenLevel(this, TEXT("Lv_Main"));
    }
}

바인딩 이전 Broadcast 보충

NativeConstruct 시점에는 이미 BeginPlay 가 끝나서 Health/Score/Time 의 초기 Broadcast 가 지나간 후. → HUD 의 디폴트 텍스트가 그대로 남는다.

해결: 바인딩 직후 현재값으로 핸들러 1회 직접 호출.

1
2
3
4
5
6
7
8
9
void UVOIDHUDWidget::BindToPlayer(APawn* P)
{
    if (auto* H = P->FindComponentByClass<UVOIDHealthComponent>())
    {
        H->OnHealthChanged.AddDynamic(this, &UVOIDHUDWidget::HandleHealthChanged);
        HandleHealthChanged(H->GetHealth(), H->GetMaxHealth()); // 현재값 1회 push
    }
    // ... Inventory / GameState 도 동일 패턴
}

이 한 줄이 빠지면 PIE 시작 직후 1초 동안 디폴트 텍스트가 보였다가 첫 데미지 받으면 그제서야 갱신되는 회귀가 생긴다.

Inventory / Repair / Score 텍스트 포맷

  • ScoreText: Score : N
  • WaveText: Wave : N
  • TimeText: Time : MM:SS
  • RepairText: Repair : N/3
  • InventoryText: 슬롯 순회하며 InvSlot.ItemData->DisplayName x Quantity 줄바꿈 누적


기술 구현 3 — 레벨을 갈지 않는 게임오버

흐름

  1. 좀비가 플레이어를 공격 → UVOIDHealthComponent::ApplyDamage(N) → HP 0 도달
  2. OnDeath Broadcast
  3. AVOIDPlayerController::HandlePlayerDeath 수신 → AVOIDGameMode::HandleGameOver 호출
  4. GameMode → HUD SetHUDMode(GameOver) (레벨 전환 X)

UVOIDBaseCharacter::BeginPlay — HealthComponent 누락 fallback

BP 측에서 HealthComponent 가 빠진 인스턴스가 한두 개 있어 nullptr 크래시 발생. 런타임 fallback 추가:

1
2
3
4
5
6
7
8
9
void AVOIDBaseCharacter::BeginPlay()
{
    Super::BeginPlay();
    if (!HealthComponent)
    {
        HealthComponent = NewObject<UVOIDHealthComponent>(this, TEXT("HealthComponent"));
        HealthComponent->RegisterComponent();
    }
}

HealthComponent 진단 로그

1
2
[Health] BP_VOIDPlayerCharacter_C_0 ApplyDamage(10.0) — HP 100.0 → 90.0 / 100.0
[Health] BP_VOIDPlayerCharacter_C_0 ApplyDamage(10.0) — HP 10.0 → 0.0 / 100.0 [DEAD]

[DEAD] 토큰 하나 보면 OnDeath 가 나간 시점이 명확히 식별돼 게임오버 미발동 회귀 진단이 빨라진다.



같은 날 함께 마무리한 차량 부품 슬롯 블로커 해소, 인벤토리 스택과 좀비 처치 점수, 디버프 3종은 이 글의 주제와 달라 덜어냈다.

핵심 요약 — 델리게이트는 바인딩 이전에 나간 Broadcast가 그냥 사라진다. 그래서 UI는 바인딩 직후 현재값으로 핸들러를 1회 직접 호출해 초기 상태를 보충해야 빈 화면으로 뜨지 않는다. 그리고 게임오버는 레벨을 갈아엎지 않고 HUD 모드만 토글하면 시체와 플레이 상태가 배경에 남아 연출이 살아난다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.