언리얼엔진/구현·실습

[UE5] 장비 슬롯과 숫자 단축키를 데이터 기반 무기 교체 시스템으로 연결하기

write76465 2026. 7. 16. 18:58

기존에는 숫자 키를 누르면 블루프린트에 직접 작성한 RowName으로 무기를 교체했다.

1번 키 → AK47
2번 키 → Bubble
3번 키 → Pistol

이 방식은 빠르게 기능을 확인하기에는 좋지만, 장비창의 내용이 바뀌어도 단축키가 계속 기존 무기를 장착한다는 문제가 있다.

플레이어가 장비 슬롯 1에 어떤 무기를 넣었는지와 관계없이 1번 키가 항상 AK47을 요청하기 때문이다.

이번 작업에서는 숫자 키에 특정 무기를 하드코딩하는 대신 다음과 같은 데이터 흐름을 만들었다.

숫자 키 입력
→ 대응하는 장비 슬롯 선택
→ 슬롯의 ItemRowName 확인
→ DT_Item에서 아이템 정보 조회
→ WeaponRowName 추출
→ 캐릭터에게 무기 교체 요청

이를 통해 장비창의 상태와 실제 캐릭터의 장착 무기를 연결했다.


1. 기존 구조의 문제

처음에는 캐릭터 블루프린트에 무기 RowName이 직접 작성돼 있었다.

1번 키 → EquipWeaponByRowName("AK47")
2번 키 → EquipWeaponByRowName("Bubble")
3번 키 → EquipWeaponByRowName("Pistol")

기존 블루프린트

 

 

이 구조에서는 UI 장비 슬롯과 실제 무기 교체가 서로 독립적으로 동작한다.

예를 들어 장비 슬롯 1의 무기를 AK47에서 다른 라이플로 교체해도 1번 키는 여전히 AK47을 장착한다.

장비 슬롯 1
→ 새로운 라이플

숫자 키 1
→ 여전히 AK47을 장착

즉, 화면에 표시되는 장비 상태와 캐릭터의 실제 장착 상태가 일치하지 않았다.

또한 무기가 추가될 때마다 캐릭터 블루프린트를 수정해야 했다.

신규 무기 추가
→ 데이터 테이블 Row 추가
→ 장비 UI 수정
→ 숫자 키 로직의 RowName 수정

데이터 테이블을 사용하고 있음에도 실제 선택 로직은 하드코딩에 의존하고 있었다.


2. 두 종류의 RowName 구분하기

현재 아이템 시스템에는 두 종류의 데이터 테이블이 존재한다.

아이템 데이터 테이블

DT_Item은 인벤토리와 UI에 필요한 정보를 관리한다.

Name
Description
Item Type
Mesh
Icon
Max Stack
Weapon Row Name
Price

무기 데이터 테이블

DT_Weapon은 실제 전투에 필요한 무기 데이터를 관리한다.

Weapon Type
Weapon Mesh
Damage
Magazine Size
Max Ammo
Fire Rate
Reload Time
Range
Recoil
Muzzle Flash
Tracer Effect

장비 슬롯에 저장된 값은 DT_Item의 RowName이다.

예를 들어:

장비 슬롯의 ItemRowName
→ AK47_Item

그러나 캐릭터의 EquipWeaponByRowName() 함수가 필요로 하는 값은 DT_Weapon의 RowName이다.

EquipWeaponByRowName에 필요한 값
→ AK47

따라서 슬롯의 RowName을 캐릭터에게 바로 넘기면 안 된다. 중간에 DT_Item을 조회해 Weapon Row Name을 추출해야 한다.

AK47_Item
→ DT_Item 조회
→ Weapon Row Name = AK47
→ DT_Weapon의 AK47 장착

이 관계를 명확히 분리하면 아이템 이름과 무기 데이터 이름이 반드시 같을 필요가 없다.

DT_Item RowName
→ Rifle_AK47_Item

DT_Item.WeaponRowName
→ Weapon_AK47

DT_Weapon RowName
→ Weapon_AK47

