포스트

TIL 2026-05-22

TIL 2026-05-22

2026-05-22 Ch3 Portal 분기 시스템 + 좀비→플레이어 피격 + 시각 피드백 3종(카메라 셰이크·HP 게이지·비네팅)

목차


오늘 한 일 요약

  1. Portal 레벨 분기 시스템 — 단일 TargetLevelNameNormalLevelName/RewardLevelName/BossLevelName + NormalLevelLoopCount(5) 4개 디테일 멤버로 교체. 일반↔보상 토글 + 5사이클 후 보스맵 자동 진입. NBC_GameInstanceNormalLevelsCleared int 카운터와 bIsInRewardLevel bool 토글을 영속화해서 OpenLevel로 GameState가 매번 재생성돼도 진행도가 살아남게. PR #50
  2. 무한 보상맵 루프 버그 추적·수정Portal::OnPortalOverlapRewardComp->SaveWeaponsToInstance()GI->ClearSavedData()가 매 포털마다 호출되며 bIsInRewardLevel을 false로 리셋시키는 부작용 발견. 진행도 리셋을 ClearSavedData에 끼워넣은 게 원인 → ResetProgression() 별도 분리, ClearSavedData는 무기 전용으로 복구
  3. UTF-8 BOM·한글 주석 복원 — CP949로 깨진 4개 파일을 PowerShell [System.Text.UTF8Encoding]::new($true)로 BOM 강제. 의미 추정 복원에 [장식 복원], 신규 작성에 [장식 추가]/[장식 수정] 표기로 작성자 분리
  4. 총기 반동 파라미터 PR 분리 — Portal 작업에 무기 BP 3개(Revolver/Rifle/Shotgun) 반동 수정이 섞여 들어갔다가, 별도 브랜치 feat/weapon-recoil 생성 + cherry-pick으로 PR #52 단독 분리
  5. 좀비 → 플레이어 피격 시스템WeaponComponent::ApplyHitDamageFindComponentByClass<UHealthComponent> 동적 위임 패턴을 미러링. PlayerCharacter에 UHealthComponent C++ 부착(BP 컴포넌트 제거)·OnPlayerDeath → NBC_GameMode::OnPlayerDied 위임 + BTTask_Attack에 거리(180f)·각도(35도) 게이트 + ApplyDamage(10) 호출. ZombieCharacter에 AttackDamage/AttackRange/AttackHalfAngleDeg EditAnywhere 노출. LogCombat 커스텀 카테고리로 Output Log 분리. PR #54 머지됨
  6. 시각 피드백 3종 — (a) UDamageCameraShake C++ 완결 — ULegacyCameraShake 파생, 생성자에서 Pitch 2.5·Yaw 1.5·Loc Y 1.0 진동 + 0.25초, PlayerCharacter 생성자에서 StaticClass() 기본 할당으로 BP 작업 0. (b) WBP_HUD OnPlayerHPUpdated 커스텀 이벤트 + OnHealthChanged 바인딩으로 HP 게이지·텍스트 갱신. (c) M_Vignette 머티리얼 — User Interface/Translucent, 흑백 텍스처에서 흰 배경 투명화하기. PR #57

미해결 — 처형 트리거(주말 작업 예정), 히트스톱 의심2 패치(여전히 이월), 24번 정밀도 보강·graphify CS 풀 빌드·Bootcamp-TIL 미커밋 정리 줄줄이 이월.


작업 환경

  • 외부 프로젝트D:\Unreal\8th-Team11-CH3-Project (Ch3 팀플, 발표 2026-05-26 잔여 4일)
  • 건드린 C++ 파일
    • Source/NBC_Ch3_TeamProject/Private/System/Portal.cpp · .../Public/System/Portal.h
    • Source/.../Private/Game/NBC_GameInstance.cpp · .../Public/Game/NBC_GameInstance.h
    • Source/.../Private/Combat/HealthComponent.cpp · 헤더
    • Source/.../Private/Player/PlayerCharacter.cpp · 헤더
    • Source/.../Private/AI/Zombie/ZombieCharacter.cpp · 헤더
    • Source/.../Private/AI/BTT/BTTask_Attack.cpp · 헤더
    • Source/.../Public/Camera/DamageCameraShake.h (신규)
  • 빌드 의존성NBC_Ch3_TeamProject.Build.csEngineCameras 모듈 추가, .uprojectEngineCameras 플러그인 활성화
  • 머티리얼·BP 자산Content/UI/M_Vignette.uasset 신규 + WBP_HUD 결선
  • PR 4건 — #50(Portal 분기)·#52(총기 반동)·#54(좀비 피격, 머지됨)·#57(시각 피드백)


1. Portal 레벨 분기 시스템 — 노멀↔보상 토글 + 5사이클 후 보스

어제 창욱님 자작 Portal로 한 PIE 세션 안에 룸 전환까지 통과된 것을 받아, 그 Portal에 의미(일반·보상·보스 분기)를 부여하는 즉흥 작업. 시연 동선의 룸 진행이 단순 이동이 아니라 게임 진행도와 묶이게.

기존 구조 — 단일 TargetLevelName

