TIL 2026-04-29
2026-04-29 8번 과제 종결 (Day 5) — 차량 슬롯 마무리 + 레벨 전환 + HUD 4페이지 + 디버프 발동
목차
- 2026-04-29 8번 과제 종결 (Day 5) — 차량 슬롯 마무리 + 레벨 전환 + HUD 4페이지 + 디버프 발동
오늘 한 일 요약
- 차량 부품 슬롯 PIE 검증 중 6개 블로커 연쇄 해소 — 헬기 콜리전 / ChildActor Mobility /
AddIgnoredActor루프 / Sweep 차단 fallback /OwnerVehicle자동 검색 /Slot변수명 충돌 HandleEscapeSuccess본 구현 —TryStartEngine성공 시 GameMode 가OpenLevel(Lv_Escape)호출- 새 레벨 3개 신설 —
Lv_Main(메뉴) /Lv_VoidProto(본편) /Lv_Escape(탈출 엔딩) - 10분 탈출 타이머 — GameMode 가 1초 Tick 으로 GameState
OnTimeChangedBroadcast → HUDTimeText갱신, 만료 시HandleGameOver GameStateClass생성자 강제 — BP 캐싱 회귀 차단, 어떤 환경에서도AVOIDGameState사용 보장- HUD 4페이지 모드 — 단일
WBP_HUD안Lv_1/Lv_2두 컨테이너로MainMenu / InGame / GameOver / GameClear통합 - 자동 모드 결정 —
NativeConstruct가 현재 레벨명 보고 모드 자동 선택, PlayerController 측 추가 설정 0개 - GameOver 는 같은 레벨에서 모드만 토글 —
OpenLevel없이 시체/플레이 상태 배경 유지, ReStart 시점에야 레벨 리셋 - HUD 버튼 자동 바인딩 —
AddUniqueDynamic으로 BP 그래프 OnClicked 노드 0개, 재바인딩 안전 - Exit 버튼 레벨별 분기 — Lv_Main / Lv_Escape 에서는
QuitGame, 그 외엔OpenLevel(Lv_Main) - 바인딩 이전 Broadcast 보충 패턴 —
NativeConstruct/BindToPlayer에서 현재값으로 핸들러 1회 직접 호출 → PIE 시작 직후 디폴트 텍스트 회귀 차단 - 인벤토리 스택 — 같은 ItemData 면
Quantity누적 (MaxStackPerItem=2), 슬롯 무한 점유 방지 - 좀비 처치 점수 —
AVOIDZombieCharacter::BeginPlay가 자기 OnDeath →AVOIDGameState::AddScore(10) UVOIDWeaponConfig메타 추가 —DisplayName(FText)+Icon(UTexture2D*)→ HUDWeaponNameText/WeaponIcon데이터 소스- 시작 무기 = 샷건 + 좀비 데미지 20 → 10 — 디버프 시스템과 페어링, 발동 체감 가능한 데미지 윈도우 확보
- 디버프 트리거 연결 — 무게 비율 ≥ 50% Overweight, 피격 시 30% Bleeding(5s) + 30% Fracture(10s)
- Bleeding 도트 + 이동속도 곱셈 — Tick 에서 1초마다 -1 HP,
MaxWalkSpeed *= GetMoveSpeedMultiplier()매 프레임 반영 - 게임오버 처리 — Health OnDeath → PlayerController → GameMode → HUD
SetHUDMode(GameOver)위임 체인 HealthComponent누락 fallback —AVOIDBaseCharacter::BeginPlay에서NewObject+RegisterComponent로 런타임 복구- 5개 파트별 커밋 분리 — Vehicle / GameMode / HUD / Inventory+Score / Debuff+Health
- CS 모의면접 (vector vs list) — capacity·캐시 라인·iterator 무효화·Big-O 키워드 정리
차량 부품 슬롯 마무리 — 블로커 6개 연쇄 해소
08:00 PIE 검증 시작 직후부터 다단계로 막혔다. 어제 콜리전·PartType 매칭은 끝났다고 생각했지만, 실제로 시동 분기 진입까지 6개 회귀가 추가로 잡혔다.
블로커 1 — 헬기 시각 메시 콜리전 분리
증상: BP_VoidVehicle 의 Body 콜리전이 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
슬롯의 InteractionVolume 도 Visibility=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 |
|---|---|---|---|---|
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줄바꿈 누적
좀비 처치 점수 + 인벤토리 스택 + 무기 메타
좀비 처치 → 점수
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폴백- 좀비
AttackDamage20 → 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);
}
}
UpdateOverweightFromInventory 가 Ratio >= 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개.
- 데미지 소스 비종속 — 트랩/낙하/굶주림 어떤 소스든 동일한 확률 분포 적용
- 단일 책임 — 플레이어가 자기 상태를 안다
- 튜닝 용이 — 확률·지속시간 한 곳에서 조정
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();
}
}
게임오버 처리
흐름
- 좀비가 플레이어를 공격 →
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 가 나간 시점이 명확히 식별돼 게임오버 미발동 회귀 진단이 빨라진다.
코드 커밋 (5 파트 분리)
| # | 해시 | 메시지 | 범위 |
|---|---|---|---|
| 1 | 8bc8fe2 | Finish vehicle part slot system — engine start branch + auto-find OwnerVehicle | 차량 슬롯 마무리 |
| 2 | 8d73ff7 | GameMode flow — escape/gameover level transitions + 10min escape timer | 레벨 전환 + 타이머 + 게임오버 |
| 3 | 405a083 | HUD system — 4-mode panel (Menu/InGame/GameOver/GameClear) + auto level routing | HUD 4페이지 |
| 4 | c9f5238 | Inventory stacking + score system + zombie balance pass | 인벤토리 스택 + 점수 |
| 5 | 96961c1 | Debuff 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개 해소 → bRepairComplete → TryStartEngine |
| 레벨 전환 | ✅ | 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 런타임 fallback | ✅ | BP 누락 시 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번 과제 코드를 베이스로 어떤 모듈을 살리고 어떤 걸 일반화할지 정리 필요
오늘 배운 것 정리
회귀는 어제 코드에서 나온다. 어제 헬기 메시 컷용으로 추가한
AddIgnoredActor루프(블로커 3) 가 오늘 시동 분기 영구 차단의 원인이었다. “어제 정상이었으니 오늘도 정상” 가정은 위험. 검증 시점에 어제 추가분도 다시 의심해야 한다.콜리전은 채널별 응답을 명시적으로.
BlockAll프리셋은 디버깅 지옥의 시작. 차량 본체는Visibility=Ignore+Pawn=Block처럼 채널별로 의미 분리해야 Sweep / 캐릭터 충돌이 양립한다. 트레이스 채널과 캐릭터 충돌 채널은 다른 의미.BP 캐싱 회귀는 C++ 생성자로 차단.
GameStateClass처럼 핵심 클래스는 BP 측 지정에 의존하지 말고 생성자에서 강제. BP 가 옛날 값을 들고 있어도 무력화. “코드가 단일 진실 공급원” 원칙.UI 컨테이너 분리 vs 위젯 분리. 4 화면을 위젯 4개로 만드는 대신
Lv_1 + Lv_2두 컨테이너로 통합한 결정이 PlayerController 측 코드를 간단하게 만든다. swap 로직 0개. enum 토글 1줄이 위젯 swap 보다 훨씬 가볍다.델리게이트는 바인딩 이전 Broadcast 가 사라진다.
NativeConstruct시점은 이미BeginPlayBroadcast 가 끝난 후. 바인딩 직후 현재값으로 핸들러 1회 직접 호출하는 패턴을 표준화해야 디폴트 텍스트 회귀가 안 생긴다. 이벤트 시스템에서 흔한 함정.단일 책임 — 디버프는 자기 자신이 안다. 좀비가 디버프를 발생시키는 게 아니라 플레이어가 자기 피격 이벤트를 듣고 자기 디버프를 켠다. 데미지 소스 비종속 + 한 곳에서 튜닝 + 트랩/낙하/굶주림 어떤 소스에도 자동 적용.
AddUniqueDynamic패턴.AddDynamic은 동일 핸들러 중복 바인딩이 가능해서NativeConstruct두 번 호출되면 OnClicked 두 번 발화.AddUniqueDynamic은 자동 dedup → 재바인딩 안전. 라이프사이클 함수에서 델리게이트 등록할 때 디폴트로 사용.GameOver 는 레벨 전환이 아니다.
OpenLevel호출하면 시체/플레이 상태가 다 사라져서 “내가 어디서 죽었지?” 가 사라진다. 같은 레벨에서SetHUDMode(GameOver)만 토글하면 시체 + 좀비 + 인벤토리 상태가 배경에 남아 몰입감 유지. ReStart 시점에야 레벨 리셋.블로커 6개를 한 페이즈에 잡았다. 작은 회귀가 누적되면 수십 분씩 추적 시간이 늘어나는데, 로그 + 가드 추가 + fallback 패턴 3종을 함께 적용해 Day 5 안에 클로즈. 실제 디버깅에서 “한 번에 한 가설씩 검증” 보다 “여러 가설을 동시에 잡고 로그로 좁히기” 가 빠를 때가 있다.
5 파트 분리 커밋 = 코드 리뷰 비용 1/5. 한 거대한 커밋으로 묶으면 리뷰어가 어디서 어떤 변경이 났는지 추적 어렵다. 5개 파트로 나누고 각 파트 메시지에 범위 명시 → 차후 PR 분할도 그대로 가능. 커밋은 단위 작업 = 리뷰 단위 = 롤백 단위.
Slot같은 흔한 변수명은 부모 멤버를 가린다.UWidget::Slot같은 부모 멤버가 있는 클래스에서 같은 이름의 로컬 변수는 C4458 경고. WarningsAsErrors 환경에선 빌드 실패. 짧은 변수명도 컨텍스트에서는 의미 있는 prefix (InvSlot등) 가 필요할 때가 있다.8번 과제 종결 (D-2 → D 0 조기 마감). 마감 D-2 (05-01) 였는데 오늘 안에 차량 슬롯 + 레벨 전환 + HUD + 게임오버/클리어 + 디버프까지 다 닫음. 내일은 회귀 검증 + 마스터 과제 제출 + 팀플 준비로 전환 가능. 일정 여유는 다음 단계 준비에 쓴다.