핵심 조건은 다음 하나다.

DT_Item.WeaponRowName
=
DT_Weapon.RowName

3. RowName 변경으로 발생할 수 있는 참조 문제

작업 중 권총의 RowName이 변경되면서 기본 무기가 정상적으로 생성되지 않는 문제도 확인했다.

기본 권총 장착 함수는 특정 문자열을 직접 사용하지 않고 WeaponDataMap의 Pistol 항목을 조회하고 있었다.

void APlayerCharacter::EquipDefaultWeapon()
{
    const FDataTableRowHandle DefaultWeaponData =
        GetWeaponDataRowByType(EWeaponType::Pistol);

    if (DefaultWeaponData.IsNull())
    {
        return;
    }

    EquipWeaponByRowName(DefaultWeaponData.RowName);
}

코드에 권총 이름이 하드코딩되어 있지 않더라도 블루프린트의 FDataTableRowHandle은 이전 RowName을 계속 참조할 수 있다.

DT_Weapon RowName 변경
→ 기존 DataTableRowHandle이 이전 이름을 참조
→ IsNull 또는 Row 조회 실패
→ 기본 권총 생성 실패

따라서 RowName을 변경할 때는 다음 참조를 함께 갱신해야 한다.

BP_PlayerCharacter
└─ WeaponDataMap
   └─ Pistol
      └─ RowName

DT_Item
└─ 권총 아이템
   └─ Weapon Row Name

이 문제를 통해 데이터 테이블의 RowName은 단순한 표시 이름이 아니라 시스템 사이를 연결하는 식별자라는 점을 확인했다.

RowName을 변경하는 작업은 사실상 데이터 키를 변경하는 작업이므로 모든 참조 지점을 함께 점검해야 한다.


4. 기존 장비 슬롯 구조 활용

WBP_Equipment에는 이미 네 개의 장비 슬롯 위젯이 존재했다.

WBP_EquipSlot_1
WBP_EquipSlot_2
WBP_EquipSlot_3
WBP_EquipSlot_소비

각 슬롯에는 생성 시점에 고유한 EquipSlotIndex가 설정돼 있었다.

WBP_EquipSlot_1 → Index 0
WBP_EquipSlot_2 → Index 1
WBP_EquipSlot_3 → Index 2
소비 슬롯       → Index 3

 

 

새로운 슬롯 배열을 추가하기보다 기존 슬롯 참조와 인덱스를 활용했다.

현재 요구사항은 다음과 같았다.

숫자 키 1
→ WBP_EquipSlot_1의 무기

숫자 키 2
→ WBP_EquipSlot_2의 무기

숫자 키 3
→ 고정 권총

따라서 키보드 번호와 내부 슬롯 인덱스를 다음처럼 매핑했다.

키보드 1 → Slot Index 0
키보드 2 → Slot Index 1

5. 슬롯 조회 함수를 별도로 만든 이유

숫자 키 이벤트마다 데이터 테이블 조회 로직을 작성하면 동일한 노드가 반복된다.

1번 키
→ Slot 1 ItemRowName
→ DT_Item 조회
→ WeaponRowName
→ 유효성 검사

2번 키
→ Slot 2 ItemRowName
→ DT_Item 조회
→ WeaponRowName
→ 유효성 검사

이 구조는 슬롯이 늘어날수록 중복이 증가한다.

따라서 WBP_Equipment 내부에 슬롯 번호를 받아 무기 RowName을 반환하는 함수를 만들었다.

GetWeaponRowNameBySlotIndex

입력:

Slot Index : Integer

출력:

Weapon Row Name : Name
Is Valid : Boolean

이 함수는 UI 내부 구조를 외부에 직접 노출하지 않고, 외부에서는 슬롯 인덱스만 전달하도록 만든다.

외부
→ 0번 슬롯의 무기 RowName을 요청

WBP_Equipment
→ 어떤 위젯이 0번인지 판단
→ DT_Item 조회
→ 최종 WeaponRowName 반환

캐릭터는 장비 슬롯 위젯의 내부 구조를 알 필요가 없다.