기존 Portal은 디테일 패널에 TargetLevelName 한 개만 노출 — 디자이너가 BP_Portal 인스턴스마다 다음 맵 이름을 직접 적어 넣는 방식. 룸 전환은 깔끔하지만 “지금 어느 맵에서 어느 맵으로 가는가” 라는 진행도 의미가 전혀 없다. 다섯 번 일반맵을 돌아도 여섯 번째도 일반맵, 보상맵 → 일반맵으로 돌아오는 흐름도 사람이 디자이너가 직접 BP를 다시 짜야 함.

디테일 멤버 4개로 교체 + GameInstance 영속화

1
2
3
4
5
6
7
8
9
10
11
12
// Portal.h — 디테일 노출 4개
UPROPERTY(EditAnywhere, Category="Portal")
FName NormalLevelName;  // 일반맵 다음 행선지

UPROPERTY(EditAnywhere, Category="Portal")
FName RewardLevelName;  // 보상맵 행선지

UPROPERTY(EditAnywhere, Category="Portal")
FName BossLevelName;    // 보스맵 행선지

UPROPERTY(EditAnywhere, Category="Portal", meta=(ClampMin=1))
int32 NormalLevelLoopCount = 5;  // 보스 진입까지 일반 사이클 수

같은 BP_Portal 하나로 일반·보상 양쪽을 모두 다룰 수 있게 — 디자이너는 맵 이름 3개 + 카운트만 채운다.

진행도가 OpenLevel 호출로 GameState가 매번 재생성돼도 살아남아야 하니까 NBC_GameInstance에 영속화 ——

1
2
3
4
5
6
7
8
9
10
11
12
// NBC_GameInstance.h
UPROPERTY(VisibleAnywhere, Category="Progression")
int32 NormalLevelsCleared = 0;

UPROPERTY(VisibleAnywhere, Category="Progression")
bool bIsInRewardLevel = false;

int32 GetNormalLevelsCleared() const { return NormalLevelsCleared; }
void IncNormalLevelsCleared() { ++NormalLevelsCleared; }
void ResetProgression();  // 게임오버·메인메뉴 복귀용
bool IsInRewardLevel() const { return bIsInRewardLevel; }
void SetInRewardLevel(bool b) { bIsInRewardLevel = b; }

OnPortalOverlap의 분기 로직 ——

현재 위치카운터 도달?다음 행선지카운터·토글
일반맵 (bIsInRewardLevel=false)NormalLevelsCleared+1 >= LoopCountBossLevelName도달 시 ResetProgression()
일반맵 (bIsInRewardLevel=false)아직RewardLevelNameSetInRewardLevel(true)
보상맵 (bIsInRewardLevel=true)NormalLevelNameIncNormalLevelsCleared() + SetInRewardLevel(false)

흐름이 일반→포털→보상→포털→일반… 5사이클 후 자동 보스. 디자이너 입장에서는 BP_Portal 인스턴스 하나만 일반맵·보상맵에 각각 배치하면 됨.

무한 보상맵 루프 버그 — 매번 호출되는 메서드에 1회성 책임을 끼워 넣은 죄

작업 후 PIE 검증에서 — 보상맵 진입 후 포털을 또 타면 다시 보상맵으로 가는 증상 발견. 같은 BP인데 토글이 안 뒤집힌다? 한참 디버그하다가 호출 체인 추적 ——

1
2
3
4
5
Portal::OnPortalOverlap
  → RewardComp->SaveWeaponsToInstance()        // Portal.cpp:53
    → GI->ClearSavedData()                       // WeaponRewardComponent.cpp:196
      → ★ NormalLevelsCleared = 0;
      → ★ bIsInRewardLevel = false;             // ← 매 포털마다 토글이 false로 리셋

원인 — ClearSavedData()에 진행도 리셋을 같이 끼워 넣었던 것. Save~매번 호출되는 저장 메서드인데, Clear~가 그 뒤에 따라붙으면서 진행도까지 같이 리셋됨. 보상맵에서 포털을 타고 다음 맵으로 넘어갈 때 Save → Clear 흐름이 돌아 bIsInRewardLevel=true가 false로 뒤집힘 → 분기 로직이 “지금 일반맵” 으로 오해 → 또 보상맵으로 보냄.

수정 ——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// NBC_GameInstance.cpp
void UNBC_GameInstance::ClearSavedData()
{
    // [장식 수정] 진행도 리셋을 빼고 무기 데이터만 정리
    SavedWeapons.Empty();
    SavedEquipped = INDEX_NONE;
    // NormalLevelsCleared·bIsInRewardLevel은 더 이상 건드리지 않음
}

void UNBC_GameInstance::ResetProgression()
{
    // [장식 추가] 게임오버·메인메뉴 복귀 시 명시적으로만 호출
    NormalLevelsCleared = 0;
    bIsInRewardLevel = false;
}

PIE 재현 후 정상 동작 확인. 진단 로그는 주석으로 보존 — 다음에 비슷한 증상이 재발하면 즉시 활성화하기 쉽게.

학습매번 호출되는 메서드(Save~)와 1회성 메서드(Clear~)에 같은 책임을 묶지 말 것. 의미가 다른 두 리셋(무기 데이터 정리 vs 진행도 리셋)은 별도 함수로 분리. 함수 이름에 책임을 정확히 담아두면 — ClearSavedData가 무기 전용이라는 게 호출부에서도 명확해진다.

UTF-8 BOM·CP949 깨진 한글 주석 복원 — [장식 복원/추가/수정] 표기

