포스트

TIL 2026-04-29

TIL 2026-04-29

2026-04-29 8번 과제 종결 (Day 5) — 차량 슬롯 마무리 + 레벨 전환 + HUD 4페이지 + 디버프 발동

목차


오늘 한 일 요약

  1. 차량 부품 슬롯 PIE 검증 중 6개 블로커 연쇄 해소 — 헬기 콜리전 / ChildActor Mobility / AddIgnoredActor 루프 / Sweep 차단 fallback / OwnerVehicle 자동 검색 / Slot 변수명 충돌
  2. HandleEscapeSuccess 본 구현TryStartEngine 성공 시 GameMode 가 OpenLevel(Lv_Escape) 호출
  3. 새 레벨 3개 신설Lv_Main (메뉴) / Lv_VoidProto (본편) / Lv_Escape (탈출 엔딩)
  4. 10분 탈출 타이머 — GameMode 가 1초 Tick 으로 GameState OnTimeChanged Broadcast → HUD TimeText 갱신, 만료 시 HandleGameOver
  5. GameStateClass 생성자 강제 — BP 캐싱 회귀 차단, 어떤 환경에서도 AVOIDGameState 사용 보장
  6. HUD 4페이지 모드 — 단일 WBP_HUDLv_1/Lv_2 두 컨테이너로 MainMenu / InGame / GameOver / GameClear 통합
  7. 자동 모드 결정NativeConstruct 가 현재 레벨명 보고 모드 자동 선택, PlayerController 측 추가 설정 0개
  8. GameOver 는 같은 레벨에서 모드만 토글OpenLevel 없이 시체/플레이 상태 배경 유지, ReStart 시점에야 레벨 리셋
  9. HUD 버튼 자동 바인딩AddUniqueDynamic 으로 BP 그래프 OnClicked 노드 0개, 재바인딩 안전
  10. Exit 버튼 레벨별 분기 — Lv_Main / Lv_Escape 에서는 QuitGame, 그 외엔 OpenLevel(Lv_Main)
  11. 바인딩 이전 Broadcast 보충 패턴NativeConstruct / BindToPlayer 에서 현재값으로 핸들러 1회 직접 호출 → PIE 시작 직후 디폴트 텍스트 회귀 차단
  12. 인벤토리 스택 — 같은 ItemData 면 Quantity 누적 (MaxStackPerItem=2), 슬롯 무한 점유 방지
  13. 좀비 처치 점수AVOIDZombieCharacter::BeginPlay 가 자기 OnDeath → AVOIDGameState::AddScore(10)
  14. UVOIDWeaponConfig 메타 추가DisplayName(FText) + Icon(UTexture2D*) → HUD WeaponNameText/WeaponIcon 데이터 소스
  15. 시작 무기 = 샷건 + 좀비 데미지 20 → 10 — 디버프 시스템과 페어링, 발동 체감 가능한 데미지 윈도우 확보
  16. 디버프 트리거 연결 — 무게 비율 ≥ 50% Overweight, 피격 시 30% Bleeding(5s) + 30% Fracture(10s)
  17. Bleeding 도트 + 이동속도 곱셈 — Tick 에서 1초마다 -1 HP, MaxWalkSpeed *= GetMoveSpeedMultiplier() 매 프레임 반영
  18. 게임오버 처리 — Health OnDeath → PlayerController → GameMode → HUD SetHUDMode(GameOver) 위임 체인
  19. HealthComponent 누락 fallbackAVOIDBaseCharacter::BeginPlay 에서 NewObject + RegisterComponent 로 런타임 복구
  20. 5개 파트별 커밋 분리 — Vehicle / GameMode / HUD / Inventory+Score / Debuff+Health
  21. CS 모의면접 (vector vs list) — capacity·캐시 라인·iterator 무효화·Big-O 키워드 정리


차량 부품 슬롯 마무리 — 블로커 6개 연쇄 해소

08:00 PIE 검증 시작 직후부터 다단계로 막혔다. 어제 콜리전·PartType 매칭은 끝났다고 생각했지만, 실제로 시동 분기 진입까지 6개 회귀가 추가로 잡혔다.

블로커 1 — 헬기 시각 메시 콜리전 분리

증상: BP_VoidVehicleBody 콜리전이 Visibility=Block 이라 차량 본체 앞에서 SweepMultiByChannel 이 차량에서 끊김 → 슬롯이 후보 목록에 포함되지 않음.