6. Switch on Int로 슬롯 선택

함수 입력으로 받은 Slot Index는 Switch on Int에 연결했다.

Slot Index
→ Switch on Int

분기는 다음처럼 구성했다.

0 → WBP_EquipSlot_1
1 → WBP_EquipSlot_2
Default → 유효하지 않은 요청

각 슬롯 위젯에서 Item Row Name을 가져온 뒤 공통 로컬 변수에 저장했다.

SelectedItemRowName
Switch 0
→ WBP_EquipSlot_1.ItemRowName
→ Set SelectedItemRowName
Switch 1
→ WBP_EquipSlot_2.ItemRowName
→ Set SelectedItemRowName

 

Switch On Int와 연결준비하는 모습

 

처음에는 슬롯마다 서로 다른 로컬 변수를 만들었지만, 그렇게 하면 이후 데이터 조회 로직을 공통으로 사용할 수 없다.

SelectedItemRowName
SelectedItemRowName01

두 분기가 같은 목적의 값을 선택하는 것이므로 하나의 로컬 변수로 합치는 것이 적절했다.

여러 입력 분기
→ 하나의 SelectedItemRowName
→ 공통 데이터 조회 로직

7. DT_Item에서 실제 WeaponRowName 추출

선택한 ItemRowName으로 DT_Item을 조회했다.

Get Data Table Row
Data Table = DT_Item
Row Name = SelectedItemRowName

조회에 성공하면 Break Item Data를 통해 Weapon Row Name을 추출한다.

SelectedItemRowName
→ Get Data Table Row(DT_Item)
→ Break Item Data
→ Weapon Row Name

 

이 과정이 필요한 이유는 장비 슬롯이 무기 데이터가 아니라 아이템 데이터를 보관하고 있기 때문이다.

장비 슬롯
→ 아이템의 정체성 및 UI 정보

DT_Item.WeaponRowName
→ 실제 무기 데이터로 이동하기 위한 참조

DT_Weapon
→ 전투 성능과 무기 Actor 생성 정보

즉, Weapon Row Name은 아이템 시스템과 무기 시스템 사이를 연결하는 외래 키와 비슷한 역할을 한다.


8. 성공과 실패를 명시적으로 반환

슬롯이 비어 있거나 데이터 테이블에서 Row를 찾지 못했을 때 무기 교체를 시도하면 안 된다.

따라서 함수 출력에 Is Valid를 추가했다.

성공 조건:

DT_Item Row Found
AND Weapon Row Name != None

성공 시:

Weapon Row Name = 조회된 WeaponRowName
Is Valid = true

실패 시:

Weapon Row Name = None
Is Valid = false

실패 경로는 다음 세 가지다.

지원하지 않는 Slot Index
DT_Item Row Not Found
Weapon Row Name이 None

연결완료된 최종함수

 

최종 함수 흐름은 다음과 같다.

Slot Index
→ Switch on Int
→ 해당 슬롯의 ItemRowName 선택
→ DT_Item 조회
→ WeaponRowName != None 검사
├─ 성공 → Return(WeaponRowName, true)
└─ 실패 → Return(None, false)

Is Valid를 별도로 반환함으로써 호출 측에서는 실패 원인을 다시 추론할 필요 없이 Branch 하나로 장착 가능 여부를 판단할 수 있다.


9. 숫자 키와 장비 슬롯 연결

캐릭터 블루프린트에는 인벤토리·장비 UI를 가리키는 InvenRef가 있었다.

이를 통해 WBP_Equipment에 접근한 뒤 새로 만든 함수를 호출했다.

숫자 1:

1 Pressed
→ InvenRef
→ WBP_Equipment
→ GetWeaponRowNameBySlotIndex(0)
→ Branch(Is Valid)
→ EquipWeaponByRowName

숫자 2:

2 Pressed
→ InvenRef
→ WBP_Equipment
→ GetWeaponRowNameBySlotIndex(1)
→ Branch(Is Valid)
→ EquipWeaponByRowName