4개 파일(Portal.cpp·Portal.h·NBC_GameInstance.cpp·NBC_GameInstance.h) 모두 팀원이 작성한 한글 주석이 CP949로 깨져 있는 상태였음. Visual Studio·Rider·VS Code 환경 차이로 한 번 깨지면 다시 복원하기 어려운데 — 그대로 두면 내가 새로 추가한 주석도 잘못된 인코딩으로 저장될 위험.

PowerShell로 4개 파일 모두 UTF-8 BOM 강제 ——

1
2
3
4
5
6
$utf8Bom = [System.Text.UTF8Encoding]::new($true)
$files = @('Portal.cpp','Portal.h','NBC_GameInstance.cpp','NBC_GameInstance.h')
foreach ($f in $files) {
    $content = [System.IO.File]::ReadAllText($f, [System.Text.Encoding]::GetEncoding(949))
    [System.IO.File]::WriteAllText($f, $content, $utf8Bom)
}

BOM이 있으면 Visual Studio·Rider·VS Code 어디서든 강제 UTF-8 인식 — 팀원 환경 차이로 한글 다시 깨질 위험 차단.

주석 본문 자체는 세 가지 표기로 작성자 분리 ——

표기의미
// [장식 복원]원래 팀원 주석이었는데 CP949로 깨져서 의미 추정 후 한글 복원
// [장식 추가]내가 신규로 추가한 주석
// [장식 수정]기존 주석의 동작이 바뀌어 내가 수정한 주석

이렇게 표기를 분리해두면 — 후속 코드 리뷰에서 누가 쓴 주석인지 명확하고, 추정 복원이라는 점도 드러나서 원작자가 다시 보면 의미 보정이 가능하다. 메모리 규칙(데코 패턴 금지)에 따라 // --- ... --- 같은 장식은 안 쓰고 간결한 한 줄로 표기.



develop에서 무기 BP 3개(Revolver·Rifle·Shotgun) 반동 파라미터 수정 작업이 있었는데, 작업 도중 분기 정리가 꼬여서 처음엔 PR #50(Portal) 브랜치에 같이 들어가 있었음. 무기 반동은 Portal 분기 시스템과 의미가 전혀 다른 변경이라 리뷰어 입장에서 한 PR로 묶이면 리뷰 부담이 두 배.

PR 분리 — Portal과 무기 반동을 다른 PR로 떼어내는 cherry-pick 워크플로우

1
2
3
4
5
1. git checkout develop
2. git checkout -b feat/weapon-recoil       # 무기 반동 전용 브랜치 신규
3. git cherry-pick <무기 반동 커밋 SHA>      # Portal 브랜치에서 무기 커밋만 떼옴
4. git push -u origin feat/weapon-recoil    # PR #52 생성
5. (Portal 브랜치) git rebase -i HEAD~N로 무기 커밋 drop → force-push로 PR #50 정리

Editor unlink 에러 — 브랜치 전환 중 Unreal Editor가 .uasset을 잡고 있어서 git이 파일을 잠금 해제 못함:

1
fatal: cannot unlink 'Content/Blueprints/Weapons/BP_Revolver.uasset': Permission denied

.uasset은 텍스트가 아니라 바이너리(GUID·외부 자산 레퍼런스 포함)라 Editor가 켜져 있으면 파일 핸들을 잡고 안 놔준다. Editor 닫고 진행해야 함. 작은 함정이지만 — Source/cpp만 다루다가 .uasset 묶음 PR을 처음 떼낼 때 자주 막힌다는 걸 학습.

PR #52는 단독으로 분리 완료. develop 머지 대기.



3. 좀비 → 플레이어 피격 시스템 — 동적 위임 패턴 미러링

어제 시연 동선 검증에서 “좀비가 플레이어에게 데미지를 못 준다”는 결손 확인. WeaponComponent의 데미지 진입점은 이미 있는데, 좀비→플레이어 방향은 완전히 끊겨 있는 상태였음. 발표 4일 전 ROI가 가장 큰 트랙으로 잡음.

코드 분석 — 두 가지 결손 지점

기존 데미지 흐름(Weapon → Zombie)을 먼저 읽음 ——

1
2
3
4
5
6
7
8
// WeaponComponent::ApplyHitDamage — 이미 동작 중인 흐름
void UWeaponComponent::ApplyHitDamage(AActor* HitActor, float Damage)
{
    if (UHealthComponent* Health = HitActor->FindComponentByClass<UHealthComponent>())
    {
        Health->ApplyDamage(Damage);  // ★ 동적 위임 — 컴포넌트가 있으면 데미지
    }
}

FindComponentByClass<UHealthComponent>타겟 액터에 HealthComponent가 붙어 있으면 위임, 없으면 무시하는 패턴. 데미지 처리부에 종류 분기가 없어서 확장이 쉽다.

좀비→플레이어 방향에서 끊긴 지점 두 가지:

#결손영향
(a)BTTask_Attack이 몽타주만 재생하고 데미지 호출이 없음좀비가 공격해도 플레이어 HP가 안 줄어듦
(b)APlayerCharacter에 HealthComponent 부착 자체가 없음(a)를 고쳐도 FindComponentByClass가 nullptr 반환

두 가지 결손을 STEP 1·2로 분리해서 작업. STEP 1이 (b) 부착, STEP 2가 (a) BTTask 호출.

방식 A vs B — 트레이스/거리 게이트 vs AnimNotify+콜리전

작업 시작 전에 두 가지 구현 방식을 비교 ——

