티스토리 뷰

🟩 학습 목표

  • UI 위젯의 드래그 오퍼레이션 파이프라인을 구축하여 아이템 드래그 앤 드롭(Drag & Drop) 기능을 구현한다.
  • HasAuthority 가드 클로즈의 제약을 파악하고, Server RPC 구조로 전환하여 멀티플레이어 환경의 액터 스폰 권한을 제어한다. 
  • 인벤토리 시스템의 본질인 '데이터와 실물 액터의 분리 구조'를 이해하고 이를 기반으로 한 아키텍처를 분석한다.
  • MVP 완성 이후의 구조적 확장성을 위해 슬롯 데이터 내 원본 클래스 정보를 복원하는 리팩터링 방향을 수립한다.

🟧 1. C++ 함수 블루프린트 리플렉션 노출

🟦 리플렉션 지정자 누락 이슈

C++ 내부에서 정상적으로 구현한 슬롯 데이터 반영 함수인 SetSlotData가 블루프린트 위젯 에디터의 노드 검색 창에서 식별되지 않는 현상이 발생하였다.

🟦 원인 및 해결 방법

언리얼 엔진 컴파일러에게 블루프린트 노출 허가를 주지 않아 발생한 문제다. 가상 머신 및 리플렉션 시스템이 해당 메서드를 인식하고 블루프린트 가상 노드로 파싱할 수 있도록, 헤더 파일(.h)의 선언부에 BlueprintCallable 지정을 명시하여 해결하였다.

UFUNCTION(BlueprintCallable, Category = "Inventory UI")
void SetSlotData(FGameplayTag ItemTag, int32 Quantity);

🟧 2. 드래그 오퍼레이션의 데이터 흐름(Data Flow) 정상화

🟦 드래그 오퍼레이션 내부 데이터 유실 현상

슬롯 위젯에서 아이템을 드래그하여 월드 뷰포트에 드롭하는 인터랙션까지는 정상 도달하였으나, 버려진 아이템의 세부 정보인 태그와 수량이 None 및 0으로 소실되는 현상이 관찰되었다.

🟦 원인 및 해결 방법

On Drag Detected 함수 내에서 우편배달부 역할을 하는 Create Drag Drop Operation 노드를 생성할 때, 슬롯이 이미 들고 있던 C++ 원본 변수들을 입력 핀들에 연결하지 않고 비워두어 빈 상자만 전송된 것이 원인이었다. 슬롯에 캐싱되어 있던 CurrentItemTag와 CurrentQuantity 변수를 블루프린트 생성 노드의 입력 핀에 다이렉트로 바인딩하여 데이터 흐름을 정상화하였다.


🟧 3. 멀티플레이어 권한(Authority) 처리와 연쇄 버그 수정

🟦 클라이언트 입력 무시 및 상호작용(Loot) 락아웃 현상

클라이언트 프록시 화면에서 아이템을 드래그하여 바닥에 드롭할 때 아무런 반응이 없고, 이 과정에서 정상 작동하던 F 키 상호작용(LootItem) 기능까지 갑자기 먹통이 되는 연쇄 버그가 발생하였다.

🟦 HasAuthority 가드 클로즈 원인 분석

C++ 컴포넌트 내부 코어 로직 최상단에 배치된 네트워크 방어용 가드 클로즈 조건문으로 인해 발생하였다.

if (!GetOwner()->HasAuthority()) return false;

권한이 없는 로컬 클라이언트 단에서 아이템 줍기/버리기 함수를 직접 호출하자, 위 방어 코드에 걸려 서버가 요청을 무시하고 패킷 신호가 로컬 단에서 멸실되는 구조적 문제가 원인이었다.

🟦 Server RPC 구조로의 파이프라인 개선

멀티플레이어 환경에서 월드에 액터를 스폰(SpawnActor)하거나 가방 데이터를 영구 수정하는 작업은 반드시 서버 권한을 가진 주체가 실행해야 한다. 따라서 클라이언트의 로컬 직접 조작 방식을 전면 배제하고, 서버에 대리 실행을 요청하는 Server RPC 아키텍처로 개편하였다.

// NCInventoryBaseComponent.h 핵심 선언부
UFUNCTION(Server, Reliable)
void Server_DropItem(FGameplayTag ItemTag, int32 Quantity);

// NCInventoryBaseComponent.cpp 실 구현부
void UNCInventoryBaseComponent::Server_DropItem_Implementation(FGameplayTag ItemTag, int32 Quantity)
{
    // 서버 권한(Authority) 내부에서 안전하게 데이터 차감 및 월드 스폰 로직 처리
    if (DecreaseItem(ItemTag, Quantity))
    {
        SpawnItemInWorld(ItemTag, Quantity);
    }
}

