본문 바로가기
Unity

Unity(13) - Character Equipment

by Srff5123 2026. 6. 23.

레피시도 등록완료 한 상태이니 레시피를 통해 만든 장착 아이템을 아이템 타입(머리, 몸, 다리, 발)에 따라 장착할 수 있는 칸 구현

 

public enum SlotGroup : byte
{
    Inventory, // 인벤토리 슬롯
    Hotbar, // 하단 핫바 슬롯
    CraftingInput, // 제작대 9칸 슬롯
    CraftingResult, // 제작 결과 슬롯
    Equipment // 장비 장착 슬롯
}

SlotGroup에서 인벤토리 슬롯 구분을 위해 Equipment 그룹을 만들어준다

using System;
using UnityEngine;


public class EquipmentInventory : MonoBehaviour
{
    public const int SlotCount = 4; // 장비 슬롯 개수, 머리, 몸, 다리 ,신발

    [SerializeField] private ItemStack head;
    [SerializeField] private ItemStack chest;
    [SerializeField] private ItemStack legs;
    [SerializeField] private ItemStack feet;

    public event Action Changed; // 장비 상태 변경 이벤트

    public ItemStack GetSlot(int index)
    {
        return index switch
        {
            0 => head,
            1 => chest,
            2 => legs,
            3 => feet,
            _ => new ItemStack(ItemId.None, 0)
        };
        // 번호에 맞는 장비 슬롯 반환
        // 잘못된 번호가 들어오면 빈 ItemStack 반환
    }

    public bool SetSlot(int index, ItemStack stack)
    {
        if (!CanPlace(index, stack))
            return false;
        // 해당 슬롯에 현재 아이템을 넣을 수 있는지 검사
        // 

        switch (index)
        {
            case 0:
                head = stack;
                break;

            case 1:
                chest = stack;
                break;

            case 2:
                legs = stack;
                break;

            case 3:
                feet = stack;
                break;

            default:
                return false;
        }
        // 번호에 맞는 실제 장비 필드에 저장
        Changed?.Invoke();
        // 장비 상태 변경 이벤트 발생, 나중 캐릭터한테 직접 입혀주거나 스탯 재계산할때 사용
        return true;
        // 장착 성공
        
        
    }

    public bool CanPlace(int index, ItemStack stack)
    {
        if (index < 0 || index >= SlotCount)
            return false;

        if (stack.IsEmpty)
            return true;
        // 비어있는 아이템 허용
        // 장착 해제 

        if (stack.count != 1)
            return false;
        // 장비 아이템은 1개만 장착 가능하도록

        if (ItemDb.Kind(stack.id) != ItemKind.Equipment)
            return false;
        // 아이템 종류가 장착인 경우에만 장착 가능

        CsvEquipmentSlot requiredSlot = IndexToEquipmentSlot(index);
        // 번호에 맞는 장비 부위 가져오기
        CsvEquipmentSlot itemSlot = ItemDb.EquipmentSlot(stack.id);
        // 현재 아이템이 csv에서 어떤 부위인지 가져옴

        return itemSlot == requiredSlot;
        // 장비 우위와 슬롯 부위가 일치해야 장착
    }

    public int TotalDefense()
    {
        int total = 0;

        total += ItemDb.Defense(head.id);
        total += ItemDb.Defense(chest.id);
        total += ItemDb.Defense(legs.id);
        total += ItemDb.Defense(feet.id);

        return total;

        // 현재 장착된 모든 장비의 방어력 합계 반환하기
        // 몬스터 한테 피격될 때 데미지 감소를 위해
    }

    public CsvEquipmentSlot IndexToEquipmentSlot(int index)
    {
        return index switch
        {
            0 => CsvEquipmentSlot.Head,
            1 => CsvEquipmentSlot.Chest,
            2 => CsvEquipmentSlot.Legs,
            3 => CsvEquipmentSlot.Feet,
            _ => CsvEquipmentSlot.None
        };
        // 장비 슬롯 indxe를 CsvEquipmentSlot으로 변환
        // Inventory UI에서는 슬롯을 index로 다루고
        // csv 데이터에서는 장비 부위를 head, chest... 로 다루기에 변환이 필요
    }
}

 

장비 슬롯에 아이템을 넣는 경우 아이템의 종류와 장비 부위를 검사하여 장착 시켜준다.

또한 현재 csv 데이터에는 장비 부위가 string 문자열로 head, chest로 표기가 되어져있기에 변환작업도 같이 해준다,

 

다음 InventoryUIContoller에서 장비 슬롯 UI를 직접 관리하기 위해 관련 함수들을 추가

[SerializeField] private EquipmentInventory equipmentInv;
[SerializeField] private Transform equipmentSlotsParent;
// 참조 추가


