프로젝트/건즈앤 레이첼스

[유니티 프로젝트] 인벤토리 스탯창 구현 5 - 완료

Bueong_E 2023. 4. 13. 18:42
반응형
SMALL

 

생각보다 규모가 커진 인벤토리와 스탯창 제작 작업이 끝났다.

우선 구현 기능은 아래와 같다.

  • 유저 능력치창과 인벤토리의 유연한 연결 ( 인벤토리 아이템의 영향을 받는 스탯창 )
  • 장비하고 있는 무기에 따른 UI 변경
  • 아이템의 생성을 담당하는  factory 클래스 들을 이용한 추상 factory 패턴
  • Equipment를 상속받은 각 장비 클래스 (고유 수치 를 가지며 업캐스팅을 통해 유연한 스크립트 서치)
  • Info매니저와 atlas 매니저를 싱클톤 패턴으로 이용, 유저의 데이터를 영구저장(영구저장이 안되는 info 및 저장이 되는 기능을 분리하여 메서드 작성)
  • UX 를 고려하여 제작한 무기 스킬 상세 팝업과 인벤토리 스크롤 그리드 구현 및 인벤토리 상세 팝업 로직 구현
    • 유저가 장비 터치시 팝업 및 셀렉션 하일라이트
    • 유저가 스크롤 그리드를 내릴시 팝업 Set OFF
    • 팝업 그리드는 유저의 장비 소지 가능수에 따라 Set On Off
    • 버리기 버튼 혹은 x버튼을 선택시 셀렉션 하일라이트 제어 및 팝업 제어
  • 유저가 인벤토리를 정렬할 필요 없이 자동으로 정렬을 해주는 인벤토리 기능

 

* 이전에는 정렬부분에서 문제가 있었는데 가만히 생각해보니 한 프레임 안에서 해결해 주려고 해서 그런것 같다는 생각이 들어 코루틴으로 제어하니 금방 해결되었다.

 

코드를 작성하다보니 메서드를 공유할일이 많이 생기고 (특히 매니저 싱글톤 클래스들) 그럴때 어떤 기능인지 한번에 알수 있도록 주석을 잘 달아주는 것이 중요하다는 걸 깨닳아 좀더 주석에도 공을 많이 들게되었다.

아직 협업을 하면서 또 어떤 문제점이 발생하게 될지는 모르지만 이런식으로 모두가 사용할 메서드 혹은 스크립트에 주석을 잘 달아두는 습관을 들여둔다면 분명히 나중에도 큰 도움일 될듯하다.

 

 

변경된 주요 코드들은 아래와 같다.

UIInventory 스크립트

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using System.IO;
using UnityEngine.UI;

public class UIInventory : MonoBehaviour
{
    [SerializeField]
    private ScrollRect contentGridRect;

    [SerializeField]
    private Transform content;  

    [SerializeField]
    private GameObject equipmentCell;

    //혹시 나중에 접근할 사람이 있을까 하여 뚫어 놨습니다.
    public List<GameObject> inventoryList;
       
    private EquipmentFactory equipmentFactory;

    [SerializeField]
    private UIPlayerStats uiPlayerStats;

    [SerializeField]
    private UIInventoryDetailPopup uIInventoryDetailPopup;

    public System.Action<string,int> onSelected;
    public System.Action<int> onDiscardEquipment;
    public System.Action<int> onSetOffSelect;
    public System.Action onSetOffpopup;    

