TIL 2026-04-28
2026-04-28 8번 과제 Day 4 — 무기 시스템 캡슐화 + 비동기 가시선 + 차량 슬롯 E 키 통합
목차
- 2026-04-28 8번 과제 Day 4 — 무기 시스템 캡슐화 + 비동기 가시선 + 차량 슬롯 E 키 통합
오늘 한 일 요약
- 무기 시스템 캡슐화 —
UVOIDWeaponComponent도입, Pawn 은 입력 → 컴포넌트 위임만 담당 (단일 책임) - 샷건/라이플 DataAsset 분리 —
UVOIDWeaponConfig(UDataAsset) 로 펠릿/스프레드/레인지/반동/소음/FX 전 파라미터 외부화 →DA_Rifle/DA_Shotgun인스턴스 - 반동 시스템 — 발사 시
PendingRecoilPitch/Yaw누적 +TickRecoil에서FInterpTo로 0 보간, Pawn Tick 에서 컨트롤러 입력 적용 - 1/2 키 무기 스왑 —
IA_SwitchToRifle/ShotgunEnhanced Input +RifleConfig/ShotgunConfigBP Defaults 슬롯 - CurrentWeapon dangling 가드 —
IsValid체크 + 쓰레기값 (Interval=532053344) 디버그 케이스 폴백 VOIDWeaponConfig디렉토리 이동 —Items/→Weapon/(모듈 분리는 불필요, 디렉토리만)- ADS 적용 확인 — FOV/SpringArm 보간 + 이동속도 곱셈 합성 (무게·ADS) 정상 동작
- 비동기 가시선 (도전 과제) —
AVOIDZombieAIController에AsyncLineTraceByChannel+FTraceDelegate콜백,bUseAsyncSight토글로 실행, 콜백에서 BBTargetActor갱신 - 비동기 트레이스 디버그 시각화 — 가시선 확보 = 녹색 라인 + 청록 구체, 차단 = 빨강 라인 + 노랑 구체 (
bDrawAsyncSightDebug토글) - 차량 부품 슬롯 인터랙션 (E 키 통합) — F 키 분리 폐기,
Interact()한 곳에서 슬롯 → 차량 시동 → 픽업 우선순위 분기 PartType매칭 추가 —UVOIDItemDataAsset::PartType필드 신설,InventoryComponent::FindPartByType로 슬롯 타입에 맞는 부품 자동 선택- Sweep 콜리전 진단 — 헬기 메시(StaticMeshActor)가 ECC_Visibility Block 응답이라 SweepMulti 가 헬기에서 끊김 → 헬기 데코 콜리전 OFF + 코드에서
AStaticMeshActor후보 컷 - CS 모의면접 — “객체 복사 방지” —
12_prevent_copy.md파일 핸들·뮤텍스·소켓 RAII 키워드 보강, 슬라이싱·Rule of Five/Zero·friend 정리 - 마스터.md 작성 —
D:\Unreal\VoidUnreal\마스터.md필수/도전 과제 핵심 코드 라인 매핑 + 검증 체크리스트
무기 시스템 캡슐화 — UVOIDWeaponComponent
왜 분리했나
기존: AVOIDPlayerCharacter 가 CurrentWeapon 멤버 + 발사 로직 + 반동 누적 + 사운드 재생까지 다 들고 있었음 → Pawn 비대화, 무기를 다른 액터(터렛/NPC)에 붙일 때 코드 복제 필요.
변경: 무기 관련 책임을 UActorComponent 로 빼고 Pawn 은 입력 → 컴포넌트 위임만 담당.
인터페이스
1
2
3
4
5
6
7
8
9
class UVOIDWeaponComponent : public UActorComponent
{
void EquipWeapon(UVOIDWeaponConfig* NewWeapon);
bool TryFire(const FVector& MuzzleLoc, const FRotator& AimRot, AActor* DamageInstigator, float SpreadMul);
void TickRecoil(float DeltaTime, float& OutPitchDelta, float& OutYawDelta);
FOnWeaponEquipped OnWeaponEquipped;
FOnWeaponFired OnWeaponFired;
};
Dangling 가드
Interval=532053344 같은 쓰레기값이 찍힌 디버거 사례를 보고 추가:
1
2
3
4
5
6
UVOIDWeaponConfig* Weapon = IsValid(CurrentWeapon) ? CurrentWeapon : nullptr;
if (!Weapon && CurrentWeapon)
{
UE_LOG(LogTemp, Warning, TEXT("[Weapon] CurrentWeapon is dangling — clearing"));
CurrentWeapon = nullptr;
}
GC 후에도 TObjectPtr 가 한 프레임 동안 dangling 일 수 있어서 IsValid 강제 체크. 이후로는 폴백(25dmg/단발)로 동작 → 크래시 없음.
무기 DataAsset 분리 — UVOIDWeaponConfig
왜 DataAsset 인가
샷건/라이플은 EVOIDWeaponClass 만 다른 게 아니라 모든 파라미터가 다름 → BP 변수가 아니라 별도 DataAsset 인스턴스로 두는 게 OOP 적으로 깔끔.
UDataAsset= 디자이너가 에디터에서 만지는 데이터 인스턴스TSoftObjectPtr= 지연 로딩 — 사운드 1MB 가 항상 메모리 상주하지 않음- 코드 수정 없이
DA_Pistol,DA_Sniper추가 가능 (확장에 열려있음)
필드 목록
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
UCLASS(BlueprintType)
class UVOIDWeaponConfig : public UDataAsset
{
EVOIDWeaponClass WeaponClass;
int32 PelletCount; // 라이플=1, 샷건=8
float SpreadDegrees; // 산탄 각도
float Range;
float DamagePerPellet;
float FireInterval;
float RecoilPitch;
float RecoilYawJitter;
float RecoilRecoverPerSec;
float NoiseRadius;
TSoftObjectPtr<USoundBase> FireSound; // 지연 로딩
TSoftClassPtr<UCameraShakeBase> CameraShakeClass;
};
Items/ 아래 두면 PickupBase 와 섞여서 검색 어려움 → Weapon/ 디렉토리로 이동. 모듈 분리는 비용 대비 이득이 작아 보류.
반동 시스템 — 누적 + FInterpTo 회복
모델
1
2
3
4
5
6
7
8
9
Fire 발생
→ PendingRecoilPitch += RecoilPitch
→ PendingRecoilYaw += FRandRange(-Jitter, +Jitter)
Tick (매 프레임)
→ AddControllerPitchInput(-PendingRecoilPitch) // 음수가 위쪽
→ AddControllerYawInput( PendingRecoilYaw)
→ PendingRecoilPitch = FInterpTo(PendingRecoilPitch, 0, dt, RecoilRecoverPerSec)
→ PendingRecoilYaw = FInterpTo(PendingRecoilYaw, 0, dt, RecoilRecoverPerSec)
Pawn 측 처리
1
2
3
4
5
6
7
8
9
10
11
12
void AVOIDPlayerCharacter::Tick(float dt)
{
Super::Tick(dt);
TickAds(dt);
if (WeaponComp)
{
float P=0, Y=0;
WeaponComp->TickRecoil(dt, P, Y);
if (!FMath::IsNearlyZero(P)) AddControllerPitchInput(P);
if (!FMath::IsNearlyZero(Y)) AddControllerYawInput(Y);
}
}
샷건 (RecoilPitch=4.0) 은 위로 강하게 튀고 0.25초 안에 복귀. 라이플 (1.2) 은 미세하게 튐.
1/2 키 무기 스왑 (Enhanced Input)
헤더 슬롯 추가
1
2
3
4
5
6
7
8
9
10
11
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Input")
TObjectPtr<UInputAction> SwitchToRifleAction;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Input")
TObjectPtr<UInputAction> SwitchToShotgunAction;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Loadout")
TObjectPtr<UVOIDWeaponConfig> RifleConfig;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Loadout")
TObjectPtr<UVOIDWeaponConfig> ShotgunConfig;
바인딩 + 핸들러
1
2
3
4
5
6
7
8
9
if (SwitchToRifleAction)
EIC->BindAction(SwitchToRifleAction, ETriggerEvent::Started, this, &AVOIDPlayerCharacter::OnSwitchToRifle);
if (SwitchToShotgunAction)
EIC->BindAction(SwitchToShotgunAction, ETriggerEvent::Started, this, &AVOIDPlayerCharacter::OnSwitchToShotgun);
void AVOIDPlayerCharacter::OnSwitchToRifle(const FInputActionValue&)
{
if (RifleConfig) EquipWeapon(RifleConfig);
}
에디터에서 IA_SwitchToRifle/Shotgun Input Action + IMC 매핑(1/2 키) + BP Defaults 4 슬롯(IA 2 + DataAsset 2) 할당.
비동기 가시선 트레이스 (도전 과제)
청각(AIPerception)과 시각(AsyncTrace) 역할 분담
| 시스템 | 트리거 | 용도 |
|---|---|---|
| AIPerception Hearing | 발사음·픽업음 | 노이즈 방향 감지 |
| AsyncLineTrace | 0.2초 주기 Tick | 시야 안 진입 감지 (벽 너머는 컷) |
둘 다 같은 BB TargetActor / bHasTarget 를 공유 → BT 가 일관된 추격 로직.
비동기 호출 + 콜백
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void AVOIDZombieAIController::RequestAsyncSight()
{
GetWorld()->AsyncLineTraceByChannel(
EAsyncTraceType::Single,
Start, End,
ECC_Visibility,
Params,
FCollisionResponseParams::DefaultResponseParam,
&AsyncTraceDelegate); // 콜백
}
void AVOIDZombieAIController::HandleAsyncTrace(const FTraceHandle&, FTraceDatum& Data)
{
const bool bBlocked = (Data.OutHits.Num() > 0 && Data.OutHits[0].bBlockingHit);
if (UBlackboardComponent* BB = GetBlackboardComponent())
{
if (!bBlocked)
{
BB->SetValueAsObject(TEXT("TargetActor"), UGameplayStatics::GetPlayerPawn(this, 0));
BB->SetValueAsBool(TEXT("bHasTarget"), true);
}
}
}
핵심: 게임 스레드 블로킹 없음. 동기 LineTraceSingleByChannel 을 좀비 N마리에 매 틱 돌리면 stat unit 에서 게임 스레드 정체. 비동기로 분산 → 콜백은 다음 틱에 자동.
디버그 시각화
bDrawAsyncSightDebug 토글 → 콜백에서 라인/구체 그림. 가시선 확보 = 녹색 / 차단 = 빨강. 라인 수명을 AsyncTraceInterval * 1.1f 로 줘서 끊김 없이 갱신.
차량 부품 슬롯 — E 키 통합
F 키 폐기 → E 키 단일화
처음엔 IA_StartEngine (F키) 별도로 두려 했지만 인터랙션 키가 둘이면 UX 산만 → Interact() 한 곳에서 우선순위 분기:
1
2
3
4
5
6
7
8
9
10
11
12
if (Best->Implements<UVOIDVehiclePartSlot>())
{
// 1) 부품 설치
return;
}
if (auto* Vehicle = Cast<AVOIDVehicle>(Best))
{
// 2) 차량 시동
Vehicle->TryStartEngine(this);
return;
}
// 3) 일반 픽업
PartType 정확 매칭
기존: Interact() 가 CandidatePart=nullptr 로 호출 → TryInstallPart 첫 가드(!IsValid(Part))에서 즉시 false.
수정: UVOIDItemDataAsset 에 EVOIDVehiclePartType PartType 필드 추가 + UVOIDInventoryComponent::FindPartByType(SlotType) 로 인벤토리에서 슬롯 타입에 맞는 부품 검색.
1
2
3
4
const EVOIDVehiclePartType SlotType = IVOIDVehiclePartSlot::Execute_GetRequiredPartType(Best);
UVOIDItemDataAsset* CandidatePart = InventoryComponent->FindPartByType(SlotType);
if (!CandidatePart) return;
IVOIDVehiclePartSlot::Execute_TryInstallPart(Best, CandidatePart, this);
콜리전 충돌 디버깅 — SweepMulti 가 헬기 메시에서 끊김
증상
E 키 눌러도 슬롯이 Hit 목록에 안 들어옴. 로그:
1
2
3
[Interact] Sweep hits=2
[Interact] Hit: lowpoly_heli_node_62_StaticMeshActor (...)
[Interact] Hit: lowpoly_heli_node_18_StaticMeshActor (...)
원인
SweepMultiByChannel(ECC_Visibility)는 Block 응답 첫 히트에서 멈춤- 헬기 메시 액터들이
BlockAll프리셋 → Sweep 이 헬기에서 끊겨서 슬롯이 후보 목록에 영영 안 들어옴 - 처음엔 차량 BP 의 ChildActor 일 거라 가정하고
TActorIterator<AVOIDVehicle>로 무시했지만 — 헬기 메시는 레벨에 직접 배치된 별도 StaticMeshActor 였음
해결
- 레벨 헬기 메시 콜리전 OFF (Outliner 다중 선택 → Collision Preset = NoCollision)
- 코드 방어 —
AStaticMeshActor후보는 Hit 루프에서 컷:
1
2
const bool bStaticDecor = HitActor->IsA(AStaticMeshActor::StaticClass());
if (bStaticDecor) continue;
PIE 검증은 내일로 이월.
CS — 객체 복사 방지 모의면접
오늘 모의면접 주제. 12_prevent_copy.md 키워드 추가:
막는 방법
= delete(C++11)private복사 생성자 (C++03)noncopyable패턴 (boost)unique_ptr멤버 (자동 전이)
왜 막는가 — RAII 자원의 이중 해제
- 파일 핸들 (
FILE*,std::ifstream) — 같은 fd 두 번 close - 뮤텍스 (
std::mutex) — 동일 락 두 번 unlock → UB - 소켓 (
SOCKET) — fd 재사용 후 잘못된 fd 닫힘 unique_ptr— 동일 힙 객체 두 번 delete
슬라이싱 / Rule of Three~Five~Zero / friend
- 슬라이싱:
Base = Derived값 복사 →Derived부분 잘림 + vtable 손상 → 다형성 깨짐 - Three (C++03): 복사 생성자 / 복사 대입 / 소멸자
- Five (C++11+): + 이동 생성자 / 이동 대입
- Zero: RAII 멤버에 위임해서 명시 정의 0개 (모던 권장)
- friend: 다른 클래스/함수에
private접근 허용. 캡슐화의 의도적 예외 (연산자 오버로딩, 팩토리, 테스트)
마스터.md 작성
언리얼 마스터 과제(샷건+반동+커스터마이징 / 비동기 Trace) 별도 레포 제출용 가이드. 각 항목마다 파일경로:라인번호 매핑 + PIE 검증 체크리스트. 채점자가 코드 위치를 즉시 점프 가능.
Day 4 진행 정리
| 항목 | 상태 | 비고 |
|---|---|---|
| 무기 시스템 캡슐화 | ✅ | UVOIDWeaponComponent + Pawn 위임 |
| 샷건/라이플 DataAsset 분리 | ✅ | DA_Rifle, DA_Shotgun |
| 반동 누적 + 회복 | ✅ | FInterpTo + Tick 처리 |
| 1/2 키 무기 스왑 | ✅ | Enhanced Input + IMC 매핑 |
| ADS FOV/SpringArm 보간 | ✅ | 무게·ADS 이동속도 곱셈 합성 |
| 비동기 가시선 (도전) | ✅ | AsyncLineTraceByChannel + 콜백 |
| 비동기 디버그 시각화 | ✅ | 녹/빨/청록/노랑 토글 |
| E 키 통합 인터랙션 | ✅ | 슬롯 → 시동 → 픽업 우선순위 |
PartType 매칭 | ✅ | FindPartByType 추가 |
| 헬기 콜리전 OFF | 부분 완료 | 코드 수정·콜리전 변경 끝, PIE 검증 내일 |
마스터.md 작성 | ✅ | VoidUnreal 별도 레포 제출용 |
| Day 4 커밋 | 진행 중 | — |
Day 4 → Day 5 인계:
- 헬기 콜리전 OFF 후 PIE 검증 (E 키로 슬롯 3개 부품 설치 → 무게 감소 →
bRepairComplete=true) - 차량 시동 (
TryStartEngine) 분기 정상 동작 확인 - GUI (HUD) 활성화 — 인벤토리 무게 게이지, 현재 무기, 웨이브/타이머, 차량 수리 진행도
- 레벨 전환 (8번 발제 추가 요구사항) — Lv_Main / Lv_VoidProto / Lv_Escape
오늘 배운 것 정리
컴포넌트 분리는 진작 했어야 함 — Pawn 에 무기 로직 다 박아놓다가 1/2 키 스왑 추가하려니 멤버 정리부터 시작했다. 처음부터
UVOIDWeaponComponent로 갔으면 깔끔. “이 책임은 누구의 것인가” 를 매 멤버마다 묻는 습관이 단일 책임 원칙으로 가는 길.TObjectPtr도 dangling 가능 — GC 시점 직후 한 프레임 동안 raw 포인터처럼 쓰레기값을 가질 수 있다.IsValid가드를 디폴트로 두고, 디버거에서 비정상값 (Interval=532053344) 보이면 dangling 의심. 폴백 경로(기본 데미지/단발) 로 크래시 없이 진행.콜리전 디버깅은 로그 + 시각화가 답 —
SweepMulti가 어디서 끊기는지 Hit 목록 출력으로 즉시 진단했다.lowpoly_heli_node_*가 후보로 잡히는 순간 원인 확정. 추측보다 증거를 먼저.UDataAsset= 데이터 인스턴스, 디자이너 친화 + 코드 폐쇄 — 무기 종류 추가가 코드 수정 0줄로 가능 (DA_Pistol 추가 + BP Defaults 슬롯 매핑만). OCP(개방-폐쇄) 가 데이터 측에서 구현되면 시스템 확장이 가벼워진다.TSoftObjectPtr지연 로딩 — 사운드/머티리얼 같은 큰 자산은 강한 참조로 들고 있으면 모든 무기 DA 가 메모리 상주. Soft 로 두면 실제 사용 시점에만 LoadSync. 메모리 풋프린트가 즉시 줄어든다.반동은 “변위 누적 + 보간 회복” 두 변수로 충분 — 복잡한 reset 타이머 없이 매 Tick
FInterpTo한 줄이면 자연스러운 반동 회복이 된다. 게임 시스템에서 시간 기반 상태 머신을 만들기 전에 “보간 한 줄로 표현 가능한가” 를 먼저 물어보자.비동기 트레이스의 게임 스레드 양보 — N마리 좀비가 매 틱 동기 LineTrace 하면 게임 스레드 정체.
AsyncLineTraceByChannel+ 콜백으로 분산하면 stat unit 의 GameThread 부담이 즉시 떨어진다. 워커 스레드 1개만 있어도 N개 트레이스 분산 가능.인터페이스 우선순위 분기로 키 통합 — F 키 별도로 두지 말고 E 키 안에서
Implements<UVOIDVehiclePartSlot>→IsA<AVOIDVehicle>→Implements<UVOIDItemInterface>순으로 분기. 입력 키 1개로 모든 인터랙션 통합. UX 일관성 + 코드 진입점 1곳.객체 복사 방지의 본질은 RAII 자원의 이중 해제 — 파일 핸들·뮤텍스·소켓·
unique_ptr가 복사되면 같은 자원을 두 객체가 들고 있다가 둘 다 해제하면 더블 free.= delete/ 이동 시맨틱 /unique_ptr멤버 패턴이 다 같은 문제를 다른 각도에서 푼다.Rule of Five 보다 Rule of Zero 가 모던 — 직접 정의 0개로 두고 RAII 멤버(
std::vector,unique_ptr) 가 알아서 처리. 직접 정의가 늘면 늘수록 버그 표면적도 늘어난다. 정의는 정말 필요한 한 가지에만 집중.마스터 가이드 = file:line 매핑 — 채점자/리뷰어가 코드 위치를 즉시 점프할 수 있게
Source/.../File.cpp:123형식으로 핵심 라인 표기. PR 설명·과제 가이드는 “어디 어디 어떻게” 가 아니라 “여기 클릭해서 보세요” 가 더 빠르다.