방식 A — 거리·각도 게이트 + ApplyDamage방식 B — AnimNotify + 콜리전
구현 복잡도BTTask 한 곳에 거리·각도 if 두 줄 + ApplyDamage 한 줄AnimNotify Blueprint + 콜리전 컴포넌트 + 노티 콜백 결선
데미지 진입점 일관성WeaponComponent와 같은 FindComponentByClass→ApplyDamage 패턴 재사용별도 콜리전 오버랩 이벤트 → 데미지 진입점이 분기됨
시각 피드백 트랙과의 동선BTTask에서 거리·각도 통과 시점에 카메라 셰이크·HP 갱신·비네팅을 같은 데미지 진입점 하나에 합류노티 타이밍과 데미지 타이밍이 별도라 시각 피드백 동기화에 노티 → 카메라 셰이크 → HP 갱신 별도 경로 필요
4일 마감 ROI좋음 — BTTask 한 파일 + ZombieCharacter EditAnywhere 3개만 추가나쁨 — 노티 BP·콜리전 자산·콜백 결선까지 BP 작업이 두꺼움
확장성보스·중간 좀비 추가 시 AttackDamage/AttackRange만 변경보스마다 별도 노티·콜리전 셋업 필요

방식 A 채택. 결정 근거 세 가지:

  1. 기존 동적 위임 패턴 재사용FindComponentByClass<UHealthComponent> 진입점이 이미 검증된 패턴. 좀비 공격도 같은 진입점을 쓰면 데미지 처리부가 단일화됨
  2. 시각 피드백 트랙과 단일 합류 — 카메라 셰이크·HP 갱신·비네팅이 모두 HealthComponent::OnHealthChanged 델리게이트 한 곳에서 갈라져 나가야 동기화가 쉬움. 방식 B는 노티 타이밍과 데미지 타이밍이 분리돼서 동기화 비용이 든다
  3. 4일 마감 ROI 우위 — BP 작업을 최소화해야 발표일까지 시각 피드백 3종까지 마감 가능

STEP 1 — PlayerCharacter에 UHealthComponent C++ 부착 + OnPlayerDeath 위임

기존엔 BP_PlayerBP_HealthComponent가 BP 컴포넌트로 붙어 있는 상태였음. BP 컴포넌트는 — (a) 코드 리뷰에서 안 보임, (b) 다른 캐릭터(좀비)도 같은 패턴이지만 좀비는 C++ 부착이라 일관성 깨짐, (c) PlayerCharacter 생성자에서 직접 제어 불가. 그래서 BP 컴포넌트 제거 → C++ 부착으로 전환.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// PlayerCharacter.h
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Combat")
TObjectPtr<UHealthComponent> HealthComponent;

// PlayerCharacter.cpp 생성자
HealthComponent = CreateDefaultSubobject<UHealthComponent>(TEXT("HealthComponent"));

// BeginPlay
HealthComponent->OnDeath.AddDynamic(this, &APlayerCharacter::OnPlayerDeath);

void APlayerCharacter::OnPlayerDeath()
{
    if (ANBC_GameMode* GM = GetWorld()->GetAuthGameMode<ANBC_GameMode>())
    {
        GM->OnPlayerDied();  // [장식 추가] 게임모드에 사망 위임
    }
}

OnDeath → OnPlayerDeath → NBC_GameMode::OnPlayerDied 위임 사슬 — GameMode는 게임 오버 화면 띄우기·진행도 리셋(ResetProgression()) 호출을 담당. 사망 처리부가 한 곳에 모이게.

STEP 2 — BTTask_Attack에 거리·각도 게이트 + ApplyDamage 호출

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
// BTTask_Attack.cpp 핵심
EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    AZombieCharacter* Zombie = Cast<AZombieCharacter>(OwnerComp.GetAIOwner()->GetPawn());
    AActor* Target = ...;  // 블랙보드에서 플레이어 참조

    // 1. 거리 게이트
    const float DistSq = FVector::DistSquared(Zombie->GetActorLocation(), Target->GetActorLocation());
    if (DistSq > FMath::Square(Zombie->AttackRange))
    {
        UE_LOG(LogCombat, Verbose, TEXT("Attack OUT OF RANGE: %.0f"), FMath::Sqrt(DistSq));
        return EBTNodeResult::Failed;
    }

    // 2. 각도 게이트
    const FVector ToTarget = (Target->GetActorLocation() - Zombie->GetActorLocation()).GetSafeNormal();
    const FVector Forward = Zombie->GetActorForwardVector();
    const float DotVal = FVector::DotProduct(Forward, ToTarget);
    const float HalfAngleCos = FMath::Cos(FMath::DegreesToRadians(Zombie->AttackHalfAngleDeg));
    if (DotVal < HalfAngleCos)
    {
        UE_LOG(LogCombat, Verbose, TEXT("Attack OUT OF ANGLE: dot=%.2f"), DotVal);
        return EBTNodeResult::Failed;
    }

    // 3. ApplyDamage — WeaponComponent와 같은 동적 위임 패턴 미러링
    if (UHealthComponent* PlayerHealth = Target->FindComponentByClass<UHealthComponent>())
    {
        PlayerHealth->ApplyDamage(Zombie->AttackDamage);
        UE_LOG(LogCombat, Log, TEXT("Zombie attack HIT: dmg=%.1f"), Zombie->AttackDamage);
    }

    // 몽타주 재생은 기존대로
    return EBTNodeResult::Succeeded;
}