    public void Init()
    {
        //초기 설정
        this.onSelected = (EuipmentName,hesh) =>
        {
            this.onSetOffSelect(hesh);
            this.uIInventoryDetailPopup.gameObject.SetActive(true);
            this.uIInventoryDetailPopup.RefreshPopup(EuipmentName, hesh);          
        };

        this.onDiscardEquipment = (hesh) =>
        {
            this.DiscardEquipment(hesh);
        };

        this.onSetOffSelect = (hesh) =>
        {
            for (int i = 0; i < this.content.childCount; i++)
            {
                if (this.content.GetChild(i).GetHashCode() != hesh && this.content.GetChild(i).transform.childCount == 2)
                {
                    this.content.GetChild(i).transform.GetChild(1).GetChild(0).gameObject.SetActive(false);
                }
            }
        };

        this.onSetOffpopup = () =>
        {
            this.uIInventoryDetailPopup.gameObject.SetActive(false);
        };

        // 테스트용, 나중에는 게임 메인에서 구현하면 됩니다.
        string path = string.Format("{0}/Inventory_Info.json", Application.persistentDataPath);
        if (File.Exists(path))
        {
            InfoManager.instance.LoadInventoryInfo();
        }
        else
        {
            InfoManager.instance.SaveInventoryInfo();
        }

        //InfoManager.instance.SaveInventoryInfo();


        this.inventoryList = new List<GameObject>();
        this.equipmentFactory = this.GetComponent<EquipmentFactory>();
        var count = InfoManager.instance.inventoryInfo.InventoryCount;
        this.InitInventoryCells(count);
        this.CheckContentGrid(count);

        this.uIInventoryDetailPopup.Init();

    }

    public void InitInventoryCells(int count)
    {
        for (int i = 0; i < count; i++)
        {
            var equipmentCell = GameObject.Instantiate(this.equipmentCell, this.content);
            this.inventoryList.Add(equipmentCell);
        }
        if(InfoManager.instance.inventoryInfo.isEquipment == true)
        {
            for (int i =0; i < InfoManager.instance.inventoryInfo.currentEquipments.Length; i++)
            {
                this.AddEquipment(InfoManager.instance.inventoryInfo.currentEquipments[i]); 
            }
        }
    }

    private void CheckContentGrid(int count)
    {
        if (count < 16)
        {
            this.contentGridRect.enabled = false;
        }
        else
        {
            this.contentGridRect.enabled = true;
        }
    }

    public void AddInventoryCells() 
    { 
        var count = InfoManager.instance.IncreaseInventoryCount();
        var equipmentCell = GameObject.Instantiate(this.equipmentCell, this.content);
        this.inventoryList.Add(equipmentCell);
        this.CheckContentGrid(count);
    }


    /// <summary>
    /// 현재 인벤토리의 아이템을 버립니다.
    /// </summary>
    /// <param name="cellHash">버릴 cell의 heshcode</param>
    /// <returns>버려진 아이템의 이름을 반환합니다.</returns>
    public string DiscardEquipment(int cellHash)
    {
        var count = InfoManager.instance.inventoryInfo.InventoryCount;
        string discardEuipmentName = null;
        for (int i = 0; i < count; i++)
        {
            if (this.content.GetChild(i).GetHashCode() == cellHash)
            {
                var equipment = this.content.GetChild(i).transform.GetChild(1).GetChild(1);
                var euipmentComp = equipment.GetComponent<Equipment>();
                euipmentComp.UnSetEquipmentStat();
                this.uiPlayerStats.UpdatePlayerStatUI();
                discardEuipmentName = equipment.gameObject.name;
                Destroy(this.content.GetChild(i).transform.GetChild(1).gameObject);
                break;
            }
        }
        this.StartCoroutine(this.SorthInventoryCells(count));
        return discardEuipmentName;
    }

    private IEnumerator SorthInventoryCells(int count)
    {
        yield return null;
        for (int i = 0; i < count; i++)
        {
            if (this.content.GetChild(i).childCount == 1)
            {
                this.content.GetChild(i).SetAsLastSibling();
            }
        }
    }
    public void AddEquipment(string name)
    {
        var count = InfoManager.instance.inventoryInfo.InventoryCount;
        GameObject cell = null;
        for (int i = 0; i < count; i++)
        {
            if (this.content.GetChild(i).childCount == 1)
            {
                cell = this.content.GetChild(i).gameObject;
                break;
            }
        }

        if (cell != null)
        {
            var go = this.equipmentFactory.MakeEquipment(name, cell);
            this.uiPlayerStats.UpdatePlayerStatUI();
        }
        else
        {
            Debug.Log("장비를 더 이상 지닐수 없습니다.");
        }
    }