해결: Body 콜리전 프리셋을 Custom 으로 전환, Visibility=Ignore + Pawn=Block. Sweep 는 차량을 통과하되 캐릭터는 차에 막힘.

블로커 2 — ChildActorComponent Mobility 충돌

1
LogActor: AttachTo: ... is not static, cannot attach ... Aborting.

원인: 헬기 시각 메시들 (lowpoly_heli_node_* StaticMeshActor) 이 Static, 부모 BP_VoidVehicle 가 Movable. Static 자식이 Movable 부모에 attach 불가.

해결: 헬기 시각 메시는 BP 에서 들어내고 레벨에 직접 배치. BP 에는 Body + StartEngineVolume + 슬롯 3개만 남김.

블로커 3 — AddIgnoredActor(AVOIDVehicle) 루프가 시동 분기 영구 차단

1
2
3
4
5
// 어제 작성, 오늘 제거 (VOIDPlayerCharacter.cpp:172~176)
for (TActorIterator<AVOIDVehicle> It(GetWorld()); It; ++It)
{
    Params.AddIgnoredActor(*It);
}

어제 헬기 메시 컷용으로 추가한 TActorIterator<AVOIDVehicle> AddIgnoredActor 루프가 지금은 차량 본체까지 무시함 → Cast<AVOIDVehicle>(Best) 가 영구 실패해 시동 분기 진입 불가.

해결: 4줄 통째로 삭제.

블로커 4 — InteractionVolume 이 Sweep 차단 → 차량 fallback

슬롯의 InteractionVolumeVisibility=Block → 슬롯을 모두 채워도 그 다음 단계인 차량 본체가 Sweep Best 후보로 안 잡힘.

해결: Interact() 후보 검색 끝난 뒤 Best == nullptr 이면 5m 이내 AVOIDVehicle 직접 검색 fallback 추가.

1
2
3
4
5
6
7
8
9
10
11
12
if (!Best)
{
    // Sweep 가 슬롯에서 끊겨도 차량 본체 fallback
    for (TActorIterator<AVOIDVehicle> It(World); It; ++It)
    {
        if (FVector::Dist(It->GetActorLocation(), Origin) <= 500.f)
        {
            Best = *It;
            break;
        }
    }
}

블로커 5 — 슬롯 OwnerVehicle 가 BP 자식 노드에서 설정 불가

EditInstanceOnly 로 선언된 OwnerVehicle 가 BP_VoidVehicle 의 자식 ChildActorComponent 에서 회색 비활성 → 결국 bRepairComplete=false 로 영구 고정.

해결: TryInstallPart_Implementation 끝부분에 OwnerVehicle 미설정 시 GetParentActor() 체인을 따라 AVOIDVehicle 자동 검색.

1
2
3
4
5
6
7
8
9
if (!IsValid(OwnerVehicle))
{
    AActor* Parent = GetParentActor();
    while (Parent && !OwnerVehicle)
    {
        OwnerVehicle = Cast<AVOIDVehicle>(Parent);
        Parent = Parent->GetParentActor();
    }
}

블로커 6 — Slot 변수가 UWidget::Slot 가림 (C4458)

HUD 측 for (auto& Slot : Inventory->GetSlots())UWidget::Slot 멤버를 가린다는 컴파일 경고. WarningsAsErrors 가 켜져 있어서 빌드 실패.

해결: 루프 변수명 InvSlot 으로 rename. 수정량 적은 쪽으로 회피.

결과

슬롯 3개 설치 → 무게 감소 → bRepairComplete=true → 차량 앞 E → 시동 분기 진입 → 로그:

1
2
3
[Vehicle] Engine started by BP_VOIDPlayerCharacter_C_0
[VOID] HandleEscapeSuccess by BP_VOIDPlayerCharacter_C_0 — OpenLevel(Lv_Escape)
LogWorld: SeamlessTravel ...

Lv_Escape 로딩까지 끝까지 통과.



레벨 전환 시스템 (Lv_Main / Lv_VoidProto / Lv_Escape)

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 에 옛날 값 캐싱” 회귀를 원천 차단.



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 줄바꿈 누적


좀비 처치 점수 + 인벤토리 스택 + 무기 메타

좀비 처치 → 점수

