구현계획 — 레벨전환_GUI
레벨 전환 + GUI — 구현계획 (2026-04-29)
작성일: 2026-04-29 (수) 마감: 2026-05-01 (D-2) — 직후 팀플 시작 대상 레포:
D:\Unreal\VoidUnreal(코드/콘텐츠) / 본 레포(Bootcamp-TIL)는 학습·문서 전용 기반 문서:
8번과제_구현계획.md§HUD 표시 / §1265 / §1834~1925 (위젯·Vehicle 델리게이트)차량부품슬롯마무리_구현계획.md§3.3 GameMode 위임 흐름2026-04-29.md(오늘 할 일 — 레벨 전환 / GUI / 호드 제외 결정)- graphify Community 9 “UI 애니메이션 & 메뉴” + 하이퍼엣지 “UI 파이프라인 (PlayerController → WBP_HUD / WBP_MainMenu ← GameState 데이터)”
0. 컨텍스트
8번 과제(언리얼 좀비 게임) Day 5. 차량 부품 슬롯 마무리(차량부품슬롯마무리_구현계획.md)에서 슬롯 3개 설치 + Sweep 가드 + IsInstalled 가드 + TryStartEngine 진입까지 동작 확인 완료. 남은 두 항목:
| # | 항목 | 마감 영향 | 의존성 |
|---|---|---|---|
| 1 | 레벨 전환 (Lv_Main / Lv_VoidProto / Lv_Escape 3 레벨) | 필수 — 8번 발제 명시 | HandleEscapeSuccess ↔ TryStartEngine cpp:38 TODO |
| 2 | GUI / HUD 4종 위젯 + (선택) 메인 메뉴 | 필수 — 8번 발제 §HUD 표시 | UVOIDHUDWidget 골격 존재 / AVOIDPlayerController::BeginPlay 위젯 생성 코드 존재 |
0.1 의존관계 표
| 작업 | 선행 | 후행 |
|---|---|---|
AVOIDGameMode::HandleEscapeSuccess 신설 | — | AVOIDVehicle::TryStartEngine cpp:38 TODO 제거 |
Lv_Escape 빈 레벨 생성 | — | HandleEscapeSuccess 의 OpenLevel("Lv_Escape") |
HUD BindToPlayer(GetPawn()) 호출 보강 | UVOIDHUDWidget::BindToPlayer (이미 구현됨) | 무게/체력/소음/디버프 게이지 실시간 갱신 |
| HUD 무기 아이콘 / 수리 진행도 위젯 추가 | UVOIDWeaponComponent::OnWeaponEquipped, AVOIDVehicle::OnSlotInstalled (이미 선언됨) | WBP_HUD BP 디자인 |
(선택) Lv_Main + WBP_MainMenu | Lv_Main 레벨 + 메인 메뉴 GameMode | 시작 버튼 → OpenLevel("Lv_VoidProto") |
0.2 04-29 시점 코드 레포 실측 (D:\Unreal\VoidUnreal grep 결과 — 추정 아님)
| 파일 | 내용 | 라인 | 상태 |
|---|---|---|---|
Public/Core/VOIDGameMode.h | 헤더에 웨이브 함수만 — HandleEscapeSuccess 없음 | 24~67 | 신설 필요 |
Private/Core/VOIDGameMode.cpp | BeginPlay/StartWave/SpawnFloor — Escape 함수 없음 | 1~133 | 신설 필요 |
Private/Vehicle/VOIDVehicle.cpp:38 | // TODO: AVOIDGameMode::HandleEscapeSuccess(Driver) 본 구현 | cpp:38 | TODO 활성화 필요 |
Public/UI/VOIDHUDWidget.h | BindToPlayer / HandleHealthChanged / HandleWeightChanged / HandleNoiseChanged / HandleDebuffUpdated / HandleScoreChanged / HandleWaveChanged / HandleTimeChanged 모두 존재 | 22~46 | 부분 활성화 (무기/수리 위젯만 추가) |
Private/UI/VOIDHUDWidget.cpp | NativeConstruct 에서 GameState 3 델리게이트 + BindToPlayer 에서 컴포넌트 4 델리게이트 자동 바인딩 | 10~96 | 이미 동작 — 호출만 보장 |
Public/Core/VOIDPlayerController.h | HUDWidgetClass (TSubclassOf) + HUDWidgetInstance 멤버 존재 | 21~25 | 이미 존재 |
Private/Core/VOIDPlayerController.cpp:8~20 | BeginPlay 에서 CreateWidget + AddToViewport 동작 | cpp:8~20 | BindToPlayer 호출 누락 — 보강 필요 |
Public/Vehicle/VOIDVehicle.h:12~13 | FOnSlotInstalled(EVOIDVehiclePartType) + FOnRepairComplete() 델리게이트 선언 + Broadcast 동작 | h:12~13 / cpp:25,30 | 이미 동작 |
Public/Components/VOIDInventoryComponent.h:10 | FVOIDOnWeightChanged(float, float) 2-Param | h:10 | 이미 동작 |
Public/Components/VOIDWeaponComponent.h:10 | FOnWeaponEquipped(UVOIDWeaponConfig*) | h:10 | HUD 미바인딩 — 추가 필요 |
Content/VoidUnreal/Maps/ | Lv_VoidProto.umap, Lv_ZombieTest.umap, NewWorld.umap, ZombieTest.umap | — | Lv_Main, Lv_Escape 신규 생성 |
1. 레벨 전환
1.1 레벨 구성 + 트리거 매트릭스
| From → To | 트리거 | 구현 위치 | 비고 |
|---|---|---|---|
(게임 시작) → Lv_Main | 프로젝트 기본 맵 (Project Settings) | Editor 설정 | 메인 메뉴 GameMode 별도 |
Lv_Main → Lv_VoidProto | 시작 버튼 (WBP_MainMenu) 또는 트리거 박스 | BP OnClicked → OpenLevel("Lv_VoidProto") | (선택) — 시간 부족 시 Editor 기본 맵을 Lv_VoidProto 로 |
Lv_VoidProto → Lv_Escape | AVOIDVehicle::TryStartEngine 성공 | AVOIDGameMode::HandleEscapeSuccess | 필수 |
(Lv_VoidProto 내부) 1F→2F→3F | 기존 AVOIDFloorTransitionTrigger | 그대로 유지 | 같은 레벨 안 |
1.2 GameMode 변경
1.2.1 Public/Core/VOIDGameMode.h — HandleEscapeSuccess 선언 추가
변경 전 (라인 24~44):
1
2
3
4
5
6
7
8
public:
AVOIDGameMode();
UFUNCTION(BlueprintCallable, Category="Wave")
void StartWave(int32 WaveIndex);
// ...
UFUNCTION(BlueprintPure, Category="Wave")
FVOIDWaveData GetCurrentWaveData() const { return CurrentWaveData; }
변경 후 (라인 44 직후 추가):
1
2
3
4
5
6
/** 차량 시동 성공 시 호출 — 탈출 레벨로 전환 */
UFUNCTION(BlueprintCallable, Category="Escape")
void HandleEscapeSuccess(AActor* Driver);
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Escape")
FName EscapeLevelName = TEXT("Lv_Escape");
1.2.2 Private/Core/VOIDGameMode.cpp — 본문 추가
파일 끝(TryLoadWaveRow 직후)에 추가:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void AVOIDGameMode::HandleEscapeSuccess(AActor* Driver)
{
UE_LOG(LogTemp, Warning, TEXT("[VOID] HandleEscapeSuccess by %s — OpenLevel(%s)"),
*GetNameSafe(Driver), *EscapeLevelName.ToString());
// 웨이브 타이머 즉시 종료 (혹시 남아있으면)
GetWorldTimerManager().ClearTimer(WaveTimerHandle);
CurrentPhase = EVOIDWavePhase::Completed;
// OpenLevel 직전 캐싱 — World cleanup 시점에 Driver 포인터 무효화 가능
UWorld* World = GetWorld();
if (!World) { return; }
UGameplayStatics::OpenLevel(World, EscapeLevelName);
}
1.2.3 Private/Vehicle/VOIDVehicle.cpp:38 TODO 제거 + GM 호출
변경 전 (cpp:34~45):
1
2
3
4
5
6
7
8
9
10
11
12
bool AVOIDVehicle::TryStartEngine(AActor* Driver)
{
if (!bRepairComplete) return false;
// TODO: AVOIDGameMode::HandleEscapeSuccess(Driver) 본 구현
if (AGameModeBase* GM = UGameplayStatics::GetGameMode(this))
{
(void)GM;
UE_LOG(LogTemp, Display, TEXT("[Vehicle] Engine started by %s"), *GetNameSafe(Driver));
}
return true;
}
변경 후:
1
2
3
4
5
6
7
8
9
10
11
12
bool AVOIDVehicle::TryStartEngine(AActor* Driver)
{
if (!bRepairComplete) return false;
UE_LOG(LogTemp, Display, TEXT("[Vehicle] Engine started by %s"), *GetNameSafe(Driver));
if (AVOIDGameMode* VOIDGM = Cast<AVOIDGameMode>(UGameplayStatics::GetGameMode(this)))
{
VOIDGM->HandleEscapeSuccess(Driver);
}
return true;
}
include 추가:
#include "Core/VOIDGameMode.h"
1.3 레벨 에셋 작업 (사용자가 에디터에서)
| 작업 | 절차 |
|---|---|
Lv_Escape 생성 | Content/VoidUnreal/Maps/ → 우클릭 → Level → 이름 Lv_Escape |
Lv_Escape 내용 | Empty Level → SkyLight + DirectionalLight + PlayerStart + (선택) WBP_EscapeEnding 텍스트 위젯 띄우는 GameMode |
(선택) Lv_Main 생성 | 동일 방식 → 메인 메뉴 카메라 + WBP_MainMenu 자동 추가 GameMode |
| Project Settings → Maps & Modes | Editor Startup Map = Lv_VoidProto (디버깅) / Game Default Map = Lv_Main (또는 시간 부족 시 Lv_VoidProto) |
Lv_VoidProto World Settings → GameMode Override | AVOIDGameMode 유지 |
1.4 검증 시나리오 (PIE)
| # | 상태 | 입력 | 기대 동작 | 의심 시 확인 |
|---|---|---|---|---|
| 1 | 슬롯 3/3 + bRepairComplete=true | 차량 본체 앞 E | [Vehicle] Engine started by ... → [VOID] HandleEscapeSuccess by ... → Lv_Escape 로딩 | GM 캐스팅 실패 시 nullptr — 로그 확인 |
| 2 | 슬롯 0~2/3 (bRepairComplete=false) | 차량 본체 앞 E | TryStartEngine 즉시 false 반환, 레벨 전환 X | bRepairComplete 디버그 카메라 |
| 3 | (선택) Lv_Main 시작 버튼 | 마우스 클릭 | Lv_VoidProto 로딩 + HUD 자동 생성 | PlayerController 가 Lv_Main 의 메뉴 PC 인지 |
| 4 | Lv_Escape 진입 후 | (자동) | 엔딩 텍스트 위젯 표시 | Lv_Escape GameMode 가 메뉴/엔딩용 PC 사용 |
2. GUI / HUD
기존 UVOIDHUDWidget 골격은 이미 7종 핸들러 + BindToPlayer 동작. 부족한 두 가지(무기 아이콘 / 수리 진행도)만 헤더+cpp 추가, BP 측 위젯 디자인 작업.
2.1 위젯 분해
| 위젯 슬롯 | 클래스/위치 | 데이터 소스 | 갱신 방식 | 코드 상태 |
|---|---|---|---|---|
| 체력바 (HealthBar) | UVOIDHUDWidget::HealthBar | UVOIDHealthComponent::OnHealthChanged | Delegate | 이미 바인딩 (cpp:29) |
| 무게 게이지 (WeightBar) | UVOIDHUDWidget::WeightBar | UVOIDInventoryComponent::OnWeightChanged | Delegate (TwoParams: float Total, float Max) | 이미 바인딩 (cpp:33) |
| 소음 게이지 (NoiseBar) | UVOIDHUDWidget::NoiseBar | UVOIDNoiseComponent::OnNoiseChanged | Delegate | 이미 바인딩 (cpp:37) |
| 디버프 텍스트 (DebuffText) | UVOIDHUDWidget::DebuffText | UVOIDDebuffComponent::OnDebuffUpdated | Delegate | 이미 바인딩 (cpp:41) |
| 점수 (ScoreText) | UVOIDHUDWidget::ScoreText | AVOIDGameState::OnScoreChanged | Delegate | 이미 바인딩 (cpp:16) |
| 웨이브 (WaveText) | UVOIDHUDWidget::WaveText | AVOIDGameState::OnWaveChanged | Delegate | 이미 바인딩 (cpp:17) |
| 시간 (TimeText) | UVOIDHUDWidget::TimeText | AVOIDGameState::OnTimeChanged | Delegate | 이미 바인딩 (cpp:18) |
| 현재 무기 아이콘 | UVOIDHUDWidget::WeaponIcon (신설) | UVOIDWeaponComponent::OnWeaponEquipped(UVOIDWeaponConfig*) | Delegate | 추가 작업 §2.3 |
| 수리 진행도 | UVOIDHUDWidget::RepairText (신설) | AVOIDVehicle::OnSlotInstalled + OnRepairComplete | Delegate | 추가 작업 §2.4 |
2.2 PlayerController BeginPlay — BindToPlayer 호출 보강
변경 전 (VOIDPlayerController.cpp:8~20):
1
2
3
4
5
6
7
8
9
10
11
12
13
void AVOIDPlayerController::BeginPlay()
{
Super::BeginPlay();
if (HUDWidgetClass)
{
HUDWidgetInstance = CreateWidget<UUserWidget>(this, HUDWidgetClass);
if (HUDWidgetInstance)
{
HUDWidgetInstance->AddToViewport();
}
}
}
변경 후:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void AVOIDPlayerController::BeginPlay()
{
Super::BeginPlay();
if (HUDWidgetClass)
{
HUDWidgetInstance = CreateWidget<UUserWidget>(this, HUDWidgetClass);
if (HUDWidgetInstance)
{
HUDWidgetInstance->AddToViewport();
// BindToPlayer 호출 — Pawn 의 컴포넌트 델리게이트 자동 바인딩
if (UVOIDHUDWidget* HUD = Cast<UVOIDHUDWidget>(HUDWidgetInstance))
{
if (APawn* P = GetPawn())
{
HUD->BindToPlayer(P);
}
else
{
// BeginPlay 시점에 Pawn 미Possess 가능 — OnPossess 에서 재시도
UE_LOG(LogTemp, Warning, TEXT("[VOID PC] BindToPlayer skipped (Pawn null) — will retry on Possess"));
}
}
}
}
}
void AVOIDPlayerController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (UVOIDHUDWidget* HUD = Cast<UVOIDHUDWidget>(HUDWidgetInstance))
{
HUD->BindToPlayer(InPawn);
}
}
헤더에
virtual void OnPossess(APawn* InPawn) override;선언 추가 + cpp 에#include "UI/VOIDHUDWidget.h".
2.3 HUD 위젯 — 무기 아이콘 추가
Public/UI/VOIDHUDWidget.h 추가:
1
2
3
4
5
6
7
8
9
10
11
12
class UImage;
class UVOIDWeaponConfig;
// — protected 섹션 —
UFUNCTION()
void HandleWeaponEquipped(UVOIDWeaponConfig* NewWeapon);
UPROPERTY(meta=(BindWidget))
TObjectPtr<UImage> WeaponIcon;
UPROPERTY(meta=(BindWidget, OptionalWidget=true))
TObjectPtr<UTextBlock> WeaponNameText;
Private/UI/VOIDHUDWidget.cpp — BindToPlayer 안 추가:
1
2
3
4
if (UVOIDWeaponComponent* Weapon = PlayerPawn->FindComponentByClass<UVOIDWeaponComponent>())
{
Weapon->OnWeaponEquipped.AddDynamic(this, &UVOIDHUDWidget::HandleWeaponEquipped);
}
HandleWeaponEquipped 본문:
1
2
3
4
5
6
7
8
9
10
11
12
void UVOIDHUDWidget::HandleWeaponEquipped(UVOIDWeaponConfig* NewWeapon)
{
if (!NewWeapon) { return; }
if (WeaponIcon && NewWeapon->Icon)
{
WeaponIcon->SetBrushFromTexture(NewWeapon->Icon);
}
if (WeaponNameText)
{
WeaponNameText->SetText(FText::FromName(NewWeapon->WeaponName));
}
}
UVOIDWeaponConfig의Icon(UTexture2D*)·WeaponName(FName) 필드는 DA 측에 이미 있다면 그대로 사용. 없으면 BP Default 만으로 BindWidget 디자인.
2.4 HUD 위젯 — 차량 수리 진행도 추가
Public/UI/VOIDHUDWidget.h 추가:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class AVOIDVehicle;
enum class EVOIDVehiclePartType : uint8;
UFUNCTION(BlueprintCallable, Category="HUD")
void BindToVehicle(AVOIDVehicle* Vehicle);
UFUNCTION()
void HandleSlotInstalled(EVOIDVehiclePartType PartType);
UFUNCTION()
void HandleRepairComplete();
UPROPERTY(meta=(BindWidget))
TObjectPtr<UTextBlock> RepairText;
private:
TWeakObjectPtr<AVOIDVehicle> BoundVehicle;
int32 InstalledCountCached = 0;
Private/UI/VOIDHUDWidget.cpp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void UVOIDHUDWidget::BindToVehicle(AVOIDVehicle* Vehicle)
{
if (!Vehicle) { return; }
BoundVehicle = Vehicle;
Vehicle->OnSlotInstalled.AddDynamic(this, &UVOIDHUDWidget::HandleSlotInstalled);
Vehicle->OnRepairComplete.AddDynamic(this, &UVOIDHUDWidget::HandleRepairComplete);
if (RepairText)
{
RepairText->SetText(FText::FromString(TEXT("0/3")));
}
}
void UVOIDHUDWidget::HandleSlotInstalled(EVOIDVehiclePartType /*PartType*/)
{
++InstalledCountCached;
if (RepairText)
{
RepairText->SetText(FText::FromString(FString::Printf(TEXT("%d/3"), InstalledCountCached)));
}
}
void UVOIDHUDWidget::HandleRepairComplete()
{
if (RepairText)
{
RepairText->SetText(FText::FromString(TEXT("3/3 ✓ Press E to Start")));
}
}
Vehicle ↔ HUD 연결 지점: AVOIDPlayerController::BeginPlay 의 HUD 생성 직후 또는 첫 슬롯 설치 시 자동 연결 어렵다. 가장 안전한 방법은 AVOIDVehicle::BeginPlay 에서 PlayerController 캐싱 후 호출:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// VOIDVehicle.cpp BeginPlay 추가 (헤더에 BeginPlay override 선언)
void AVOIDVehicle::BeginPlay()
{
Super::BeginPlay();
if (APlayerController* PC = UGameplayStatics::GetPlayerController(this, 0))
{
if (AVOIDPlayerController* VPC = Cast<AVOIDPlayerController>(PC))
{
if (UVOIDHUDWidget* HUD = Cast<UVOIDHUDWidget>(VPC->GetHUDWidgetInstance()))
{
HUD->BindToVehicle(this);
}
}
}
}
AVOIDPlayerController에UUserWidget* GetHUDWidgetInstance() const { return HUDWidgetInstance; }getter 추가 필요.차량 BeginPlay 가 PC BeginPlay 보다 먼저 실행되면 HUD 가 nullptr — 차량 측에서
GetWorldTimerManager().SetTimerForNextTick으로 1 tick 지연 권장.
2.5 BP 작업 가이드 (사용자가 에디터에서)
코드 변경은 04-29 적용 완료. 아래는 BP/위젯 측 작업 절차 — 위에서 아래로 순서대로 진행.
2.5.1 WBP_HUD 생성 + BindWidget 매핑
생성
- Content Browser →
Content/VoidUnreal/Blueprints/UI/폴더 우클릭 → User Interface → Widget Blueprint - Parent Class 선택 창에서
VOIDHUDWidget선택 (검색창에VOIDHUDWidget입력) - 이름
WBP_HUD로 저장
Designer 위젯 배치 — 이름 정확히 일치 필수 (BindWidget 매핑)
| 위젯 종류 | 변수명 (Designer 좌측 패널 이름) | 용도 |
|---|---|---|
| ProgressBar | HealthBar | 체력 (0~1) |
| ProgressBar | WeightBar | 무게 (0~1) |
| ProgressBar | NoiseBar | 소음 (0~1) |
| TextBlock | ScoreText | 점수 |
| TextBlock | WaveText | “Wave 1” 등 |
| TextBlock | TimeText | “MM:SS” |
| TextBlock | DebuffText | “[출혈] [골절]” 등 |
| Image | WeaponIcon (선택) | 라이플/샷건 아이콘 |
| TextBlock | WeaponNameText (선택) | “Rifle” / “Shotgun” |
| TextBlock | RepairText (선택) | “0/3” → “3/3 - Press E to Start” |
ProgressBar 와 TextBlock 9개는 필수, Image 1개 + TextBlock 2개는 선택 (BindWidgetOptional). 미배치 시 컴파일은 통과하지만 해당 슬롯 표시만 비어있음.
배치 팁
- Canvas Panel 위에 좌상단 Vertical Box 1개 → ProgressBar 3개 + TextBlock 4개 (Health/Weight/Noise/Score/Wave/Time/Debuff)
- 우하단 Horizontal Box 1개 → WeaponIcon (64×64) + WeaponNameText
- 화면 중앙 상단 → RepairText (큰 폰트, 차량 근처 진행도)
- 각 위젯 클릭 후 좌측 트리에서 이름 더블클릭으로 변경 — 위 표의 변수명과 정확히 일치해야 함 (대소문자 포함)
- 좌측 트리에서 위젯 클릭 → 우측 Details → Is Variable 체크 박스 ON 확인 (BindWidget 동작 조건)
컴파일
- 상단 Compile 클릭 → 에러 없이 통과해야 함
- 만약
WBP_HUD has missing required widget binding 'HealthBar'같은 에러가 뜨면 → 그 변수명과 일치하는 ProgressBar/TextBlock 이 누락된 것. 이름 다시 확인.
2.5.2 PlayerController 에 WBP_HUD 연결
옵션 A — BP 인스턴스 사용 (권장)
- Content Browser →
Content/VoidUnreal/Blueprints/Core/(없으면 생성) → 우클릭 → Blueprint Class → ParentVOIDPlayerController→ 이름BP_VOIDPlayerController - BP 열기 → 좌측 패널 My Blueprint → Variables 가 아닌, Class Defaults (상단 메뉴) 클릭
- Details 패널 →
UI카테고리 → HUD Widget Class 드롭다운 →WBP_HUD선택 - Compile + Save
Lv_ZombieTest (또는 Lv_VoidProto) 의 GameMode 지정
- 이미
AVOIDGameMode가 있고PlayerControllerClass가AVOIDPlayerController라면 →BP_VOIDPlayerController로 교체 권장 - 또는 GameMode BP 만들어서
PlayerControllerClass = BP_VOIDPlayerController설정 - World Settings → GameMode Override 확인
옵션 B — C++ 디폴트로 직접 지정 (BP 안 쓰는 경우)
AVOIDPlayerController::AVOIDPlayerController()생성자에서static ConstructorHelpers::FClassFinder<UUserWidget> WBP(TEXT("/Game/.../WBP_HUD")); HUDWidgetClass = WBP.Class;— 권장하지 않음 (경로 하드코딩).
2.5.3 UVOIDWeaponConfig 의 Icon / DisplayName 설정 (선택)
무기 아이콘을 보여주려면 기존 DA 두 개에 텍스처/이름 추가:
- Content Browser →
DA_Rifle,DA_Shotgun더블클릭 Details 패널 → **Weapon Identity** 카테고리: - Display Name =
소총/샷건(FText, 한국어 OK) - Icon = 텍스처 어셋 드래그 (없으면 비워둬도 됨, BP 측에서 빈 Image 처리)
- Display Name =
- 저장
Icon 텍스처가 없으면
WeaponIcon슬롯이 자동으로 Hidden 처리됨 (cpp 측에서 처리). DisplayName 비어있으면WeaponId가 대신 표기됨.
2.5.4 (선택) WBP_MainMenu — Lv_Main 메인 메뉴
구조
Lv_Main 레벨 신규 생성 (이미 있으면 스킵) — Content Browser 우클릭 → Level → Empty Level →
Lv_MainBP_MenuGameMode생성- Blueprint Class → Parent
GameModeBase(반드시AVOIDGameMode가 아닌 베이스 클래스 — 메뉴에서 좀비 스폰 막기 위함) - Class Defaults:
- Player Controller Class =
BP_MenuPlayerController(다음 단계에서 생성) - Default Pawn Class =
DefaultPawn또는None - HUD Class =
None
- Player Controller Class =
- Blueprint Class → Parent
BP_MenuPlayerController생성- Blueprint Class → Parent
PlayerController - Event Graph:
1 2 3 4 5
Event BeginPlay ├ Set Show Mouse Cursor = true (Target: Self) ├ Create Widget (Class: WBP_MainMenu, Owning Player: Self) → return value 보관 ├ Add to Viewport (Target: 위 위젯) └ Set Input Mode UI Only (PlayerController: Self, In Widget to Focus: 위 위젯)
- Blueprint Class → Parent
WBP_MainMenu위젯- User Interface → Widget Blueprint → Parent
UserWidget→ 이름WBP_MainMenu - Designer:
- Canvas Panel
- 가운데 Vertical Box
- Button
Btn_Start— Text “시작” - Button
Btn_Quit— Text “종료”
- Button
- Graph (각 버튼 Details → Events → OnClicked + 클릭):
1 2 3 4 5 6
OnClicked (Btn_Start) └ Open Level (by Name) Level Name: Lv_ZombieTest (실제 본편 레벨 이름) World Context Object: Self OnClicked (Btn_Quit) └ Quit Game - Compile
- User Interface → Widget Blueprint → Parent
- Lv_Main 의 GameMode 지정
- Lv_Main 열기 → Window → World Settings → GameMode Override =
BP_MenuGameMode - 저장
- Lv_Main 열기 → Window → World Settings → GameMode Override =
- 시작 레벨 변경
- Edit → Project Settings → Maps & Modes
- Editor Startup Map =
Lv_Main - Game Default Map =
Lv_Main
2.5.5 (선택) WBP_EscapeEnding — Lv_Escape 엔딩 화면
WBP_EscapeEnding위젯- Parent
UserWidget - Designer: 검은 배경 + 큰 텍스트 “ESCAPED” + Button
Btn_ToMain(“메인 메뉴”) - Graph:
1 2
OnClicked (Btn_ToMain) └ Open Level (by Name) Level Name: Lv_Main
- Parent
- Lv_Escape 에 자동 표시
- 가장 간단한 방법:
Lv_Escape의 Level Blueprint- Open Level Blueprint (Window → Level Blueprint)
- Event Graph:
1 2 3 4 5
Event BeginPlay ├ Get Player Controller (0) ├ Set Show Mouse Cursor = true ├ Create Widget (Class: WBP_EscapeEnding) → AddToViewport └ Set Input Mode UI Only
- 또는 Lv_Escape 전용 GameMode 만들어서 PlayerController 측에서 처리
- 가장 간단한 방법:
2.5.6 (선택) WBP_GameOver — 사망 시 표시되는 게임오버 위젯
코드 측 처리 (04-29 적용 완료):
UVOIDHealthComponent::OnDeathBroadcast →AVOIDPlayerController::HandlePlayerDeath→AVOIDGameMode::HandleGameOverHandleGameOver는GameOverWidgetClass가 지정돼 있으면 위젯을 viewport 에 추가 +GameOverReturnDelay초 후GameOverLevelName로 OpenLevel
위젯 만들기
- User Interface → Widget Blueprint → Parent
UserWidget→ 이름WBP_GameOver - Designer:
- 검은 반투명 배경 (Image + Color Alpha 0.7)
- 큰 텍스트 “GAME OVER” (가운데, 빨간색)
- 작은 텍스트 “Returning to main menu in 3 seconds…” (선택)
GameMode 에 위젯 클래스 지정
본편 레벨(Lv_ZombieTest 또는 Lv_VoidProto)의 GameMode 가 AVOIDGameMode 라면:
- Content Browser 에서 GameMode BP 가 있으면 그것을, 없으면
BP_VOIDGameMode신규 생성 (ParentAVOIDGameMode) - Class Defaults → GameOver 카테고리:
Game Over Widget Class=WBP_GameOverGame Over Level Name=Lv_Main(기본값)Game Over Return Delay=3.0(초 단위)
Game Over Widget Class비어있어도 동작 — 위젯 없이 3초 후 메뉴로 전환만.
검증
- PIE → 좀비에게 5번 맞아 체력 0 →
[VOID PC] HandlePlayerDeath+[VOID] HandleGameOver로그 → WBP_GameOver 표시 → 3초 후 Lv_Main 로딩
2.5.7 BP 작업 체크리스트
- WBP_HUD 생성 + BindWidget 9개 (필수) + 3개 (선택)
- WBP_HUD 컴파일 에러 없음
- BP_VOIDPlayerController 생성 + HUDWidgetClass = WBP_HUD
- Lv_ZombieTest GameMode 의 PlayerControllerClass = BP_VOIDPlayerController
- (선택) DA_Rifle / DA_Shotgun 의 DisplayName + Icon 입력
- (선택) Lv_Main + BP_MenuGameMode + BP_MenuPlayerController + WBP_MainMenu
- (선택) Project Settings 시작 맵 = Lv_Main
- (선택) WBP_EscapeEnding + Lv_Escape Level Blueprint 자동 표시
- (선택) WBP_GameOver + GameMode BP 에 GameOverWidgetClass 지정
2.6 검증 시나리오 (PIE)
| # | 입력 | 기대 동작 | 의심 시 확인 |
|---|---|---|---|
| 1 | PIE 시작 | HealthBar 100%, WeightBar 0%, NoiseBar 0%, Wave 1, Time MM:SS | BindToPlayer 호출 누락 / BindWidget 이름 불일치 |
| 2 | 부품 픽업 | WeightBar 즉시 갱신 | OnWeightChanged.Broadcast 누락 (DataAsset Weight=0?) |
| 3 | 1키 → 라이플 / 2키 → 샷건 | WeaponIcon 토글 | OnWeaponEquipped.AddDynamic 누락, Icon 필드 nullptr |
| 4 | Battery 슬롯 설치 | RepairText 0/3 → 1/3 | BindToVehicle 미호출 (PC ↔ Vehicle 타이밍) |
| 5 | 3슬롯 설치 완료 | RepairText 3/3 ✓ Press E to Start | OnRepairComplete.Broadcast (VOIDVehicle.cpp:30) 동작 |
| 6 | 좀비 피격 | HealthBar 즉시 감소 | DebuffComponent Bleeding 발동 시 DebuffText [출혈] |
| 7 | 웨이브 종료 | WaveText Wave 1 → Wave 2, TimeText 리셋 | AVOIDGameState::SetCurrentWave (이미 호출됨) |
3. 작업 순서 (마감 D-2)
| 순번 | 작업 | 예상 시간 | 의존성 | 우선순위 |
|---|---|---|---|---|
| 1 | AVOIDGameMode::HandleEscapeSuccess 헤더+cpp 신설 | 15분 | — | P0 |
| 2 | AVOIDVehicle::TryStartEngine cpp:38 TODO 제거 + GM 캐스팅 호출 | 10분 | 1 | P0 |
| 3 | Lv_Escape 빈 레벨 생성 + GameMode 지정 | 10분 | — | P0 |
| 4 | 레벨 전환 PIE 검증 (§1.4 1·2번) | 10분 | 1+2+3 | P0 |
| 5 | AVOIDPlayerController::BeginPlay BindToPlayer 호출 + OnPossess 보강 | 15분 | — | P0 |
| 6 | WBP_HUD 디자인 + 기존 7개 슬롯 BindWidget | 30분 | 5 | P0 |
| 7 | HUD WeaponIcon / RepairText 헤더+cpp 추가 | 30분 | — | P1 |
| 8 | AVOIDPlayerController::GetHUDWidgetInstance getter + AVOIDVehicle::BeginPlay BindToVehicle 호출 | 15분 | 7 | P1 |
| 9 | WBP_HUD 에 WeaponIcon/RepairText 슬롯 추가 + BP 디자인 | 20분 | 7 | P1 |
| 10 | HUD 전체 PIE 검증 (§2.6 전부) | 20분 | 5+6+7+8+9 | P0 |
| 11 | (선택) Lv_Main + WBP_MainMenu | 30분 | 3 | P2 |
| 12 | (선택) Lv_Escape 엔딩 위젯 | 20분 | 3 | P2 |
총합: P0 만 약 2시간, P0+P1 약 3시간 30분, 전체 약 4시간 20분.
4. 블로커 / 리스크
| 항목 | 영향 | 완화책 |
|---|---|---|
OpenLevel 직후 World cleanup → Driver 포인터 무효화 | HandleEscapeSuccess 내부에서 Driver 사용 시 크래시 | OpenLevel 직전에 필요한 정보 모두 캐싱(이름·점수 등). 본 구현은 로그만 — 안전 |
BeginPlay 시점에 GetPawn() nullptr | BindToPlayer 미호출 → 게이지 안 움직임 | OnPossess override 에서 재호출 (§2.2) |
| BindWidget 이름 불일치 → 컴파일 OK / Runtime nullptr | 게이지 무반응 (가장 흔한 함정) | BP Designer 의 위젯 이름과 헤더 멤버 이름 정확히 일치 확인 |
AVOIDVehicle::BeginPlay 가 PC BeginPlay 보다 먼저 → HUD nullptr | 수리 진행도 미반영 | SetTimerForNextTick 1 tick 지연 또는 OnRepairComplete 첫 호출 시 다시 BindToVehicle 시도 |
UVOIDWeaponConfig::Icon / WeaponName 필드 부재 | HandleWeaponEquipped 컴파일 실패 | DA 헤더에 UTexture2D* Icon, FName WeaponName 추가 또는 BP에서만 처리 |
| 같은 레벨 안에서 PIE 재시작 시 WaveTimer 잔존 | Lv_Escape 로딩 후 의도치 않은 타이머 발동 | HandleEscapeSuccess 에서 ClearTimer(WaveTimerHandle) 선처리 (§1.2.2 포함됨) |
Lv_Escape GameMode 미지정 → AVOIDGameMode::BeginPlay 가 다시 StartWave(1) | 엔딩 레벨에서 좀비 스폰 | Lv_Escape 전용 가벼운 GameMode (AGameModeBase 직접 또는 BP_VOIDEscapeGameMode) 사용 |
5. 완료 기준 (DoD)
1차 목표 (P0 — 마감 필수)
- 슬롯 3 설치 → 차량 본체 E →
Lv_Escape로 전환 (HandleEscapeSuccess로그 확인) bRepairComplete=false상태로 차량 E → 레벨 전환 X (가드 동작)- HUD: 체력/무게/소음/디버프/점수/웨이브/시간 7종 게이지 실시간 갱신
WBP_HUDBindWidget 이름 매핑 모두 동작 (Output Log Warning 0건)
2차 목표 (P1 — 권장)
- HUD WeaponIcon: 1키/2키 무기 스왑 시 즉시 토글
- HUD RepairText: 슬롯 설치 시
0/3 → 1/3 → 2/3 → 3/3 ✓갱신 OnRepairComplete시 텍스트 색상/문구 변화
3차 목표 (P2 — 시간 남으면)
Lv_Main+WBP_MainMenu시작 버튼 →Lv_VoidProto진입Lv_Escape엔딩 위젯 (WBP_EscapeEnding) 표시- PIE 5분 풀 플레이로 회귀 0건
6. 참고
8번과제_구현계획.md§HUD 표시 (91~155) — UI 원본 설계8번과제_구현계획.md§1265, 1834~1925 —HandleEscapeSuccess,OnSlotInstalled/OnRepairComplete델리게이트 원본차량부품슬롯마무리_구현계획.md§3.3 / §4 P2 — 레벨 전환 위임 흐름2026-04-29.md— 오늘 할 일 (레벨 전환 / GUI / 호드 제외 결정)2026-04-28_코드리뷰.md— 어제 코드리뷰 (델리게이트 명세 일부)- graphify Community 9 “UI 애니메이션 & 메뉴” —
WBP_MainMenu,UMG Animation,ProcessEventC++↔BP 호출 패턴 - graphify 하이퍼엣지 “UI 파이프라인 (PlayerController → WBP_HUD / WBP_MainMenu ← GameState 데이터)” — 본 구현계획의 데이터 흐름 그대로
7. 디버깅 기록 (작업 중 추가)
PIE 도중 발견 사항을 시간순으로 기록.
04-29 코드 적용 완료 (게임오버 추가)
VOIDGameMode.h/cpp—HandleGameOver()+GameOverLevelName/Delay/WidgetClass추가, 위젯 표시 +GameOverReturnDelay후OpenLevel(Lv_Main)VOIDPlayerController.h/cpp—OnPossess에서HealthComponent->OnDeath바인딩 +HandlePlayerDeath→ GameMode 위임VOIDHUDWidget.cpp—NativeConstruct/BindToPlayer에서 GameState 와 컴포넌트 현재값으로 핸들러 1회 직접 호출 (바인딩 이전 Broadcast 보충)
04-29 코드 적용 완료
VOIDGameMode.h/cpp—HandleEscapeSuccess(AActor* Driver)+EscapeLevelName=Lv_Escape추가VOIDVehicle.cpp—TryStartEngineTODO 제거 + GameMode 캐스팅 호출 +BeginPlay신설 (1 tick 지연 후 HUDBindToVehicle)VOIDVehicle.h—BeginPlayoverride 선언VOIDPlayerController.h/cpp—OnPossessoverride +GetHUDWidgetInstance()getter,BeginPlay에서 HUDBindToPlayer호출VOIDHUDWidget.h/cpp—BindToVehicle+HandleWeaponEquipped/HandleSlotInstalled/HandleRepairComplete+WeaponIcon/WeaponNameText/RepairText(BindWidgetOptional) 추가VOIDWeaponConfig.h—DisplayName(FText)+Icon(UTexture2D*)필드 추가 (BindWidgetOptional 슬롯 채울 데이터 소스)- 레벨 전환 PIE 검증 통과: 슬롯 3개 설치 → 차량 앞 E →
[Vehicle] Engine started+[VOID] HandleEscapeSuccess+ Lv_Escape 로딩 ✅
04-29 차량 부품 슬롯 마무리 작업 중 발견
- 슬롯 ChildActorComponent
OwnerVehicle (EditInstanceOnly)가 BP 자식 노드에서 설정 불가 →TryInstallPart_Implementation끝부분에GetParentActor()체인 자동 검색 fallback 추가 (VOIDVehiclePartSlotActor.cpp) - Sweep 이 슬롯 InteractionVolume(Visibility=Block) 에서 차단되어 차량 본체에 도달 못함 →
Interact후보 검색 후 Best=nullptr 이면 5m 이내AVOIDVehicle자동 검색 fallback 추가 (VOIDPlayerCharacter.cpp) - 어제 추가했던
AddIgnoredActor(AVOIDVehicle)루프는 시동 분기 영구 차단의 원인이라 제거됨 - 헬기 시각 메시는 BP 안 ChildActorComponent (
lowpoly_heli_node_*) Mobility 충돌로 attach 실패 → 메시는 레벨에 직접 배치, BP_VoidVehicle 에는 Body + StartEngineVolume + 슬롯 3개만 유지