    /// <summary>
    /// 현재 인벤토리에 있는 모든 장비 리스트를 생성합니다.
    /// </summary>
    /// <returns>현재 유저가 들고 있는 장비 리스트</returns>
    public List<string> MakeCurrentInventoryList() 
    { 
        List<string> equipmentList = new List<string>();    

        for (int i = 0;i < this.content.transform.childCount; i++) 
        {
            if (this.content.GetChild(i).transform.childCount == 2)
            {
                var EuquipmentName = this.content.GetChild(i).transform.GetChild(1).GetChild(1).gameObject.name;
                equipmentList.Add(EuquipmentName);
            }           
        }
        return equipmentList;   
    }







    // 테스트용
    public void Update()
    {
        if (Input.GetKeyDown(KeyCode.Q))
        {
            this.AddEquipment("Iron_Sword");
        }
        else if (Input.GetKeyDown(KeyCode.W))
        {
            this.AddEquipment("Diamond_Wand");
        }
        else if (Input.GetKeyDown(KeyCode.E))
        {
            this.AddEquipment("Gold_Arrow");
        }
        else if (Input.GetKeyDown(KeyCode.R))
        {
            this.AddEquipment("Wood_Axe");
        }
        else if (Input.GetKeyDown(KeyCode.T))
        {
            for(int i = 0; i < 1000; i++) {
                var index = Random.Range(0, this.content.childCount);                
                if (this.content.GetChild(i).transform.childCount == 2)
                {
                    var hesh = this.content.GetChild(i).GetHashCode();
                    this.DiscardEquipment(hesh);
                    break;
                }
                else continue;
            }                      
        }
        else if (Input.GetKeyDown(KeyCode.Y))
        {
            this.AddInventoryCells();
        }
        else if (Input.GetKeyDown(KeyCode.A))
        {
            InfoManager.instance.AddEquipmentInfoFromTown(this.MakeCurrentInventoryList());
        }
    }
}

InfoManager 싱글톤 스크립트

using Newtonsoft.Json;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using System.IO;
using Unity.VisualScripting;

public class InfoManager
{
    public enum Permanent
    {
        No,Yes
    }

    public static readonly InfoManager instance = new InfoManager();
    private InfoManager() { }

    public StatInfo statInfo = new StatInfo();
    public InventoryInfo inventoryInfo = new InventoryInfo();
    public WeaponCharactoristicInfo charactoristicInfo = new WeaponCharactoristicInfo();   

    /// <summary>
    /// 능력치 초기화 메서드 + 자동저장
    /// </summary>
    /// <param name="powerStat">최종 수정 공격력</param>
    /// <param name="fireRateStat">최종 수정 공격속도</param>
    /// <param name="criticalHitAmount">최종 수정 치명타 피해</param>
    /// <param name="criticalHitChance">최종 수정 치명타 확률</param>
    public void InitStats(int powerStat,int fireRateStat,
        int criticalHitAmount,int criticalHitChance)
    {
        this.statInfo.powerStat = powerStat;
        this.statInfo.fireRateStat = fireRateStat;
        this.statInfo.criticalHitAmount = criticalHitAmount;
        this.statInfo.criticalHitChance = criticalHitChance;
        this.SaveStatInfo();       
    }

