내일배움캠프/TIL

유니티 숙련주차 팀프로젝트 - 병합 완료

서보훈 2024. 11. 5. 20:24

제출까지 2일남은 시점, 제작한 내용을 모두 병합하였습니다.

 

아이템 관련 내용을 당담하였기 때문에, 개인시연으로는 아이템 관련 부분만 진행하였습니다.

 

Tab 키를 누르면 인벤토리가 열리고, 인벤토리에서 장비, 제작창을 열 수 있는 버튼을 만들어주었습니다.

장비 탭에서는 현재 플레이어의 상태를 볼 수 있으며, 플레이어의 방어력, 추가 공격력등 장비의 스테이터스를 볼 수 있습니다.

 

단, 현재 장비의 스텟 변화가 실제 캐릭터에 적용된 상태는 아닙니다.

장비, 소모품을 사용한 버프로 인한 스테이터스 변화는 별도의 스크립트가 저장하고 있으며, 이 내용을 캐릭터에 연결해주는 작업이 남았습니다.

더보기
public class CharacterBuffManager : MonoBehaviour
{
    private Equipment equips;
    //현재 적용중인 버프를 관리하는 딕셔너리
    private Dictionary<int, Coroutine> nowBuffs = new Dictionary<int, Coroutine>();

    public UnityAction onStatusUpdate;

    private void Start()
    {
        equips = PlayerManager.Instance.Player.inventory.equip;
        equips.onEquipChange += EquipStatusCheck;
    }

    //공격력
    private float equipAttack = 0;
    private float buffAttack = 0;
    public float AttackUp =>  equipAttack + buffAttack;

    //방어력
    private float equipDef = 0;
    private float buffDef = 0;
    public float DefUp => equipDef + buffDef;

    //최대체력
    private float equipHealth = 0;
    private float buffHealth = 0;
    public float HealthUp => equipHealth + buffHealth;

    //최대 스테미나
    private float equipSta = 0;
    private float buffSta = 0;
    public float StaminaUp => equipSta + buffSta;

    //속도 증가
    private float equipSpeed = 0;
    private float buffSpeed = 0;
    public float SpeedUp => equipSpeed + buffSpeed;

    //저온 저항
    private float equipLowTem = 0;
    private float buffLowTem = 0;
    public float LowTemResist => equipLowTem + buffLowTem;

    //고온 저항
    private float equipHighTem = 0;
    private float buffHighTem = 0;
    public float HighTemResist => equipHighTem + buffHighTem;

    public void EquipStatusCheck()
    {
        float attack = 0;
        float def = 0;
        float health = 0;
        float sta = 0;
        float speed = 0;
        float lowTem = 0;
        float highTem = 0;

        for(int i = 0; i < equips.NowEquipItems.Length; i++)
        {
            if (equips.NowEquipItems[i].slotItem == null)
            {
                continue;
            }

            ItemData item = equips.NowEquipItems[i].slotItem;
            for (int j = 0; j < item.equipDatas.Length; j++)
            {
                switch (item.equipDatas[j].statusType)
                {
                    case StatusType.Attack:
                        if (item.equipType != EquipType.Weapon)
                        {
                            attack += item.equipDatas[j].value;
                        }
                        break;
                    case StatusType.Defence:
                        def += item.equipDatas[j].value;
                        break;
                    case StatusType.Health:
                        health += item.equipDatas[j].value;
                        break;
                    case StatusType.Stamina:
                        sta += item.equipDatas[j].value;
                        break;
                    case StatusType.Speed:
                        speed += item.equipDatas[j].value;
                        break;
                    case StatusType.TemResist_Low:
                        lowTem += item.equipDatas[j].value;
                        break;
                    case StatusType.TemResist_High:
                        highTem += item.equipDatas[j].value;
                        break;
                }
            }
        }

        equipAttack = attack;
        equipDef = def;
        equipHealth = health;
        equipSta = sta;
        equipSpeed = speed;
        equipLowTem = lowTem;
        equipHighTem = highTem;
    }

