포스트

구현계획 — 레벨전환_GUI

구현계획 — 레벨전환_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번 발제 명시HandleEscapeSuccessTryStartEngine cpp:38 TODO
2GUI / HUD 4종 위젯 + (선택) 메인 메뉴필수 — 8번 발제 §HUD 표시UVOIDHUDWidget 골격 존재 / AVOIDPlayerController::BeginPlay 위젯 생성 코드 존재

0.1 의존관계 표

작업선행후행
AVOIDGameMode::HandleEscapeSuccess 신설AVOIDVehicle::TryStartEngine cpp:38 TODO 제거
Lv_Escape 빈 레벨 생성HandleEscapeSuccessOpenLevel("Lv_Escape")
HUD BindToPlayer(GetPawn()) 호출 보강UVOIDHUDWidget::BindToPlayer (이미 구현됨)무게/체력/소음/디버프 게이지 실시간 갱신
HUD 무기 아이콘 / 수리 진행도 위젯 추가UVOIDWeaponComponent::OnWeaponEquipped, AVOIDVehicle::OnSlotInstalled (이미 선언됨)WBP_HUD BP 디자인
(선택) Lv_Main + WBP_MainMenuLv_Main 레벨 + 메인 메뉴 GameMode시작 버튼 → OpenLevel("Lv_VoidProto")

0.2 04-29 시점 코드 레포 실측 (D:\Unreal\VoidUnreal grep 결과 — 추정 아님)

파일내용라인상태
Public/Core/VOIDGameMode.h헤더에 웨이브 함수만 — HandleEscapeSuccess 없음24~67신설 필요
Private/Core/VOIDGameMode.cppBeginPlay/StartWave/SpawnFloor — Escape 함수 없음1~133신설 필요
Private/Vehicle/VOIDVehicle.cpp:38// TODO: AVOIDGameMode::HandleEscapeSuccess(Driver) 본 구현cpp:38TODO 활성화 필요
Public/UI/VOIDHUDWidget.hBindToPlayer / HandleHealthChanged / HandleWeightChanged / HandleNoiseChanged / HandleDebuffUpdated / HandleScoreChanged / HandleWaveChanged / HandleTimeChanged 모두 존재22~46부분 활성화 (무기/수리 위젯만 추가)
Private/UI/VOIDHUDWidget.cppNativeConstruct 에서 GameState 3 델리게이트 + BindToPlayer 에서 컴포넌트 4 델리게이트 자동 바인딩10~96이미 동작 — 호출만 보장
Public/Core/VOIDPlayerController.hHUDWidgetClass (TSubclassOf) + HUDWidgetInstance 멤버 존재21~25이미 존재
Private/Core/VOIDPlayerController.cpp:8~20BeginPlay 에서 CreateWidget + AddToViewport 동작cpp:8~20BindToPlayer 호출 누락 — 보강 필요
Public/Vehicle/VOIDVehicle.h:12~13FOnSlotInstalled(EVOIDVehiclePartType) + FOnRepairComplete() 델리게이트 선언 + Broadcast 동작h:12~13 / cpp:25,30이미 동작
Public/Components/VOIDInventoryComponent.h:10FVOIDOnWeightChanged(float, float) 2-Paramh:10이미 동작
Public/Components/VOIDWeaponComponent.h:10FOnWeaponEquipped(UVOIDWeaponConfig*)h:10HUD 미바인딩 — 추가 필요
Content/VoidUnreal/Maps/Lv_VoidProto.umap, Lv_ZombieTest.umap, NewWorld.umap, ZombieTest.umapLv_Main, Lv_Escape 신규 생성

1. 레벨 전환

1.1 레벨 구성 + 트리거 매트릭스

From → To트리거구현 위치비고
(게임 시작) → Lv_Main프로젝트 기본 맵 (Project Settings)Editor 설정메인 메뉴 GameMode 별도
Lv_MainLv_VoidProto시작 버튼 (WBP_MainMenu) 또는 트리거 박스BP OnClickedOpenLevel("Lv_VoidProto")(선택) — 시간 부족 시 Editor 기본 맵을 Lv_VoidProto
Lv_VoidProtoLv_EscapeAVOIDVehicle::TryStartEngine 성공AVOIDGameMode::HandleEscapeSuccess필수
(Lv_VoidProto 내부) 1F→2F→3F기존 AVOIDFloorTransitionTrigger그대로 유지같은 레벨 안