AttackDamage=10 / AttackRange=180.f / AttackHalfAngleDeg=35°는 ZombieCharacter에 EditAnywhere 노출 — 디자이너가 좀비 종류마다 BP에서 조절 가능. 35도 반각이면 정면 70도 시야 안에 플레이어가 있어야 데미지 통과.

거리 비교에 DistSquared + FMath::Square로 sqrt 호출 회피, 각도 비교에 dot product + FMath::Cos(DegToRad(...))로 acos 회피 — 게이트가 매 틱은 아니지만 BTTask마다 호출되니까 비싼 함수를 안 쓰는 게 깔끔.

LogCombat 커스텀 로그 카테고리 — Output Log 노이즈 분리

데미지·피격 흐름을 추적하다가 LogTemp에 묻혀서 로그가 안 보이는 문제. UE에서 UE_LOG(LogTemp, ...)로 찍으면 Output Log에 엔진 메시지·다른 시스템 로그와 다 섞이는데, Verbose 단계까지 켜면 노이즈가 너무 크다.

1
2
3
4
5
// HealthComponent.h 또는 별도 헤더
DECLARE_LOG_CATEGORY_EXTERN(LogCombat, Log, All);

// HealthComponent.cpp
DEFINE_LOG_CATEGORY(LogCombat);

선언 매크로 두 줄로 카테고리 신설. 이후 데미지·피격 흐름은 UE_LOG(LogCombat, ...)로 통일 — Output Log Filter에서 LogCombat만 보면 피격 흐름이 깔끔하게 나옴. Verbose 단계까지 켜도 다른 로그는 영향 없음.

작은 인프라지만 — 디버깅 효율이 큰 차이를 만든다. WeaponComponent의 데미지 호출도 같은 카테고리로 묶으면 무기·좀비 양방향이 한 필터로 추적 가능.

좀비 피격은 PR #54로 머지 완료. 일반 좀비 AI는 본인 작업, 보스 피격은 재봉님 분담.



4. 시각 피드백 3종 — 카메라 셰이크(C++) + HP 게이지/비네팅(BP·머티리얼)

좀비 피격 시스템이 들어가자 — 다음은 “맞은 게 보이게” 트랙. 어제 우선순위 재조정에서 잡힌 “시각 피드백 3종이 발표 임팩트 직격”이 그대로 적용되는 시점.

4-1. 카메라 셰이크 — EngineCameras 모듈 의존성과 LegacyCameraShake

ULegacyCameraShake 파생 클래스로 신설. 생성자에서 모든 파라미터를 박아 두면 BP 작업이 0이 됨.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// DamageCameraShake.h
UCLASS()
class UDamageCameraShake : public ULegacyCameraShake
{
    GENERATED_BODY()
public:
    UDamageCameraShake();
};

// DamageCameraShake.cpp
UDamageCameraShake::UDamageCameraShake()
{
    OscillationDuration = 0.25f;
    RotOscillation.Pitch.Amplitude = 2.5f; RotOscillation.Pitch.Frequency = 60.f;
    RotOscillation.Yaw.Amplitude   = 1.5f; RotOscillation.Yaw.Frequency   = 50.f;
    LocOscillation.Y.Amplitude     = 1.0f; LocOscillation.Y.Frequency     = 70.f;
}

// PlayerCharacter.cpp 생성자에서 기본 클래스 할당
DamageCameraShakeClass = UDamageCameraShake::StaticClass();

StaticClass() 기본 할당 — BP에서 손댈 필요 없이 처음부터 동작.

빌드 에러와 해결 — LegacyCameraShake.h 못 찾음

1
error C1083: Cannot open include file: 'LegacyCameraShake.h': No such file or directory

처음 Build.csGameplayCameras 모듈을 추가했더니 같은 에러. UE 5.5에서 LegacyCameraShake가 어디 있는지 직접 엔진 경로를 검색 ——

1
Engine/Plugins/Cameras/EngineCameras/Source/EngineCameras/Public/LegacyCameraShake.h

정답은 EngineCameras (GameplayCameras 아님). 둘 다 비슷한 이름이라 헷갈리는데, EngineCameras가 카메라 셰이크 레거시 구현체, GameplayCameras는 GAS·카메라 노드 그래프 관련 별도 플러그인.

1
2
3
4
5
6
7
8
9
// NBC_Ch3_TeamProject.uproject  플러그인 활성화
"Plugins": [
    { "Name": "EngineCameras", "Enabled": true }
]

// NBC_Ch3_TeamProject.Build.cs  모듈 의존성
PrivateDependencyModuleNames.AddRange(new string[] {
    "EngineCameras",  // [장식 추가] LegacyCameraShake 사용
});

.uprojectBuild.cs 둘 다 수정해야 함 — 한 쪽만 하면 컴파일은 통과해도 런타임에 모듈 로딩이 안 됨. 5분 정도 막혔던 부분.

4-2. HP 게이지 — OnHealthChanged 위젯 결선

WBP_HUD 결선 (BP 작업):

1
2
3
4
5
6
7
1. WBP_HUD에 OnPlayerHPUpdated 커스텀 이벤트 신설 (Current·Max 입력 두 개)
2. PlayerCharacter::BeginPlay (또는 HUD 클래스) 에서
     Player → GetHealthComponent → OnHealthChanged → AddDynamic(WBP_HUD::OnPlayerHPUpdated)