private readonly List<UIItemSlotView> equipmentViews = new List<UIItemSlotView>();
// 장비 슬롯 view 리스트

// 장비 슬롯 캐싱
        if (equipmentSlotsParent != null)
        // 장비 슬롯 UI 찾기
        {
            equipmentViews.Clear();

            UIItemSlotView[] equipmentSlotArray =
                equipmentSlotsParent.GetComponentsInChildren<UIItemSlotView>(true);
                // 비활성화된 슬롯 포함해서 찾기

            equipmentViews.AddRange(equipmentSlotArray);

            for (int i = 0; i < equipmentViews.Count; i++)
                equipmentViews[i].Bind(this, SlotGroup.Equipment, i);
        }
        
  // 장비 슬롯에 아이템을 넣는 경우 제한
    private bool CanPlaceIntoSlot(SlotGroup group, int index, ItemStack stack)
    {
        if (stack.IsEmpty)
            return true;

        if (group == SlotGroup.Equipment)
        {
            if (equipmentInv == null)
                return false;

            return equipmentInv.CanPlace(index, stack);
        }

        return true;
    }
        
 // 장비 슬롯 UI 갱신       
    private void RefreshEquipment()
    {
        if (equipmentInv == null)
            return;

        for (int i = 0; i < equipmentViews.Count; i++)
        {
            ItemStack stack = equipmentInv.GetSlot(i);
            equipmentViews[i].Set(stack.id, stack.count, Icon(stack.id));
        }
    }    
    
    // 슬롯에 있는 아이템을 스왑하는 경우 기존 아이템이 되돌아갈 슬롯도 검사
    // 장비슬롯은 머리, 가슴, 다리.. .타입에 따라 다르기 때문에
    // 잘못 넣었을 경우 기존 아이템이 다시 돌아가기 위해 필요
        private void TryMoveOrSwap(SlotGroup fromGroup, int fromIndex, SlotGroup toGroup, int toIndex)
    {
        ItemStack from = GetStack(fromGroup, fromIndex);
        ItemStack to = GetStack(toGroup, toIndex);

        if (from.IsEmpty)
            return;
        if (!CanPlaceIntoSlot(toGroup, toIndex, from))
            return;

        if (!CanPlaceIntoSlot(fromGroup, fromIndex, to))
            return;

        if (from.id == to.id)
        {
            int max = ItemDb.MaxStack(from.id);
            int canPut = Mathf.Max(0, max - to.count);

            if (canPut <= 0)
                return;

            int move = Mathf.Min(canPut, from.count);

            to.count += move;
            from.count -= move;

            SetStack(toGroup, toIndex, to);
            SetStack(fromGroup, fromIndex, from.count > 0 ? from : new ItemStack(ItemId.None, 0));
            return;
        }

        SetStack(toGroup, toIndex, from);
        SetStack(fromGroup, fromIndex, to);
    }
    
    // 드래그 그롭에서 스택 분할 팝업을 띄울지를 판단하는 경우 장비 슬롯 제외
            bool targetAcceptsSplit =
            toGroup != SlotGroup.Equipment &&
            (
                to.IsEmpty ||
                (to.id == from.id && to.count < ItemDb.MaxStack(to.id))
            );
            
            
    // 슬롯 그룹별로 서로 다른 저장소를 사용하기 위해 장비 슬롯도 추가         
             private ItemStack GetStack(SlotGroup group, int index)
    {
        switch (group)
        {
            case SlotGroup.Inventory:
                return playerInv != null ? playerInv.Get(index) : default;

            case SlotGroup.Hotbar:
                return hotbar != null ? hotbar.GetSlot(index) : default;

            case SlotGroup.CraftingInput:
                return craftingInv != null ? craftingInv.Get(index) : default;

            case SlotGroup.Equipment:
                return equipmentInv != null ? equipmentInv.GetSlot(index) : default;

            default:
                return default;
        }
    }
   
       private void SetStack(SlotGroup group, int index, ItemStack stack)
    {
        switch (group)
        {
            case SlotGroup.Inventory:
                if (playerInv != null)
                    playerInv.Set(index, stack);
                break;

            case SlotGroup.Hotbar:
                if (hotbar != null)
                    hotbar.SetSlot(index, stack.id, stack.count);
                break;

            case SlotGroup.CraftingInput:
                if (craftingInv != null)
                    craftingInv.Set(index, stack);
                break;

            case SlotGroup.Equipment:
                if (equipmentInv != null)
                    equipmentInv.SetSlot(index, stack);
                break;
        }
    }

 

현재 용광로를 통한 광물을 녹이는 것이 구현이 안되어 있기 때문에 

