포스트

TIL 2026-04-27

TIL 2026-04-27

2026-04-27 8번 과제 Day 3 — DataTable/DataAsset 디버깅 + 무게→이동/소음 동적 효과 + Press E 인터랙션

목차


오늘 한 일 요약

  1. DT_VOIDWaveData RowStruct 미스매치 디버깅FindRow: incorrect type 에러 추적, DT 삭제·재생성으로 해결, RowStruct는 사후 변경 불가라는 사실 학습
  2. ItemData = DataAsset임을 재인지 — DataTable로 만들려다 실패, UDataAsset 베이스 → 6개 인스턴스(DA_Item_* / DA_Part_*) 생성 후 픽업 BP 슬롯 연결
  3. SpawnVolume 라인 트레이스 안착 — 볼륨 천장에서 아래로 트레이스해 Hit.ImpactPoint로 스폰, 캐릭터 가슴 높이 공중 부유 문제 해결
  4. AVOIDPickupBase 자동 픽업 흐름 확립bAutoPickup=true 자동 바인딩, BP 중복 등록 금지, MeshComp NoCollision + TriggerSphere만 OverlapAllDynamic
  5. 무게 → 이동속도 동적 감속 (D-2) — Tick에서 MaxWalkSpeed = 450 * (1 - WeightRatio * 0.6) 갱신, 만재 30kg 시 180까지 감속
  6. 무게 → 소음 배율 적용 (D-2)EmitNoise(Gunshot, 1 + WeightRatio * 1.2) → 좀비 청각 트리거, “Noise is Currency” 1차 검증
  7. Press E Sphere Trace 인터랙션 (D-4) — 정면 200cm 반경 80 Sweep → Implements<UVOIDItemInterface>() 필터 → 가장 가까운 1개 픽업
  8. PIE 검증 완료 — 6종 픽업 자동 + Press E 모두 정상, 무게 누적·감속·소음 증가·좀비 추적 흐름 확인
  9. Day 3 체크리스트 6/7 완료 — D-3 파밍 3종은 차량 부품으로 검증 충분 → 스킵


DT_VOIDWaveData RowStruct 타입 미스매치 디버깅

가장 많이 시간을 빼앗긴 삽질. 증상은 단순했는데 원인 후보가 여럿이라 한 시간 가까이 헤맸다.

증상과 원인 파악

에러 로그:

1
2
LogDataTable: Error: UDataTable::FindRow : 'VOIDGameMode' specified
incorrect type for DataTable '/Game/VoidUnreal/Data/DT_VOIDWaveData'.

GameMode가 WaveTable->FindRow<FVOIDWaveData>(RowName, ...)을 호출했는데 요청 타입과 DataTable의 RowStruct가 일치하지 않아 거부된 것.

원인 후보 3가지를 순서대로 의심:

후보검증 방법결과
C++ 구조체 빌드 누락Source/VoidUnreal/Public/Waves/VOIDWaveData.hUSTRUCT(BlueprintType) + GENERATED_BODY() 확인정상
Live Coding 캐시 깨짐Binaries/Intermediate/Saved 삭제 + Rebuild Solution증상 동일
DT 자체의 RowStruct 잘못 지정DT 우클릭 → Properties에서 RowStruct 확인다른 구조체로 잡혀 있었음

DT 생성 시 “Pick Row Structure” 다이얼로그에서 FVOIDWaveData가 아닌 다른 구조체를 골랐거나, 다른 DT를 Duplicate한 흔적이었다.

DataTable RowStruct는 사후 변경 불가

여기서 처음 알게 된 사실:

  • DT의 RowStruct는 생성 시 한 번만 지정 가능
  • DataTable Editor 어디에도 RowStruct를 변경하는 UI가 없음
  • Editor Property Matrix·Source Code 어떤 경로로도 사후 교체 불가
  • DT를 다른 DT로부터 “상속”하거나 “캐스팅”할 수 없음

잘못된 타입의 DT는 삭제·재생성 외에 답이 없다.

해결 절차:

  1. Content/VoidUnreal/Data/DT_VOIDWaveData 삭제
  2. Content Browser → Miscellaneous → Data Table → “Pick Row Structure”에서 FVOIDWaveData 명시 선택
  3. Floor_1, Floor_2, Floor_3 행 추가 (TimeLimit / ZombieCount / ItemSpawnCount / FloorTheme / GoalType)
  4. PIE 재실행 → LogVOIDGameMode: WaveData loaded for Floor 1 정상 로그