AVOIDZombieCharacter::BeginPlay 에서 자기 HealthComponent OnDeath 바인딩 → AVOIDGameState::AddScore(ScoreReward) 호출. ScoreReward=10 디폴트.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void AVOIDZombieCharacter::BeginPlay()
{
    Super::BeginPlay();
    if (HealthComponent)
    {
        HealthComponent->OnDeath.AddDynamic(this, &AVOIDZombieCharacter::HandleDeath);
    }
}

void AVOIDZombieCharacter::HandleDeath()
{
    if (auto* GS = GetWorld()->GetGameState<AVOIDGameState>())
    {
        GS->AddScore(ScoreReward);
    }
}

인벤토리 스택 (같은 ItemData 누적)

UVOIDInventoryComponent::TryAddItem 가 기존엔 무조건 새 슬롯 사용 → 슬롯 5칸이라 부품 6개 들면 Drop. 같은 ItemData 면 Quantity 누적하도록 변경.

1
2
3
4
5
6
7
8
9
10
for (FVOIDInventorySlot& InvSlot : Slots)
{
    if (InvSlot.ItemData == ItemData && InvSlot.Quantity < MaxStackPerItem)
    {
        InvSlot.Quantity += 1;
        OnWeightChanged.Broadcast(GetTotalWeight(), MaxWeight);
        return true;
    }
}
// 같은 항목 슬롯 없으면 새 슬롯 점유

MaxStackPerItem=2. 8번 과제 스펙상 슬롯 수가 제한되어 있어서 무한 스택은 원하지 않음.

UVOIDWeaponConfig 메타 필드 추가

HUD WeaponNameText / WeaponIcon 슬롯의 데이터 소스용.

1
2
3
4
5
UPROPERTY(EditDefaultsOnly, Category="Weapon|Identity")
FText DisplayName;

UPROPERTY(EditDefaultsOnly, Category="Weapon|Identity")
TObjectPtr<UTexture2D> Icon;

시작 무기 = 샷건 + 좀비 데미지 10

  • AVOIDPlayerCharacter::BeginPlay 에서 ShotgunConfig 우선 장착, nullptr 일 때만 RifleConfig 폴백
  • 좀비 AttackDamage 20 → 10 — 디버프 시스템과 페어링하기 위함. 데미지 자체가 너무 크면 디버프 발동을 체감하기 전에 사망


디버프 시스템 발동 (Bleeding / Fracture / Overweight)

UVOIDDebuffComponent 코드는 어제 완성됐지만 호출 트리거가 없어 사실상 dormant 상태였다. 오늘 두 트리거를 연결해 실제 발동까지 검증.

트리거 1 — 무게로 Overweight

1
2
3
4
5
6
7
8
void AVOIDPlayerCharacter::OnInventoryWeightChanged(float Total, float Max)
{
    const float Ratio = (Max > 0.f) ? Total / Max : 0.f;
    if (DebuffComponent)
    {
        DebuffComponent->UpdateOverweightFromInventory(Ratio);
    }
}

UpdateOverweightFromInventoryRatio >= 0.5f 이면 Overweight 토글 ON, 미만이면 OFF. 이동 속도 곱셈에 자동 반영.

트리거 2 — 피격 시 Bleeding / Fracture 확률 발동

1
2
3
4
5
6
7
8
9
void AVOIDPlayerCharacter::OnPlayerHealthChanged(float NewHP, float MaxHP)
{
    if (NewHP < LastHP)  // 데미지일 때만
    {
        if (FMath::FRand() < 0.3f) DebuffComponent->ApplyDebuff(EVOIDDebuffType::Bleeding, 5.0f);
        if (FMath::FRand() < 0.3f) DebuffComponent->ApplyDebuff(EVOIDDebuffType::Fracture, 10.0f);
    }
    LastHP = NewHP;
}

설계 결정: 디버프 적용 책임은 좀비(데미지 소스) 가 아닌 플레이어 자신. 이유 3개.

  1. 데미지 소스 비종속 — 트랩/낙하/굶주림 어떤 소스든 동일한 확률 분포 적용
  2. 단일 책임 — 플레이어가 자기 상태를 안다
  3. 튜닝 용이 — 확률·지속시간 한 곳에서 조정

