좀비→플레이어 피격과 시각 피드백 3종
카메라 셰이크·HP 게이지·비네팅
맞았다는 걸 알아채게 만드는 건 데미지 숫자가 아니라 흔들리는 화면과 줄어드는 게이지, 붉어지는 가장자리다. 이미 만든 히트 위임 구조를 반대 방향으로 미러링해 경로를 뚫었다.
맞았다는 걸 플레이어가 알아채게 만드는 건 데미지 숫자가 아니라 화면이 흔들리고, 게이지가 줄고, 가장자리가 붉어지는 세 가지다. 이 글에서는 좀비가 플레이어를 때리는 경로를 붙이고 그 위에 시각 피드백 3종을 얹은 과정을 이야기하려 한다 — 구현은 이미 만들어 둔 히트 처리 위임 구조를 반대 방향으로 미러링한 피격 시스템과, 카메라 셰이크(C++)·HP 게이지·비네팅(머티리얼)이다.
같은 날 함께 진행한 Portal 레벨 분기와 총기 반동 파라미터 정리, PR 운영은 이 글의 주제와 달라 덜어냈다.
기술 구현 1 — 피격 시스템, 위임 구조를 반대로 미러링
어제 시연 동선 검증에서 “좀비가 플레이어에게 데미지를 못 준다”는 결손 확인. 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 채택. 결정 근거 세 가지:
- 기존 동적 위임 패턴 재사용 —
FindComponentByClass<UHealthComponent>진입점이 이미 검증된 패턴. 좀비 공격도 같은 진입점을 쓰면 데미지 처리부가 단일화됨 - 시각 피드백 트랙과 단일 합류 — 카메라 셰이크·HP 갱신·비네팅이 모두
HealthComponent::OnHealthChanged델리게이트 한 곳에서 갈라져 나가야 동기화가 쉬움. 방식 B는 노티 타이밍과 데미지 타이밍이 분리돼서 동기화 비용이 든다 - 4일 마감 ROI 우위 — BP 작업을 최소화해야 발표일까지 시각 피드백 3종까지 마감 가능
STEP 1 — PlayerCharacter에 UHealthComponent C++ 부착 + OnPlayerDeath 위임
기존엔 BP_Player에 BP_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는 본인 작업, 보스 피격은 재봉님 분담.
기술 구현 2 — 시각 피드백 3종
좀비 피격 시스템이 들어가자 — 다음은 “맞은 게 보이게” 트랙. 어제 우선순위 재조정에서 잡힌 “시각 피드백 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.cs에 GameplayCameras 모듈을 추가했더니 같은 에러. 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 사용
});
.uproject와 Build.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.
| 시행착오 | 증상 | 원인 | 해결 |
|---|---|---|---|
| 1 | UI에서 머티리얼이 안 보임 | Material Domain이 Surface | Material Domain을 User Interface로 |
| 2 | 비네팅이 빨강이 아니라 흰색으로 출력 | Vector4 파라미터의 R 채널 핀 하나만 끌어서 Final Color에 연결 → 스칼라가 RGB 전체에 복사됨 | 통째로 RGB 핀 연결 또는 Constant3Vector 사용 |
| 3 | BP에서 OneMinus 검색해도 안 나옴 | OneMinus는 머티리얼 노드 전용 — BP 그래프에는 없음 | BP에서는 Subtract(float-float, 1.0 - x) 또는 - 검색 |
| 4 | ProgressBar가 거꾸로 차오름 (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으로 디테일 패널에서 설정 |
| 6 | VignetteImage 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 대기.
정리 — 피격 연출에서 남은 것
매번 호출되는 메서드(
Save~)와 1회성 메서드(Clear~)에 같은 책임을 묶지 말 것 — Portal 무한 보상맵 루프 버그의 근본 원인.ClearSavedData()가 무기 데이터 정리 + 진행도 리셋 두 가지 책임을 들고 있었고,SaveWeaponsToInstance매 호출마다Clear가 같이 따라가면서 진행도 토글이 의도치 않게 false로 리셋. 함수 이름에 책임을 정확히 담아두면 호출부에서도 책임 범위가 명확해진다 —ClearSavedData는 무기 전용,ResetProgression은 진행도 전용. 한 함수 안에 두 의미가 섞이는 순간 호출 흐름의 누군가가 의도 밖 부작용을 받는다데미지 진입점을 동적 위임 패턴(
FindComponentByClass<T>)으로 통일하면 좀비 ↔ 플레이어·무기 ↔ 적 양방향이 한 패턴으로 끝난다 — WeaponComponent의ApplyHitDamage가 이미 동적 위임 패턴이라, 좀비 공격도 같은 패턴으로 미러링하면 데미지 처리부가 한 곳에 모인다. 방식 B(AnimNotify+콜리전)를 안 고른 이유도 결국 — 데미지 진입점이 분기되면 시각 피드백 트랙(카메라 셰이크·HP·비네팅)이 한 델리게이트(OnHealthChanged) 뒤에 합류 못한다. 진입점을 단일화하면 그 뒤의 위임 사슬이 자동으로 깔끔해진다모듈 의존성 에러는 엔진 경로를 직접 검색해서 정확한 모듈명을 찾는 게 빠르다 —
LegacyCameraShake.h못 찾음 에러에 처음엔GameplayCameras를 추가했는데 비슷한 이름이라 같은 에러. UE 5.5에서Engine/Plugins/Cameras/EngineCameras/Source/EngineCameras/Public/을 직접 검색해서 정답이EngineCameras(GameplayCameras 아님)임을 확인. 모듈 이름 추측은 시간 낭비. 헤더 파일 위치에서 모듈 폴더명을 역추적하는 게 정확하다..uproject(플러그인 활성화) +Build.cs(모듈 의존성) 둘 다 수정 필요 — 한쪽만 하면 컴파일은 통과해도 런타임에 모듈 로딩이 안 됨알파 채널 없는 흑백 텍스처에서 흰 배경을 투명화하려면
Texture.R → OneMinus → Opacity트릭 — 비네팅 텍스처가 흑백(검정 가운데·흰 가장자리)에 알파 없음. User Interface 도메인 + Translucent 블렌드 + Red 채널을 OneMinus한 값을 Opacity로 사용하면 흰 배경이 0(투명)·검정 가운데가 1(불투명)로 풀린다. 색상은 별도 Constant3Vector로 Final Color에 — 이때 Vector4의 R 핀만 따로 끌면 스칼라가 RGB 전체에 복사돼 흰색이 나온다. RGB 통째 핀 또는 Constant3Vector를 써야 색이 보존됨변화 이벤트(
OnHealthChanged) 기반 UI는 “시작 상태”를 디테일 패널 기본값으로 미리 박아둬야 한다 — 비네팅이 게임 시작 시 진하게 떴다가 첫 피격에 사라지는 증상의 본질. 변화 이벤트는 변화 시에만 발동 — 시작 시점엔 이벤트가 안 떨어지니까 디테일 패널 기본값이 그대로 유지된다. VignetteImage Render Opacity 기본값을 0으로 두는 게 정답. 이벤트 기반 UI는 (a) 초기 상태 = 디테일 기본값, (b) 변화 후 상태 = 이벤트 콜백, 두 가지를 동시에 설계해야 함ProgressBar와 RenderOpacity는 의미가 정반대 — “남은 비율 vs 잃은 비율”을 분리해야 한 번에 풀린다 — HP 게이지(
Set Percent)는 남은 비율 그대로(Current/Max)·비네팅 강도(Set Render Opacity)는 잃은 비율(1 - Current/Max). 처음에 둘 다 같은 Subtract 노드에 묶어서 둘 다 잘못 동작. 공통 입력(HP 비율)에서 두 출력의 의미가 정반대면 노드도 분리해야 한다 — 같은 값을 양쪽에 그대로 넣지 말 것PR을 의미 단위로 쪼개면 부분 머지·롤백·리뷰 부담이 모두 좋아진다 — Portal·총기 반동·좀비 피격·시각 피드백 4개 트랙을 한 PR로 묶었으면 — 리뷰어 부담 4배, 부분 머지 불가, 한 트랙 깨지면 전체 revert. 의미 단위로 4개 PR(#50·#52·#54·#57)로 쪼개니까 — PR #54(좀비 피격)가 먼저 머지돼 다른 작업의 의존성으로 작동 가능, 리뷰는 한 영역씩, 발표 직전 롤백도 단위가 작음. cherry-pick + 새 브랜치 분리 워크플로우는 작업이 한 브랜치에 섞여 들어간 뒤에도 PR 단위 정리가 가능하다는 점에서 자주 쓸 패턴
CP949로 깨진 한글 주석 복원 + 표기로 작성자 분리 — 팀원이 작성한 CP949 주석을 그대로 두면 내가 추가한 주석도 잘못된 인코딩으로 저장될 위험. PowerShell
[System.Text.UTF8Encoding]::new($true)로 4개 파일 모두 BOM 강제 → Visual Studio·Rider·VS Code 환경 어디서든 강제 UTF-8 인식.[장식 복원](추정 복원) /[장식 추가](신규) /[장식 수정](변경) 세 가지 표기로 작성자 분리해두면 — 후속 리뷰에서 누가 쓴 주석인지 명확하고, 추정 복원이라는 점도 드러나 원작자가 다시 보면 의미 보정 가능. 메모리 규칙(데코 패턴 금지)에 따라// --- ... ---같은 장식은 안 쓰고 간결한 한 줄로커스텀 로그 카테고리(
LogCombat)는 두 줄 매크로로 만들고 디버깅 효율은 큰 차이를 만든다 —DECLARE_LOG_CATEGORY_EXTERN·DEFINE_LOG_CATEGORY두 줄로 끝나지만, Output Log Filter에서LogCombat만 필터링하면 데미지·피격 흐름이 깔끔하게 추적된다. Verbose 단계까지 켜도 다른 로그는 영향 없음. WeaponComponent·HealthComponent·BTTask_Attack 셋이 같은 카테고리로 묶이면 한 필터로 전 흐름 추적 가능 — 작은 인프라가 디버깅 시간을 크게 줄이는 패턴
핵심 요약 — 무한 보상맵 루프의 원인은 매번 호출되는 저장 메서드(
Save~)에 1회성 진행도 리셋이 끼어 있던 것 — 의미가 다른 책임은 함수부터 분리해야 한다. 그리고 데미지 진입점을FindComponentByClass동적 위임 하나로 통일하면 카메라 셰이크·HP·비네팅 시각 피드백까지 델리게이트 하나 뒤에 합류한다.