프로젝트/건즈앤 레이첼스
[유니티 프로젝트] 인벤토리 스탯창 구현 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