    //버프 추가
    public void AddBuffs(BuffData[] buffs)
    {
        for (int i = 0; i < buffs.Length; i++)
        {
            //같은 ID의 버프가 있으면 해당 버프 삭제
            if (nowBuffs.ContainsKey(buffs[i].buffId))
            {
                StopCoroutine(nowBuffs[buffs[i].buffId]);
                SetBuffs(buffs[i], false);
            }

            //버프 추가
            SetBuffs(buffs[i], true);
            Coroutine buffCoroutine = StartCoroutine(EndBuff(buffs[i].time, buffs[i]));
            nowBuffs.Add(buffs[i].buffId, buffCoroutine);
        }

        onStatusUpdate?.Invoke();
    }

    //버프를 실제로 적용하는 메서드
    private void SetBuffs(BuffData buff, bool isAdd)
    {
        float value = isAdd ? buff.value : buff.value * -1;

        switch (buff.buffType)
        {
            case BuffType.StaminaUp:
                buffSta += value;
                break;
            case BuffType.SpeedUp:
                buffSpeed += value; 
                break;
            case BuffType.AttackUp:
                buffAttack += value;
                break;
            case BuffType.DefenceUp:
                buffDef += value;
                break;
        }
        
    }

    //버프 지속시간 코루틴
    private IEnumerator EndBuff(float time, BuffData data)
    {
        yield return new WaitForSeconds(time);
        SetBuffs(data, false);
        nowBuffs.Remove(data.buffId);
        onStatusUpdate?.Invoke();
    }
}

 

제작기능의 경우 인벤토리에 가지고 있는 아이템을 확인하고, 인벤토리에 제작에 필요한 아이템이 모두 존재할경우 제작목록에 아이콘이 나타나도록 만들었습니다.

인벤토리는 현재 가지고있는 아이템의 아이디를 Key, 가지고 있는 갯수를 Value로 하는 딕셔너리를 가지고있습니다.

인벤토리에서 이 딕셔너리를 불러와서 아이템을 얼마나 가지고 있는지 판단하고, 제작 가능여부를 결정하도록 만들었습니다.

더보기
public class CraftManager : MonoBehaviour
{
    public ItemDatabaseSO itemDatabase;

    private List<ItemData> craftItemData = new List<ItemData>();
    private Dictionary<int, GameObject> craftUIs = new Dictionary<int, GameObject>();
    private Dictionary<int, int> playerItems = new Dictionary<int, int>();

    private Inventory playerInven;

    public GameObject slotUIPrefab;
    public Transform craftUISlotParent;

    private CraftSlotUI curSelectedSlot;

    [Header("Craft UI")]
    public Transform infoUIParent;
    private CraftNeedItemInfoUI[] infoUis;

    public TextMeshProUGUI craftItemName;
    public GameObject craftButton;

    //제작가능 아이템들 등록
    private void Awake()
    {
        foreach(ItemData item in itemDatabase.itemObjects)
        {
            playerItems.Add(item.itemId, 0);

            if(item.isCraftable)
            {
                craftItemData.Add(item);

                GameObject slotUI = Instantiate(slotUIPrefab, craftUISlotParent);
                slotUI.GetComponent<CraftSlotUI>().SetupSlotUI(item, this);
                slotUI.SetActive(false);

                craftUIs.Add(item.itemId, slotUI);
            }
        }

        infoUis = new CraftNeedItemInfoUI[infoUIParent.childCount];

        for(int i = 0; i < infoUIParent.childCount; i++)
        {
            infoUis[i] = infoUIParent.GetChild(i).GetComponent<CraftNeedItemInfoUI>();
            infoUis[i].ClearInfo();
        }

        craftItemName.text = string.Empty;
        craftButton.SetActive(false);
    }