    /// <summary>
    /// 총기특성 초기화 + 총기 숙련도 초기화 ( 던전씬 엔드시 반드시 호출 )
    /// </summary>
    public void InitCharactorlisticsAndWeaponProficiency()
    {
        this.charactoristicInfo.movespeedCharacteristic = 1;
        this.charactoristicInfo.knockBackCharacteristic = 1;
        this.charactoristicInfo.bulletAmountCharacteristic = 1;
        this.charactoristicInfo.dashRecoverCharacteristic = 1;
        this.charactoristicInfo.penetrateCharacteristic = 1;
        this.charactoristicInfo.weaponProficiencyLevel = 0;
        this.charactoristicInfo.weaponProficiencyEXP = 0;
    }


    /// <summary>
    /// 인벤토리 정보 초기화 + 자동 저장( 던전씬 엔드시 반드시 호출 )
    /// </summary>
    public void InitInventoryInfo()
    {
        this.inventoryInfo.isEquipment = false;
        this.inventoryInfo.currentEquipments = null;    
        this.SaveInventoryInfo();   
    }

    /// <summary>
    /// 캐릭터의 공격력을 증가시킵니다. **영구 증가인 경우 Enum 타입 인자 필요**
    /// <para>첫 번째 정수: <paramref name="amount"/> 증가시키고 싶은 정도.</para>
    /// <para>두 번째 정수: <paramref name="type"/> 영구 증가 인지 여부 (기본값 No).</para>
    /// </summary>
    /// <param name="amount">공격력 증가 정도</param>
    /// <param name="type">영구적 증가 여부</param>
    public void IncreasePowerStat(int amount, Permanent type = Permanent.No)
    {
        if(type == Permanent.Yes) 
        {
            this.statInfo.powerStat += amount;
            this.SaveStatInfo();
        }
        else
        {
            this.statInfo.powerStat += amount;            
        }      
    }


    /// <summary>
    /// 캐릭터의 공격속도를 증가시킵니다. **영구 증가인 경우 Enum 타입 인자 필요**
    /// <para>첫 번째 정수: <paramref name="amount"/> 증가시키고 싶은 정도.</para>
    /// <para>두 번째 정수: <paramref name="type"/> 영구 증가 인지 여부 (기본값 No).</para>
    /// </summary>
    /// <param name="amount">공격속도 증가 정도</param>
    /// <param name="type">영구적 증가 여부</param>
    public void IncreaseFireRateStat(int amount = 1, Permanent type = Permanent.No)
    {
        if(type == Permanent.Yes)
        {
            this.statInfo.fireRateStat += amount;
            this.SaveStatInfo();
        }   
        else
        {
            this.statInfo.fireRateStat += amount;   
        }
    }

    /// <summary>
    /// 캐릭터의 치명타 피해량을 증가시킵니다. **영구 증가인 경우 Enum 타입 인자 필요**
    /// <para>첫 번째 정수: <paramref name="amount"/> 증가시키고 싶은 정도.</para>
    /// <para>두 번째 정수: <paramref name="type"/> 영구 증가 인지 여부 (기본값 No).</para>
    /// </summary>
    /// <param name="amount">치명타 피해량 증가 정도</param>
    /// <param name="type">영구적 증가 여부</param>
    public void IncreaseCriticalHitAmountStat(int amount, Permanent type = Permanent.No)
    {
        if(type == Permanent.Yes)
        {
            this.statInfo.criticalHitAmount += amount;
            this.SaveStatInfo();
        }
        else
        {
            this.statInfo.criticalHitAmount += amount;
        }
    }

    /// <summary>
    /// 캐릭터의 치명타 확률을 증가시킵니다. **영구 증가인 경우 Enum 타입 인자 필요**
    /// <para>첫 번째 정수: <paramref name="amount"/> 증가시키고 싶은 정도.</para>
    /// <para>두 번째 정수: <paramref name="type"/> 영구 증가 인지 여부 (기본값 No).</para>
    /// </summary>
    /// <param name="amount">치명타 확률 증가 정도</param>
    /// <param name="type">영구적 증가 여부</param>
    public void IncreaseCriticalHitChanceStat(int amount = 1, Permanent type = Permanent.No)
    {
        if(type == Permanent.Yes)
        {
            this.statInfo.criticalHitChance += amount;
            this.SaveStatInfo();
        }
        else
        {
            this.statInfo.criticalHitChance += amount;
        }
    }