Live Coding 캐시 의심 → 풀 리빌드

원인 추적 중간에 Live Coding 캐시도 의심해 한 번 풀 리빌드를 했다. 결과적으로는 RowStruct 문제가 본질이었지만, C++ 구조체가 의심될 때 풀 리빌드를 먼저 해두면 “구조체는 이상 없음”을 확정할 수 있다는 점에서 의미 있었다.

1
2
Editor 종료 → Binaries / Intermediate / Saved 삭제 → uproject 우클릭
→ Generate VS project files → Rebuild Solution → Editor 재기동

언리얼 엔진 워크플로우에서 “Live Coding이 매개변수만 핫리로드하고 USTRUCT 메타데이터는 안 갱신하는” 케이스가 있어서, USTRUCT를 추가/수정한 직후엔 풀 리빌드가 안전하다.



ItemData = DataTable이 아니라 DataAsset 인스턴스

DT 삽질이 끝나자마자 곧바로 다음 함정. 픽업 BP가 분명히 overlap을 받는데 인벤토리에 안 들어갔다.

증상

디버그 로그:

1
2
[VOID Pickup] BP_Item_Food BeginPlay  bAutoPickup=1  TriggerSphere=OK  ItemData=NULL
[VOID Pickup] overlap by BP_VoidPlayerCharactor

ItemData=NULL 한 줄이 전부였다. HandleBeginOverlap 진입부의 if (!OtherActor || !ItemData) return; 게이트에서 즉시 리턴. 픽업이 닿기는 했는데 데이터를 모르니 무시된 상황.

인지 오류 — “데이터니까 DT여야 한다”

WaveData를 막 DT로 만들고 나서 이걸 ItemData에도 적용하려 했다. “아이템도 데이터니까 행이 여러 개 있는 DT면 되겠지” → 틀렸다.

Source/VoidUnreal/Public/Items/VOIDItemDataAsset.h:28 확인:

1
2
UCLASS(BlueprintType)
class VOIDUNREAL_API UVOIDItemDataAsset : public UDataAsset

베이스가 UDataAsset. 픽업 BP의 ItemData 슬롯도 UVOIDItemDataAsset* 포인터라 DataAsset 인스턴스 에셋을 받는 자리지 DT 행을 받는 자리가 아니다.

DataTable vs DataAsset 비교

이번 기회에 두 개념을 명확히 정리:

구분DataTableDataAsset
베이스 클래스UDataTableUDataAsset (UObject 직계)
단위행(Row) — FTableRowBase 구조체 시트단일 인스턴스 — UObject 1개
식별RowName(FName) + RowStruct(UScriptStruct*)에셋 경로 + 클래스
적합한 용도Wave 설정, 캐릭터 스탯, 무기 밸런스 등 형식개별 아이템·머티리얼·장비 등 단품
인스펙터행 그리드 편집UPROPERTY 폼 편집
참조 방식WaveTable->FindRow<T>(RowName, Context)UPROPERTY 포인터 직접 할당
상속RowStruct 상속 가능클래스 상속 가능 (DataAsset 자체가 UObject)

행이 여러 개고 키로 조회 → DT, 객체 1개를 데이터로 직렬화 → DA. WaveData는 층마다 행 → DT, ItemData는 아이템마다 단일 인스턴스 → DA. 두 시스템이 같은 프로젝트에 공존.

6개 DataAsset 인스턴스 생성

Content Browser → Miscellaneous → Data AssetVOIDItemDataAsset 클래스 선택 → 6개 생성:

자산 이름용도WeightType
DA_Item_Food식량0.5 kgConsumable
DA_Item_Medkit응급치료3.0 kgConsumable
DA_Item_Ammo탄약1.0 kgResource
DA_Part_Battery차량 부품 18.0 kgVehiclePart
DA_Part_FuelTank차량 부품 212.0 kgVehiclePart
DA_Part_SparkPlug차량 부품 32.0 kgVehiclePart

각 픽업 BP의 Class Defaults → ItemData 슬롯에 대응 DA를 드래그 연결.

이후 같은 BP에서 ItemData=DA_Item_Food 로그가 찍히고, 인벤토리 누적이 정상 작동했다.



SpawnVolume 라인 트레이스 — 바닥 안착

Day 3 C-3(층별 SpawnVolume) 배치 단계에서 SpawnVolume을 두자 픽업이 캐릭터 가슴~머리 높이로 공중에 떠 있는 시각적 버그가 발생.

원인

