인벤토리 기반 무기 장착 및 스탯 반영 구조
1. 시스템 요구 사항
플레이어는 인벤토리 슬롯에 존재하는 장비 아이템을 선택하여 무기를 장착할 수 있어야 한다.
장착이 이루어질 경우 해당 무기의 스탯은 즉시 플레이어 상태에 반영되어야 하며, UI에는 현재 장착 중인 무기의 아이콘이 표시되어야 한다.
이미 동일한 무기가 장착되어 있다면 중복 장착이 발생하지 않아야 하며, 장착 슬롯을 다시 클릭하면 무기를 해제할 수 있어야 한다.
장착 로직은 단순 UI 반영에 그치지 않고, 실제 플레이어 스탯 값 변경과 월드 무기 장착 요청까지 이어져야 한다.
그러나 UI 계층이 직접 프리팹 생성 로직을 처리해서는 안 된다. 각 계층은 자신의 책임 범위 내에서만 동작해야 한다.
2. 설계 목표
- 인벤토리 슬롯과 장착 상태를 명확히 분리
- 스탯 변경을 장착 로직 내부에서 단일 책임으로 처리
- 동일 무기 중복 장착 방지
- UI 계층과 실제 장비 프리팹 생성 계층 분리
3. 흐름도
인벤토리 슬롯 우클릭
↓
EquipWeaponBySlot(slotIndex)
↓
장착 가능 여부 검증
↓
equippedData 갱신
↓
플레이어 스탯 반영
↓
아이콘 반영
↓
EquipWeaponAtHand에 장착 요청
해제는 OnPointerClick에서 처리된다.
4. 구현
4.1. StatusWeaponSlot 클래스 구조
public class StatusWeaponSlot : MonoBehaviour, IPointerClickHandler
{
public static StatusWeaponSlot instance;
Image equipWeapon;
InventoryItemData equippedData;
public int EquippedWeaponItemId => equippedData != null ? equippedData.ItemId : -1;
private void Awake() => instance = this;
}StatusWeaponSlot은 무기 장착 상태를 관리하는 UI 계층 매니저다.
MonoBehaviour를 상속하며, IPointerClickHandler를 구현하여 이벤트 기반 입력을 처리한다.
Update를 사용하지 않고 Unity의 이벤트 시스템을 사용하는 이유는, 입력이 발생한 순간에만 처리하도록 하기 위함이다.
매 프레임 검사 방식보다 명확하고 비용이 적다.
equippedData는 현재 장착된 무기 데이터를 저장하는 변수다.
이 값이 곧 장착 상태의 단일 진실 원천(Single Source of Truth)이다.
장착 여부는 이 변수의 null 여부로 판단된다.
별도의 bool 값을 두지 않은 이유는 중복 상태를 만들지 않기 위함이다.
EquippedWeaponItemId는 읽기 전용 프로퍼티로 외부 시스템이 현재 장착된 무기의 ID를 안전하게 조회할 수 있도록 만든 인터페이스다.
null 방어를 내부에서 처리함으로써 외부 호출부의 안정성을 높였다.
4.2. 장착 단일 진입점
public void EquipWeaponBySlot(int slotIndex)
{
var inv = InventoryManager.instance;
var slot = inv.GetSlot(slotIndex);
if (slot == null || slot.IsEmpty) return;
var equipData = slot.data as EquipmentItmeData;
if (equipData == null) return;
if (equippedData != null && equippedData.ItemId == equipData.ItemId)
return;
equippedData = equipData;
PlayerManager.instance.PlayerStatus.WeaponAddMagicAttack = equipData.MagicAttack;
equipWeapon.enabled = true;
equipWeapon.sprite = equipData.Icon;
EquipWeaponAtHand.instance.EquipByItemId(equipData.ItemId);
SlotManager.instance.PoplateSlots();
}이 함수는 장착의 단일 진입점이다.
모든 장착 로직은 반드시 이 함수를 통해 수행된다.
먼저 인벤토리에서 슬롯 데이터를 가져오고 null 또는 빈 슬롯일 경우 즉시 return한다.
이는 잘못된 입력으로 인해 상태가 오염되는 것을 차단하는 방어적인 설계다.
slot.data를 EquipmentItmeData로 캐스팅하는 부분은 장비 아이템만 장착 가능하도록 제한하기 위한 구조다.
as 키워드는 캐스팅 실패 시 예외를 발생시키지 않고 null을 반환하므로 안정적이다.
동일 무기 중복 장착 방지는 ItemId 기반 비교로 처리한다.
참조 비교 대신 ID 비교를 사용하는 이유는, 동일 아이템이 서로 다른 인스턴스일 가능성까지 고려한 설계다.
데이터 확장성을 고려한 선택이다.
equippedData를 교체한 뒤, 플레이어 스탯에 MagicAttack 값을 반영한다.
여기서 중요한 점은 장착 데이터 확정 이후 스탯 반영이라는 순서를 유지한다는 것이다.
스탯 반영은 장착 상태의 파생 결과다.
순서를 뒤집으면 상태 불일치가 발생할 수 있다.
equipWeapon.enabled와 sprite 설정은 UI 표현 계층이다.
UI는 장착 상태를 시각적으로 표시하는 책임을 가진다.
이 구조는 데이터 계층과 표현 계층을 분리한다.
EquipByItemId 호출은 월드 계층에 대한 요청이다.
StatusWeaponSlot은 프리팹 생성 방법을 모른다.
단지 이 ID의 무기를 장착해 달라고 요청할 뿐이다.
이 분리는 계층 간 결합도를 낮춘다.
마지막으로 PoplateSlots를 호출한다.
장착 상태가 바뀌면 인벤토리 UI 역시 갱신되어야 하기 때문이다.
장착 중인 아이템을 슬롯에서 표시하거나, 중복 장착을 막는 표시가 필요할 수 있다.
이 호출은 상태 변경 이후 UI 동기화를 보장한다.
4.2. 무기 해제
public void OnPointerClick(PointerEventData eventData)
{
if (equippedData == null) return;
EquipWeaponAtHand.instance.Unequip();
equipWeapon.enabled = false;
equippedData = null;
PlayerManager.instance.PlayerStatus.WeaponAddMagicAttack = 0;
SlotManager.instance.PoplateSlots();
}OnPointerClick은 현재 장착 슬롯 UI를 클릭했을 때 실행된다.
장착된 무기가 없으면 아무 동작도 하지 않는다.
이 null 검사는 해제 로직의 안전성을 보장한다.
해제는 장착의 역순으로 진행된다.
먼저 월드에서 무기를 제거하고, UI 아이콘을 끄고, 장착 데이터를 null로 만들고, 스탯을 0으로 초기화한다.
이 순서를 유지하는 이유는 상태 일관성을 유지하기 위함이다.
특히 WeaponAddMagicAttack를 0으로 설정하는 부분은 매우 중요하다.
장착 해제 후에도 공격력이 유지된다면 데이터 오염이 발생한다.
장착과 해제는 반드시 대칭 구조를 가져야 한다.
5. 개발 의도
이 장착 시스템은 UI 계층은 상태를 관리하고, 월드 계층은 시각적 구현을 담당한다는 책임 분리 원칙에 따라 설계되었다.
장착 상태는 equippedData 하나로 관리되며, 이 값이 모든 후속 로직의 기준이 된다.
이는 상태 분산을 방지하고 디버깅을 단순화한다.
스탯 반영은 장착 로직 내부에서 직접 처리되었지만, 확장 시에는 이벤트 기반 구조로 변경할 수 있다.
예를 들어 OnWeaponEquipped 이벤트를 발행하고, PlayerStatus가 이를 구독해 스탯을 계산하도록 만들면 더 느슨한 결합 구조를 만들 수 있다.
현재 구조는 단일 무기 슬롯 기준이지만, 방어구 슬롯, 악세서리 슬롯, 세트 효과 시스템으로 확장할 수 있는 기반을 갖고 있다.
장착 데이터 관리 방식을 일반화하면 다중 장비 시스템으로 자연스럽게 확장 가능하다.
