오늘은 어제 정리한 MVC 패턴 글을 참고하여 인벤토리의 기능을 구조화했다.
1. 기존 인벤토리 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System;
using UnityEngine.UI;
using static TMPro.SpriteAssetUtilities.TexturePacker_JsonArray;
public class Inventory : MonoBehaviour
{
[SerializeReference] public List<ItemSlotInfo> items = new List<ItemSlotInfo>();
[Space]
[Header("Inventory Menu Components")]
[SerializeField] private GameObject _inventoryMenu;
[SerializeField] private GameObject _itemPanel;
[SerializeField] private GameObject _itemPanelGrid;
public Mouse mouse;
private List<ItemPanel> _existingPanels = new List<ItemPanel>();
private Dictionary<string, Item> _allItemsDictionary = new Dictionary<string, Item>();
[Header("Quick Slot")]
[SerializeField] private List<QuickSlot> quickSlots = new List<QuickSlot>();
[Space]
private ItemPanel _curSelectedPanel;
private int _inventorySize = 10;
private int _updateQuickSlotSize = 0;
private int _testWheel = 0;
public bool isPickUp;
private void Start()
{
for (int i = 0; i < _inventorySize; i++)
{
items.Add(new ItemSlotInfo(null, 0));
}
List<Item> allItems = GetAllItems().ToList();
string itemInDictionary = "Items in Dictionary: ";
foreach (Item i in allItems)
{
if (!_allItemsDictionary.ContainsKey(i.GiveName()))
{
_allItemsDictionary.Add(i.GiveName(), i);
itemInDictionary += ", " + i.GiveName();
}
else
{
Debug.Log("" + i + "already exists in Dictionary " + _allItemsDictionary[i.GiveName()]);
}
}
itemInDictionary += ".";
Debug.Log(itemInDictionary);
//test
AddItem("Axe", 40);
AddItem("Carrot", 5);
EquipItem(quickSlots[0].quickSlotPanel, 0);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Tab))
{
if (_inventoryMenu.activeSelf)
{
_inventoryMenu.SetActive(false);
mouse.EmptySlot();
RefreshQuickSlots();
//Cursor.lockState = CursorLockMode.Locked;
}
else
{
_inventoryMenu.SetActive(true);
//Cursor.lockState = CursorLockMode.None;
RefreshInventory();
}
}
if (Input.GetKeyDown(KeyCode.Mouse1) && mouse.itemSlot.item != null)
{
RefreshInventory();
}
if (Input.GetKeyDown(KeyCode.F))
{
UpdateList();
}
if (Input.GetKeyDown(KeyCode.Alpha1))
{
EquipItem(quickSlots[0].quickSlotPanel, 0);
}
if (Input.GetKeyDown(KeyCode.Alpha2))
{
EquipItem(quickSlots[1].quickSlotPanel, 1);
}
if (Input.GetKeyDown(KeyCode.Alpha3))
{
EquipItem(quickSlots[2].quickSlotPanel, 2);
}
if (Input.GetKeyDown(KeyCode.Alpha4))
{
EquipItem(quickSlots[3].quickSlotPanel, 3);
}
if (Input.GetKeyDown(KeyCode.Alpha5))
{
EquipItem(quickSlots[4].quickSlotPanel, 4);
}
if (Input.GetAxis("Mouse ScrollWheel") > 0 && _testWheel < quickSlots.Count - 1)
{
_testWheel++;
EquipItem(quickSlots[_testWheel].quickSlotPanel, _testWheel);
}
if (Input.GetAxis("Mouse ScrollWheel") < 0 && _testWheel > 0)
{
_testWheel--;
EquipItem(quickSlots[_testWheel].quickSlotPanel, _testWheel);
}
}
private void EquipItem(ItemPanel seletedPanel, int testNum)
{
_testWheel = testNum;
if (_curSelectedPanel != null)
{
_curSelectedPanel.equipOutline.enabled = false;
}
if (_curSelectedPanel == seletedPanel)
{
UnEquipItem(seletedPanel);
return;
}
_curSelectedPanel = seletedPanel;
_curSelectedPanel.equipOutline.enabled = true;
RefreshInventory();
RefreshQuickSlots();
}
private void UnEquipItem(ItemPanel equippedPanel)
{
_curSelectedPanel = null;
equippedPanel.equipOutline.enabled = false;
}
private void UpdateList()
{
if (_updateQuickSlotSize + quickSlots.Count < _inventorySize) _updateQuickSlotSize = quickSlots.Count;
else _updateQuickSlotSize = 0;
RefreshQuickSlots();
}
private int AddItem(string itemName, int amount)
{
Item item = null;
_allItemsDictionary.TryGetValue(itemName, out item);
if (item == null)
{
Debug.Log("Could not find Item in Dictionary");
return amount;
}
foreach (ItemSlotInfo i in items)
{
if (i.item != null)
{
if (i.item.GiveName() == item.GiveName())
{
if (amount > i.item.MaxStacks() - i.stacks)
{
amount -= i.item.MaxStacks() - i.stacks;
i.stacks = i.item.MaxStacks();
}
else
{
i.stacks += amount;
if (_inventoryMenu.activeSelf) RefreshInventory();
return 0;
}
}
}
}
foreach (ItemSlotInfo i in items)
{
if (i.item == null)
{
if (amount > item.MaxStacks())
{
i.item = item;
i.stacks = item.MaxStacks();
amount -= item.MaxStacks();
}
else
{
i.item = item;
i.stacks = amount;
if (_inventoryMenu.activeSelf) RefreshInventory();
return 0;
}
}
}
if (_inventoryMenu.activeSelf) RefreshInventory();
return amount;
}
private IEnumerable<Item> GetAllItems()
{
return System.AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes()).Where(type => type.IsSubclassOf(typeof(Item)))
.Select(type => System.Activator.CreateInstance(type) as Item);
}
public void ClearSlot(ItemSlotInfo slot)
{
slot.item = null;
slot.stacks = 0;
}
public void RefreshInventory()
{
_existingPanels = _itemPanelGrid.GetComponentsInChildren<ItemPanel>().ToList();
if (_existingPanels.Count < _inventorySize)
{
int amountToCreate = _inventorySize - _existingPanels.Count;
for (int i = 0; i < amountToCreate; i++)
{
GameObject newPanel = Instantiate(_itemPanel, _itemPanelGrid.transform);
_existingPanels.Add(newPanel.GetComponent<ItemPanel>());
}
}
int index = 0;
foreach (ItemSlotInfo i in items)
{
i.name = "" + (index + 1);
if (i.item != null) i.name += ": " + i.item.GiveName();
else i.name += ": -";
ItemPanel panel = _existingPanels[index];
panel.name = i.name + "Panel";
if (panel != null)
{
panel.inventory = this;
panel.itemSlot = i;
if (i.item != null)
{
panel.itemImage.gameObject.SetActive(true);
panel.itemImage.sprite = i.item.GiveItemImage();
panel.itemImage.CrossFadeAlpha(1f, 0.05f, true);
panel.stackText.gameObject.SetActive(true);
panel.stackText.text = "" + i.stacks;
}
else
{
panel.itemImage.gameObject.SetActive(false);
panel.stackText.gameObject.SetActive(false);
}
}
index++;
}
mouse.EmptySlot();
}
public void RefreshQuickSlots()
{
for (int i = 0; i < quickSlots.Count; i++)
{
quickSlots[i].inventorySlot = items[i + _updateQuickSlotSize];
quickSlots[i].quickSlotPanel.inventory = this;
quickSlots[i].quickSlotPanel.itemSlot = quickSlots[i].inventorySlot;
quickSlots[i].quickSlotPanel.isQuickSlot = true;
QuickSlot quickslot = quickSlots[i];
ItemSlotInfo inventorySlot = quickslot.inventorySlot;
ItemPanel quickSlotPanel = quickslot.quickSlotPanel;
if (inventorySlot.item != null)
{
quickSlotPanel.itemImage.gameObject.SetActive(true);
quickSlotPanel.itemImage.sprite = inventorySlot.item.GiveItemImage();
quickSlotPanel.itemImage.CrossFadeAlpha(1f, 0.05f, true);
quickSlotPanel.stackText.gameObject.SetActive(true);
quickSlotPanel.stackText.text = "" + inventorySlot.stacks;
}
else
{
quickSlotPanel.itemImage.gameObject.SetActive(false);
quickSlotPanel.stackText.gameObject.SetActive(false);
}
}
}
}
그냥 코드만 봐도 뭔가 숨막힌다..
인벤토리의 기능을 빠르게 구현함과 동시에 퀵슬롯을 기능까지 덕지덕지 붙여놔서 Inventory 클래스에 기능이
너무 집중되있는 걸 볼 수 있다.
원래는 인벤토리 코드의 리팩토링을 준비하고 있었는데 어떻게 기능을 나눌지 정말 막막했다.
한 클래스에는 하나의 역할만 담당해야한다는 개념을 알고 있음에도 불구하고 이게 이 클래스의 기능이 맞나..?
생각하며 고민에 빠졌었다.
하지만 MVC 패턴의 개념을 정리하며 구조화의 틀을 잡아놓으니 인벤토리를 어떻게 나눌지 감이 확 잡혔다.
2. 인벤토리 컨트롤러
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class InventoryController : MonoBehaviour
{
[SerializeReference] public List<ItemSlotInfo> items = new List<ItemSlotInfo>();
private int _inventorySize = 10;
private int _testWheel = 0;
private InventoryModel _inventoryModel;
private InventoryView _inventoryView;
[Header("Quick Slot")]
[SerializeField] private List<QuickSlot> quickSlots = new List<QuickSlot>();
public void Start()
{
_inventoryModel = new InventoryModel(_inventorySize, items, quickSlots);
InitializedInventoryView();
_inventoryModel.OnUpdateInventory += _inventoryView.Refresh;
_inventoryModel.InitializeInventoryModel();
_inventoryModel.AddItem("Axe", 40);
_inventoryModel.AddItem("Carrot", 5);
_inventoryModel.EquipItem(quickSlots[0].quickSlotPanel);
_inventoryView.RefreshInventory();
}
private void OnDestroy()
{
_inventoryModel.OnUpdateInventory -= _inventoryView.Refresh;
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Alpha1))
{
_inventoryModel.EquipItem(quickSlots[0].quickSlotPanel);
_testWheel = 0;
}
if (Input.GetKeyDown(KeyCode.Alpha2))
{
_inventoryModel.EquipItem(quickSlots[1].quickSlotPanel);
_testWheel = 1;
}
if (Input.GetKeyDown(KeyCode.Alpha3))
{
_inventoryModel.EquipItem(quickSlots[2].quickSlotPanel);
_testWheel = 2;
}
if (Input.GetKeyDown(KeyCode.Alpha4))
{
_inventoryModel.EquipItem(quickSlots[3].quickSlotPanel);
_testWheel = 3;
}
if (Input.GetKeyDown(KeyCode.Alpha5))
{
_inventoryModel.EquipItem(quickSlots[4].quickSlotPanel);
_testWheel = 4;
}
if (Input.GetAxis("Mouse ScrollWheel") > 0 && _testWheel < quickSlots.Count - 1)
{
_testWheel++;
_inventoryModel.EquipItem(quickSlots[_testWheel].quickSlotPanel);
}
if (Input.GetAxis("Mouse ScrollWheel") < 0 && _testWheel > 0)
{
_testWheel--;
_inventoryModel.EquipItem(quickSlots[_testWheel].quickSlotPanel);
}
}
private void InitializedInventoryView()
{
_inventoryView = GetComponent<InventoryView>();
_inventoryView.items = items;
_inventoryView.quickSlots = quickSlots;
}
}
컨트롤러는 인벤토리의 핵심 로직을 가져야 하며 Model과 View의 중재자 역할을 해줘야한다.
처음에 코드를 분배할 때는 Model과 View를 어떻게 관리해야 될지 고민했다.
Model도 아이템과 퀵슬롯의 리스트가 필요했고 View도 똑같은 객체의 아이템과 퀵슬롯의 리스트가 필요했다.
그래서 객체의 안정성을 지켜야 할 정보들은 Controller가 갖도록 했다. -> CSV 데이터 매니저 코드가 완료되면
싱글톤으로 데이터를 가져올 예정
결과로 Controller에서 Model과 View의 객체를 생성하여 같은 리스트의 객체값을 넣어주니 문제없이 진행됐다.
다른 문제로는 Model에서 아이템을 추가하는 메서드가 있는데 여기서 인벤토리를 업데이트 해주는 메서드가
필요했다. Model과 View에서 직접 객체를 생성하여 참조하면 결합도가 굉장히 높아질 거 같아서 고민끝에
event Action을 통한 델리게이트 형식으로 사용했다. 확실히 Controller가 중재자 역할을 하니
Model과 View가 서로 정보를 몰라도 되서 기능 배분이 편했다. 살짝 Controller가 게임매니저 같은 느낌?..이었다.
그렇게 정보들을 잘 넣어주고 Model과 View의 핵심 로직을 담아서 Controller의 기능을 마무리했다.
3. 인벤토리 모델
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
[System.Serializable]
public class QuickSlot
{
public ItemSlotInfo inventorySlot;
public ItemPanel quickSlotPanel;
}
[System.Serializable]
public class ItemSlotInfo
{
public Item item;
public string name;
public int stacks;
public ItemSlotInfo(Item newItem, int newStacks)
{
item = newItem;
stacks = newStacks;
}
}
public class InventoryModel
{
private List<ItemSlotInfo> _items;
private List<QuickSlot> _quickSlots;
private ItemPanel _curSelectedPanel;
private int _inventorySize;
private Dictionary<string, Item> _allItemsDictionary = new Dictionary<string, Item>();
public event Action OnUpdateInventory;
public InventoryModel(int inventorySize, List<ItemSlotInfo> itemsList, List<QuickSlot> quickSlotsList)
{
_items = itemsList;
_quickSlots = quickSlotsList;
_inventorySize = inventorySize;
}
public void InitializeInventoryModel()
{
for (int i = 0; i < _inventorySize; i++)
{
_items.Add(new ItemSlotInfo(null, 0));
}
List<Item> allItems = GetAllItems().ToList();
string itemInDictionary = "Items in Dictionary: ";
foreach (Item i in allItems)
{
if (!_allItemsDictionary.ContainsKey(i.GiveName()))
{
_allItemsDictionary.Add(i.GiveName(), i);
itemInDictionary += ", " + i.GiveName();
}
else
{
Debug.Log("" + i + "already exists in Dictionary " + _allItemsDictionary[i.GiveName()]);
}
}
itemInDictionary += ".";
Debug.Log(itemInDictionary);
}
public int AddItem(string itemName, int amount)
{
Item item = null;
_allItemsDictionary.TryGetValue(itemName, out item);
if (item == null)
{
Debug.Log("Could not find Item in Dictionary");
return amount;
}
foreach (ItemSlotInfo i in _items)
{
if (i.item != null)
{
if (i.item.GiveName() == item.GiveName())
{
if (amount > i.item.MaxStacks() - i.stacks)
{
amount -= i.item.MaxStacks() - i.stacks;
i.stacks = i.item.MaxStacks();
}
else
{
i.stacks += amount;
OnUpdateInventory?.Invoke();
return 0;
}
}
}
}
foreach (ItemSlotInfo i in _items)
{
if (i.item == null)
{
if (amount > item.MaxStacks())
{
i.item = item;
i.stacks = item.MaxStacks();
amount -= item.MaxStacks();
}
else
{
i.item = item;
i.stacks = amount;
OnUpdateInventory?.Invoke();
return 0;
}
}
}
OnUpdateInventory?.Invoke();
return amount;
}
public void EquipItem(ItemPanel seletedPanel)
{
if (_curSelectedPanel != null)
{
_curSelectedPanel.equipOutline.enabled = false;
}
if (_curSelectedPanel == seletedPanel)
{
UnEquipItem(seletedPanel);
return;
}
_curSelectedPanel = seletedPanel;
_curSelectedPanel.equipOutline.enabled = true;
OnUpdateInventory?.Invoke();
}
public void UnEquipItem(ItemPanel equippedPanel)
{
_curSelectedPanel = null;
equippedPanel.equipOutline.enabled = false;
}
public IEnumerable<Item> GetAllItems()
{
return System.AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes()).Where(type => type.IsSubclassOf(typeof(Item)))
.Select(type => System.Activator.CreateInstance(type) as Item);
}
}
모델에는 아이템의 처리와 관련된 기능을 넣으려고 했다.
인벤토리 기능에선 아이템 추가, 초기화, 장착, 해제, 정보 추출 기능을 아이템을 처리하는 과정이라고 판단했고
이를 모델로 분리했다.
컨트롤러 영역에서 설명했듯이 아이템 추가 메서드에서 인벤토리의 업데이트가 필요했기 때문에 델리게이트로
결합도를 느슨하게 해줬다.
그리고 아직 아이템 데이터를 담당하는 데이터 매니저 부분이 완성되지 않아서 아이템 리스트를 컨트롤러 부터
받아오는 과정을 거쳐 서로 Model과 View가 영향을 주지 않게 제작했다.
나중에 아이템을 제작할 때 아이템 정보를 불러와야 하는 부분이나 아이템 삭제 기능이 추가될 때
이 부분에 추가해주면 좋을 것 같다.
4. 인벤토리 뷰
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System;
using UnityEngine.UI;
using static TMPro.SpriteAssetUtilities.TexturePacker_JsonArray;
public class InventoryView : MonoBehaviour
{
public List<ItemSlotInfo> items;
public List<QuickSlot> quickSlots;
public Mouse mouse;
public bool isPickUp;
private List<ItemPanel> _existingPanels = new List<ItemPanel>();
private InventoryModel _inventoryModel;
private int _updateQuickSlotSize = 0;
private int _inventorySize = 10;
[Space]
[Header("Inventory Menu Components")]
[SerializeField] public GameObject _inventoryMenu;
[SerializeField] public GameObject _itemPanel;
[SerializeField] public GameObject _itemPanelGrid;
private void Update()
{
if (Input.GetKeyDown(KeyCode.Tab))
{
if (_inventoryMenu.activeSelf)
{
_inventoryMenu.SetActive(false);
mouse.EmptySlot();
RefreshInventory();
//Cursor.lockState = CursorLockMode.Locked;
}
else
{
_inventoryMenu.SetActive(true);
//Cursor.lockState = CursorLockMode.None;
RefreshInventory();
}
}
if (Input.GetKeyDown(KeyCode.F))
{
UpdateList();
}
}
public void RefreshInventory()
{
_existingPanels = _itemPanelGrid.GetComponentsInChildren<ItemPanel>().ToList();
if (_existingPanels.Count < _inventorySize)
{
int amountToCreate = _inventorySize - _existingPanels.Count;
for (int i = 0; i < amountToCreate; i++)
{
GameObject newPanel = Instantiate(_itemPanel, _itemPanelGrid.transform);
_existingPanels.Add(newPanel.GetComponent<ItemPanel>());
}
}
int index = 0;
foreach (ItemSlotInfo i in items)
{
i.name = "" + (index + 1);
if (i.item != null) i.name += ": " + i.item.GiveName();
else i.name += ": -";
ItemPanel panel = _existingPanels[index];
panel.name = i.name + "Panel";
if (panel != null)
{
panel.inventory = this;
panel.itemSlot = i;
if (i.item != null)
{
panel.itemImage.gameObject.SetActive(true);
panel.itemImage.sprite = i.item.GiveItemImage();
panel.itemImage.CrossFadeAlpha(1f, 0.05f, true);
panel.stackText.gameObject.SetActive(true);
panel.stackText.text = "" + i.stacks;
}
else
{
panel.itemImage.gameObject.SetActive(false);
panel.stackText.gameObject.SetActive(false);
}
}
index++;
}
mouse.EmptySlot();
RefreshQuickSlots();
}
public void RefreshQuickSlots()
{
for (int i = 0; i < quickSlots.Count; i++)
{
quickSlots[i].inventorySlot = items[i + _updateQuickSlotSize];
quickSlots[i].quickSlotPanel.inventory = this;
quickSlots[i].quickSlotPanel.itemSlot = quickSlots[i].inventorySlot;
quickSlots[i].quickSlotPanel.isQuickSlot = true;
QuickSlot quickslot = quickSlots[i];
ItemSlotInfo inventorySlot = quickslot.inventorySlot;
ItemPanel quickSlotPanel = quickslot.quickSlotPanel;
if (inventorySlot.item != null)
{
quickSlotPanel.itemImage.gameObject.SetActive(true);
quickSlotPanel.itemImage.sprite = inventorySlot.item.GiveItemImage();
quickSlotPanel.itemImage.CrossFadeAlpha(1f, 0.05f, true);
quickSlotPanel.stackText.gameObject.SetActive(true);
quickSlotPanel.stackText.text = "" + inventorySlot.stacks;
}
else
{
quickSlotPanel.itemImage.gameObject.SetActive(false);
quickSlotPanel.stackText.gameObject.SetActive(false);
}
}
}
public void UpdateList()
{
if (_updateQuickSlotSize + quickSlots.Count < _inventorySize) _updateQuickSlotSize = quickSlots.Count;
else _updateQuickSlotSize = 0;
RefreshInventory();
}
public void ClearSlot(ItemSlotInfo slot)
{
slot.item = null;
slot.stacks = 0;
}
public void Refresh()
{
if (_inventoryMenu.activeSelf) RefreshInventory();
}
}
뷰에는 UI와 관련된 기능들로만 구성했다.
주로 인벤토리가 어떤 기능에 의해 UI가 업데이트 되는
인벤토리업데이트, 퀵슬롯 업데이트, 슬롯 업데이트, 퀵슬롯 리스트 갱신, 인벤토리 On/Off 기능들을 뷰가
담당하도록 했다.
근데 지금보니깐 인벤토리 On/Off 와 퀵슬롯 리스트 갱신은 키보드의 입력을 받아서 실행되는데
키보드 입력을 컨트롤러로 빼고 기능들을 뷰의 메서드로 제작하여 사용하는게 맞는 거 같다.
빠르게 고쳐야겠다.
뷰는 Monobehavior를 받고 있고 인스펙터 상에서도 적용되기 때문에 UI데이터를 받는 공간에서
UI기능을 다루니 한 층더 수월하고 간편했다. 앞으로 아이템과 관련된 정보 팝업이나 장착, 소모로 인한
UI 변화를 이곳에 추가하며 기능을 만들어 나가면 될 것 같다.
결론
아직 MVC 패턴에 대한 확실한 이해가 더 필요할 거 같아서 헤드퍼스트 디자인 패턴 책을
한번 더 읽어볼 예정이다. 그래도 어느정도 구조화가 됐다는 것에 큰 만족감을 느꼈고 이를 통해
다른 디자인패턴도 많이 익히고 싶다는 생각이 들었다.
'프로젝트 기록 > Project N' 카테고리의 다른 글
인벤토리 개발 기록 (창고와 인벤토리) (0) | 2024.04.03 |
---|---|
UIManager 구현 기록 (1) | 2024.02.13 |
UIManager는 왜 만드는 걸까? (0) | 2024.02.07 |
MVC 패턴에 대한 이해 (1) | 2024.01.30 |
팀원 코드 분석 / CSV란? (CSV Data) (2) | 2024.01.30 |