AVOIDSpawnVolume::GetRandomPointInVolume()이 BoxComponent의 X·Y 범위만 랜덤으로 잡고 Z 오프셋을 0으로 잠그고 있어서, 결과적으로 SpawnVolume 액터 자체의 World Z가 그대로 픽업 위치가 됐다. 액터를 캐릭터 키 높이에 두면 픽업이 그 높이에 그대로 박혔다.

해결 — 바닥 트레이스

Source/VoidUnreal/Private/Waves/VOIDSpawnVolume.cpp에 라인 트레이스 추가:

1
2
3
4
5
6
7
8
9
10
11
12
const FVector Top = Origin + FVector(RX, RY, BoxExtent.Z);
const FVector End = Top - FVector(0, 0, BoxExtent.Z * 2.f + 500.f);

FHitResult Hit;
FCollisionQueryParams Params;
Params.AddIgnoredActor(this);

if (GetWorld()->LineTraceSingleByChannel(Hit, Top, End, ECC_Visibility, Params))
{
    return Hit.ImpactPoint + FVector(0, 0, 2.f);  // 살짝 띄움
}
return Origin;  // 폴백

흐름: 볼륨 천장에서 시작 → 아래로 Extent.Z * 2 + 500까지 트레이스 → 바닥(StaticMesh 등)에 닿은 ImpactPoint를 사용.

라인 트레이스로 바닥을 검출하면 볼륨이 어느 높이에 있든, 바닥이 경사라도, 계단 위라도 동일하게 안착. 볼륨 Z를 천장 근처로 띄워두면 트레이스 거리도 자동 확보된다.

메시 피벗이 액터 중심이 아니어서 픽업이 바닥에 박혀 보이는 건 SpawnVolume이 아니라 BP의 MeshComp Z 오프셋으로 해결. 책임 분리.



AVOIDPickupBase 자동 픽업 작동 흐름

Day 3 D-1·D-3에서 6개 픽업 BP를 만들면서 베이스 클래스의 작동 흐름을 정확히 이해할 필요가 생겼다.

자동 바인딩 vs BP 중복 등록 함정

VOIDPickupBase.cpp:28 부근:

1
2
3
4
5
6
7
8
9
void AVOIDPickupBase::BeginPlay()
{
    Super::BeginPlay();
    if (bAutoPickup && TriggerSphere)
    {
        TriggerSphere->OnComponentBeginOverlap.AddDynamic(
            this, &AVOIDPickupBase::HandleBeginOverlap);
    }
}

bAutoPickup=true 시 BeginPlay에서 자동 바인딩된다.

함정: BP에서 별도로 OnComponentBeginOverlap 이벤트를 등록하면 같은 콜백이 두 번 호출된다. 같은 액터가 한 프레임에 두 번 픽업되거나, 한 번 Destroy된 후 두 번째 호출에서 이미 파괴된 컴포넌트에 접근해 크래시 위험.

BP에서는 OnComponentBeginOverlap을 절대 등록하지 않는다. C++가 알아서 한다.

HandleBeginOverlap 가드 흐름

1
2
3
4
5
6
7
8
9
10
11
void AVOIDPickupBase::HandleBeginOverlap(...)
{
    if (!OtherActor || !ItemData) return;                              // (1)
    auto* Inv = OtherActor->FindComponentByClass<UVOIDInventoryComponent>();
    if (!Inv) return;                                                  // (2)
    if (!Inv->TryAddItem(ItemData)) return;                            // (3)

    Execute_OnPickedUp(this, OtherActor);                              // (4)
    if (NoiseComp) NoiseComp->EmitNoise(...);                          // (5)
    Destroy();                                                         // (6)
}
단계게이트실패 시
(1)OtherActor·ItemData NULL 검사무시
(2)인벤토리 컴포넌트 부착 여부무시 (좀비 등이 닿아도 픽업 안 됨)
(3)TryAddItem 무게 게이트무게 초과 시 false → 픽업 안 됨, 액터 유지
(4)인터페이스 콜백 (BP에서 추가 효과)
(5)소음 발신
(6)Destroy

검증 로그(디버그용 추가):

1
2
3
[VOID Pickup] BP_Item_Food BeginPlay  bAutoPickup=1  TriggerSphere=OK  ItemData=DA_Item_Food
[VOID Pickup] overlap by BP_VoidPlayerCharactor
[VOID Inventory] TryAddItem returned 1  (Weight now 8.00 / 30.00)

만재(MaxCarry 30kg) 도달 후엔 TryAddItem returned 0이 정상 출력 — 게이트가 정상 거부.