3. WBP_HUD::OnPlayerHPUpdated 안에서
     - HPProgressBar.SetPercent(Current / Max)
     - HPText.SetText(FormatText("{0}/100", Truncate(Current)))
     - VignetteImage.SetRenderOpacity(1 - Current/Max)

OnHealthChanged 델리게이트는 좀비 피격 시스템 작업 때 이미 HealthComponent에 있는 상태. 델리게이트 결선만으로 데미지 → HP UI 전 흐름이 한 줄로 이어진다 — 동적 위임 패턴의 본 가치.

4-3. 빨강 비네팅 — M_Vignette 머티리얼과 6연속 시행착오

비네팅(화면 가장자리 빨간 어둠)이 가장 시행착오가 많았던 트랙. 단순해 보여도 머티리얼·BP 핀 연결·앵커 정렬에서 6번 막힘.

M_Vignette 머티리얼 설계 — User Interface 도메인 / Translucent 블렌드. 비네팅 텍스처는 가운데 검정·가장자리 흰색의 흑백 이미지(알파 채널 없음). 흰 배경을 투명하게 만들어야 한다.

1
2
3
4
Texture.R   ──┐
              ├─ OneMinus ──→ Opacity   (흰 배경 0, 검정 가운데 1)
              │
RedConstant ──→ Final Color (빨강 RGB)

핵심 트릭 — 알파 채널이 없는 흑백 텍스처에서 Red 채널을 OneMinus한 값을 Opacity로 사용해서 흰 배경을 투명화. 빨강은 별도 상수 Vector로 Final Color.

시행착오증상원인해결
1UI에서 머티리얼이 안 보임Material Domain이 SurfaceMaterial Domain을 User Interface
2비네팅이 빨강이 아니라 흰색으로 출력Vector4 파라미터의 R 채널 핀 하나만 끌어서 Final Color에 연결 → 스칼라가 RGB 전체에 복사됨통째로 RGB 핀 연결 또는 Constant3Vector 사용
3BP에서 OneMinus 검색해도 안 나옴OneMinus는 머티리얼 노드 전용 — BP 그래프에는 없음BP에서는 Subtract(float-float, 1.0 - x) 또는 - 검색
4ProgressBar가 거꾸로 차오름 (HP 깎이면 게이지가 늘어남)Set Percent.In Percent에 Subtract 결과(1-x)가 잘못 들어감Set Percent는 비율 그대로(Divide), Set Render Opacity는 반전(Subtract) 가 올바른 매핑
5게임 시작 시 비네팅이 처음부터 진하게 떴다가 피격하면 사라짐OnHealthChanged는 변화 시에만 발동 → 시작 시점에 Render Opacity 기본값 1.0이 그대로 유지VignetteImage Render Opacity 기본값을 0으로 디테일 패널에서 설정
6VignetteImage Anchor가 중앙 한 점(0.5,0.5)이라 화면 전체를 덮으려면 Position을 음수로 밀어 사이즈를 키운 임시방편Anchor 중앙 1점 + Position offset으로 풀스크린을 흉내내고 있었음Anchor 풀스크린(0,0,1,1) + Offset 0으로 정리

시행착오 2 — Vector4 파라미터에서 R·G·B·A 각 채널 핀을 따로 끌 수 있는데, 색상이 핀 색(빨강)이라고 R 핀만 끌어서 Final Color에 연결하면 — 그건 스칼라 값으로 평가돼서 RGB 전부에 같은 값이 들어가 흰색이 나옴. 색을 통째로 전달하려면 RGB 핀(상위 핀) 또는 Constant3Vector(RGB 세트)를 써야 함.

시행착오 4 — ProgressBar와 RenderOpacity가 의미가 정반대.

  • ProgressBar: 남은 비율 (HP 100/100 → 1.0, HP 0/100 → 0.0) → Set Percent(Current/Max) 그대로
  • RenderOpacity (비네팅): 잃은 비율 (HP 100/100 → 0.0 투명, HP 0/100 → 1.0 진하게) → Set Render Opacity(1 - Current/Max) 반전

처음에 둘 다 같은 노드(Subtract)에 묶어서 둘 다 잘못 동작. 남은 비율 vs 잃은 비율을 분리한 뒤 한 번에 해결.

시행착오 5 — 가장 헷갈렸던 부분. OnHealthChanged변화 시에만 발동하는 이벤트. 게임 시작 시점에는 HP 변화가 없으니 이벤트가 안 떨어지고, VignetteImage의 디테일 패널 기본값(Render Opacity = 1.0)이 그대로 유지됨 → 시작부터 비네팅이 진하게. 피격하면 OnHealthChanged 발동 → 1 - 100/100 = 0이 들어가 비네팅 사라짐 → 다음 피격에는 정상 동작.

해결은 단순 — VignetteImage의 Render Opacity 기본값을 0으로 디테일 패널에서 설정. 시작 시 투명, 변화 이벤트가 오면 그때부터 정상 동작.

시행착오 6 — 풀스크린 비네팅인데 Image의 Anchor가 (0.5, 0.5) 중앙 한 점으로 잡혀 있어서, 화면 전체를 덮으려면 Position에 음수를 넣고 Size를 화면 사이즈보다 크게 키운 임시방편 상태였음. Anchor를 풀스크린(0,0,1,1)으로 바꾸고 Offset Left·Top·Right·Bottom을 모두 0으로 두면 부모 컨테이너 전체를 자동으로 덮는다. 해상도 변경에도 자연스럽게 따라가는 정상 상태.