    private void Start()
    {
        playerInven = PlayerManager.Instance.Player.inventory;
        playerInven.onToggleSubUI += CloseUI;

        gameObject.SetActive(false);
    }

    //인벤토리의 아이템이 변할때, 제작가능 목록 업데이트
    private void OnEnable()
    {
        if (playerInven != null)
        {
            playerInven.onInventoryChanged += UpdateCraftMenu;
        }
    }

    private void OnDisable()
    {
        if (playerInven != null)
        {
            playerInven.onInventoryChanged -= UpdateCraftMenu;
        }
    }

    public void UpdateCraftMenu()
    {
        //제작 메뉴를 열 때, 플레이어 인벤토리의 아이템 정보를 확인
        for (int i = 0; i < playerInven.InvenItems.Count; i++)
        {
            ItemSlot itemSlot = playerInven.InvenItems[i];
            if (itemSlot.slotItem != null)
            {
                playerItems = playerInven.OwnItemsCount;
            }
        }

        for(int i = 0; i < craftItemData.Count; i++)
        {
            if(CheckCraftable(craftItemData[i]))
            {
                //TODO : UI 조작 (제작 가능)
                craftUIs[craftItemData[i].itemId].SetActive(true);
            }
            else
            {
                //TODO : UI 조작 (제작 불가능)
                craftUIs[craftItemData[i].itemId].SetActive(false);
            }
        }
    }

    //아이템의 제작가능 여부 확인
    private bool CheckCraftable(ItemData craftItem)
    {
        for(int i = 0; i < craftItem.craftDatas.Length; i++)
        {
            if (playerItems[craftItem.craftDatas[i].material.itemId] < craftItem.craftDatas[i].needCount)
                return false;
        }

        return true;
    }

    //제작창 선택
    public void SelectSlot(int itemId)
    {
        if(itemId < 0)
        {
            curSelectedSlot.IsSelected = false;
            curSelectedSlot.ControllSelectedImage();
            curSelectedSlot = null;
            SetCraftInfoUI();
            craftButton.SetActive(false);
            return;
        }

        if(curSelectedSlot != null)
        {
            curSelectedSlot.IsSelected = false;
            curSelectedSlot.ControllSelectedImage();
        }

        curSelectedSlot = craftUIs[itemId].GetComponent<CraftSlotUI>();
        curSelectedSlot.IsSelected = true;
        curSelectedSlot.ControllSelectedImage();
        craftButton.SetActive(true);

        SetCraftInfoUI();
    }

    //제작 정보 표기
    private void SetCraftInfoUI()
    {
        if (curSelectedSlot == null)
        {
            foreach(var infoUi in infoUis)
            {
                infoUi.ClearInfo();
            }

            craftItemName.text = string.Empty;
            return;
        }

        ItemData itemData = itemDatabase.GetItemdataById(curSelectedSlot.itemId);

        craftItemName.text = itemData.itemName;
        for (int i = 0; i < infoUis.Length; i++)
        {
            if (i < itemData.craftDatas.Length)
            {
                infoUis[i].SetInfo(itemData.craftDatas[i]);
            }
            else
            {
                infoUis[i].ClearInfo();
            }
        }
    }

    //제작버튼을 누르면, 재료를 소모하고 제작 아이템을 인벤토리에 추가
    public void OnCraftButtonClick()
    {
        ItemData itemData = itemDatabase.GetItemdataById(curSelectedSlot.itemId);

        for (int i = 0; i < itemData.craftDatas.Length; i++)
        {
            for(int j = 0; j < itemData.craftDatas[i].needCount; j++)
            {
                playerInven.RemoveItem(itemData.craftDatas[i].material);
            }
        }

        playerInven.AddItem(itemData);

        if(!CheckCraftable(itemData))
        {
            SelectSlot(-1);
        }
    }