MeshComp 콜리전 함정

처음에 픽업 메시가 마법처럼 캐릭터를 막아서 TriggerSphere에 못 닿는 현상. 원인:

  • StaticMesh의 기본 콜리전 프리셋이 BlockAll
  • 캐릭터 캡슐이 메시에 막혀 TriggerSphere 반경 안에 들어오지 못함
  • 동시에 총알도 픽업에 막혀 픽업이 “방패” 역할

콜리전 분담: | 컴포넌트 | Collision Preset | |—|—| | MeshComp | NoCollision | | TriggerSphere | OverlapAllDynamic (또는 커스텀 — Pawn만 Overlap) |

메시는 시각만 담당, 충돌 검출은 TriggerSphere가 단독으로 책임.



무게 → 이동속도 / 소음 동적 효과 (D-2)

구현계획 D-2에 정의된 무게 기반 동적 효과를 AVOIDPlayerCharacter에 추가했다. Void GDD의 “Noise is Currency” 필러를 구현 단에서 처음 검증하는 단계.

헤더 추가

1
2
3
4
5
6
7
8
9
10
11
// Tick 활성화
PrimaryActorTick.bCanEverTick = true;

UPROPERTY(EditAnywhere, Category = "Void|Tuning")
float BaseWalkSpeed = 450.f;

UPROPERTY(EditAnywhere, Category = "Void|Tuning")
float WeightSpeedPenalty = 0.6f;   // 만재 시 60% 감속

UPROPERTY(EditAnywhere, Category = "Void|Tuning")
float NoiseWeightFactor = 1.2f;    // 만재 시 +120% 소음

Tick — 매 프레임 이동속도 갱신

1
2
3
4
5
6
7
8
9
void AVOIDPlayerCharacter::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    if (!Inventory || !GetCharacterMovement()) return;

    const float Ratio = Inventory->GetWeightRatio();        // 0.0 ~ 1.0
    const float NewSpeed = BaseWalkSpeed * (1.f - Ratio * WeightSpeedPenalty);
    GetCharacterMovement()->MaxWalkSpeed = NewSpeed;
}
무게WeightRatioMaxWalkSpeed
0 kg0.0450 (기본)
15 kg0.5315
30 kg1.0180 (만재, 60% 감속)

Fire — 소음 배율 적용

1
2
3
4
5
6
7
8
9
10
void AVOIDPlayerCharacter::Fire()
{
    // ... 발사 로직 ...
    if (NoiseComponent)
    {
        const float Ratio = Inventory ? Inventory->GetWeightRatio() : 0.f;
        const float Multiplier = 1.f + Ratio * NoiseWeightFactor;
        NoiseComponent->EmitNoise(ENoiseType::Gunshot, Multiplier);
    }
}

UVOIDNoiseComponent::EmitNoise(Type, WeightMultiplier)가 인자로 곱셈 계수를 받아 소음 반경을 함께 키운다. 만재 30kg에서 1발 사격 → 소음 반경 2.2배.

“Noise is Currency” 1차 검증

PIE 흐름:

  1. 식료품·약·부품 픽업 누적 → 무게 25 kg
  2. 이동속도 자동 감속 (450 → 200대)
  3. 좀비 추적 회피 어려움 — 일반 보행이 좀비 보행속도와 비슷
  4. 사격 시 소음 반경 1.83배 → 평소 안 들리던 거리의 좀비도 청각 트리거
  5. LogVOIDPerception: Zombie perceived noise from Player at <World Loc> 출력 확인
  6. 인벤토리가 가득 차면 좀비한테 잡히는 게 정상 결과

게임 컨셉이 시스템 단에서 작동한다는 첫 검증. 무게가 단순 페널티가 아니라 소음·은신·전투 모두에 영향 주는 단일 지표가 됐다.



Press E Sphere Trace 인터랙션 (D-4)

자동 픽업과 별도로, 비자동 픽업(bAutoPickup=false)을 손으로 집을 수 있어야 한다. Press E 인터랙션 구현.