실제 장비 장착이 제대로 이루어지는지에 대한 테스트를 하기 위해서는 따로 디버깅용 플레이어 인벤토리에 아이템을 추가하는 코드를 만들어야 한다.

F7키를 누르면 인벤토리에 자동으로 여러 재료들이 들어가게됌

using System;
using System.Collections.Generic;
using UnityEngine;


public class InventoryDebugItemGiver : MonoBehaviour
{
    [Serializable]
    public class DebugItemEntry
    {
        public string itemKey;


        public int count = 64;
        // 지급할 개수.
    }

    [Header("Refs")]
    [SerializeField] private PlayerInventory playerInventory;
    // 아이템을 넣을 인벤토리

    [SerializeField] private HotbarInventory hotbarInventory;
    // 인벤토리에 공간이 부족할 경우 핫바

    [SerializeField] private InventoryUIController inventoryUIController;
    // 인벤토리 UI 갱신용

    [Header("Options")]
    [SerializeField] private bool fallbackToHotbar = true;
    // PlayerInventory에 공간이 부족하면 HotbarInventory에도 넣을지 여부

    [SerializeField] private KeyCode giveKey = KeyCode.F7;
    // 플레이 중 이 키를 누르면 기본 재료를 지급

    [Header("Default Debug Items")]
    [SerializeField]
    private List<DebugItemEntry> defaultItems = new List<DebugItemEntry>
    {
        new DebugItemEntry { itemKey = "Wood",    count = 64 },
        new DebugItemEntry { itemKey = "Stick",   count = 64 },
        new DebugItemEntry { itemKey = "Stone",   count = 64 },
        new DebugItemEntry { itemKey = "Iron",    count = 64 },
        new DebugItemEntry { itemKey = "Diamond", count = 64 },
        new DebugItemEntry { itemKey = "Coal",    count = 64 },
        // 인벤토리에 넣어줄 재료와 개수
    };

    private void Awake()
    {
        AutoFindRefs();
        // 게임이 시작되면 필요한 참조 자동찾기
    }

    private void Reset()
    {
        AutoFindRefs();
    }

    private void Update()
    {
        if (Input.GetKeyDown(giveKey))
        {
            GiveDefaultItems();
        }
        // 키를 누르고 있는 동안 매 프레임 반복 지급되는 경우를 막아줌
    }

    private void AutoFindRefs()
    {
        if (playerInventory == null)
            playerInventory = FindFirstObjectByType<PlayerInventory>();

        if (hotbarInventory == null)
            hotbarInventory = FindFirstObjectByType<HotbarInventory>();

        if (inventoryUIController == null)
            inventoryUIController = FindFirstObjectByType<InventoryUIController>();
    }

    [ContextMenu("Give Default Debug Items")]
    public void GiveDefaultItems()
    {
        AutoFindRefs();

        if (playerInventory == null)
        {
            Debug.LogWarning("PlayerInventory를 찾지 못했습니다.");
            return;
        }

        if (!ItemDatabase.IsLoaded)
        {
            Debug.LogWarning(
                "ItemDatabase가 아직 로드되지 않았습니다. " +
                "GameDataBootstrap이 먼저 실행되어야 합니다."
            );
            return;
        }

        int totalAdded = 0;
        // 이번 실행에서 지급된 아이템 수

        for (int i = 0; i < defaultItems.Count; i++)
        {
            DebugItemEntry entry = defaultItems[i];

            if (entry == null)
                continue;

            int added = GiveItemByKey(entry.itemKey, entry.count);
            totalAdded += added;
            // 아이템 추가된 개수 누적
        }

        RefreshInventoryUI();
        // ui 갱신

        Debug.Log($"[InventoryDebugItemGiver] Debug items added. TotalAdded={totalAdded}");
    }

    public int GiveItemByKey(string itemKey, int count)
    {
        if (string.IsNullOrWhiteSpace(itemKey))
            return 0;
        // key가 비어있으면 지급 x

        if (count <= 0)
            return 0;
        // 수량이 0이하여도 지급 x

        if (!ItemDatabase.TryGetItemIdByKey(itemKey, out int rawItemId))
        {
            Debug.LogWarning($"[InventoryDebugItemGiver] ItemData.csv에서 key를 찾지 못했습니다. key={itemKey}");
            return 0;
        }

        ItemId itemId = (ItemId)rawItemId;
     

        int added = AddItem(itemId, count);
        // 실제 인벤토리 또는 핫바에 아이템 추가

        if (added < count)
        {
            Debug.LogWarning(
                $"일부만 지급되었습니다. " +
                $"key={itemKey}, requested={count}, added={added}, remain={count - added}"
            );
        }
        else
        {
            Debug.Log($"[InventoryDebugItemGiver] Added {itemKey} x{added}");
        }

        return added;
    }

