PART 5 · 강의 3/7

장비 시스템

장비 장착/해제 및 GAS 연동 스탯 적용

01

장비 슬롯 구조

캐릭터 장비 슬롯 시스템

RPG 캐릭터는 여러 장비 슬롯을 가지며, 각 슬롯에는 해당하는 타입의 장비만 장착할 수 있습니다. 장비를 장착하면 GameplayEffect가 적용되어 캐릭터 스탯이 변경됩니다.

🎩
Head
👕
Chest
🧤
Hands
👖
Legs
👢
Feet
⚔️
MainHand
🛡️
OffHand
💍
Accessory1
📿
Accessory2

예시: 철갑 흉갑 스탯

Defense Power +25
Max Health +100
Movement Speed -5%
02

장비 장착 구현

인벤토리에서 장비 슬롯으로 이동

┌─────────────────────────────────────────────────────────────────┐
│                        장비 장착 플로우                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  EquipItem(SlotIndex)                                            │
│       │                                                          │
│       ▼                                                          │
│  ┌─────────────────┐                                             │
│  │ 인벤토리 슬롯   │                                             │
│  │ 유효성 검증     │                                             │
│  └────────┬────────┘                                             │
│           │                                                      │
│           ▼                                                      │
│  ┌─────────────────┐                                             │
│  │ Equippable      │  ← Fragment 확인                            │
│  │ Fragment 확인   │                                             │
│  └────────┬────────┘                                             │
│           │                                                      │
│           ▼                                                      │
│  ┌─────────────────┐     ┌─────────────────┐                    │
│  │ 기존 장비 있음? │ Yes │ UnequipItem()   │                    │
│  │                 │────▶│ 기존 장비 해제  │                    │
│  └────────┬────────┘     └─────────────────┘                    │
│           │ No                                                   │
│           ▼                                                      │
│  ┌─────────────────┐                                             │
│  │ 인벤토리에서    │                                             │
│  │ 제거            │                                             │
│  └────────┬────────┘                                             │
│           │                                                      │
│           ▼                                                      │
│  ┌─────────────────┐                                             │
│  │ 장비 슬롯에     │                                             │
│  │ 추가            │                                             │
│  └────────┬────────┘                                             │
│           │                                                      │
│           ▼                                                      │
│  ┌─────────────────┐                                             │
│  │ ApplyEquipment  │  ← GameplayEffect 적용                      │
│  │ Effects(true)   │  ← Ability 부여                             │
│  └────────┬────────┘                                             │
│           │                                                      │
│           ▼                                                      │
│  ┌─────────────────┐                                             │
│  │ 델리게이트      │                                             │
│  │ 브로드캐스트    │                                             │
│  └─────────────────┘                                             │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
InventoryComponent.cpp bool UInventoryComponent::EquipItem(int32 InventorySlotIndex) { if (!InventorySlots.IsValidIndex(InventorySlotIndex)) return false; UItemInstance* Item = InventorySlots[InventorySlotIndex]; if (!Item || !Item->ItemDefinition) return false; // 장비 프래그먼트 확인 const UItemFragment_Equippable* EquipFragment = Item->ItemDefinition->FindFragment<UItemFragment_Equippable>(); if (!EquipFragment || EquipFragment->Slot == EEquipmentSlot::None) return false; EEquipmentSlot TargetSlot = EquipFragment->Slot; // 기존 장비 해제 if (EquippedItems.Contains(TargetSlot)) { UnequipItem(TargetSlot); } // 인벤토리에서 제거 InventorySlots[InventorySlotIndex] = nullptr; // 장비 슬롯에 추가 EquippedItems.Add(TargetSlot, Item); // 이펙트 적용 ApplyEquipmentEffects(Item, true); // 델리게이트 브로드캐스트 OnItemEquipped.Broadcast(Item, TargetSlot); OnInventoryChanged.Broadcast(nullptr, InventorySlotIndex); return true; } bool UInventoryComponent::UnequipItem(EEquipmentSlot Slot) { UItemInstance* EquippedItem = GetEquippedItem(Slot); if (!EquippedItem) return false; // 빈 슬롯 찾기 int32 EmptySlot = FindEmptySlot(); if (EmptySlot == INDEX_NONE) { // 인벤토리 가득 참 - 해제 불가 return false; } // 이펙트 제거 ApplyEquipmentEffects(EquippedItem, false); // 장비 슬롯에서 제거 EquippedItems.Remove(Slot); // 인벤토리로 이동 InventorySlots[EmptySlot] = EquippedItem; OnItemEquipped.Broadcast(nullptr, Slot); OnInventoryChanged.Broadcast(EquippedItem, EmptySlot); return true; }
03

GAS 이펙트 적용

장비 스탯과 어빌리티 부여

InventoryComponent.cpp void UInventoryComponent::ApplyEquipmentEffects( UItemInstance* Item, bool bApply) { if (!Item || !Item->ItemDefinition) return; const UItemFragment_Equippable* EquipFragment = Item->ItemDefinition->FindFragment<UItemFragment_Equippable>(); if (!EquipFragment) return; // AbilitySystemComponent 가져오기 AActor* Owner = GetOwner(); IAbilitySystemInterface* ASI = Cast<IAbilitySystemInterface>(Owner); if (!ASI) return; UAbilitySystemComponent* ASC = ASI->GetAbilitySystemComponent(); if (!ASC) return; if (bApply) { // ===== 이펙트 적용 ===== if (EquipFragment->EquipEffect) { FGameplayEffectContextHandle Context = ASC->MakeEffectContext(); Context.AddSourceObject(Item); FGameplayEffectSpecHandle Spec = ASC->MakeOutgoingSpec( EquipFragment->EquipEffect, 1, Context); if (Spec.IsValid()) { // 이펙트 핸들 저장 (나중에 제거용) FActiveGameplayEffectHandle Handle = ASC->ApplyGameplayEffectSpecToSelf(*Spec.Data.Get()); // Item에 핸들 저장 (확장 필요) } } // ===== 어빌리티 부여 ===== for (const TSubclassOf<UGameplayAbility>& AbilityClass : EquipFragment->GrantedAbilities) { if (AbilityClass) { FGameplayAbilitySpec Spec(AbilityClass, 1); ASC->GiveAbility(Spec); } } } else { // ===== 이펙트 제거 ===== // 저장된 핸들로 제거하거나 태그로 제거 if (EquipFragment->EquipEffect) { // 태그 기반 제거 예시 FGameplayTagContainer EffectTags; EquipFragment->EquipEffect.GetDefaultObject()->GetAssetTags(EffectTags); ASC->RemoveActiveEffectsWithTags(EffectTags); } // ===== 어빌리티 제거 ===== for (const TSubclassOf<UGameplayAbility>& AbilityClass : EquipFragment->GrantedAbilities) { if (AbilityClass) { FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromClass(AbilityClass); if (Spec) { ASC->ClearAbility(Spec->Handle); } } } } }
이펙트 핸들 관리

실제 프로덕션 코드에서는 ApplyGameplayEffectSpecToSelf가 반환하는 FActiveGameplayEffectHandle을 저장해두고, 해제 시 이 핸들로 정확히 해당 이펙트만 제거해야 합니다. 태그 기반 제거는 같은 태그를 가진 다른 이펙트도 제거할 수 있습니다.

04

장비 메시 부착

캐릭터 소켓에 장비 시각화

EquipmentVisualizerComponent.h #pragma once #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "EquipmentVisualizerComponent.generated.h" UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) class MYGAME_API UEquipmentVisualizerComponent : public UActorComponent { GENERATED_BODY() public: UEquipmentVisualizerComponent(); // 장비 시각화 업데이트 UFUNCTION(BlueprintCallable, Category = "Equipment") void OnEquipmentChanged(UItemInstance* Item, EEquipmentSlot Slot); protected: virtual void BeginPlay() override; UPROPERTY() TMap<EEquipmentSlot, USkeletalMeshComponent*> EquipmentMeshes; UPROPERTY() USkeletalMeshComponent* OwnerMesh; private: void AttachEquipmentMesh(UItemInstance* Item, EEquipmentSlot Slot); void RemoveEquipmentMesh(EEquipmentSlot Slot); };
EquipmentVisualizerComponent.cpp void UEquipmentVisualizerComponent::BeginPlay() { Super::BeginPlay(); // 오너의 메시 컴포넌트 찾기 if (ACharacter* Character = Cast<ACharacter>(GetOwner())) { OwnerMesh = Character->GetMesh(); } // 인벤토리 컴포넌트의 델리게이트에 바인딩 if (UInventoryComponent* Inventory = GetOwner()->FindComponentByClass<UInventoryComponent>()) { Inventory->OnItemEquipped.AddDynamic( this, &UEquipmentVisualizerComponent::OnEquipmentChanged); } } void UEquipmentVisualizerComponent::OnEquipmentChanged( UItemInstance* Item, EEquipmentSlot Slot) { if (Item) { AttachEquipmentMesh(Item, Slot); } else { RemoveEquipmentMesh(Slot); } } void UEquipmentVisualizerComponent::AttachEquipmentMesh( UItemInstance* Item, EEquipmentSlot Slot) { if (!OwnerMesh || !Item || !Item->ItemDefinition) return; const UItemFragment_Equippable* EquipFragment = Item->ItemDefinition->FindFragment<UItemFragment_Equippable>(); if (!EquipFragment || !EquipFragment->EquipmentMesh.IsValid()) return; // 기존 메시 제거 RemoveEquipmentMesh(Slot); // 비동기 로드 USkeletalMesh* Mesh = EquipFragment->EquipmentMesh.LoadSynchronous(); if (!Mesh) return; // 새 메시 컴포넌트 생성 USkeletalMeshComponent* NewMeshComp = NewObject<USkeletalMeshComponent>( GetOwner()); NewMeshComp->SetSkeletalMesh(Mesh); NewMeshComp->RegisterComponent(); // 소켓에 부착 FName SocketName = EquipFragment->AttachSocketName; NewMeshComp->AttachToComponent( OwnerMesh, FAttachmentTransformRules::SnapToTargetNotIncludingScale, SocketName); EquipmentMeshes.Add(Slot, NewMeshComp); } void UEquipmentVisualizerComponent::RemoveEquipmentMesh(EEquipmentSlot Slot) { if (USkeletalMeshComponent** Found = EquipmentMeshes.Find(Slot)) { if (*Found) { (*Found)->DestroyComponent(); } EquipmentMeshes.Remove(Slot); } }
SUMMARY

핵심 요약

  • 장비 슬롯 시스템 — EEquipmentSlot 열거형으로 장비 타입별 슬롯을 관리합니다.
  • GAS 연동 — 장비 장착 시 GameplayEffect로 스탯을 수정하고, GameplayAbility를 부여합니다.
  • 스왑 로직 — 같은 슬롯에 새 장비 장착 시 기존 장비를 자동으로 해제합니다.
  • 이펙트 핸들 관리 — 적용된 이펙트의 핸들을 저장하여 정확한 제거가 가능하도록 합니다.
  • 시각화 분리 — 장비 로직과 시각화를 별도 컴포넌트로 분리하여 관심사를 분리합니다.
PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: 장비 슬롯 시스템 구현

EEquipSlot(Head, Body, Weapon, Shield, Accessory)을 정의하고, URPGEquipmentComponent에서 TMap로 장착 슬롯을 관리하세요. EquipItem()과 UnequipItem()을 구현하세요.

실습 2: 장비 스탯 반영

장비 변경 시 캐릭터의 AttributeSet에 GameplayEffect를 적용/제거하여 스탯을 반영하세요. GE_EquipSword(AttackPower +50), GE_EquipArmor(Defense +30) 등을 구현하세요.

심화 과제: 장비 비주얼 시스템

장비 변경 시 캐릭터의 SkeletalMesh에 해당 장비 메시를 부착(AttachToComponent)하는 비주얼 시스템을 구현하세요. 소켓 기반 부착과 TSoftObjectPtr 비동기 로딩을 조합하세요.