Interact() 본문

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
void AVOIDPlayerCharacter::Interact()
{
    const FVector Start = GetActorLocation() + GetActorForwardVector() * 50.f;
    const FVector End = Start + GetActorForwardVector() * 200.f;

    TArray<FHitResult> Hits;
    FCollisionQueryParams Params;
    Params.AddIgnoredActor(this);

    GetWorld()->SweepMultiByChannel(
        Hits, Start, End, FQuat::Identity,
        ECC_Visibility, FCollisionShape::MakeSphere(80.f), Params);

    AActor* Best = nullptr;
    float BestDistSq = TNumericLimits<float>::Max();

    for (const FHitResult& Hit : Hits)
    {
        AActor* Actor = Hit.GetActor();
        if (!Actor) continue;
        if (!Actor->Implements<UVOIDItemInterface>()) continue;       // (A)

        const float DistSq = FVector::DistSquared(GetActorLocation(), Actor->GetActorLocation());
        if (DistSq < BestDistSq) { BestDistSq = DistSq; Best = Actor; }
    }

    if (!Best) { /* DrawDebugSphere 노란색 */ return; }

    UVOIDItemDataAsset* Data = IVOIDItemInterface::Execute_GetItemData(Best);   // (B)
    if (!Data || !Inventory->TryAddItem(Data)) return;
    IVOIDItemInterface::Execute_OnPickedUp(Best, this);
    if (NoiseComponent) NoiseComponent->EmitNoise(ENoiseType::Pickup, 1.f);
    Best->Destroy();
    /* DrawDebugSphere 녹색 */
}
단계동작
Sweep캐릭터 정면 50~250cm 사이, 반경 80 Sphere
필터 (A)IVOIDItemInterface 구현체만 채택
최단 선택가장 가까운 1개
픽업 (B)Execute_GetItemData → 무게 게이트 → Execute_OnPickedUp → 소음 → Destroy

BlueprintNativeEvent 인터페이스 호출 패턴

이번에 처음 명확해진 부분. IVOIDItemInterfaceBlueprintNativeEvent로 선언돼 있어서 C++ 구현체와 BP 구현체 양쪽을 잡으려면:

안 되는 호출되는 호출
Cast<IVOIDItemInterface>(Actor)Actor->Implements<UVOIDItemInterface>()
IPtr->GetItemData()IVOIDItemInterface::Execute_GetItemData(Actor)

이유:

  • BlueprintNativeEvent는 BP에서 구현하면 C++ 인터페이스 vtable에 등록되지 않음
  • Cast<I>는 vtable 기반이라 BP만 구현한 액터를 놓침
  • Implements<U>()는 UObject reflection 기반이라 BP 구현도 인식
  • Execute_FuncName(Object, Args...)는 정적 디스패처 — vtable과 무관하게 BP/C++ 양쪽 호출

정리:

  • BlueprintNativeEvent 인터페이스 → Implements<UInterfaceClass>() + Execute_FuncName(Object, ...)
  • 순수 C++ 인터페이스(no UFUNCTION) → Cast<IInterface> + IPtr->Func() 사용 가능

자동 픽업과 공존

모드bAutoPickup동작
자동 픽업true닿는 즉시 자동 흡수, 빠른 파밍
수동 픽업falsePress E로만, 의도적 수집 (희귀 부품·트랩성 아이템 등)

하나의 베이스 클래스로 두 픽업 모드 분기. BP의 변수 토글만으로 픽업 정책을 바꾼다. 계획상 차량 부품(DA_Part_*)은 bAutoPickup=false로 두고, 식료품·약은 true로 분리할 예정 (현재 PIE 검증은 모두 자동으로 통일).

디버그 시각화

1
2
3
4
5
6
#if !(UE_BUILD_SHIPPING)
    DrawDebugSphere(GetWorld(), Best ? Best->GetActorLocation() : End,
                    80.f, 12,
                    Best ? FColor::Green : FColor::Yellow,
                    false, 1.f);
#endif

성공 시 녹색, 미발견 시 노란색. 인터랙션 디버깅에서 시각 피드백이 로그 100줄보다 빠르다.



Day 3 진행 정리

항목상태비고
C-1 SpawnVolume cpp기존 코드에 라인 트레이스 추가바닥 안착
C-2 DT_VOIDWaveData 생성재생성RowStruct 미스매치 → 삭제·재생성
C-3 층별 SpawnVolume 배치1층 2개·2층 3개·3층 4개 배치
D-1 InventoryComponent 무게 누적TryAddItem 게이트 + WeightRatio
D-2 무게→이동속도, 소음 배율Tick·Fire에 동적 효과 적용“Noise is Currency” 1차 검증
D-3 Pickup 3종 (Food/Medkit/Ammo)스킵차량 부품(DA_Part_*) 3개로 무게·픽업 흐름 검증 충분
D-4 Press E Sphere Trace정면 200·반경 80 Sweep + 인터페이스 필터
F. Day 3 커밋진행 예정