    public void ToggleUI()
    {
        gameObject.SetActive(!gameObject.activeInHierarchy);
        if(gameObject.activeInHierarchy == true)
        {
            if(playerInven.invenUi.activeInHierarchy == false)
            {
                playerInven.ToggleUI();
            }

            UpdateCraftMenu();
            playerInven.EnableCraftUI();
        }
    }

    //장비창이 열릴때 닫기
    public void CloseUI()
    {
        if(playerInven.IsCraftUiActive == false)
        {
            gameObject.SetActive(false);
        }
    }
}

 

아이템의 경우 개별 아이템들은 스크립터블 오브젝트를 통해 아이템의 정보를 가지고 있으며, 이들을 관리하는 ItemDatabaseSO 스크립터블 오브젝트가 가진 배열에 아이템들이 저장되었습니다.

아이템을 배열에 등록할때 아이템의 아이디가 정해지며, 배열에 등록하면서 인벤토리에서 아이템 정보를 발생시키는 전략패턴과 아이템이 사용 전략패턴을 아이템 스크립터블 오브젝트에 등록하게 됩니다.

더보기
/// <summary>
/// 아이템 전체 데이터를 관리하고, 고유 ID를 부여하는 스크립터블 오브젝트
/// </summary>
[CreateAssetMenu(fileName ="ItemDatabase", menuName ="Item/ItemDatabase")]
public class ItemDatabaseSO : ScriptableObject
{
    public ItemData[] itemObjects;

    //데이터베이스에 아이템을 등록할 때 아이템의 아이디를 정해줌
    public void OnValidate()
    {
        for(int i = 0; i < itemObjects.Length; i++)
        {
            itemObjects[i].itemId = i;
            itemObjects[i].infoStrategy = SetInfoStrategy(itemObjects[i]);
            itemObjects[i].useItemStrategy = SetUseStrategy(itemObjects[i]);
        }
    }

    public ItemData GetItemdataById(int itemId)
    {
        return itemObjects[itemId];
    }

    private IItemInfoStrategy SetInfoStrategy(ItemData data)
    {
        IItemInfoStrategy infoStrategy;

        switch(data.itemType)
        {
            case ItemType.Equipable:
                infoStrategy = new EquipInfo(data);

                break;
            case ItemType.Consumable:
                infoStrategy = new ConsumableInfo(data);
                break;
            default:
                infoStrategy = new NoneInfo();
                break;
        }

        return infoStrategy;
    }

    private IUseItemStrategy SetUseStrategy(ItemData data)
    {
        IUseItemStrategy useStrategy;

        switch(data.itemType)
        {
            case ItemType.Equipable:
                useStrategy = new Equip(data);
                break;
            case ItemType.Consumable:
                useStrategy = new Consume(data);
                break;
            default:
                useStrategy = new NotUseable();
                break;
        }

        return useStrategy;
    }
}

 

개인적으로 아쉬운건, 현재 Inventory 스크립트가 UI 와 실제 인벤토리 정보를 동시에 관리하고 있습니다.

제작의 경우, UI에서 제작이 진행되기 때문에 CraftManager에서 UI관리와 실제 제작을 동시에 처리해도 큰 문제가 없을것으로 예상되지만, Inventory의 경우 씬이 넘어가는경우 스크립트의 UI 관련 정보를 모두 잃어버리게 됩니다.

 

이 부분을 지금 나누기에는 실질적으로 무리가 있다고 생각하고, 아직 무기 장착시 무기 프리팹 생성, 무기 프리팹의 모션 생성과 자원, 동물과 무기 프리팹의 상호작용 부분에 대한 확인이 이루어지지 않았기 때문에, 이 부분에 더 집중하고자 합니다.

 

또한, 개인적으로 창고 시스템을 만들어보고자 했지만.. 이것도 시간적 여유가 부족할것 같습니다.

슬롯을 사용하지 않고 아이템 정보만 저장하는 방식으로 시간을 줄여보고자 하였으나 추가적인 UI 제작을 하기에는 시간적으로 문제가 있다는 판단을 하였습니다.