Tick — Bleeding 도트 + 이동 속도 곱셈 적용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void AVOIDPlayerCharacter::Tick(float dt)
{
    Super::Tick(dt);
    // ...
    if (DebuffComponent && DebuffComponent->IsBleeding())
    {
        BleedAccum += dt;
        if (BleedAccum >= 1.0f)
        {
            HealthComponent->ApplyDamage(1.f);
            BleedAccum -= 1.0f;
        }
    }
    if (auto* Move = GetCharacterMovement())
    {
        Move->MaxWalkSpeed = BaseSpeed * DebuffComponent->GetMoveSpeedMultiplier();
    }
}


게임오버 처리

흐름

  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 가 나간 시점이 명확히 식별돼 게임오버 미발동 회귀 진단이 빨라진다.



코드 커밋 (5 파트 분리)

#해시메시지범위
18bc8fe2Finish vehicle part slot system — engine start branch + auto-find OwnerVehicle차량 슬롯 마무리
28d73ff7GameMode flow — escape/gameover level transitions + 10min escape timer레벨 전환 + 타이머 + 게임오버
3405a083HUD system — 4-mode panel (Menu/InGame/GameOver/GameClear) + auto level routingHUD 4페이지
4c9f5238Inventory stacking + score system + zombie balance pass인벤토리 스택 + 점수
596961c1Debuff system trigger + health logging + interact sweep fallback + default shotgun디버프 + Health + 시작 무기


CS — std::vector vs std::list 모의면접

어제(04-28) 작성한 raw/cs-notion/13_vector_vs_list.md 기반 답변. 정리한 키워드:

  • size vs capacity (논리적 원소 수 vs 할당된 슬롯 수)
  • 캐시 라인 64B + 공간/시간 지역성 — vector 의 연속 메모리 prefetch 이점
  • vector 재할당 동작 + reserve(N) 으로 사전 예약 → 재할당 늦추기
  • growth factor 2배 확장으로 amortized O(1)
  • iterator 무효화: vector 재할당 시 전체 / list / map 은 삭제 노드만
  • Big-O 표기법 (평균/최악)

내일(04-30) 모의면접 주제는 std::map. raw/cs-notion/14_std_map.md 작성 예정 (Red-Black Tree + unordered_map / set / multimap 비교).



Day 5 진행 정리 (8번 과제 종결)

항목상태비고
인벤토리 시스템무게 / 픽업 / 스택 (MaxStackPerItem=2) / 부품 매칭
무기 시스템라이플 / 샷건 / 반동 / ADS, 시작 무기 = 샷건
웨이브 / 스폰볼륨 / 좀비 AI비동기 가시선 (어제) + 좀비 처치 점수 (오늘)
차량 부품 슬롯 + 시동 분기블로커 6개 해소 → bRepairCompleteTryStartEngine
레벨 전환Lv_Main → Lv_VoidProto → Lv_Escape, HandleEscapeSuccess
10분 탈출 타이머EscapeTimeLimit=600, 만료 시 GameOver
HUD 4페이지 모드MainMenu / InGame / GameOver / GameClear, 자동 모드 결정
게임오버 / 게임클리어OnDeath → PC → GM → HUD 위임 체인, ReStart / Exit 분기
디버프 시스템 발동Bleeding 30%/5s, Fracture 30%/10s, Overweight 무게비율 ≥50%
Bleeding 도트 + 이동 속도 곱셈Tick 에서 1초마다 -1 HP + MaxWalkSpeed *= Mult
HealthComponent 런타임 fallbackBP 누락 시 NewObject + RegisterComponent
5개 파트별 커밋 분리Vehicle / GameMode / HUD / Inventory+Score / Debuff+Health

Day 5 → 마감(05-01) 인계:

  • 마감(05-01) 까지 남은 일은 회귀 sweep + 마스터 과제 별도 레포 제출 두 가지뿐
  • 8번 과제 본체 기능은 오늘 모두 닫음 — 마감 D-2 였는데 D-2 → D 0 조기 마감
  • 05-01 부터 시작될 4주 팀플 (팀장 역할) — 8번 과제 코드를 베이스로 어떤 모듈을 살리고 어떤 걸 일반화할지 정리 필요