남은 것: Day 3 커밋 → Day 4 진입 (옥상 차량 수리 + 호드 + ADS 조준).

Day 3 → Day 4 인계:

  • 차량 부품 3종 DA(DA_Part_Battery/FuelTank/SparkPlug)는 이미 만들어둠 — Day 4 차량 수리 시스템에서 그대로 사용
  • 인벤토리 무게 시스템이 부품(8/12/2 kg)을 들고 옮길 때 핵심 디버프로 작용 — 옥상까지 운반 단계가 게임의 클라이맥스가 됨
  • Press E 인터랙션은 차량 수리 슬롯 인터랙션에 그대로 재사용 가능 (인터페이스 필터만 IVOIDVehiclePartSlot로 교체)


오늘 배운 것 정리

  1. DataTable의 RowStruct는 생성 시 1회 결정, 사후 변경 불가. 잘못된 타입이면 삭제·재생성만이 답. UE5 에디터 어디에도 변경 UI가 없다는 사실을 모르면 한참 헤맨다. 새 DT 만들 때는 “Pick Row Structure” 다이얼로그를 정확히 확인하는 습관.

  2. DataTable과 DataAsset은 용도가 명확히 다르다. “행이 여러 개고 키로 조회” → DT(UDataTable + FTableRowBase). “객체 1개를 데이터로 직렬화” → DA(UDataAsset). 같은 프로젝트에서 두 시스템이 공존하는 게 정상이고, ItemData가 DA인지 DT인지를 베이스 클래스로만 판단할 수 있어야 함정에 빠지지 않는다.

  3. SpawnVolume은 액터 위치가 아니라 라인 트레이스 결과로 스폰한다. 볼륨이 어느 높이에 있든 항상 바닥에 안착. 이 패턴이 자리 잡으면 레벨 디자인의 자유도가 크게 올라간다 — 볼륨을 천장 근처에 띄워둬도 픽업/좀비는 정확히 바닥에 생성된다.

  4. 자동 바인딩이 있는 베이스 클래스에서 BP가 같은 이벤트를 또 등록하면 중복 호출. OnComponentBeginOverlap.AddDynamic 같은 델리게이트 바인딩은 BeginPlay에서 단 한 곳에서만 해야 한다. BP 측에서는 OnComponentBeginOverlap 이벤트를 추가로 등록하지 않는다.

  5. 메시 콜리전과 트리거 콜리전의 책임 분리. MeshComp는 시각만(NoCollision), TriggerSphere가 충돌·overlap 단독 담당(OverlapAllDynamic 또는 커스텀). 메시 콜리전이 BlockAll로 남아있으면 “총알을 막는 픽업·트리거에 못 닿는 픽업” 같은 이상 동작이 발생.

  6. Tick 기반 동적 디버프의 “단일 지표” 패턴. 무게 하나를 WeightRatio로 표현하면 이동속도·소음·시야 흔들림·체력 회복 속도 같은 게임 전반의 시스템에 한 줄로 반영된다. Void의 “Noise is Currency” 필러가 코드 단에서는 곱셈 계수 하나로 떨어진다.

  7. BlueprintNativeEvent 인터페이스는 Cast<I>가 아니라 Implements<U>() + Execute_F(Object, ...). vtable 기반 캐스팅은 BP-only 구현체를 놓친다. UObject reflection을 써야 BP 구현까지 잡힌다 — BP가 구현할 수 있는 인터페이스면 Execute_ 패턴.

  8. 하나의 베이스 클래스로 두 모드를 분기. bAutoPickup 토글 하나로 자동/수동 픽업이 모두 가능. BP에서 변수만 바꿔도 정책이 변경돼서 코드 수정 없이 픽업 정책을 바꿔가며 실험할 수 있다.

  9. 디버그 로그보다 디버그 시각화가 빠르다. DrawDebugSphere 한 줄로 Sphere Trace 위치·반경·결과가 한눈에 보인다. 로그 100줄 뒤지는 것보다 색깔 구체 1개로 인터랙션 버그를 더 빨리 잡았다.

  10. Live Coding 의심은 USTRUCT 변경 직후엔 합리적이다. 핫리로드가 메서드는 갱신해도 USTRUCT 메타데이터는 못 따라오는 케이스가 있다. 구조체 추가/수정 후 이상한 에러를 만나면 풀 리빌드(Binaries/Intermediate/Saved 삭제 + Rebuild Solution)를 한 번 해두면 “구조체는 이상 없음”을 확정하고 다른 원인을 찾을 수 있다.

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