    private int AddItem(ItemId itemId, int count)
    {
        if (itemId == ItemId.None || count <= 0)
            return 0;

        int remaining = count;
        // 아직 넣지 못한 아이템 수량
        int maxStack = Mathf.Max(1, ItemDb.MaxStack(itemId));
        // 현재 아이템의 최대 스택

        remaining = AddToPlayerInventory(itemId, remaining, maxStack);
        // 일반 인벤토리에 먼저 추가

        if (remaining > 0 && fallbackToHotbar && hotbarInventory != null)
        {
            remaining = AddToHotbar(itemId, remaining, maxStack);
        }
        // 인벤토리와 핫바에 다 넣지 못했을 경우

        return count - remaining;
        // 남은 개수 반환해줌
    }

    private int AddToPlayerInventory(ItemId itemId, int count, int maxStack)
    {
        int remaining = count;
        // 아직 넣지 못한 수량

        for (int i = 0; i < playerInventory.Count; i++)
        {
            ItemStack stack = playerInventory.Get(i);

            if (stack.IsEmpty)
                continue;

            if (stack.id != itemId)
                continue;

            if (stack.count >= maxStack)
                continue;

            int add = Mathf.Min(maxStack - stack.count, remaining);
            stack.count += add;
            remaining -= add;

            playerInventory.Set(i, stack);

            if (remaining <= 0)
                return 0;
            // 모든 수량을 기존 스택에 합쳤다면 즉시 종료
        }

        // 남은 아이템은 빈 슬롯에 넣는다
        for (int i = 0; i < playerInventory.Count; i++)
        {
            ItemStack stack = playerInventory.Get(i);
            // 현재 일반 인벤토리 슬롯 데이터를 가져옴

            if (!stack.IsEmpty)
                continue;
            // 비어 있지 않은 슬롯은 건너뜀

            int add = Mathf.Min(maxStack, remaining);
            // 빈 슬롯 하나에 넣을 수 있는 최대 수량과 남은 수량 중 더 작은 값 사용
            playerInventory.Set(i, new ItemStack(itemId, add));
            // 빈 슬롯에 새로운 ItemStack을 생성해 저장
            remaining -= add;
            // 아직 지급하지 못한 수량 감소

            if (remaining <= 0)
                return 0;
            // 모든 수량을 넣었다며 즉시 종료
        }

        return remaining;
        // 끝까지 넣지 못한 수량 반환
    }

    private int AddToHotbar(ItemId itemId, int count, int maxStack)
    {
        // 핫바에 넣기
        int remaining = count;

        // 먼저 같은 아이템 스택에 합친다
        for (int i = 0; i < hotbarInventory.SlotCount; i++)
        {
            ItemStack stack = hotbarInventory.GetSlot(i);

            if (stack.IsEmpty)
                continue;

            if (stack.id != itemId)
                continue;

            if (stack.count >= maxStack)
                continue;

            int add = Mathf.Min(maxStack - stack.count, remaining);
            stack.count += add;
            remaining -= add;

            hotbarInventory.SetSlot(i, stack.id, stack.count);

            if (remaining <= 0)
                return 0;
        }

        // 남은 아이템은 빈 슬롯에 넣는다
        for (int i = 0; i < hotbarInventory.SlotCount; i++)
        {
            ItemStack stack = hotbarInventory.GetSlot(i);

            if (!stack.IsEmpty)
                continue;

            int add = Mathf.Min(maxStack, remaining);
            hotbarInventory.SetSlot(i, itemId, add);
            remaining -= add;
            // 비어 있는 핫바에 넣어줌

            if (remaining <= 0)
                return 0;
            // 모든 아이템 지급 완료
        }

        return remaining;
    }

    private void RefreshInventoryUI()
    {
        // 아이템 넣어줬으니 UI 갱신
        if (inventoryUIController == null)
            inventoryUIController = FindFirstObjectByType<InventoryUIController>();

        if (inventoryUIController != null)
        {
            inventoryUIController.SendMessage(
                "RefreshAll",
                SendMessageOptions.DontRequireReceiver
            );
        }
    }
}

 

게임을 실행하고 인게임으로 들어가 F7을 통해 인벤토리에 아이템을 지급받고 제작대를 만든다음

3X3 레시피를 통해 장비아이템을 제작, 제작된 아이템을 ItemType에 알맞는 장비 슬롯에 넣어주는 부분 까지 완성했다

 

실행 영상

https://youtu.be/Ln-IsfRnpiQ

 

다음 글에서는 용광로를 통해 광석을 석탄을 이용해 녹이는 것과 캐릭터 프리뷰를 마무리 할것이다

그리고 더 나아간다면 실제 애셋을 좀 이용해서 캐릭터와 장비아이템을 적용할 예정이다.