숫자 3은 고정 권총이므로 기존처럼 권총 RowName을 전달했다.

3 Pressed
→ EquipWeaponByRowName(PistolRowName)

최종 블루프린트 노드

 

 

처음 연결했을 때 Branch의 Condition이 체크박스 true로 고정되어 있었다.

Branch Condition = true

이 상태에서는 빈 슬롯이거나 조회에 실패해도 무기 교체 함수가 실행된다.

이를 함수의 Is Valid 출력과 연결했다.

GetWeaponRowNameBySlotIndex.IsValid
→ Branch.Condition

이제 빈 슬롯을 선택하면 아무 작업도 수행하지 않는다.


10. 실제 무기 교체는 기존 서버 로직 활용

블루프린트에서는 최종적으로 EquipWeaponByRowName()만 호출한다.

void APlayerCharacter::EquipWeaponByRowName(
    FName WeaponRowName)
{
    if (WeaponRowName.IsNone())
    {
        return;
    }

    if (HasAuthority())
    {
        EquipWeaponByRowNameInternal(WeaponRowName);
    }
    else
    {
        ServerEquipWeaponByRowName(WeaponRowName);
    }
}

클라이언트에서 숫자 키를 눌렀다면 Server RPC를 통해 서버에 교체를 요청한다.

로컬 숫자 입력
→ 슬롯에서 WeaponRowName 추출
→ EquipWeaponByRowName
→ ServerEquipWeaponByRowName
→ 서버에서 무기 데이터 검증

내부에서는 전달받은 RowName이 실제 WeaponDataMap에 존재하는지도 다시 확인한다.

if (!FindWeaponByRowName(
    WeaponRowName,
    NewWeaponType,
    WeaponData))
{
    return;
}

따라서 UI가 반환한 RowName을 곧바로 신뢰하지 않고 서버가 보유한 데이터에서 다시 검증한다.

이후 무기 변경을 즉시 처리하지 않고 PendingWeaponRowName에 저장한다.

PendingWeaponRowName = WeaponRowName;
MulticastPlayEquipMontage(NewWeaponType);

무기 교체 몽타주의 ChangeWeapon 노티파이 시점에서 실제 무기를 교체한다.

숫자 키 입력
→ 서버에 RowName 전달
→ 무기 교체 몽타주 재생
→ ChangeWeapon Notify
→ CommitPendingWeaponChange
→ 기존 무기 제거
→ 새 무기 Actor 생성

이 구조 덕분에 손에 든 무기가 애니메이션 시작과 동시에 갑자기 바뀌지 않고, 몽타주에서 지정한 정확한 프레임에 교체된다.


11. 전체 데이터 흐름

최종 데이터 흐름은 다음과 같다.

숫자 키 1 입력
        │
        ▼
GetWeaponRowNameBySlotIndex(0)
        │
        ▼
WBP_EquipSlot_1.ItemRowName
        │
        ▼
DT_Item 조회
        │
        ▼
ItemData.WeaponRowName
        │
        ▼
EquipWeaponByRowName
        │
        ▼
Server RPC
        │
        ▼
DT_Weapon 및 WeaponDataMap 검증
        │
        ▼
PendingWeaponRowName
        │
        ▼
무기 교체 몽타주
        │
        ▼
ChangeWeapon Notify
        │
        ▼
새 무기 Actor 생성 및 장착

UI는 “어떤 아이템이 장비 슬롯에 들어 있는가”를 제공하고, 서버는 “해당 이름의 무기를 실제로 장착할 수 있는가”를 검증한다.


12. 구현하면서 어려웠던 점

ItemRowName과 WeaponRowName의 혼동

장비 슬롯에서 바로 얻을 수 있는 값은 DT_Item의 RowName이다. 이를 그대로 무기 장착 함수에 전달하면 DT_Weapon에서 Row를 찾지 못한다.

두 데이터 테이블의 역할과 식별자를 구분하는 것이 중요했다.

UI 상태와 실제 전투 상태의 연결

장비 UI에 아이콘이 표시되는 것과 캐릭터가 해당 무기를 장착하는 것은 별개의 시스템이다.

