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
실습 2: 장비 스탯 반영
장비 변경 시 캐릭터의 AttributeSet에 GameplayEffect를 적용/제거하여 스탯을 반영하세요. GE_EquipSword(AttackPower +50), GE_EquipArmor(Defense +30) 등을 구현하세요.
심화 과제: 장비 비주얼 시스템
장비 변경 시 캐릭터의 SkeletalMesh에 해당 장비 메시를 부착(AttachToComponent)하는 비주얼 시스템을 구현하세요. 소켓 기반 부착과 TSoftObjectPtr 비동기 로딩을 조합하세요.