UE 레벨 전환·HUD 4페이지·게임오버 구현
바인딩 전에 나간 Broadcast는 그냥 사라진다
기능이 다 돌아가도 메뉴→본편→엔딩 루프가 닫히지 않으면 게임이 아니다. 레벨 셋을 잇는 전환과 탈출 타이머, 위젯 하나로 네 페이지를 처리하는 HUD, 레벨을 갈지 않는 게임오버.
기능이 다 돌아가도 게임은 아직 게임이 아니다. 메뉴에서 시작해 본편으로 들어가고, 죽거나 탈출하면 끝 화면으로 이어지는 루프가 닫혀야 비로소 플레이가 된다. 이 글에서는 그 루프를 붙인 과정을 이야기하려 한다 — 구현은 레벨 세 개를 잇는 전환 흐름과 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 |
|---|---|---|---|---|
MainMenu | StartButton + ExitButton | hidden | 보임 | UI Only |
InGame | hidden | Score/Wave/Time/Health/Repair… | 숨김 | Game Only |
GameOver | GameOverText + ReStartButton + ExitButton | hidden | 보임 | UI Only |
GameClear | “ESCAPED” 텍스트 + ExitButton | hidden | 보임 | 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 : NWaveText:Wave : NTimeText:Time : MM:SSRepairText:Repair : N/3InventoryText: 슬롯 순회하며InvSlot.ItemData->DisplayName x Quantity줄바꿈 누적
기술 구현 3 — 레벨을 갈지 않는 게임오버
흐름
- 좀비가 플레이어를 공격 →
UVOIDHealthComponent::ApplyDamage(N)→ HP 0 도달 OnDeathBroadcastAVOIDPlayerController::HandlePlayerDeath수신 →AVOIDGameMode::HandleGameOver호출- 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 모드만 토글하면 시체와 플레이 상태가 배경에 남아 연출이 살아난다.