티스토리 뷰
🟩 학습 목표
- 의존성을 제거한 FNCLootDropData 모듈화 구조를 확립하고, 가중치 기반 랜덤 스폰 로직을 정립한다.
- 언리얼 헤더 툴(UHT)의 인터페이스 가상 함수 처리 및 BlueprintNativeEvent 구현 규칙을 준수하여 컴파일 에러를 해결한다.
- 세션 유지 및 멀티플레이 동기화 무결성을 위해 인벤토리 컴포넌트의 소유권을 Character에서 PlayerState로 리팩토링한다.
- 아키텍처 개편에 따른 UI 위젯의 데이터 참조 포인터를 교정하고 드래그 오퍼레이션 분기 처리를 고도화한다.
🟧 1. 파밍 상자(Loot Box) 확률형 스폰 시스템 구현
🟦 드랍 테이블 구조체 분리 (FNCLootDropData)
아이템 ID, 드랍 가중치(Weight), 최소/최대 수량, 속성 태그를 관리하는 전용 구조체를 설계하였다. 이를 특정 액터 클래스 내부 헤더에 종속시키지 않고 독립된 NCLootDropData.h 파일로 분리하여 모듈화하였다. 향후 몬스터 처치 보상이나 필드 드랍 오브젝트 등 타 시스템에서 의존성 인클루드 꼬임 없이 깔끔하게 재사용할 수 있는 구조적 확장성을 확보하였다.
🟦 가중치 기반 랜덤 룩업 알고리즘 (GenerateLoot)
데이터 테이블에서 가중치 데이터를 순회하며 누적 합산(Total Weight)을 구한 뒤, FMath::FRandRange를 통해 의사 난수를 추출하여 확률적으로 아이템을 선별하는 로직을 구현하였다.
// NCLootBoxActor.cpp 가중치 선택 로직 알고리즘
void ANCLootBoxActor::GenerateLoot()
{
if (!LootDataTable) return;
TArray<FNCLootDropData*> AllRows;
LootDataTable->GetAllRows(TEXT("FetchLootData"), AllRows);
float TotalWeight = 0.f;
for (const auto* Row : AllRows) { TotalWeight += Row->DropWeight; }
float DiceRoll = FMath::FRandRange(0.f, TotalWeight);
float CurrentWeightSum = 0.f;
for (const auto* Row : AllRows)
{
CurrentWeightSum += Row->DropWeight;
if (DiceRoll <= CurrentWeightSum)
{
// 당첨된 아이템 ID와 수량을 코어 인벤토리 함수에 전달
int32 SpawnQuantity = FMath::RandRange(Row->MinQuantity, Row->MaxQuantity);
InventoryComponent->AddItem(Row->ItemID, SpawnQuantity);
break;
}
}
}
추출된 데이터 결과물은 기존에 구축해 둔 만능 데이터 함수인 UNCInventoryBaseComponent::AddItem과 연동하였다. 이를 통해 별도의 추가 예외 처리 없이도 내부 컨테이너 배열 안에서 자동 스택 병합 및 빈 슬롯 그리드 할당이 유기적으로 수행되도록 설계하였다.
🟧 2. 언리얼 인터페이스(Interface) UHT 컴파일 에러 해결
🟦 BlueprintNativeEvent 상속 선언 문법 오류
UINTERFACE를 통해 구현된 상호작용 인터페이스를 자식 액터 클래스에 상속하고 구현하는 과정에서 언리얼 헤더 툴(UHT) 빌드 오류가 발생하였다.
🟦 원인 분석 및 해결 규격 정립
- 원인 1: 인터페이스 헤더 선언 시 C++ 표준 규격처럼 virtual void Interact();와 같이 가상 함수 키워드를 명시하였다. 하지만 언리얼 엔진의 BlueprintNativeEvent 지정을 받은 함수는 UHT가 빌드 타임에 자동으로 내부 가상 함수 파이프라인을 생성하므로, 개발자가 virtual을 중복 선언하면 헤더 툴 충돌 에러가 발생한다. 인터페이스 원본 헤더에서 해당 키워드를 소거하여 해결하였다.
- 원인 2: 상속받는 구체 클래스(시민 액터) 선언부에서 원본 명세인 Interact()를 한 번 더 기술하여 컴파일러 시그니처 락이 발생하였다. BlueprintNativeEvent 속성을 오버라이드할 때는 원본 함수가 아닌 엔진 복제 규격인 _Implementation 접미사를 붙여 선언 및 구현해야 구조가 완성된다.
// NCLootBoxActor.h 정석적인 오버라이드 명세
virtual void Interact_Implementation() override;
🟧 3. 핵심 아키텍처 리팩토링: 인벤토리 소유권 이사 (Character ➔ PlayerState)
🟦 액터 수명주기(Skeletal Life Cycle)에 따른 데이터 휘발 문제
기존 시스템은 ANCPlayerCharacter 내부에 인벤토리 컴포넌트가 결합되어 있었다. 이는 익스트랙션 서바이벌 장르 특성상 캐릭터 사망 후 리스폰(Destroy 후 Spawn) 시 힙 메모리에 있던 가방 데이터가 통째로 증발하는 치명적인 아키텍처 결함이 존재하였다. 또한 캐릭터 액터의 잦은 소멸과 생성은 멀티플레이어 환경에서 네트워크 레플리케이션 패킷 안정성을 떨어뜨린다.
🟦 PlayerState 중심의 지속성 데이터 레이어 구축
레벨 전이 및 캐릭터 액터 소멸 시에도 세션이 유지되는 동안 영속성을 지니는 ANCPlayerState 클래스로 인벤토리 컴포넌트를 이관 배치(CreateDefaultSubobject 위치 수정)하였다.
캐릭터는 덩치 큰 인벤토리 데이터를 직접 소유하지 않으며, 오직 PlayerInventoryRef 포인터 변수를 통해 주소 값만 바인딩하여 바라보는 약참조 구조로 변경하였다.
🟦 서버-클라이언트 투 트랙(Two-Track) 포인터 바인딩 동기화
캐릭터 액터가 생성되고 컨트롤러가 배정되는 생명주기 시점에 맞추어 양방향 포인터 안전 바인딩을 구현하였다.
- 서버 권한 (Authority): 캐릭터가 서버에 의해 정상possessed 되는 시점인 PossessedBy(AController* NewController) 가상 함수 내에서 PlayerState를 취득하여 인벤토리 주소를 연결한다.
- 로컬 클라이언트 (Autonomous Proxy): 복제 패킷이 도달하여 내부 주소가 동기화되는 런타임 시점인 OnRep_PlayerState() 콜백 함수 내에서 인벤토리 주소를 동적으로 갱신한다.
디자인 패턴 인사이트: "캐릭터는 물리적 껍데기(렌트카)일 뿐이며, 진짜 세션 데이터(내 지갑)는 PlayerState가 전담한다." 사망 시 모든 장비를 월드에 떨어뜨려야 하는 기획이더라도 데이터 연산은 PlayerState에서 통제하고, 소멸 직전 시체 상자(LootBox) 오브젝트로 데이터를 안전하게 이관(TransferItemTo)한 뒤 컴포넌트를 청소하는 구조가 멀티플레이 아키텍처 관점에서 훨씬 무결함을 실증하였다.
🟧 4. 리팩토링 후속 조치 및 UI 블루프린트 트러블슈팅
🟦 UI 주소 레퍼런스 단절 해결
컴포넌트 소유권이 이동함에 따라 기존 UMG 위젯 내에서 Get Player Pawn ➔ Get InventoryComponent 형태로 경로를 탐색하던 모든 블루프린트 로직이 컴파일 에러를 뿜으며 무력화되었다. 데이터 허브 타깃 노드를 Get Player State로 전환하고 Get Component by Class 노드를 연동하여 런타임 갱신 에러를 완벽히 해결하였다.
🟦 캐스팅 오류 우회를 통한 장비 장착 스왑 로직 고도화
슬롯 드래그 인터랙션 시, 일반 가방 아이템 정보가 담긴 BP_ItemDragDrop 객체와 이미 장비창에 올라가 있던 정보인 BP_EquipmentDragDrop 객체가 단일 UI 타깃 내에서 충돌하며 조건 누락이 발생하였다.
이를 정교하게 정류하기 위해 UI 파트 팀원과 협업하여 Cast Failed(캐스팅 실패 분기 체인) 구조를 수립하였다.
- OnDrop 이벤트 진입 시 먼저 BP_ItemDragDrop으로 캐스팅을 시도한다.
- 성공 시: 가방에서 장비창으로 아이템을 신규 장착하는 비즈니스 C++ 함수를 가동한다.
- 실패(Cast Failed) 시: 하위 체인으로 BP_EquipmentDragDrop 캐스팅을 재수행한다.
- 성공 시: 장비창 슬롯 간의 위치 교환 혹은 내부 스왑 연산 구문을 실행한다.
이를 통해 입력 장치 오작동을 차단하고 예외 처리를 정밀화하였다.
🟧 핵심 요약
- NCLootDropData.h 구조체 모듈화를 통해 결합도를 낮추고 FRandRange 기반의 가중치 랜덤 스폰 시스템을 확립하였다.
- BlueprintNativeEvent 사용 시 virtual 명시를 제거하고 부모 명세인 _Implementation 함수만 기술하여 UHT 컴파일 락을 해결하였다.
- 캐릭터 사망 시 데이터 유실을 방지하기 위해 인벤토리 컴포넌트 소유권을 PlayerState로 리팩토링하여 네트워크 무결성을 획득하였다.
- PossessedBy와 OnRep_PlayerState 시점에 맞춰 주소 값을 할당하여 멀티플레이 환경의 양방향 런타임 참조 구조를 완성하였다.
- UI 드롭 핸들러에 Cast Failed 폭포수 분기문을 설계하여 가방 에셋과 장비창 에셋 간의 다형성 조작 예외 처리를 제어하였다.
'내일배움캠프 Unreal_7기 > 본캠프' 카테고리의 다른 글
| 인벤토리 시스템 고도화 : 네이티브 태그 도입 및 파밍 동시 접근 제어 (0) | 2026.06.12 |
|---|---|
| OnConstruction SSOT 수립, 무기 스왑 인터락 및 복제 안전장치 설계 (0) | 2026.06.10 |
| UMG Drag & Drop 이벤트 처리의 함정과 인벤토리 세이브/로드 아키텍처 (0) | 2026.06.08 |
| 데이터 주도형(Data-Driven) 인벤토리 리팩터링 및 에디터 작업 프로세스 최적화 (0) | 2026.06.05 |
| 멀티플레이 인벤토리 - 드래그 앤 드롭 구현 및 데이터·액터 분리 구조의 이해 (0) | 2026.06.02 |