1.2 GameMode 변경

1.2.1 Public/Core/VOIDGameMode.hHandleEscapeSuccess 선언 추가

변경 전 (라인 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 & ModesEditor Startup Map = Lv_VoidProto (디버깅) / Game Default Map = Lv_Main (또는 시간 부족 시 Lv_VoidProto)
Lv_VoidProto World Settings → GameMode OverrideAVOIDGameMode 유지

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)차량 본체 앞 ETryStartEngine 즉시 false 반환, 레벨 전환 XbRepairComplete 디버그 카메라
3(선택) Lv_Main 시작 버튼마우스 클릭Lv_VoidProto 로딩 + HUD 자동 생성PlayerController 가 Lv_Main 의 메뉴 PC 인지
4Lv_Escape 진입 후(자동)엔딩 텍스트 위젯 표시Lv_Escape GameMode 가 메뉴/엔딩용 PC 사용

2. GUI / HUD

기존 UVOIDHUDWidget 골격은 이미 7종 핸들러 + BindToPlayer 동작. 부족한 두 가지(무기 아이콘 / 수리 진행도)만 헤더+cpp 추가, BP 측 위젯 디자인 작업.

2.1 위젯 분해

위젯 슬롯클래스/위치데이터 소스갱신 방식코드 상태
체력바 (HealthBar)UVOIDHUDWidget::HealthBarUVOIDHealthComponent::OnHealthChangedDelegate이미 바인딩 (cpp:29)
무게 게이지 (WeightBar)UVOIDHUDWidget::WeightBarUVOIDInventoryComponent::OnWeightChangedDelegate (TwoParams: float Total, float Max)이미 바인딩 (cpp:33)
소음 게이지 (NoiseBar)UVOIDHUDWidget::NoiseBarUVOIDNoiseComponent::OnNoiseChangedDelegate이미 바인딩 (cpp:37)
디버프 텍스트 (DebuffText)UVOIDHUDWidget::DebuffTextUVOIDDebuffComponent::OnDebuffUpdatedDelegate이미 바인딩 (cpp:41)
점수 (ScoreText)UVOIDHUDWidget::ScoreTextAVOIDGameState::OnScoreChangedDelegate이미 바인딩 (cpp:16)
웨이브 (WaveText)UVOIDHUDWidget::WaveTextAVOIDGameState::OnWaveChangedDelegate이미 바인딩 (cpp:17)
시간 (TimeText)UVOIDHUDWidget::TimeTextAVOIDGameState::OnTimeChangedDelegate이미 바인딩 (cpp:18)
현재 무기 아이콘UVOIDHUDWidget::WeaponIcon (신설)UVOIDWeaponComponent::OnWeaponEquipped(UVOIDWeaponConfig*)Delegate추가 작업 §2.3
수리 진행도UVOIDHUDWidget::RepairText (신설)AVOIDVehicle::OnSlotInstalled + OnRepairCompleteDelegate추가 작업 §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.cppBindToPlayer 안 추가:

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));
    }
}

UVOIDWeaponConfigIcon(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);
            }
        }
    }
}

AVOIDPlayerControllerUUserWidget* 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 매핑

생성

  1. Content Browser → Content/VoidUnreal/Blueprints/UI/ 폴더 우클릭 → User Interface → Widget Blueprint
  2. Parent Class 선택 창에서 VOIDHUDWidget 선택 (검색창에 VOIDHUDWidget 입력)
  3. 이름 WBP_HUD 로 저장

Designer 위젯 배치 — 이름 정확히 일치 필수 (BindWidget 매핑)

위젯 종류변수명 (Designer 좌측 패널 이름)용도
ProgressBarHealthBar체력 (0~1)
ProgressBarWeightBar무게 (0~1)
ProgressBarNoiseBar소음 (0~1)
TextBlockScoreText점수
TextBlockWaveText“Wave 1” 등
TextBlockTimeText“MM:SS”
TextBlockDebuffText“[출혈] [골절]” 등
ImageWeaponIcon (선택)라이플/샷건 아이콘
TextBlockWeaponNameText (선택)“Rifle” / “Shotgun”
TextBlockRepairText (선택)“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 인스턴스 사용 (권장)

  1. Content Browser → Content/VoidUnreal/Blueprints/Core/ (없으면 생성) → 우클릭 → Blueprint Class → Parent VOIDPlayerController → 이름 BP_VOIDPlayerController
  2. BP 열기 → 좌측 패널 My Blueprint → Variables 가 아닌, Class Defaults (상단 메뉴) 클릭
  3. Details 패널 → UI 카테고리 → HUD Widget Class 드롭다운 → WBP_HUD 선택
  4. Compile + Save