    /// <summary>
    /// 플레이어 StatInfo 불러오기
    /// </summary>
    public void LoadStatInfo()
    {
        string path = string.Format("{0}/stat_info.json", 
            Application.persistentDataPath);
        string json = File.ReadAllText(path);
        this.statInfo = JsonConvert.DeserializeObject<StatInfo>(json);
        Debug.Log("플레이어 능력치 데이터 로드 완료");
    }

    /// <summary>
    /// 플레이어 StatInfo 저장
    /// </summary>
    public void SaveStatInfo()
    {
        string path = string.Format("{0}/stat_info.json", 
            Application.persistentDataPath);
        string json = JsonConvert.SerializeObject(this.statInfo);
        File.WriteAllText(path, json);
        Debug.Log("플레이어 능력치데이터 저장 완료");
    }

    /// <summary>
    /// 인벤토리 갯수 영구증가(1개) +  자동 저장
    /// </summary>
    public int IncreaseInventoryCount()
    {
        this.inventoryInfo.InventoryCount += 1;
        this.SaveInventoryInfo();
        return this.inventoryInfo.InventoryCount;   
    }

    /// <summary>
    /// 아이템 구매시 Inventory Info 에 구매아이템 추가 + 자동 저장
    /// </summary>
    public void AddEquipmentInfoFromTown(List<string> EquipmentList)
    {
        this.inventoryInfo.isEquipment = true;
        this.inventoryInfo.currentEquipments = EquipmentList.ToArray();
        this.SaveInventoryInfo();
    }

    /// <summary>
    /// 플레이어 InventoryInfo 불러오기
    /// </summary>
    public void LoadInventoryInfo()
    {
        string path = string.Format("{0}/Inventory_Info.json", 
            Application.persistentDataPath);
        string json = File.ReadAllText(path);
        this.inventoryInfo = JsonConvert.DeserializeObject<InventoryInfo>(json);
        Debug.Log("인벤토리 데이터 로드 완료");
    }

    /// <summary>
    /// 플레이어 Inventory 저장
    /// </summary>
    public void SaveInventoryInfo()
    {
        string path = string.Format("{0}/Inventory_Info.json", 
            Application.persistentDataPath);
        string json = JsonConvert.SerializeObject(this.inventoryInfo);
        File.WriteAllText(path, json);
        Debug.Log("인벤토리 데이터 저장 완료");
    }

    /// <summary>
    /// 총기특성  : 이동 속도 증가 + 1 
    /// </summary>
    public void IncreaseMovespeedCharacteristic()
    {
        this.charactoristicInfo.movespeedCharacteristic += 1;
    }

    /// <summary>
    /// 총기특성 : 넉백 증가 + 1 
    /// </summary>
    public void IncreaseKnockBackCharacteristic()
    {
        this.charactoristicInfo.knockBackCharacteristic += 1;
    }

    /// <summary>
    /// 총기특성 : 총알 숫자 증가 + 1 
    /// </summary>
    public void IncreaseBulletAmountCharacteristic()
    {
        this.charactoristicInfo.bulletAmountCharacteristic += 1;
    }

    /// <summary>
    /// 총기특성 : 대시 회복 속도 증가 + 1 
    /// </summary>
    public void IncreaseDashRecoverCharacteristic()
    {
        this.charactoristicInfo.dashRecoverCharacteristic += 1;
    }

    /// <summary>
    /// 총기특성 : 관통력 증가 + 1 
    /// </summary>
    public void IncreasePenetrateCharacteristic()
    {
        this.charactoristicInfo.penetrateCharacteristic += 1;
    }