오늘 배운 것 정리

  1. 회귀는 어제 코드에서 나온다. 어제 헬기 메시 컷용으로 추가한 AddIgnoredActor 루프(블로커 3) 가 오늘 시동 분기 영구 차단의 원인이었다. “어제 정상이었으니 오늘도 정상” 가정은 위험. 검증 시점에 어제 추가분도 다시 의심해야 한다.

  2. 콜리전은 채널별 응답을 명시적으로. BlockAll 프리셋은 디버깅 지옥의 시작. 차량 본체는 Visibility=Ignore + Pawn=Block 처럼 채널별로 의미 분리해야 Sweep / 캐릭터 충돌이 양립한다. 트레이스 채널과 캐릭터 충돌 채널은 다른 의미.

  3. BP 캐싱 회귀는 C++ 생성자로 차단. GameStateClass 처럼 핵심 클래스는 BP 측 지정에 의존하지 말고 생성자에서 강제. BP 가 옛날 값을 들고 있어도 무력화. “코드가 단일 진실 공급원” 원칙.

  4. UI 컨테이너 분리 vs 위젯 분리. 4 화면을 위젯 4개로 만드는 대신 Lv_1 + Lv_2 두 컨테이너로 통합한 결정이 PlayerController 측 코드를 간단하게 만든다. swap 로직 0개. enum 토글 1줄이 위젯 swap 보다 훨씬 가볍다.

  5. 델리게이트는 바인딩 이전 Broadcast 가 사라진다. NativeConstruct 시점은 이미 BeginPlay Broadcast 가 끝난 후. 바인딩 직후 현재값으로 핸들러 1회 직접 호출하는 패턴을 표준화해야 디폴트 텍스트 회귀가 안 생긴다. 이벤트 시스템에서 흔한 함정.

  6. 단일 책임 — 디버프는 자기 자신이 안다. 좀비가 디버프를 발생시키는 게 아니라 플레이어가 자기 피격 이벤트를 듣고 자기 디버프를 켠다. 데미지 소스 비종속 + 한 곳에서 튜닝 + 트랩/낙하/굶주림 어떤 소스에도 자동 적용.

  7. AddUniqueDynamic 패턴. AddDynamic 은 동일 핸들러 중복 바인딩이 가능해서 NativeConstruct 두 번 호출되면 OnClicked 두 번 발화. AddUniqueDynamic 은 자동 dedup → 재바인딩 안전. 라이프사이클 함수에서 델리게이트 등록할 때 디폴트로 사용.

  8. GameOver 는 레벨 전환이 아니다. OpenLevel 호출하면 시체/플레이 상태가 다 사라져서 “내가 어디서 죽었지?” 가 사라진다. 같은 레벨에서 SetHUDMode(GameOver) 만 토글하면 시체 + 좀비 + 인벤토리 상태가 배경에 남아 몰입감 유지. ReStart 시점에야 레벨 리셋.

  9. 블로커 6개를 한 페이즈에 잡았다. 작은 회귀가 누적되면 수십 분씩 추적 시간이 늘어나는데, 로그 + 가드 추가 + fallback 패턴 3종을 함께 적용해 Day 5 안에 클로즈. 실제 디버깅에서 “한 번에 한 가설씩 검증” 보다 “여러 가설을 동시에 잡고 로그로 좁히기” 가 빠를 때가 있다.

  10. 5 파트 분리 커밋 = 코드 리뷰 비용 1/5. 한 거대한 커밋으로 묶으면 리뷰어가 어디서 어떤 변경이 났는지 추적 어렵다. 5개 파트로 나누고 각 파트 메시지에 범위 명시 → 차후 PR 분할도 그대로 가능. 커밋은 단위 작업 = 리뷰 단위 = 롤백 단위.

  11. Slot 같은 흔한 변수명은 부모 멤버를 가린다. UWidget::Slot 같은 부모 멤버가 있는 클래스에서 같은 이름의 로컬 변수는 C4458 경고. WarningsAsErrors 환경에선 빌드 실패. 짧은 변수명도 컨텍스트에서는 의미 있는 prefix (InvSlot 등) 가 필요할 때가 있다.

  12. 8번 과제 종결 (D-2 → D 0 조기 마감). 마감 D-2 (05-01) 였는데 오늘 안에 차량 슬롯 + 레벨 전환 + HUD + 게임오버/클리어 + 디버프까지 다 닫음. 내일은 회귀 검증 + 마스터 과제 제출 + 팀플 준비로 전환 가능. 일정 여유는 다음 단계 준비에 쓴다.

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