Lv_ZombieTest (또는 Lv_VoidProto) 의 GameMode 지정

  • 이미 AVOIDGameMode 가 있고 PlayerControllerClassAVOIDPlayerController 라면 → 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 두 개에 텍스처/이름 추가:

  1. Content Browser → DA_Rifle, DA_Shotgun 더블클릭
  2. Details 패널 → **WeaponIdentity** 카테고리:
    • Display Name = 소총 / 샷건 (FText, 한국어 OK)
    • Icon = 텍스처 어셋 드래그 (없으면 비워둬도 됨, BP 측에서 빈 Image 처리)
  3. 저장

Icon 텍스처가 없으면 WeaponIcon 슬롯이 자동으로 Hidden 처리됨 (cpp 측에서 처리). DisplayName 비어있으면 WeaponId 가 대신 표기됨.

2.5.4 (선택) WBP_MainMenu — Lv_Main 메인 메뉴

구조

  1. Lv_Main 레벨 신규 생성 (이미 있으면 스킵) — Content Browser 우클릭 → Level → Empty Level → Lv_Main

  2. BP_MenuGameMode 생성
    • Blueprint Class → Parent GameModeBase (반드시 AVOIDGameMode 가 아닌 베이스 클래스 — 메뉴에서 좀비 스폰 막기 위함)
    • Class Defaults:
      • Player Controller Class = BP_MenuPlayerController (다음 단계에서 생성)
      • Default Pawn Class = DefaultPawn 또는 None
      • HUD Class = None
  3. 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: 위 위젯)
      
  4. WBP_MainMenu 위젯
    • User Interface → Widget Blueprint → Parent UserWidget → 이름 WBP_MainMenu
    • Designer:
      • Canvas Panel
      • 가운데 Vertical Box
        • Button Btn_Start — Text “시작”
        • Button Btn_Quit — Text “종료”
    • 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
  5. Lv_Main 의 GameMode 지정
    • Lv_Main 열기 → Window → World Settings → GameMode Override = BP_MenuGameMode
    • 저장
  6. 시작 레벨 변경
    • Edit → Project Settings → Maps & Modes
    • Editor Startup Map = Lv_Main
    • Game Default Map = Lv_Main

2.5.5 (선택) WBP_EscapeEnding — Lv_Escape 엔딩 화면

  1. WBP_EscapeEnding 위젯
    • Parent UserWidget
    • Designer: 검은 배경 + 큰 텍스트 “ESCAPED” + Button Btn_ToMain (“메인 메뉴”)
    • Graph:
      1
      2
      
      OnClicked (Btn_ToMain)
        └ Open Level (by Name)  Level Name: Lv_Main
      
  2. 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::OnDeath Broadcast → AVOIDPlayerController::HandlePlayerDeathAVOIDGameMode::HandleGameOver
  • HandleGameOverGameOverWidgetClass 가 지정돼 있으면 위젯을 viewport 에 추가 + GameOverReturnDelay 초 후 GameOverLevelName 로 OpenLevel

위젯 만들기

  1. User Interface → Widget Blueprint → Parent UserWidget → 이름 WBP_GameOver
  2. Designer:
    • 검은 반투명 배경 (Image + Color Alpha 0.7)
    • 큰 텍스트 “GAME OVER” (가운데, 빨간색)
    • 작은 텍스트 “Returning to main menu in 3 seconds…” (선택)

GameMode 에 위젯 클래스 지정

본편 레벨(Lv_ZombieTest 또는 Lv_VoidProto)의 GameMode 가 AVOIDGameMode 라면:

  1. Content Browser 에서 GameMode BP 가 있으면 그것을, 없으면 BP_VOIDGameMode 신규 생성 (Parent AVOIDGameMode)
  2. Class Defaults → GameOver 카테고리:
    • Game Over Widget Class = WBP_GameOver
    • Game 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)