    /// <summary>
    /// 총기 숙련도 증가 +1
    /// </summary>
    public void IncreaseWeaponPreficiencyLevel()
    {
        this.charactoristicInfo.weaponProficiencyLevel += 1;    
    }

    /// <summary>
    /// 총기 경험치 증가 +4
    /// </summary>
    public void IncreaseWeaponProficiencyEXP()
    {
        this.charactoristicInfo.weaponProficiencyEXP += 4;
    }

    /// <summary>
    /// 현재 들고 있는 총기 이름 변경 (AK47, AWP, UZI, SPAS-12) (기본값 : AK47)
    /// </summary>
    /// <param name="weaponName">현재 들고 있는 무기 이름</param>
    public void ChangeCurrentWeaponName(string weaponName = "AK47")
    {
        this.charactoristicInfo.currentWeaponName = weaponName;
    }
}

UIInventoryDetailPopup 스크립트

using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using UnityEngine;
using UnityEngine.U2D;
using UnityEngine.UI;

public class UIInventoryDetailPopup : MonoBehaviour
{

    [SerializeField]
    private Button btnInventoryDetailClose;

    [SerializeField]
    private Button btnDiscardEquipment;

    [SerializeField]
    private Image imgEquipment;

    [SerializeField]
    private Text txtEquipmentName;

    [SerializeField]
    private Text txtEquipmentStat1;

    [SerializeField]
    private Text txtEquipmentStat2;

    [SerializeField]
    private UIInventory inventory;

    private int currentEquipmentCellHesh;

    private SpriteAtlas atlas;

    public void Init()
    {
        this.atlas = AtlasManager.instance.GetAtlasByName("UIEquipmentIcon");

        this.btnInventoryDetailClose.onClick.AddListener(() =>
        {
            this.gameObject.SetActive(false);
            this.inventory.onSetOffSelect(0);
        });

        this.btnDiscardEquipment.onClick.AddListener(() =>
        {
            this.inventory.onDiscardEquipment(this.currentEquipmentCellHesh);
            this.gameObject.SetActive(false);
        });

        this.gameObject.SetActive(false);
    }

    public void RefreshPopup(string equipment, int hesh)
    {
        this.currentEquipmentCellHesh = hesh;   
        this.imgEquipment.sprite = this.atlas.GetSprite(equipment);
        this.txtEquipmentName.text = equipment;// 데이터 매니저에서 불러오기
        this.txtEquipmentStat1.text = string.Format("{0} 의 상세 능력치1",equipment);
        this.txtEquipmentStat2.text = string.Format("{0} 의 상세 능력치2",equipment);
    }
}

EquipmentBG = 인벤토리 장비 스크립트 (헷갈리지 않게 이름 변경을 해야할듯)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class EquipmentBG : MonoBehaviour, IPointerDownHandler
{
    [SerializeField]
    private GameObject EquipmentSelected;

    private UIInventory inventory;

    private void Awake()
    {
        //초기값 셋팅
        this.inventory = GameObject.FindObjectOfType<UIInventory>();
        this.EquipmentSelected.SetActive(false); 
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        this.EquipmentSelected.SetActive(true);
        var EquipmentName = this.transform.GetChild(1).name;
        var hesh = this.transform.parent.GetHashCode();
        this.inventory.onSelected(EquipmentName, hesh);   
    }
}
    • ContentGrid 스크립트  = 터치 제어를 위한 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class ContentGrid : MonoBehaviour, IDragHandler
{
    [SerializeField]
    private UIInventory inventory;
    public void OnDrag(PointerEventData eventData)
    {
        this.inventory.onSetOffpopup();
        this.inventory.onSetOffSelect(0);
    }
}

 

 

외에도 관련 스크립트가 27개 ;; 가 넘어 이제것 작업한 스크립트 들은 블로그 포스팅을 참조.

 

이제 맵 제작을 본격적으로 시작해야겠다. ㅎㅎ 

반응형
LIST