"클라이언트의 입력 검출 및 요청 ➔ Server RPC 송신 ➔ 서버의 무결성 검증 및 실행 ➔ 복제(Replication)를 통한 전체 클라이언트 동기화" 파이프라인을 정립하여 모든 권한 문제를 해결하였다.


🟧 4. 인벤토리 데이터와 실물 액터 분리의 이해 (핵심 수확)

🟦 아이템 재드롭 시 원본 외형 유실 현상

레벨에 도끼 스태틱 메쉬를 가진 BP_NCItemActor를 배치해 두고 상호작용으로 획득했다가 다시 버렸을 때, 원래 형태인 도끼가 아니라 컴포넌트에 세팅해 둔 기본 베이스 메쉬(큐브 등)로만 스폰되어 떨어지는 현상이 발견되었다.

🟦 인벤토리 아키텍처의 본질 파악

"월드에 배치된 개별 아이템을 주웠으니 가방에도 액터 객체 자체가 들어가서 그대로 뱉어낼 것"이라는 직관과 달리, 언리얼의 인벤토리는 실물 액터가 아닌 '추상적인 데이터(태그, 수량)'만 기억하는 공간이라는 아키텍처의 본질을 깨달았다.

  • 기존 로직의 한계: 아이템을 주울 때 액터에서 ItemTypeTag와 Quantity 데이터만 쏙 빼서 가방 배열에 글자로 적어두고, 실물 액터는 Destroy()로 파괴해 버리는 구조였다. 그러다 보니 버릴 때(DropItem) 가방 구조체를 열어봐도 '장비, 1개'라는 단순 정보만 있을 뿐, 이게 원래 무슨 블루프린트(도끼)였는지 알 길이 없어 시스템에 등록된 기본 껍데기(BaseItemActorClass)로만 스폰되었던 것이다.

🟧 5. 백로그 및 확장성 리팩터링 설계 (Next Steps)

현재 단계는 최소 기능 구현(MVP)의 증명을 목표로 하므로, 고정된 베이스 메쉬 클래스로 드롭 프로세스가 완벽하게 동기화되는 것까지만 확인하고 스킵하였다. 추후 주웠던 원본 아이템 모습 그대로 완벽하게 복원하여 뱉어내기 위해 아래와 같이 단일 책임 기반 리팩터링 방향을 정립하였다.

// 1. 인벤토리 구조체 데이터 확장
USTRUCT(BlueprintType)
struct FInventorySlot
{
    GENERATED_BODY()

    UPROPERTY()
    FGameplayTag ItemTag;

    UPROPERTY()
    int32 Quantity;

    // 원래 어떤 블루프린트였는지 저장할 클래스 타입 정보 기억 변수 추가
    UPROPERTY()
    TSubclassOf<class ANCItemActor> ItemClass; 
};

🟦 데이터 캡슐화 및 동적 스폰 시퀀스

  • 아이템 획득 단계 (LootItem): 아이템을 주울 때 데이터와 함께 ItemToLoot->GetClass()를 통해 원본 블루프린트(예: BP_Axe) 정보를 가져와 가방 배열 슬롯 구조체에 함께 메모한다.
  • 아이템 버리기 단계 (Server_DropItem): 고정된 베이스 클래스로 스폰하는 비효율을 걷어내고, 구조체 슬롯에 저장해 두었던 Items[SlotIndex].ItemClass 레퍼런스를 기반으로 SpawnActor 하도록 동적 스폰 구조를 고도화한다.

🟧 핵심 요약

  • UI 위젯의 변수 데이터를 DragDropOperation 입력 핀에 명시적으로 바인딩하여 인게임 드래그 시 데이터 유실을 완전 차단하였다.
  • HasAuthority() 검증 레이어로 인한 클라이언트 차단 현상은 Server RPC 구조로 전환하여 데이터 주도권을 서버에 집중시켰다.
  • 언리얼 인벤토리 아키텍처는 실물 객체가 아닌 '추상적 데이터(태그/수량)'를 관리하는 공간임을 이해하고 액터 파괴 및 생성 흐름을 파악하였다.
  • 기획적 확장성(다양한 아이템 외형 복원)을 확보하기 위해 슬롯 구조체 내부에 TSubclassOf<ANCItemActor> 메타 변수를 선언하는 차세대 백로그 로직을 정립하였다.