TIL 2026-05-15
2026-05-15 Ch3 팀플 HitReact + 히트스톱 통합 (단일 발사 경로 확립)
목차
- 2026-05-15 Ch3 팀플 HitReact + 히트스톱 통합 (단일 발사 경로 확립)
- 목차
- 오늘 한 일 요약
- 작업 환경
- 1. 테스트 환경 셋업 — BP_TestPlayerCharacter
- 2. ZombieCharacter — 콜리전 채널 정합
- 3. WeaponComponent — 본 폴백 + 히트스톱
- 4. HitReactComponent — 연사 RESTART 로직
- 5. BaseWeapon::Fire — 데드 코드 처리 + WeaponComponent 위임
- 6. BTTask_Move — 로그 잡음 제거 (0줄 수정)
- 트러블슈팅 — 코드리뷰 자주 묻는 부분
- 발사 흐름 최종 정리
- 코드리뷰에서 강조할 설계 결정
- 오늘 배운 것 정리
- 다음 단계 후보
오늘 한 일 요약
- 테스트 환경 셋업 —
BP_TestPlayerCharacter신규(BP_PlayerCharacter 상속). World Settings의 GameMode Override가 BP_NBC_GameMode로 박혀 있어 Project Settings의 BP_TestGameMode가 무시되던 함정을 World Settings에서 직접 교체. PlayerController BP에 IMC + IA_Move/Look/Jump/Fire 4개 액션을 직접 할당해야 입력이 도는 구조 파악 - ZombieCharacter 콜리전 정합 —
UHitReactComponent를 C++CreateDefaultSubobject로 자동 부착. Mesh를QueryAndPhysics+ ObjectType Pawn + Visibility/Camera/WorldStatic/WorldDynamic/Pawn/ECC_Weapon Block로 코드 일괄 설정. Capsule은 Visibility/Camera/ECC_Weapon Ignore로 라인트레이스가 캡슐을 통과해 본까지 도달하도록 정리. ECC_Weapon은Combat/CombatTypes.h에서ECC_GameTraceChannel1로 정의 — 처음에 응답 누락으로 라인트레이스가 캡슐에 막혀 BoneName=None이 떨어지던 이슈 해결 - WeaponComponent — BoneName 폴백 —
Hit.BoneName.IsNone()이면SkeletalMesh->FindClosestBone_K2(Hit.ImpactPoint)로 가장 가까운 본을 찾고,_End본은 PhysicsAsset 흔들림이 미미하므로 부모로 최대 8단계 거슬러 올라가도록 처리. PhysicsAsset 본 콜리전이 듬성듬성해도 시각적 일관성 보장 - WeaponComponent — 히트스톱 —
ApplyHitDamage의 HealthComponent 분기에ApplyHitStop(HitActor, DamageInstigator)호출 추가. 좀비 Pawn + 그AIController+ 플레이어까지CustomTimeDilation = 0.1로 동시 적용 후 World TimerManager로 0.15초 뒤 1.0 복귀. AIController도 같이 멈춰야 BT 의사결정이 진짜로 멎음 — Pawn만 멈추면 BT는 그대로 굴러서 “타격감 없는 추격”이 됨 - HitReactComponent — 연사 RESTART — 연사 중 다른 본이 맞으면 이전 본을
SetAllBodiesBelowSimulatePhysics(false)+SetAllBodiesBelowPhysicsBlendWeight(0)로 즉시 정리하고 새 본에서 재시작. 이전에는bIsReacting가드 때문에 연사 두 번째 발 이후가 통째로 무시되던 버그 제거 - BaseWeapon::Fire — 데드 코드 처리 + 위임 — Player 모듈의 A팀 코드에서
ABaseWeapon::Fire가BlueprintCallable로 노출돼 BP/AnimNotify에서 호출 시 자체 LineTrace + ApplyDamage가 별도 경로로 도는 이중 경로 발견. 진단 로그로 호출 추적 후 데드 코드 확인 — 본체 통째로 주석 처리 + Owner의UWeaponComponent를 찾아TryFire로 위임하는 안전망 1줄만 유지 - BTTask_Move 로그 정리 — 매 사이클
MoveToActor재호출로UE_LOG가 끝없이 찍히는 잡음을 로그 줄만 주석 처리. 로직은 0줄 수정 - 단일 경로 확립 — 좌클릭이든 BP Fire든 AnimNotify든 모든 발사가
UWeaponComponent::TryFire한 곳으로 모이게 통일 → HitReact·히트스톱이 어느 호출 경로에서도 동일하게 발동
작업 환경
- 외부 프로젝트 —
D:\Unreal\8th-Team11-CH3-Project - 엔진 — UE5
- 모듈 구성 —
Combat(WeaponComponent / HitReactComponent / CombatTypes),Player(BaseWeapon — A팀),AI(ZombieCharacter / BTTask_Move — B팀) - 목적 — 내일 팀 코드리뷰 준비. 핵심은 “왜 이렇게 했는지”가 즉시 보이게 정리
1. 테스트 환경 셋업 — BP_TestPlayerCharacter
A팀 BP_PlayerCharacter를 상속한 BP_TestPlayerCharacter를 만들어 GameMode/PlayerController 빙의가 정상인지 격리 검증. 본 캐릭 BP를 직접 건드리면 A팀 작업과 충돌 위험이 있어 테스트용 상속본을 따로 둠.
World Settings GameMode Override 함정
증상 — Project Settings에서 BP_TestGameMode로 바꿔도 PIE 진입 시 여전히 BP_NBC_GameMode가 떴다.
원인 — 레벨의 World Settings → GameMode Override 슬롯이 BP_NBC_GameMode로 잡혀 있었다. World Settings의 Override가 Project Settings보다 우선이라 변경이 통째로 무시된 것.
해결 — World Settings에서 직접 BP_TestGameMode로 교체.
교훈 — UE의 GameMode 해석 우선순위는 World Settings Override > Project Settings Default. 레벨 단위 디버깅 환경을 만들 땐 Project Settings 건드리기 전에 World Settings부터 본다.
PlayerController BP에 IMC + IA 직접 할당 구조
A팀이 짠 구조는 EnhancedInput을 PlayerController BP의 디테일 패널에 직접 슬롯으로 노출. C++에서 AddMappingContext 호출 같은 거 없이, BP의 InputMappingContext 슬롯에 IMC를 꽂고 IA_Move/Look/Jump/Fire 액션 각각에 함수 바인딩을 BP 그래프로 연결한 구조.
처음에 입력이 무반응이라 PlayerController.cpp부터 디버깅 시작했는데, 알고 보니 BP 슬롯에 IA가 비어 있어서 그랬다. BP의 4개 IA 슬롯(Move/Look/Jump/Fire) 전부 직접 할당해야 입력이 살아남.
이 구조의 장점은 디자이너가 C++ 건드리지 않고 입력 매핑을 바꿀 수 있다는 것, 단점은 슬롯 누락이 빌드 에러 없이 무반응으로만 드러난다는 것.
2. ZombieCharacter — 콜리전 채널 정합
D:\Unreal\8th-Team11-CH3-Project\Source\...\AI\ZombieCharacter.h/.cpp
HitReactComponent 자동 부착
1
2
3
4
5
6
// ZombieCharacter.h
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Combat")
TObjectPtr<UHitReactComponent> HitReactComp;
// ZombieCharacter.cpp - 생성자
HitReactComp = CreateDefaultSubobject<UHitReactComponent>(TEXT("HitReactComp"));
좀비 BP를 손대지 않아도 C++ 베이스만 상속하면 자동으로 컴포넌트가 붙음. B팀이 만든 다른 좀비 BP들이 있어도 동일.
Mesh 콜리전 코드 일괄 설정
좀비 Mesh가 CharacterMesh 프리셋(= Query Only, No Physics)으로 잡혀 있어 PhysicsAsset 시뮬레이션 자체가 차단된 상태였다. BP에서 일일이 바꾸지 않고 C++에서 일괄 설정.
1
2
3
4
5
6
7
8
9
10
USkeletalMeshComponent* MeshComp = GetMesh();
MeshComp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
MeshComp->SetCollisionObjectType(ECC_Pawn);
MeshComp->SetCollisionResponseToAllChannels(ECR_Ignore);
MeshComp->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
MeshComp->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
MeshComp->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
MeshComp->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Block);
MeshComp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Block);
MeshComp->SetCollisionResponseToChannel(ECC_Weapon, ECR_Block); // 핵심
핵심은 ECC_Weapon Block. 이게 빠지면 무기 라인트레이스가 Mesh를 그대로 통과해 본 정보가 안 잡힌다.
Capsule은 ECC_Weapon Ignore
1
2
3
4
UCapsuleComponent* Capsule = GetCapsuleComponent();
Capsule->SetCollisionResponseToChannel(ECC_Visibility, ECR_Ignore);
Capsule->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore);
Capsule->SetCollisionResponseToChannel(ECC_Weapon, ECR_Ignore);
캡슐이 ECC_Weapon에 Block이면 라인트레이스가 캡슐 표면에서 막혀 Hit.BoneName == None이 떨어진다. PhysicsAsset의 본까지 트레이스가 도달해야 HitReact의 본 지정이 의미가 있으므로 캡슐은 ECC_Weapon Ignore가 정답.
ECC_Weapon은 별도 정의 — Combat/CombatTypes.h:
1
#define ECC_Weapon ECC_GameTraceChannel1
Project Settings → Collision의 GameTraceChannel1을 “Weapon” 이름으로 등록한 뒤 C++에서 이 매크로로 참조한다. 처음에 ZombieCharacter 측에서 이 채널 응답을 누락해 라인트레이스가 캡슐에 막히던 게 오늘 디버깅의 시작점이었다.
3. WeaponComponent — 본 폴백 + 히트스톱
본인이 작성한 Combat 모듈의 UWeaponComponent. 핵심 기능 두 가지.
BoneName 폴백 (FindClosestBone_K2)
PhysicsAsset의 본 콜리전이 듬성듬성한 경우 Hit.BoneName이 None으로 떨어진다. 그대로 두면 HitReact가 SKIP되어 시각 반응이 사라짐.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FName ResolvedBone = Hit.BoneName;
if (ResolvedBone.IsNone())
{
if (USkeletalMeshComponent* SkelMesh = HitActor->FindComponentByClass<USkeletalMeshComponent>())
{
ResolvedBone = SkelMesh->FindClosestBone_K2(Hit.ImpactPoint);
// _End 본은 PhysicsAsset 흔들림 효과가 미미 → 부모로 거슬러 (최대 8단계)
int32 Hops = 0;
while (!ResolvedBone.IsNone() && ResolvedBone.ToString().EndsWith(TEXT("_End")) && Hops < 8)
{
ResolvedBone = SkelMesh->GetParentBone(ResolvedBone);
++Hops;
}
}
}
_End 본은 스켈레톤 말단 — hand_r_End, foot_l_End 같은 것. 콜리전이 거의 없고 PhysicsAsset에 등록도 잘 안 돼 있어 거기에 BlendWeight를 걸어도 흔들림이 시각적으로 안 보인다. 부모(hand_r, foot_l)까지 거슬러 가야 의도한 흔들림이 잡힌다. 8단계 제한은 무한 루프 가드.
히트스톱 — Pawn + AIController + Player 동시 정지
ApplyHitDamage의 HealthComponent 분기 끝에 ApplyHitStop(HitActor, DamageInstigator) 호출.
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
38
void UWeaponComponent::ApplyHitStop(AActor* HitActor, AActor* Instigator)
{
// 1) 좀비 Pawn 정지
HitActor->CustomTimeDilation = HitStopTimeScale;
// 2) 좀비 AIController 정지 — Pawn만 멈추면 BT가 계속 돌아 의사결정이 멎지 않음
if (APawn* HitPawn = Cast<APawn>(HitActor))
{
if (AController* HitController = HitPawn->GetController())
{
HitController->CustomTimeDilation = HitStopTimeScale;
}
}
// 3) 플레이어도 정지 → 발사자도 같이 느려져야 충돌감이 살아남
if (Instigator)
{
Instigator->CustomTimeDilation = HitStopTimeScale;
}
// 4) Duration 후 1.0으로 복귀
FTimerHandle Handle;
TWeakObjectPtr<AActor> WeakHit = HitActor;
TWeakObjectPtr<AActor> WeakInst = Instigator;
GetWorld()->GetTimerManager().SetTimer(Handle, [WeakHit, WeakInst]()
{
if (WeakHit.IsValid())
{
WeakHit->CustomTimeDilation = 1.f;
if (APawn* P = Cast<APawn>(WeakHit.Get()))
{
if (AController* C = P->GetController()) C->CustomTimeDilation = 1.f;
}
}
if (WeakInst.IsValid()) WeakInst->CustomTimeDilation = 1.f;
}, HitStopDuration, false);
}
AIController도 같이 멈춰야 하는 이유 — CustomTimeDilation은 액터 단위로 작동한다. Pawn만 0.1로 두면 Pawn의 Tick·이동·애니메이션은 느려지지만, 별도 액터인 AIController가 매 프레임 돌리는 BehaviorTree 평가는 그대로 풀스피드로 굴러간다. 결과적으로 “몸은 멈췄는데 BT는 다음 노드로 진행”하는 비대칭이 생긴다.
AIController도 0.1로 두면 BT의 Selector/Sequence 평가, MoveTo Task의 Tick, Wait Task의 타이머까지 전부 0.1배로 스케일 → 진짜로 의사결정이 멎는다. 그래서 BTTask 자체는 0줄 수정으로 끝나는 것(아래 6번 항목 참조).
플레이어까지 같이 멈추는 건 타격 충돌의 무게감 연출용. 발사자만 정상 속도면 “총만 빠르고 화면은 멈춤”이 되어 어색하다.
BP 노출 파라미터
1
2
3
4
5
6
7
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Combat|HitStop",
meta=(ClampMin="0.01", ClampMax="1.0"))
float HitStopTimeScale = 0.1f;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Combat|HitStop",
meta=(ClampMin="0.0", ClampMax="0.5"))
float HitStopDuration = 0.15f;
DataAsset이 아닌 컴포넌트 디테일 패널에서 튜닝하도록 노출. 무기 종류별로 강도 차이를 두고 싶을 때(샷건은 0.05/0.2, 라이플은 0.1/0.1 등) BP에서 즉시 조정.
4. HitReactComponent — 연사 RESTART 로직
기존 구조의 버그 — bIsReacting == true면 신규 PlayHitReact 호출을 통째로 SKIP. 첫 발은 정상이지만 연사 두 번째 발 이후가 모두 무시되어 “첫 발만 흔들리고 끝”이 됨.
수정 — bIsReacting이면 이전 본을 즉시 정리하고 새 본에서 재시작.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void UHitReactComponent::PlayHitReact(FName BoneName, float Strength)
{
if (!CachedMesh) { UE_LOG(..., TEXT("SKIP: CachedMesh NULL")); return; }
if (!BlendWeightCurve) { UE_LOG(..., TEXT("SKIP: BlendWeightCurve NULL")); return; }
if (BoneName.IsNone()) { UE_LOG(..., TEXT("SKIP: BoneName None")); return; }
if (ExcludedBones.Contains(BoneName)) { UE_LOG(..., TEXT("SKIP: ExcludedBones")); return; }
// 연사 중이면 이전 본 즉시 정리
if (bIsReacting && !CurrentBone.IsNone() && CurrentBone != BoneName)
{
CachedMesh->SetAllBodiesBelowSimulatePhysics(CurrentBone, false);
CachedMesh->SetAllBodiesBelowPhysicsBlendWeight(CurrentBone, 0.f);
}
// 새 본으로 재시작
CurrentBone = BoneName;
CachedMesh->SetAllBodiesBelowSimulatePhysics(BoneName, true);
bIsReacting = true;
Elapsed = 0.f;
// ... Tick에서 BlendWeightCurve 평가
}
이상 케이스 SKIP 로그는 4종만 남기고(CachedMesh NULL / BlendWeightCurve NULL / BoneName None / ExcludedBones), 정상 동작 로그는 모두 정리했다. 코드리뷰에서 로그가 어수선해 보이지 않게 정돈.
같은 본 연사(CurrentBone == BoneName)는 이전 정리 없이 Elapsed만 0으로 리셋 — 같은 본에 SetAllBodies를 두 번 호출하면 PhysicsAsset이 잠깐 깜빡이는 시각 아티팩트가 생긴다.
5. BaseWeapon::Fire — 데드 코드 처리 + WeaponComponent 위임
Player 모듈의 ABaseWeapon은 A팀이 작성한 코드인데, 코드를 읽다가 Fire()가 BlueprintCallable로 노출돼 있는 걸 발견.
1
2
3
// 기존 ABaseWeapon.h (A팀)
UFUNCTION(BlueprintCallable, Category="Weapon")
void Fire();
문제 — BP/AnimNotify/Input 등 어디서든 BaseWeapon::Fire를 호출하면 내부에서 자체 LineTrace + UGameplayStatics::ApplyDamage로 별도 경로가 돈다. 이 경로는 UWeaponComponent::TryFire를 거치지 않으니 HitReact·히트스톱이 적용되지 않는다.
오늘 PIE 디버깅에서는 좌클릭 입력이 WeaponComponent로 잘 흘러가는 게 확인됐지만, BP 어딘가에서 BaseWeapon::Fire를 추가 호출하고 있을 가능성 — 즉 이중 발사 — 을 봉인하지 않으면 코드리뷰 후 다른 팀원이 무심코 BP 노드를 연결할 위험이 남는다.
해결 1단계 — 진단 로그로 호출 추적:
1
2
3
4
5
6
void ABaseWeapon::Fire()
{
UE_LOG(LogTemp, Warning, TEXT("[BaseWeapon::Fire] CALLED by %s"),
*GetNameSafe(GetInstigatorController()));
// ... 기존 본체
}
PIE 1회 + 연사 1회를 돌려보고 로그가 안 찍히는 걸 확인 → 데드 코드 판정.
해결 2단계 — 본체 통째로 주석 + WeaponComponent로 위임:
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
38
39
void ABaseWeapon::Fire()
{
// [2026-05-15] 데드 코드 봉인. 진단 로그로 호출 0회 확인.
// 향후 BP/AnimNotify에서 호출되더라도 WeaponComponent 단일 경로로 강제.
UE_LOG(LogTemp, Warning, TEXT("[BaseWeapon::Fire] redirected to WeaponComponent"));
AActor* OwnerActor = GetOwner();
if (!OwnerActor) return;
UWeaponComponent* WC = OwnerActor->FindComponentByClass<UWeaponComponent>();
if (!WC) return;
// Muzzle 소켓이 있으면 그 위치, 없으면 액터 위치 폴백
FVector StartLoc = GetActorLocation();
if (USkeletalMeshComponent* WM = FindComponentByClass<USkeletalMeshComponent>())
{
if (WM->DoesSocketExist(TEXT("Muzzle")))
{
StartLoc = WM->GetSocketLocation(TEXT("Muzzle"));
}
}
// AimRotation은 InstigatorController의 ControlRotation, 없으면 ActorRotation 폴백
FRotator AimRot = GetActorRotation();
if (AController* IC = GetInstigatorController())
{
AimRot = IC->GetControlRotation();
}
WC->TryFire(StartLoc, AimRot);
/* ===== 기존 본체 전체 주석 (LineTrace + ApplyDamage 직접 호출 경로) =====
FHitResult Hit;
FVector Start = ...;
FVector End = ...;
GetWorld()->LineTraceSingleByChannel(...);
UGameplayStatics::ApplyDamage(...);
==========================================================================*/
}
이렇게 두면 누가 BaseWeapon::Fire를 호출하든 자동으로 WeaponComponent 경로로 단일화된다. HitReact·히트스톱은 어느 호출 경로에서도 동일하게 발동.
진단 로그(redirected to WeaponComponent)는 1줄만 안전망으로 유지 — 향후 BP에서 호출이 다시 일어나는 게 발견되면 즉시 보임. 정리는 코드리뷰에서 합의 후.
6. BTTask_Move — 로그 잡음 제거 (0줄 수정)
B팀이 만든 UBTTask_Move가 매 사이클 MoveToActor를 재호출하면서 UE_LOG를 끝없이 찍어 출력 로그를 어지럽혔다. PIE 30초만 돌려도 콘솔이 가득.
수정 — 로그 줄만 주석 처리. 로직은 0줄 수정.
1
2
3
4
5
6
7
8
9
EBTNodeResult::Type UBTTask_Move::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
// ... 기존 로직 그대로
// UE_LOG(LogTemp, Log, TEXT("[BTTask_Move] MoveToActor: %s"), *GetNameSafe(TargetActor));
AIController->MoveToActor(TargetActor, AcceptanceRadius);
return EBTNodeResult::InProgress;
}
히트스톱 검증 작업 중에 콘솔 로그가 깨끗해야 다른 로그(BaseWeapon::Fire 진단 등)가 보인다. B팀 코드라 로직은 절대 손대지 않고 로그만 정리한 게 포인트.
트러블슈팅 — 코드리뷰 자주 묻는 부분
| 증상 | 원인 | 해결 |
|---|---|---|
| 입력 무반응 | World Settings의 GameMode Override가 BP_NBC_GameMode로 박힘 | World Settings → BP_TestGameMode |
| 디버그 라인 없음, 사격 무반응 | BP_Rifle의 WeaponConfig 슬롯이 None → SwitchWeapon이 가드에서 차단 → EquipWeapon 미호출 | BP_Rifle Class Defaults에 DA_WeaponConfig 할당 |
| 좀비 데미지는 들어가는데 HitReact 안 됨 | Mesh 콜리전이 CharacterMesh 프리셋(Query Only, No Physics) | Custom + QueryAndPhysics로 코드 일괄 설정 |
| PhysicsAsset 본 일부만 QueryAndPhysics | 본 일괄 선택이 안 됨 | Skeleton Tree 전체 선택 후 일괄 변경 |
Hit.BoneName == None | 캡슐 ECC_Weapon Block → 본까지 트레이스 미도달 | FindClosestBone_K2 폴백 + 캡슐 ECC_Weapon Ignore |
| HitReact 첫 발만 흔들리고 이후 SKIP | bIsReacting 가드 | 이전 본 정리 후 새 본 재시작 (RESTART 로직) |
| 히트스톱 적용은 되는데 좀비가 안 멈춤 | Pawn만 정지, AIController는 별개 액터라 BT 계속 굴러감 | AIController에도 CustomTimeDilation 적용 |
| Live Coding 후 변경 미반영 | UPROPERTY 추가는 정식 빌드 필요 | 에디터 종료 후 IDE 빌드 |
발사 흐름 최종 정리
1
2
3
4
5
6
7
8
9
10
좌클릭 / BP Fire 호출 / AnimNotify
↓
WeaponComponent::TryFire (단일 경로)
↓
LineTrace (ECC_Weapon)
↓
ApplyHitDamage(Hit, Damage, Instigator)
├── HealthComponent->ApplyDamage (체력 차감)
├── HitReactComponent->PlayHitReact (본 흔들림, RESTART 로직)
└── ApplyHitStop (Pawn + AIController + Player CustomTimeDilation 0.15s)
세 가지 효과(체력·본 흔들림·히트스톱)가 동일한 진입점(ApplyHitDamage)에서 일괄 분기된다. 발사 호출 경로가 늘어나도(샷건·근접무기 추후 추가) 이 진입점만 거치면 일관성 유지.
코드리뷰에서 강조할 설계 결정
- 단일 경로 강제 —
BaseWeapon::Fire를 데드 코드 처리 + WeaponComponent 위임으로 통일. BP/AnimNotify에서 누가 호출해도 HitReact·히트스톱이 동일하게 발동. 이중 경로 가능성을 코드 레벨에서 봉인 - 본 폴백 (
FindClosestBone_K2+_End부모 거슬러) — PhysicsAsset 본 콜리전이 듬성듬성한 캐릭터에서도 시각적 일관성._End처리가 없으면 말단 본에 BlendWeight를 걸어도 흔들림이 안 보이는 함정 회피 - AIController 포함 히트스톱 —
CustomTimeDilation이 액터 단위라 Pawn만 멈추면 BT가 계속 돈다. AIController까지 같이 멈춰야 BT 의사결정이 진짜로 멎고 타격감이 살아남 - BT 0줄 수정 —
CustomTimeDilation이 액터 단위라 BTTask의 Tick·타이머가 자동 스케일. B팀 코드에 손 대지 않고 히트스톱 기능을 얹은 점 — 모듈 경계 보존 - 콜리전 채널 정합 (
ECC_Weapon) — Mesh는 Block / Capsule은 Ignore. 라인트레이스가 캡슐 통과 후 본에 도달하는 흐름을 코드로 일괄 보장 (BP 콜리전 프리셋에 의존하지 않음) - BP 노출 파라미터 최소화 — 히트스톱 강도·지속만 컴포넌트 디테일에서 튜닝. DataAsset까지 안 가는 이유는 무기 종류별 차이가 작아서. 차후 차이가 커지면 DA로 승격
오늘 배운 것 정리
CustomTimeDilation은 액터 단위 — Pawn만 멈추면 BT는 안 멈춘다 — AIController는 별개 액터라서 같이 0.1로 두지 않으면 의사결정이 풀스피드로 진행. 타격감의 핵심은 “몸과 결정이 같이 멈추는 것”Hit.BoneName == None은 콜리전 채널 누락의 신호 — PhysicsAsset 문제가 아니라 캡슐이 Block이라 본까지 트레이스가 못 가는 경우가 많음.FindClosestBone_K2는 안전망일 뿐, 근본 원인은 채널 응답 정합- PhysicsAsset의
_End본은 부모로 거슬러야 흔들림이 보인다 — 말단 본은 콜리전 빈약 + 등록 누락이 잦아서 SetAllBodiesBelow의 효과가 거의 없음. 최대 8단계 부모 거슬러 가드 BlueprintCallable노출 함수는 데드 코드라도 위험 — 호출 가능성이 BP 어디든 열려 있어 향후 다른 사람이 무심코 연결할 수 있음. 데드 확인되면 위임으로 단일화하는 게 안전- World Settings의 GameMode Override는 Project Settings보다 우선 — 레벨 단위 디버깅 환경 만들 땐 Project Settings 먼저 보지 말고 World Settings부터 확인. GameMode 안 바뀌면 첫 의심처
- B팀 코드는 로그만 정리하고 로직은 0줄 수정 — 모듈 경계 보존이 코드리뷰에서 가장 빨리 신뢰를 얻는 방식. “내가 책임지는 영역” vs “건드리지 않는 영역”이 디프에서 즉시 보이게
다음 단계 후보
- ⬜ 카메라 셰이크 (roles.md C-AL) — 근접/원거리 각 1종. 무기 분기와 통합
- ⬜ 피격 VFX — Niagara
SpawnEmitterAtLocation. 05/08 학습한 핸들 보관 패턴 적용(루프 PFX는 Attached 또는 멤버 보관) - ⬜ 처형 시스템 — 체력 30% 임계값. HealthComponent의
OnHealthThreshold델리게이트 신규? - ⬜
BaseWeapon::Fire의 진단 로그 정리 — 현재 안전망 1줄 유지 중. 코드리뷰에서 합의 후 제거 또는LogVerbosity=VeryVerbose로 강등 - ⬜ HitReact 본 폴백의 8단계 제한 수치 검증 — 실제 좀비 스켈레톤에서 가장 깊은
_End가 몇 단계인지 측정 후 적정값으로 조정
오후·저녁 추가 학습 — 2026-05-15
오전·낮 작업으로 Ch3 HitReact·히트스톱 통합을 마친 뒤, 오후·저녁에 진행한 다섯 가지 추가 작업을 정리한다. 핵심은 5번 — NBC_Master WeaponBox의 동기 로드를 비동기 로드로 교체한 작업이다. 나머지는 CS 모의면접 후속, 캐시 CS 파일 작성, 백트래킹 2문제, 코드리뷰 발표 가이드 보강 순으로 짧게 정리.
오후·저녁 추가 작업 요약
- CS 모의면접 — 실수 자료형 (오전 진행 완료, 후속 메모) —
24_floating_point.md흐름대로 답변. 정밀도 파트(가수 비트·ULP·machine epsilon)는 답변이 얕았다는 자평 → 별도 작업으로 큐 - CS 25번 — 캐시 히트/미스 신규 —
raw/cs-notion/25_cache_hit_miss.md작성 (1528줄, 18섹션). 2026-05-18(월) 모의면접 대비, 실제 학습은 주말 - 알고리즘 — LeetCode 백트래킹 2문제 — Subsets(78) + Permutations(46). 각 3가지 풀이 비교
- Ch3 코드리뷰 발표 준비 —
2026-05-15_코드리뷰_HitReact_히트스톱.md에 §10 PIE 시연 순서 추가 + cpp/h 6개 파일에 발표 가이드용 한 줄 주석 삽입 - NBC_Master
WeaponBox— 동기 로드 → 비동기 로드 교체 ★ —LoadSynchronous한 줄을FStreamableManager::RequestAsyncLoad+FStreamableDelegate콜백 패턴으로 전환.TSoftClassPtr<AActor>개념·IsNull/IsValid/Get차이·콜백에서의 멤버 변수 컨텍스트 보존이 오늘의 핵심 학습
1. CS 모의면접 — 실수 자료형 후속 메모
raw/cs-notion/24_floating_point.md의 6섹션 흐름(IEEE 754 구조 → 정밀도 → 비교 함정 → 게임 직결 이슈 → 결정성 → 마무리)을 그대로 따라 답변. 부호 1비트 + 지수 8비트 + 가수 23비트(float 기준)와 정규화 표현(1.xxx × 2^E)의 묵시적 leading 1 트릭까지는 매끄럽게 풀렸다.
부족했던 부분 — 정밀도 파트
- 가수 23비트 → 약 7자리 십진 정밀도라는 결론 자체는 말했지만, 왜 7자리인지 ($2^{23} \approx 8.4 \times 10^6$, $\log_{10}(2^{23}) \approx 6.92$)를 면접관이 한 번 더 물었을 때 즉답이 안 나왔다
- ULP(Unit in the Last Place) — 인접한 두 float 값 사이의 간격. 값이 커질수록 ULP도 같이 커진다(지수가 커지면 가수 1비트당 표현 간격이 배수로 벌어짐)는 직관을 말로 풀어내지 못함
- Machine epsilon —
1.0과 그 다음 표현 가능한 float 사이의 간격. float 기준 약 $1.19 \times 10^{-7}$. 비교 허용 오차의 출발점이지만 절대치 비교에는 부적합하다(큰 값에서는 epsilon이 너무 작다 → 상대 오차 비교가 필요)는 점
이 세 가지가 한 묶음으로 안 나오면 “왜 == 비교가 위험한지”의 근거가 약해진다. 다음 모의면접 전에 별도 보강 필요로 큐.
핵심 학습 — 게임 직결 이슈
IEEE 754의 표현 한계는 게임에서는 두 가지로 직결된다.
- LWC(Large World Coordinate) — 월드 원점에서 멀어질수록 float 정밀도가 떨어진다 (ULP가 커지니까). UE5가 double 기반 LWC를 도입한 이유. 1km 떨어진 지점의 1mm 단위 위치 차이가 float로는 표현이 안 됨
- 결정성(Determinism) — 같은 입력에 같은 결과를 보장해야 하는 리플레이·네트워크 동기화에서, 부동소수 연산 순서와 컴파일러·FPU 모드에 따라 결과가 달라질 수 있다. 멀티플레이어 게임이 정수·고정소수 기반 시뮬레이션을 쓰는 이유
2. CS 25번 — 캐시 히트/미스 (신규 작성, 주말 학습 예정)
파일 — raw/cs-notion/25_cache_hit_miss.md (1528줄, 18섹션)
2026-05-18(월) 모의면접 대비 자료. 오늘은 자료만 작성, 실제 학습은 주말 토·일.
핵심 토픽 정리
| 섹션 | 핵심 |
|---|---|
| 메모리 계층 | Register(0) / L1(~4 cycle) / L2(~12) / L3(~40) / DRAM(~200~300). cycle 수가 50~70배 점프 |
| 3C 미스 분류 | Compulsory(첫 접근) / Capacity(크기 부족) / Conflict(매핑 충돌). 멀티코어면 +Coherence |
| 캐시 라인 | 64B가 일반. 인접 데이터가 같은 라인에 묶임 → spatial locality 활용 |
| False sharing | 서로 다른 스레드가 같은 64B 라인 안 다른 변수를 쓰면 라인 무효화로 성능 폭락. alignas(64)로 분리 |
| Locality | Spatial(인접) vs Temporal(반복). 둘 다 캐시 친화 코드의 양대 축 |
| AoS vs SoA | Array of Structs는 한 객체 전부, Struct of Arrays는 한 필드만 묶음. DOP(Data-Oriented Programming)는 SoA 선호 |
| MESI | Modified·Exclusive·Shared·Invalid. 멀티코어 캐시 일관성 프로토콜 |
| TLB·Huge Page | 가상 주소 → 물리 주소 변환 캐시. 큰 데이터는 2MB·1GB 페이지로 TLB miss 감소 |
| 언리얼 Mass Entity | ECS 기반 SoA 구조. TArray 연속 메모리 vs TMap 해시 산재 |
학습 포인트 한 줄
- 히트율 95~99%가 일반 기준선. 1번의 miss가 DRAM까지 가면 200~300 cycle 페널티. 5%의 miss가 전체 시간의 절반 이상을 먹는 구조
graphify CS 그래프도 업데이트했다 — 594 nodes·604 edges·112 communities. 이전보다 캐시·메모리 클러스터가 한 덩어리로 명확하게 잡힘.
3. 알고리즘 — LeetCode 백트래킹 2문제
오늘은 백트래킹을 묶어서 풀었다. 두 문제 모두 “선택지 트리를 어떻게 표현할 것인가”의 차이가 핵심이다.
3-1. LeetCode 78 — Subsets (Medium)
파일 — CodeingTest/CodingTest/100zun/2026-05-15-1.cpp
3가지 풀이를 다 짜봤다.
풀이 1 — 백트래킹 (include/exclude)
1
2
3
4
5
6
7
8
9
void backtrack(int start, vector<int>& nums, vector<int>& path,
vector<vector<int>>& result) {
result.push_back(path); // 매 진입 시점이 곧 하나의 부분집합
for (int i = start; i < nums.size(); ++i) {
path.push_back(nums[i]);
backtrack(i + 1, nums, path, result);
path.pop_back();
}
}
핵심 — 별도 leaf 조건이 없다. 매 함수 진입 시점의 path 자체가 유효한 부분집합. start 인덱스를 단조 증가시켜 중복 방지.
풀이 2 — 비트마스크
1
2
3
4
5
6
for (int mask = 0; mask < (1 << n); ++mask) {
vector<int> subset;
for (int i = 0; i < n; ++i)
if (mask & (1 << i)) subset.push_back(nums[i]);
result.push_back(subset);
}
2^n을 순회하면서 비트가 켜진 인덱스만 모음. n ≤ 32일 때 깔끔.
풀이 3 — Cascading (iterative)
빈 집합에서 시작해 원소를 하나씩 추가하며 기존 집합 전부에 그 원소를 덧붙인 새 집합을 만들어 결과를 두 배씩 키움. 재귀 없이 직관적.
복잡도 — 시간 $O(N \cdot 2^N)$ (부분집합 $2^N$개 × 복사 $O(N)$), 공간 재귀 스택 $O(N)$.
3-2. LeetCode 46 — Permutations (Medium)
파일 — CodeingTest/CodingTest/100zun/2026-05-15-2.cpp
이쪽도 3풀이.
풀이 1 — used 플래그 백트래킹
1
2
3
4
5
6
7
8
9
10
void backtrack(vector<int>& nums, vector<bool>& used,
vector<int>& path, vector<vector<int>>& result) {
if (path.size() == nums.size()) { result.push_back(path); return; }
for (int i = 0; i < nums.size(); ++i) {
if (used[i]) continue;
used[i] = true; path.push_back(nums[i]);
backtrack(nums, used, path, result);
path.pop_back(); used[i] = false;
}
}
각 자리에 “아직 안 쓴 원소” 중 하나를 시도. 가독성 최상.
풀이 2 — 스왑 in-place 백트래킹
1
2
3
4
5
6
7
8
void backtrack(int first, vector<int>& nums, vector<vector<int>>& result) {
if (first == nums.size()) { result.push_back(nums); return; }
for (int i = first; i < nums.size(); ++i) {
swap(nums[first], nums[i]);
backtrack(first + 1, nums, result);
swap(nums[first], nums[i]);
}
}
used 배열도 path 벡터도 없이 원본 배열만 스왑. 추가 메모리 0. 대신 호출 직전 상태를 호출 직후에 정확히 복구해야 해서 실수 위험.
풀이 3 — STL next_permutation
1
2
sort(nums.begin(), nums.end());
do { result.push_back(nums); } while (next_permutation(nums.begin(), nums.end()));
정렬된 상태에서 사전순으로 다음 순열을 만들어 순회. 코드가 압도적으로 짧지만 백트래킹 이해엔 도움 안 됨.
복잡도 — 시간 $O(N \cdot N!)$, 공간 재귀 $O(N)$.
두 문제를 묶어서 본 학습 포인트
- 선택지 트리의 표현이 핵심이다
- 부분집합 — “이번에 쓸 다음 원소는?” →
start인덱스로 단조 증가 - 순열 — “이 자리에 쓸 원소는?” →
used플래그로 안 쓴 것 표시, 또는 스왑으로 앞쪽 영역에 고정
- 부분집합 — “이번에 쓸 다음 원소는?” →
- 메모리 트레이드오프가 분명하다
- 부분집합 비트마스크 — $2^n$ 자체가 한계 (n ≤ 32 정도)
- 순열 스왑 풀이 — 추가 메모리 0이지만 호출 전후 상태 복구 부담
- leaf 조건의 위치도 패턴이 다르다
- 부분집합 — 매 진입 시점 push (leaf 조건 없음)
- 순열 —
path.size() == n일 때만 push (명시적 leaf)
4. Ch3 코드리뷰 발표 준비
내일 팀 코드리뷰 발표 직전 준비 작업.
파일 1 — 발표 가이드 보강
scrum/Ch3-TeamProject/2026-05-15_코드리뷰_HitReact_히트스톱.md에 §10 PIE 시연 순서 섹션을 새로 추가했다.
구성:
- 시연 환경 셋업 — World Settings GameMode Override 교체, BP_TestPlayerCharacter 빙의 확인, PIE 진입
- 데모 ① 단일 사격 — 좌클릭 1발 → 좀비 본 흔들림 + 히트스톱 0.15초
- 데모 ② 본 단위 흔들림 — 머리·팔·다리 각각 사격해 본별 PhysicsAsset 반응 확인
- 데모 ③ BoneName 폴백 — Capsule을 임시로 Block 처리한 비교 영상(또는 로그)
- 데모 ④ 연사 RESTART — 빠른 연속 클릭으로 이전 본 정리 + 새 본 흔들림 전환
- 데모 ⑤ 히트스톱 — AIController까지 멈춰 BT 정지하는 모습 (좀비가 추격을 잠깐 끊고 다시 시작)
- 데모 ⑥ Fire 단일 경로 위임 —
BaseWeapon::Fire호출 시 WeaponComponent로 redirect되는 로그 확인
각 데모마다 액션 / 기대 결과 / 보여줄 코드 위치 / 말할 포인트를 네 항목으로 분리. 컴포넌트 책임 다이어그램(ASCII)과 5분 시간 배분표, 실패 대비표(콜리전 채널이 안 먹을 때 무엇부터 확인)도 같이 정리.
파일 2 — 발표 가이드용 한 줄 주석 삽입
발표 중 코드를 열면서 설명할 위치 6곳에 한 줄 주석으로 마커를 박았다.
| 파일 | 위치 | 주석 의미 |
|---|---|---|
Combat/WeaponComponent.cpp | TryFire 진입부 | 모든 발사의 단일 진입점 마커 |
Combat/WeaponComponent.cpp | ApplyHitDamage | ①체력 → ②HitReact → ③히트스톱 디스패치 순서 |
Combat/WeaponComponent.cpp | ApplyHitStop | 3중 적용(Pawn·AIController·Instigator) + WeakLambda 안전성 |
Combat/HitReactComponent.cpp | PlayHitReact | 가드·연사 RESTART·임펄스 예약 |
Combat/WeaponComponent.h | TryFire 선언 | BP 호출 가능 단일 진입 API |
Combat/HitReactComponent.h | 클래스 선언부 | 컴포넌트 책임 한 줄 요약 |
학습 포인트
코드 리뷰는 “코드를 보면서” 진행되므로 시연 흐름과 코드 위치를 1:1로 매핑해두는 게 핵심. 발표자가 IDE에서 파일을 빨리 못 찾으면 흐름이 끊긴다. 한 줄 주석은 동료 검색용 마커이자, 다른 사람이 6개월 뒤 코드를 다시 볼 때 “여기가 발사 단일 경로구나”를 즉시 알아볼 수 있는 자가 문서화이기도 함.
5. NBC_Master WeaponBox — 동기 로드 → 비동기 로드 교체 ★ (오늘 핵심 학습)
파일 — D:/Unreal/NBC_Master/Source/NBC_Master/WeaponBox.{h,cpp} 참고 패턴 — TestActorTT.cpp의 LoadWithSoftPtr + MyOnLoadCompleted
오늘의 핵심 학습이라 분량 비중을 높여서 정리한다. 코드 한 줄(LoadSynchronous) 바꾸는 작업처럼 보이지만, 그 한 줄을 비동기로 바꾸려면 콜백 패턴 / 멤버 변수 컨텍스트 / 이미 로드된 경우 분기 / Soft 포인터의 세 상태까지 한 묶음으로 이해해야 한다.
5-1. 변경 전 — 동기 로드
1
2
3
4
5
6
7
8
// AWeaponBox::SpawnSelectedWeapon (변경 전)
UClass* LoadedClass = SelectedWeapon->WeaponClass.LoadSynchronous();
if (LoadedClass)
{
FActorSpawnParameters Params;
Params.Owner = this;
GetWorld()->SpawnActor<AActor>(LoadedClass, GetActorLocation(), GetActorRotation(), Params);
}
WeaponClass는TSoftClassPtr<AActor>타입 (디스크 경로만 보관, 메모리에는 아직 없을 수 있음)LoadSynchronous()는 현재 스레드에서 즉시 디스크에서 읽어와 메모리에 올리고UClass*반환- 단순하고 직선적이지만 디스크 I/O 동안 게임 스레드 블록 → 프레임 끊김. 무기 종류·블루프린트가 무거우면 체감 hitch 발생
5-2. 변경 후 — 비동기 로드
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
38
39
40
41
42
43
// 헤더 (WeaponBox.h)
UPROPERTY(VisibleAnywhere)
TSoftClassPtr<AActor> PendingWeaponClass; // 콜백에서 .Get()용 보관
UPROPERTY(VisibleAnywhere)
FName PendingWeaponName; // 디버깅·로그용
void OnWeaponClassLoaded(); // 콜백 함수
// 구현 (WeaponBox.cpp)
void AWeaponBox::SpawnSelectedWeapon()
{
if (!SelectedWeapon) return;
PendingWeaponClass = SelectedWeapon->WeaponClass; // 멤버에 컨텍스트 보관
PendingWeaponName = SelectedWeapon->WeaponName;
if (PendingWeaponClass.IsValid())
{
// 이미 메모리에 있으면 비동기 요청 없이 즉시 콜백 (동일 경로 재사용)
OnWeaponClassLoaded();
}
else
{
FStreamableManager& Streamable = UAssetManager::GetStreamableManager();
Streamable.RequestAsyncLoad(
PendingWeaponClass.ToSoftObjectPath(),
FStreamableDelegate::CreateUObject(this, &AWeaponBox::OnWeaponClassLoaded)
);
}
}
void AWeaponBox::OnWeaponClassLoaded()
{
UClass* LoadedClass = PendingWeaponClass.Get();
if (!LoadedClass) return;
FActorSpawnParameters Params;
Params.Owner = this;
GetWorld()->SpawnActor<AActor>(LoadedClass, GetActorLocation(), GetActorRotation(), Params);
UE_LOG(LogTemp, Log, TEXT("[WeaponBox] Spawned %s"), *PendingWeaponName.ToString());
}
기존 동기 코드는 삭제하지 않고 주석으로 보존(요구사항). 비교 학습용으로 두 경로를 같이 두는 형태.
5-3. TSoftClassPtr<AActor> 개념
TSoftClassPtr는 클래스를 경로 문자열로 들고 있는 포인터다. TSubclassOf와의 차이가 중요하다.
| 타입 | 보관 형태 | 메모리 로드 | 사용 시점 |
|---|---|---|---|
TSubclassOf<AActor> | UClass* 직접 참조 | 이 변수를 가진 객체가 로드되면 클래스도 같이 로드(하드 참조) | 항상 즉시 사용 가능 |
TSoftClassPtr<AActor> | FSoftObjectPath(경로 문자열) | 소유 객체 로드와 무관. 명시적으로 로드해야 메모리에 올라옴 | 로드 후 Get() 또는 LoadSynchronous() |
TSoftClassPtr를 쓰는 이유 — 무기 50종을 데이터 에셋에 다 등록해도 WeaponBox 액터 자체가 무기 50종을 전부 메모리에 끌고 들어오지 않는다. 필요한 무기만 그때그때 로드. 이게 비동기 로드와 결합되면 메모리 풋프린트 최소화 + 프레임 끊김 없음 두 마리를 잡는다.
5-4. IsNull / IsValid / Get — Soft 포인터의 세 상태
비동기 로드를 다룰 때 가장 헷갈리는 부분. 셋이 명확히 다르다.
| 메서드 | 무엇을 확인 | 반환 |
|---|---|---|
IsNull() | 경로 자체가 비어있나 (디자이너가 슬롯을 안 채웠나) | bool |
IsValid() | 메모리에 이미 로드되어 있나 | bool |
Get() | 이미 로드되어 있으면 raw 포인터, 아니면 nullptr | UClass* |
핵심 — !IsNull()이라고 해서 Get()이 nullptr이 아닌 게 보장되지 않는다. 경로는 있어도 메모리엔 아직 없을 수 있음. 그래서:
IsNull()체크 → “디자이너 슬롯 채웠나” 검증IsValid()체크 → “비동기 로드를 또 요청할 필요 없나” 분기Get()사용 → 로드 완료가 보장되는 콜백 안에서만
LoadSynchronous()는 이 세 상태에서 자동으로 로드까지 해주고 raw 포인터를 돌려준다. 그래서 동기 코드는 단순하다. 비동기는 이 세 상태를 코드로 직접 다뤄야 한다.
5-5. UAssetManager::GetStreamableManager() 와 FStreamableDelegate
UAssetManager— 엔진 전역 싱글톤. 에셋 라이프사이클 관리FStreamableManager—UAssetManager안의 스트리밍 매니저. 비동기 로드 요청 큐 관리RequestAsyncLoad—FSoftObjectPath(또는 배열)를 받아 백그라운드 스레드에서 로드, 완료되면 델리게이트 호출FStreamableDelegate::CreateUObject— UObject 멤버 함수 포인터를 델리게이트로 바인딩. 객체가 GC되면 자동으로 안전 처리
1
FStreamableDelegate::CreateUObject(this, &AWeaponBox::OnWeaponClassLoaded)
이 한 줄이 곧 “이 액터의 OnWeaponClassLoaded를 콜백으로 등록”이다. 람다나 std::function으로 직접 짤 필요가 없다 — 엔진이 다 만들어둔 인프라를 가져다 쓰는 것.
5-6. 콜백 패턴에서의 멤버 변수 컨텍스트
비동기의 가장 큰 함정 — 콜백 시점에 지역 변수는 이미 소멸한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
void AWeaponBox::SpawnSelectedWeapon()
{
UWeaponData* LocalSelected = SelectedWeapon; // 지역 변수!
Streamable.RequestAsyncLoad(
LocalSelected->WeaponClass.ToSoftObjectPath(),
FStreamableDelegate::CreateLambda([LocalSelected]() {
// 콜백 시점에 LocalSelected가 살아있다는 보장 없음
// (이 예시는 캡처라 살지만, 그 안의 SelectedWeapon이 GC됐을 수 있음)
})
);
// 여기서 SpawnSelectedWeapon 종료
// 콜백은 N프레임 뒤 호출됨
}
그래서 PendingWeaponClass·PendingWeaponName을 멤버 변수로 보관한다. 함수가 종료돼도 액터가 살아 있는 한 멤버는 유지. 콜백에서 this->PendingWeaponClass.Get()으로 안전하게 재참조.
CreateUObject(this, ...)는 this가 GC되면 콜백을 알아서 호출 안 함. 람다보다 더 안전한 이유.
5-7. 이미 로드된 케이스 분기 — 동일 경로 재사용
1
2
3
4
5
6
7
8
if (PendingWeaponClass.IsValid())
{
OnWeaponClassLoaded(); // 직접 호출
}
else
{
Streamable.RequestAsyncLoad(...);
}
IsValid() == true면 이미 메모리에 있다. 비동기 요청을 다시 보내면 즉시 콜백되긴 하지만 불필요한 매니저 호출이다. 직접 콜백을 호출하면 로드 경로 코드 = 캐시 히트 경로 코드가 한 함수로 통일된다 (Spawn 로직이 한 군데).
이 패턴이 깔끔한 이유 — 호출자 입장에서 SpawnSelectedWeapon()은 동기처럼 보이든 비동기로 처리되든 상관없다. 결국 콜백 한 곳에서 스폰이 일어난다는 단일 책임이 보장됨.
5-8. 동기 vs 비동기 — 실무 트레이드오프
| 항목 | 동기 (LoadSynchronous) | 비동기 (RequestAsyncLoad) |
|---|---|---|
| 코드 분기 | 1줄, 직선 | 함수 분리(요청·콜백) + 멤버 변수 |
| 게임 스레드 블록 | 디스크 I/O 시간만큼 멈춤 | 안 멈춤 |
| 첫 호출 hitch | 있음(특히 콜드 캐시) | 없음 |
| 로드 완료 보장 | 호출 직후 보장 | 콜백 안에서만 보장 |
| 디버깅 난이도 | 낮음 | 콜백 호출 순서가 비결정적이라 약간 까다로움 |
| 적합한 곳 | 게임 시작·로딩 화면·확실히 가벼운 에셋 | 게임플레이 중 동적 스폰·콘텐츠 무거운 에셋 |
WeaponBox는 게임플레이 중 플레이어가 다가갈 때 스폰되므로 비동기가 맞다. 캐릭터 셀렉트 화면 초기 로드처럼 “어차피 로딩 중이다”라면 동기가 더 단순해서 나을 수도 있다.
5-9. 학습 포인트 (오늘의 핵심)
TSoftClassPtrvsTSubclassOf구분 — Soft는 경로만, Hard는 즉시 참조. 메모리 풋프린트 차이가 큼IsNull/IsValid/Get세 상태를 명확히 — 경로 존재 ≠ 메모리 로드 ≠ 즉시 사용 가능- 비동기 = 콜백 패턴이지만 엔진이 제공하는 인프라(
StreamableManager·FStreamableDelegate)를 쓰면 직접 람다·std::function·스레드 안전을 신경 쓸 필요가 없다 - 콜백에서의 컨텍스트는 멤버 변수로 — 지역 변수는 함수 종료와 함께 소멸. 비동기에서 살아남으려면 객체에 묶어야 함
- 이미 로드된 경우 직접 콜백 호출로 코드 경로를 통일 — 호출자가 동기·비동기를 신경 쓸 필요가 없게 단일 책임을 콜백으로 모음
CreateUObject가 람다보다 안전 — 대상 객체 GC 시 자동으로 콜백 무시. WeakLambda보다도 의도가 명확
가장 큰 인사이트 — “비동기 = 람다나 콜백을 직접 새로 짜기”가 아니다. 엔진이 제공하는 StreamableManager·Delegate 인프라를 조립하면 된다. 새로 발명할 필요가 없다는 게 핵심.
오후·저녁 종합 학습 정리
- 부동소수 정밀도 깊이 부족 — 가수·ULP·machine epsilon 한 묶음으로 정리 필요 (다음 모의면접 전 보강)
- 캐시 히트율 95~99%가 기준선 — 5%의 miss가 DRAM 200~300 cycle을 먹어 전체 시간의 절반을 차지하는 구조 (25번 파일 작성 완료, 학습은 주말)
- 백트래킹의 핵심은 “선택지 트리의 표현 방식” — 부분집합은 단조 증가 인덱스, 순열은 자리별로 안 쓴 원소. used 플래그 vs 스왑 인플레이스의 메모리 트레이드오프
- 코드리뷰는 코드를 보면서 진행되므로 시연 흐름과 코드 위치를 1:1로 매핑 — 한 줄 주석 마커는 동료를 위한 검색 키이자 자가 문서화
- ★
TSoftClassPtr+FStreamableManager::RequestAsyncLoad+FStreamableDelegate::CreateUObject콜백 패턴 — 비동기 로드는 엔진 인프라 조립이지 람다 직접 짜기가 아니다. 멤버 변수로 컨텍스트 보관 +IsValid()분기로 캐시 히트 경로 통일