단순히 이미지가 슬롯에 들어갔다고 해서 캐릭터의 무기 Actor가 자동으로 변경되지는 않는다.

두 시스템 사이에 다음과 같은 명시적인 변환 과정이 필요했다.

UI 슬롯
→ 아이템 데이터
→ 무기 데이터 식별자
→ 서버 장착 요청

실패 경로 관리

정상적인 Row만 생각하면 장착 로직은 간단하다. 하지만 실제 플레이에서는 다음 상황이 발생할 수 있다.

  • 빈 장비 슬롯
  • 잘못된 슬롯 번호
  • 변경된 RowName
  • 삭제된 데이터 테이블 Row
  • WeaponRowName이 없는 일반 아이템

성공과 실패를 명시적으로 나눠 반환함으로써 잘못된 장착 요청을 차단했다.

애니메이션과 실제 Actor 교체 시점 분리

입력 즉시 무기를 교체하면 몽타주와 무기 메시 변경 시점이 맞지 않는다.

따라서 “교체 요청”과 “실제 교체”를 분리했다.

요청 시점
→ PendingWeaponRowName 기록

연출 시점
→ 교체 몽타주 재생

확정 시점
→ ChangeWeapon Notify에서 Actor 교체

13. 현재 구조의 한계와 개선 방향

현재 구현에서는 캐릭터가 InvenRef를 통해 장비 UI의 슬롯 값을 조회한다.

기능적으로는 동작하지만, UI 위젯이 장비 데이터의 실질적인 저장소 역할을 한다는 한계가 있다.

만약 장비창 위젯이 아직 생성되지 않았거나 제거된 상태라면 다음 오류가 발생할 수 있다.

Accessed None trying to read InvenRef

장기적으로는 장비 상태를 UI가 아닌 별도의 데이터 객체가 보유하는 것이 더 안정적이다.

권장 구조:

InventoryComponent 또는 EquipmentComponent
├─ EquippedSlot1ItemRowName
├─ EquippedSlot2ItemRowName
└─ FixedPistolRowName

WBP_Equipment
→ Component의 데이터를 표시

숫자 키 입력
→ Component의 데이터를 조회

이 구조에서는 UI를 열지 않았더라도 무기 교체가 가능하고, 서버 복제와 저장·불러오기도 쉬워진다.

현재 구조
Character → Widget → Slot Data

개선 구조
Character → EquipmentComponent
Widget    → EquipmentComponent

UI는 데이터를 소유하지 않고 표현만 담당하는 형태다.

이번 작업에서는 기존 구조의 변경 범위를 줄이기 위해 WBP_Equipment의 슬롯 데이터를 활용했지만, 시스템이 확장된다면 장비 상태를 Component 또는 PlayerState로 이동할 수 있다.


마무리

이번 작업은 숫자 키에 직접 작성된 무기 이름을 장비 슬롯 기반의 동적 선택 구조로 변경하는 작업이었다.

핵심은 단순히 1번과 2번 키를 연결하는 것이 아니라, 서로 다른 시스템 사이의 데이터 경계를 정리하는 것이었다.

장비 슬롯
→ ItemRowName 보유

DT_Item
→ WeaponRowName 제공

DT_Weapon
→ 실제 무기 성능 제공

Character
→ 서버에 장착 요청

Anim Notify
→ 실제 무기 변경 시점 확정

최종적으로 다음 효과를 얻었다.

  • 숫자 키의 무기 RowName 하드코딩 제거
  • 장비 슬롯 변경 내용이 단축키에 즉시 반영
  • 빈 슬롯 및 잘못된 RowName 방어
  • 아이템 데이터와 무기 데이터의 역할 분리
  • 기존 서버 무기 장착 로직 재사용
  • 몽타주 노티파이를 통한 자연스러운 교체 타이밍 유지

단순한 단축키 기능처럼 보이지만, UI·데이터 테이블·서버 RPC·Actor 생성·애니메이션 노티파이가 하나의 흐름으로 연결된 작업이었다.