이 6개 시행착오 전부 ProgressBar·머티리얼·UMG 앵커의 개념을 잘못 잡은 데서 나온 것 — 알파 없는 텍스처 처리, 스칼라 vs RGB, 변화 이벤트의 본질, UMG 앵커 기본값. 모두 한 번씩 직접 막혀봐야 다음에 안 막힌다.

시각 피드백 3종은 PR #57로 묶어 develop 대기.



5. PR 운영 — Portal·총기·피격·시각 피드백 4개 PR 동시 진행

PR #브랜치내용상태
#50feat/portal-progressionPortal 분기 시스템 + GameInstance 영속화 + UTF-8 BOM 정리develop 대기
#52feat/weapon-recoil무기 BP 3개(Revolver·Rifle·Shotgun) 반동 파라미터 (#50에서 cherry-pick 분리)develop 대기
#54feat/zombie-attack좀비→플레이어 피격 시스템 + UHealthComponent 부착 + LogCombat머지됨
#57feat/damage-feedback카메라 셰이크(C++) + HP 게이지·비네팅(BP·머티리얼)develop 대기

발표 4일 전 트랙을 4개 PR로 쪼개 진행. 4개를 한 PR로 묶지 않은 이유 ——

  1. 리뷰 단위 명확화 — 각 PR이 한 가지 의미(레벨 분기 / 무기 반동 / 좀비 피격 / 시각 피드백)로 묶임. 리뷰어가 한 PR을 보면 한 가지 영역만 집중 가능
  2. 부분 머지 가능 — 머지된 PR #54(좀비 피격)가 다른 PR 작업의 의존성. 한 PR로 묶었으면 전부 같이 대기해야 함
  3. 롤백 단위 — 발표 직전에 한 트랙이 깨지면 그 PR만 revert 가능

메모리 규칙 — 푸시 전 사용자 확인 게이트도 실제로 작동. 첫 푸시 시도에서 사용자가 “잠깐, 다시 확인”으로 거부 → 변경분 재검토 후 두 번째 푸시에서 통과. 자동화된 게이트가 실수 한 번을 막아준 사례.



미해결 / 내일(5/23) 우선순위

  1. 📅 처형 트리거 구현 — 주말 작업 예정 (05/23~05/24)HealthComponent::OnHealthThreshold 델리게이트 신설 → 임계 체력(예: 30%) 진입 시 처형 가능 상태 토글. 시연에선 임시 키 입력 대체 상태
  2. PR #50·#52·#57 develop 머지 — 리뷰 받고 머지
  3. 카메라 셰이크 무기 분기 통합 — 현재는 단일 UDamageCameraShake. WeaponConfig::TSubclassOf<UCameraShakeBase>로 무기별 셰이크 분기 — 발표 임팩트 추가 보강
  4. 히트스톱 의심2 패치 — HitStopTimers 멤버 맵 도입 — 발표 임팩트 후순위로 여전히 이월. 시각 피드백 3종이 들어간 지금은 슬로우 약화가 카메라 셰이크·HP 갱신·비네팅으로 가려지는 게 더 분명해짐 — 후순위 결정이 옳았던 셈
  5. 24번 floating_point 정밀도 파트 답변 보강 — 가수 23비트·ULP·machine epsilon (이월)
  6. graphify CS 그래프 풀 빌드 — 26·27·28·29·30번 일괄 반영, graphify update . 1회 실행
  7. Bootcamp-TIL 미커밋 정리5월/2026-05-15.md(modified) + 5월/2026-05-18·19·20·21·22.md(untracked). 본 TIL과 한 묶음 커밋
  8. 오늘 미수강 심화수업 청취 — 오전 작업으로 우선순위 밀림