#입력기대 동작의심 시 확인
1PIE 시작HealthBar 100%, WeightBar 0%, NoiseBar 0%, Wave 1, Time MM:SSBindToPlayer 호출 누락 / BindWidget 이름 불일치
2부품 픽업WeightBar 즉시 갱신OnWeightChanged.Broadcast 누락 (DataAsset Weight=0?)
31키 → 라이플 / 2키 → 샷건WeaponIcon 토글OnWeaponEquipped.AddDynamic 누락, Icon 필드 nullptr
4Battery 슬롯 설치RepairText 0/31/3BindToVehicle 미호출 (PC ↔ Vehicle 타이밍)
53슬롯 설치 완료RepairText 3/3 ✓ Press E to StartOnRepairComplete.Broadcast (VOIDVehicle.cpp:30) 동작
6좀비 피격HealthBar 즉시 감소DebuffComponent Bleeding 발동 시 DebuffText [출혈]
7웨이브 종료WaveText Wave 1 → Wave 2, TimeText 리셋AVOIDGameState::SetCurrentWave (이미 호출됨)

3. 작업 순서 (마감 D-2)

순번작업예상 시간의존성우선순위
1AVOIDGameMode::HandleEscapeSuccess 헤더+cpp 신설15분P0
2AVOIDVehicle::TryStartEngine cpp:38 TODO 제거 + GM 캐스팅 호출10분1P0
3Lv_Escape 빈 레벨 생성 + GameMode 지정10분P0
4레벨 전환 PIE 검증 (§1.4 1·2번)10분1+2+3P0
5AVOIDPlayerController::BeginPlay BindToPlayer 호출 + OnPossess 보강15분P0
6WBP_HUD 디자인 + 기존 7개 슬롯 BindWidget30분5P0
7HUD WeaponIcon / RepairText 헤더+cpp 추가30분P1
8AVOIDPlayerController::GetHUDWidgetInstance getter + AVOIDVehicle::BeginPlay BindToVehicle 호출15분7P1
9WBP_HUD 에 WeaponIcon/RepairText 슬롯 추가 + BP 디자인20분7P1
10HUD 전체 PIE 검증 (§2.6 전부)20분5+6+7+8+9P0
11(선택) Lv_Main + WBP_MainMenu30분3P2
12(선택) Lv_Escape 엔딩 위젯20분3P2

총합: P0 만 약 2시간, P0+P1 약 3시간 30분, 전체 약 4시간 20분.


4. 블로커 / 리스크

항목영향완화책
OpenLevel 직후 World cleanup → Driver 포인터 무효화HandleEscapeSuccess 내부에서 Driver 사용 시 크래시OpenLevel 직전에 필요한 정보 모두 캐싱(이름·점수 등). 본 구현은 로그만 — 안전
BeginPlay 시점에 GetPawn() nullptrBindToPlayer 미호출 → 게이지 안 움직임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_HUD BindWidget 이름 매핑 모두 동작 (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. 참고


7. 디버깅 기록 (작업 중 추가)

PIE 도중 발견 사항을 시간순으로 기록.

04-29 코드 적용 완료 (게임오버 추가)

  • VOIDGameMode.h/cppHandleGameOver() + GameOverLevelName/Delay/WidgetClass 추가, 위젯 표시 + GameOverReturnDelayOpenLevel(Lv_Main)
  • VOIDPlayerController.h/cppOnPossess 에서 HealthComponent->OnDeath 바인딩 + HandlePlayerDeath → GameMode 위임
  • VOIDHUDWidget.cppNativeConstruct / BindToPlayer 에서 GameState 와 컴포넌트 현재값으로 핸들러 1회 직접 호출 (바인딩 이전 Broadcast 보충)

04-29 코드 적용 완료

  • VOIDGameMode.h/cppHandleEscapeSuccess(AActor* Driver) + EscapeLevelName=Lv_Escape 추가
  • VOIDVehicle.cppTryStartEngine TODO 제거 + GameMode 캐스팅 호출 + BeginPlay 신설 (1 tick 지연 후 HUD BindToVehicle)
  • VOIDVehicle.hBeginPlay override 선언
  • VOIDPlayerController.h/cppOnPossess override + GetHUDWidgetInstance() getter, BeginPlay 에서 HUD BindToPlayer 호출
  • VOIDHUDWidget.h/cppBindToVehicle + HandleWeaponEquipped/HandleSlotInstalled/HandleRepairComplete + WeaponIcon/WeaponNameText/RepairText (BindWidgetOptional) 추가
  • VOIDWeaponConfig.hDisplayName(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개만 유지
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.