🎮 구현 목표
단순히 아이템을 줍고 장비를 착용하는 것을 넘어, 인벤토리-장비창 간 드래그 앤 드롭으로 아이템을 자유롭게 이동하고, 상태 변화가 즉시 모든 UI에 반영되는 시스템을 구현하고자 했습니다.
🚨 문제 상황
복잡한 의존성: UI가 모든 컴포넌트를 알아야 하는 구조
초기 설계에선 편의상 UI 위젯(예: ItemSlotWidget)이 InventoryComponent나 CombatComponent를 직접 참조해 기능을 호출했습니다.
- (문제 1) 복잡성:
ItemSlotWidget이 아이템 사용을 위해InventoryComponent를, 장착을 위해CombatComponent를, 갱신을 위해EquipmentWidget을 모두 알아야 했습니다. - (문제 2) 양방향 의존:
InventoryComponent도 장비가 해제되면EquipmentWidget을 업데이트해야 했습니다. UI와 로직이 서로 거미줄처럼 얽혀, 기능 하나를 수정하면 관련된 모든 위젯을 수정해야 하는 '스파게티 코드'가 되었습니다.
핵심 문제 인식: UI가 데이터 로직을 너무 많이 알고 있다. "UI는 데이터가 변경되었음을 알기만 하면 된다"는 원칙이 필요했습니다.
💭 해결 원칙
"관심사 분리" (Separation of Concerns)
복잡한 의존성을 끊어내기 위해 데이터 흐름을 단방향으로 강제했습니다.
- 데이터는 Component가 소유:
InventoryComponent,CombatComponent가 모든 데이터(아이템 목록, 장착 정보)를 '소유'하고 변경 로직을 독점합니다. - UI는 Manager와 소통: UI 위젯은
WidgetManagerComponent라는 '중앙 관리자'를 통해서만 Component에 "기능을 요청"합니다. (GetComponentByClass 난사 방지) - 갱신은 Delegate로: Component는 데이터가 변경되면, 자신을 참조하는 UI를 찾는 대신 Delegate(이벤트)`를 방송(Broadcast)합니다.
- UI는 Delegate를 구독: 모든 UI(인벤토리, 장비창)는 이 Delegate를 '구독(Subscribe)'하고 있다가, 알림이 오면 스스로 갱신합니다.
개선된 흐름: UI 입력 → WidgetManager → Component (데이터 변경) → Delegate 방송 → 모든 구독 UI가 스스로 갱신
아키텍처 다이어그램:
[UI Layer]
ItemSlotWidget ──┐
EquipmentSlotWidget ─┤
├─→ WidgetManager ─→ [Data Layer]
InventoryWidget ──┤ InventoryComponent
EquipmentWidget ─┘ CombatComponent
↓
Delegate ←───────────────────┘
↓
[UI 자동 갱신]
🔧 핵심 구현
데이터 흐름:
1. 플레이어가 새 무기 드래그 앤 드롭
↓
2. EquipmentSlotWidget::NativeOnDrop()
↓
3. InventoryComponent->RemoveItem(원본 슬롯)
→ OnInventoryUpdated Delegate 발행
→ InventoryWidget 자동 갱신
↓
4. Equipment->EquipItem()
↓
5. CombatComponent->SetWeapon(새 무기, 슬롯 인덱스)
→ 기존 무기를 InventoryComponent->AddItem(슬롯 인덱스)
→ OnInventoryUpdated Delegate 발행
→ InventoryWidget 다시 갱신 (기존 무기 표시)
→ OnChangedWeapon Delegate 발행
→ EquipmentWidget 자동 갱신
1. Delegate를 이용한 자동 갱신
I
nventoryComponent에 "인벤토리가 갱신되었다"는 신호(Delegate)를 만듭니다.
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnInventoryUpdated);
UCLASS()
class SOUL_API UInventoryComponent : public UActorComponent
{
public:
UPROPERTY(BlueprintAssignable)
FOnInventoryUpdated OnInventoryUpdated; // UI가 구독할 Delegate
bool AddItem(FName ItemID, int32 Amount)
{
// ... 데이터 변경 로직
BroadcastInventoryUpdated(); // 변경 알림
return true;
}
void BroadcastInventoryUpdated() const
{
if (OnInventoryUpdated.IsBound())
{
OnInventoryUpdated.Broadcast();
}
}
};
위젯은 Delegate 구독만 합니다.
void UInventoryWidget::SetInventoryComponent(UInventoryComponent* NewInventoryComp)
{
if (InventoryComponent)
{
// 기존 구독 해제
InventoryComponent->OnInventoryUpdated.RemoveDynamic(this, &UInventoryWidget::RefreshInventory);
}
InventoryComponent = NewInventoryComp;
if (InventoryComponent)
{
// 새로운 구독
InventoryComponent->OnInventoryUpdated.AddDynamic(this, &UInventoryWidget::RefreshInventory);
RefreshInventory(); // 초기 상태 반영
}
}
2. DragDropOperation으로 드래그 정보 관리
// ItemDragDropOperation.h
UCLASS()
class UItemDragDropOperation : public UDragDropOperation
{
GENERATED_BODY()
public:
// 드래그 중인 아이템 정보
UPROPERTY()
FItemData ItemData;
// 드래그 시작한 원본 슬롯
UPROPERTY()
USlotWidget* SourceSlotWidget;
// swap시 바꿀 위치를 기록
UPROPERTY()
int32 SourceSlotIndex;
};
슬롯 위젯은 드롭이 감지되면, Operation 객체에 담긴 SourceSlotWidget을 확인하여 "아, 인벤토리에서 장비창으로 이동했구나" 혹은 "장비창에서 인벤토리로 이동했구나"를 판단하고, WidgetManager에 적절한 기능(장착/해제)을 요청합니다.
드롭 처리 (EquipmentSlotWidget):
bool UEquipmentSlotWidget::NativeOnDrop(...)
{
// 2. 드래그 출처 확인
if (UItemSlotWidget* ItemSlot = Cast(Operation->SourceSlotWidget))
{
// 인벤토리 → 장비창: 장착
if (ASoulItemBase* Item = Operation->ItemData.Item->GetDefaultObject())
{
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = GetOwningPlayerPawn();
ASoulEquipment* Equipment = GetWorld()->SpawnActor(
Operation->ItemData.Item,
GetOwningPlayerPawn()->GetActorTransform(),
SpawnParams
);
if (Equipment)
{
// 인벤토리에서 제거
UWidgetManagerComponent::GetInventoryComponent()->RemoveItem(Operation->SourceSlotIndex);
// 장비 장착 (내부에서 CombatComponent 업데이트)
Equipment->EquipItem(Operation->SourceSlotIndex);
// UI 갱신은 Delegate가 자동 처리!
}
}
}
else if (UEquipmentSlotWidget* EquipSlot = Cast(Operation->SourceSlotWidget))
{
// 장비창 → 인벤토리: 장착 해제
// (별도 처리)
}
}
핵심: UI는 단 한 줄(Equipment->EquipItem())만 호출하지만, 나머지는 Delegate 체인으로 자동 동기화
✅ 결과
이 아키텍처를 통해 UI와 데이터 로직을 성공적으로 분리했습니다.
- 단방향 데이터 흐름: 데이터 흐름이 명확해져 버그 추적이 쉬워졌습니다.
- 자유로운 드래그 앤 드롭: 인벤토리 ↔ 장비창 간 아이템 교체(Swap), 장착, 해제 로직이 Delegate를 통해 자동으로 동기화됩니다.
- 높은 확장성: '퀵슬롯'이나 '상점' UI를 새로 추가하더라도, WidgetManager에 접근하고 Delegate를 구독하기만 하면 기존 코드 수정 없이 즉시 시스템에 연동됩니다.
'프로젝트 회고' 카테고리의 다른 글
| [Dedicated Server] 비동기 폴링 기반 서버 접속 시도 (0) | 2025.12.02 |
|---|---|
| [UE5 팀 프로젝트] 지연 초기화: 런타임 액터 스폰, BeginPlay 호출 전 DataTable 데이터 무결성 확보 (0) | 2025.12.02 |
| [UE5 액션] 데이터 에셋 기반 무기별 전투 스타일 관리 (0) | 2025.12.02 |
| [UE5 액션] 루트 모션 기반 자연스러운 대시 구현 Motion Warping (0) | 2025.12.02 |
| [UE5 액션] 유연한 콤보 시스템: 이벤트 기반 Perfect/Mercy 구간을 통한 공격 후딜레이 캔슬 및 콤보 연계 (0) | 2025.12.02 |