오늘 배운 것 정리

  1. 매번 호출되는 메서드(Save~)와 1회성 메서드(Clear~)에 같은 책임을 묶지 말 것 — Portal 무한 보상맵 루프 버그의 근본 원인. ClearSavedData()가 무기 데이터 정리 + 진행도 리셋 두 가지 책임을 들고 있었고, SaveWeaponsToInstance 매 호출마다 Clear가 같이 따라가면서 진행도 토글이 의도치 않게 false로 리셋. 함수 이름에 책임을 정확히 담아두면 호출부에서도 책임 범위가 명확해진다ClearSavedData는 무기 전용, ResetProgression은 진행도 전용. 한 함수 안에 두 의미가 섞이는 순간 호출 흐름의 누군가가 의도 밖 부작용을 받는다

  2. 데미지 진입점을 동적 위임 패턴(FindComponentByClass<T>)으로 통일하면 좀비 ↔ 플레이어·무기 ↔ 적 양방향이 한 패턴으로 끝난다 — WeaponComponent의 ApplyHitDamage가 이미 동적 위임 패턴이라, 좀비 공격도 같은 패턴으로 미러링하면 데미지 처리부가 한 곳에 모인다. 방식 B(AnimNotify+콜리전)를 안 고른 이유도 결국 — 데미지 진입점이 분기되면 시각 피드백 트랙(카메라 셰이크·HP·비네팅)이 한 델리게이트(OnHealthChanged) 뒤에 합류 못한다. 진입점을 단일화하면 그 뒤의 위임 사슬이 자동으로 깔끔해진다

  3. 모듈 의존성 에러는 엔진 경로를 직접 검색해서 정확한 모듈명을 찾는 게 빠르다LegacyCameraShake.h 못 찾음 에러에 처음엔 GameplayCameras를 추가했는데 비슷한 이름이라 같은 에러. UE 5.5에서 Engine/Plugins/Cameras/EngineCameras/Source/EngineCameras/Public/을 직접 검색해서 정답이 EngineCameras (GameplayCameras 아님)임을 확인. 모듈 이름 추측은 시간 낭비. 헤더 파일 위치에서 모듈 폴더명을 역추적하는 게 정확하다. .uproject(플러그인 활성화) + Build.cs(모듈 의존성) 둘 다 수정 필요 — 한쪽만 하면 컴파일은 통과해도 런타임에 모듈 로딩이 안 됨

  4. 알파 채널 없는 흑백 텍스처에서 흰 배경을 투명화하려면 Texture.R → OneMinus → Opacity 트릭 — 비네팅 텍스처가 흑백(검정 가운데·흰 가장자리)에 알파 없음. User Interface 도메인 + Translucent 블렌드 + Red 채널을 OneMinus한 값을 Opacity로 사용하면 흰 배경이 0(투명)·검정 가운데가 1(불투명)로 풀린다. 색상은 별도 Constant3Vector로 Final Color에 — 이때 Vector4의 R 핀만 따로 끌면 스칼라가 RGB 전체에 복사돼 흰색이 나온다. RGB 통째 핀 또는 Constant3Vector를 써야 색이 보존됨

  5. 변화 이벤트(OnHealthChanged) 기반 UI는 “시작 상태”를 디테일 패널 기본값으로 미리 박아둬야 한다 — 비네팅이 게임 시작 시 진하게 떴다가 첫 피격에 사라지는 증상의 본질. 변화 이벤트는 변화 시에만 발동 — 시작 시점엔 이벤트가 안 떨어지니까 디테일 패널 기본값이 그대로 유지된다. VignetteImage Render Opacity 기본값을 0으로 두는 게 정답. 이벤트 기반 UI는 (a) 초기 상태 = 디테일 기본값, (b) 변화 후 상태 = 이벤트 콜백, 두 가지를 동시에 설계해야 함

  6. ProgressBar와 RenderOpacity는 의미가 정반대 — “남은 비율 vs 잃은 비율”을 분리해야 한 번에 풀린다 — HP 게이지(Set Percent)는 남은 비율 그대로(Current/Max)·비네팅 강도(Set Render Opacity)는 잃은 비율(1 - Current/Max). 처음에 둘 다 같은 Subtract 노드에 묶어서 둘 다 잘못 동작. 공통 입력(HP 비율)에서 두 출력의 의미가 정반대면 노드도 분리해야 한다 — 같은 값을 양쪽에 그대로 넣지 말 것

  7. PR을 의미 단위로 쪼개면 부분 머지·롤백·리뷰 부담이 모두 좋아진다 — Portal·총기 반동·좀비 피격·시각 피드백 4개 트랙을 한 PR로 묶었으면 — 리뷰어 부담 4배, 부분 머지 불가, 한 트랙 깨지면 전체 revert. 의미 단위로 4개 PR(#50·#52·#54·#57)로 쪼개니까 — PR #54(좀비 피격)가 먼저 머지돼 다른 작업의 의존성으로 작동 가능, 리뷰는 한 영역씩, 발표 직전 롤백도 단위가 작음. cherry-pick + 새 브랜치 분리 워크플로우는 작업이 한 브랜치에 섞여 들어간 뒤에도 PR 단위 정리가 가능하다는 점에서 자주 쓸 패턴

  8. CP949로 깨진 한글 주석 복원 + 표기로 작성자 분리 — 팀원이 작성한 CP949 주석을 그대로 두면 내가 추가한 주석도 잘못된 인코딩으로 저장될 위험. PowerShell [System.Text.UTF8Encoding]::new($true)로 4개 파일 모두 BOM 강제 → Visual Studio·Rider·VS Code 환경 어디서든 강제 UTF-8 인식. [장식 복원](추정 복원) / [장식 추가](신규) / [장식 수정](변경) 세 가지 표기로 작성자 분리해두면 — 후속 리뷰에서 누가 쓴 주석인지 명확하고, 추정 복원이라는 점도 드러나 원작자가 다시 보면 의미 보정 가능. 메모리 규칙(데코 패턴 금지)에 따라 // --- ... --- 같은 장식은 안 쓰고 간결한 한 줄로

  9. 커스텀 로그 카테고리(LogCombat)는 두 줄 매크로로 만들고 디버깅 효율은 큰 차이를 만든다DECLARE_LOG_CATEGORY_EXTERN·DEFINE_LOG_CATEGORY 두 줄로 끝나지만, Output Log Filter에서 LogCombat만 필터링하면 데미지·피격 흐름이 깔끔하게 추적된다. Verbose 단계까지 켜도 다른 로그는 영향 없음. WeaponComponent·HealthComponent·BTTask_Attack 셋이 같은 카테고리로 묶이면 한 필터로 전 흐름 추적 가능 — 작은 인프라가 디버깅 시간을 크게 줄이는 패턴

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