[섹션7] 인벤토리 실습 & 코드 정리
<인벤토리 실습 #1>
1. Prefab의 UI_Button에서 Itemicon 부분을 삭제하고 PlayerController에서 UI_Button을 부르는 부분 삭제
굉장히 다양한 UI가 있는데 딱 보면 알 수 있다. -> 그때 그때 찾아보면서 코드를 짜면 된다.
- 다양한 UI
asset store에서 제공하는 무료 UI 이용하기
1. Unity Asset store에서 원하는 UI를 다운받아서 사용한다.
2. 다음과 같이 UI 인벤토리를 만든다.
전체 인벤토리를 감싸주는 Panel을 생성하고 이름을 UI_Inven으로 한다.
item을 UI -> Panel을 통해서 만들어주고(UI_Inven_Item) 그 아래에 item 이미지를 자식으로 둔다. Panel의 Image는 1에서 다운받은 UI의 UIButtonDefault를 mapping 한다.

3. 위에서 만든 UI_Inven_Item을 Panel 아래의 여러개의 자식을 두어 표현하는데 이때 Panel에 Grid Layout Group component를 붙여주면 위의 그림과 같이 UI_Inven_Itme이 Grid로 표현할 수 있다.
(원하는 Grid 모양으로 아래의 Cell Size, Spacing 등등을 바꾸어 )

4. 위에서 만든 UI_Inven_Item과 UI_Inven을 Prefabs 폴더 아래에 Scene 폴더에 두고 다음과 같이 Prefab UI_Inven_Item을 다음과 같이 Text를 두어 수정한다.

- Binding하기
1. UI_Inven.cs와 UI_Inven_Item.cs를 Sripts -> UI -> Scene 폴더 아래에 만든다.

2. UI_Inven.cs 파일을 열어서 이전에 UI 자동화에서 배웠던대로 Binding을 하도록 한다.
*UI_Inven.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UI_Inven : UI_Scene
{
enum GameObjects
{
GridPanel
}
void Start()
{
}
void Update()
{
}
public override void Init()
{
base.Init();
// Binding 하기
Bind<GameObject>(typeof(GameObjects));
2. 그 후 기존에 있던 UI_Inven_Item을 없애고 다시 Prefab에서 원하는 만큼 UI_Inven_Item을 들고 와서 mapping을 해줘야한다.
*UI_Inven.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UI_Inven : UI_Scene
{
enum GameObjects
{
GridPanel
}
void Start()
{
}
void Update()
{
}
public override void Init()
{
base.Init();
// Binding 하기
Bind<GameObject>(typeof(GameObjects));
// 우선 기존에 있던 UI_Inven_Item을 날려야한다.
GameObject gridPanel = GET<GameObject>((int)GameObjects.GridPanel);
// gridPanel에 있는 child를 순회하면서 찾음
foreach (Transform child in gridPanel.transform)
Managers.Resource.Destroy(child.gameObject);
// 실제 인벤토리 정보를 참고해서 해야하지만 현재는 강제적으로
for (int i = 0; i < 8; i++)
{
GameObject item = Managers.Resource.Instantiate("UI/Scene/UI_Inven_Item");
item.transform.SetParent(gridPanel.transform); // 강제로 Parent를 정해줌
}
}
}
3. PlayerController.cs 에서 위의 UI_Inven이 나타나는지 Test해본다. Play해보면 인벤토리 창이 잘 뜨는 것을 확인할 수 있다.
*PlayerController.cs
void Start()
{
/*
Managers.Input.KeyAction -= OnKeyboard;
Managers.Input.KeyAction += OnKeyboard;
*/
Managers.Input.MouseAction -= OnMouseClicked;
Managers.Input.MouseAction += OnMouseClicked;
// TEMP
Managers.UI.ShowSceneUI<UI_Inven>();
}
<Item 기능 추가>
Item 버튼의 이름을 각각 바꾸고 클릭 Event를 추가한다. -> UI_Inven_Item.cs를 통해서
- Item 이름 바꾸기
1. 해당 Item에 관한 것은 UI_Popup과 UI_Scene의 상속이 아닌 (어느 곳에서도 속하지 않기 때문에) UI_Base의 상속을 받아야한다. 이때 위에서 처럼 overrid init()을 하면 UI_Base에 init함수가 없기 때문에 error가 난다.
=> init() 함수를 UI_Base로 옮기는 것이 설계적으로 더 좋다.
2. 다음과 같이 UI_Base.cs에서 Init() 함수를 abstract으로 ◆추상 클래스, 추상 메서드로 구현을 한다. 이렇게 바꾸면 UI_Popup.cs와 UI_Scene.cs에서 ovrride로 바꿔줘야한다.
*UI_Base.cs
public abstract class UI_Base : MonoBehaviour
{
// 딕셔너리를 이용하여 Buttons를 받으면 Unity Object의 Button을 반환하고,
// Texts를 받으면 Unity Object의 Text를 반환하도록
Dictionary<Type, UnityEngine.Object[]> _objects = new Dictionary<Type, UnityEngine.Object[]>();
public abstract void Init();
*UI_Base.cs & UI_Scene.cs
public override void Init()
{
Managers.UI.SetCanvas(gameObject, true);
}
=> UI_Inven_Item.cs에서 init()함수를 override를 하여 구현한다.
◆ abstract 메소드는 인터페이스의 메소드와 마찬가지로 몸통이 없다. 즉 abstract 클래스를 상속하는 클래스에서 해당 abstract 메소드를 구현해야만 하는 것이다.
3. 위에서 Binding한대로 enum으로 GameObject를 정의해야하는데 현재 Image와 Text UI component 두 개 밖에 없기 때문에 GaemObjects로 한번에 enum을 정의한다.
Bind를 해주는데 이때 GameObject로 가져온 다음 해당 Object의 Text component를 가져온다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UI_Inven_Item : UI_Base
{
enum GameObjects
{
Itemicon,
ItemNameText,
}
void Start()
{
Init();
}
public override void Init()
{
Bind<GameObject>(typeof(GameObjects)); // 각각의 GameObject를 Binding 해줌
// 해당 GameObject의 Text component를 가져온다.
GET<GameObject>((int)GameObjects.ItemNameText).GetComponent<Text>().text = "bind test";
}
void Update()
{
}
}
4. 이때 Play를 하면 text가 바뀌지않는 것을 확인할 수 있다.
=> 기존의 UI_Popup과 UI_Scene에서는 UI_Manager.cs 파일을 통해 Util.GetOrAddComponent를 통해 component를 붙여줬었다. 이 부분이 없기 때문에 실행이 되지 않았다.
5. 해결 방안은 UI_Inven.cs에서 다음과 같이 UI_Inven_Item을 추가해주거나 Unity tool을 통해 UI_Inven_Item을 직접적으로 추가해주는 것이다.
* UI_Inven.cs
// 실제 인벤토리 정보를 참고해서 해야하지만 현재는 강제적으로
for (int i = 0; i < 8; i++)
{
GameObject item = Managers.Resource.Instantiate("UI/Scene/UI_Inven_Item");
item.transform.SetParent(gridPanel.transform); // 강제로 Parent를 정해줌
// component 추가
Util.GetOrAddComponent<UI_Inven_Item>(item);
}
6. 이 상태로 실행하면 component가 추가되고 다음과 같이 Text가 바뀐것을 확인할 수 있다.

7. 사실 실제 인벤토리 정보를 가져와서 해당 item 각각에 name을 정해줘야한다.
다음과 같이 UI_Inven_Item.cs 파일에서 name을 지정해주는 함수를 만들어서 Text를 바꿔주고
UI_Inven.cs 에서 해당 name을 setting 해준다.
*UI_Inven_Item.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UI_Inven_Item : UI_Base
{
enum GameObjects
{
Itemicon,
ItemNameText,
}
string _name;
void Start()
{
Init();
}
public override void Init()
{
Bind<GameObject>(typeof(GameObjects)); // 각각의 GameObject를 Binding 해줌
// 해당 GameObject의 Text component를 가져온다.
GET<GameObject>((int)GameObjects.ItemNameText).GetComponent<Text>().text = _name;
}
public void SetInfo(string name)
{
_name = name;
}
}
*UI_Inven.cs
// 실제 인벤토리 정보를 참고해서 해야하지만 현재는 강제적으로
for (int i = 0; i < 8; i++)
{
GameObject item = Managers.Resource.Instantiate("UI/Scene/UI_Inven_Item");
item.transform.SetParent(gridPanel.transform); // 강제로 Parent를 정해줌
// component 추가
UI_Inven_Item invenItem = Util.GetOrAddComponent<UI_Inven_Item>(item);
invenItem.SetInfo($"집행검{i}번");
}
8. 실행하면 다음과 같이 번호가 나오는 것을 확인할 수 있다.

- Item Log 보여주기
1. 다음과 같이 UI_Inven_Item.cs에서 Event를 추가하여 해당 item의 Name을 출력하는 event를 추가한다.
* UI_Inven_Item.cs
public override void Init()
{
Bind<GameObject>(typeof(GameObjects)); // 각각의 GameObject를 Binding 해줌
// 해당 GameObject의 Text component를 가져온다.
GET<GameObject>((int)GameObjects.ItemNameText).GetComponent<Text>().text = _name;
GET<GameObject>((int)GameObjects.Itemicon).AddUIEvent((PointerEventData) => { Debug.Log($"아이템 클릭! {_name}"); });
}
2. play를 하면 다음과 같이 console 창에 출력되는 것을 볼 수 있다.

<코드 정리>
1. UI_Inven.cs에서 Util.GetOrAddComponent으로 component를 추가하는 것이 아니라 바로 gameobject.GetOrAddComponent으로 접근하기 위해 Util의 해당 함수를 Extension.cs에 추가한다.
*Extension.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public static class Extension
{
public static T GetOrAddComponent<T>(this GameObject go) where T : UnityEngine.Component
{
return Util.GetOrAddComponent<T>(go);
}
public static void AddUIEvent(this GameObject go, Action<PointerEventData> action, Define.UIEvent type = Define.UIEvent.Click)
{
UI_Base.AddUIEvent(go, action, type);
}
}
*UI_Inven.cs
// 실제 인벤토리 정보를 참고해서 해야하지만 현재는 강제적으로
for (int i = 0; i < 8; i++)
{
GameObject item = Managers.Resource.Instantiate("UI/Scene/UI_Inven_Item");
item.transform.SetParent(gridPanel.transform); // 강제로 Parent를 정해줌
// component 추가
UI_Inven_Item invenItem = item.GetOrAddComponent<UI_Inven_Item>();
invenItem.SetInfo($"집행검{i}번");
}
2. Unity를 Play하면 다음과 같이 Prefab에 "Clone" 문자열이 붙는다.

=> ResourceManager.cs에서 Instantiate 함수를 수정한다.
*ResourceManager.cs
public GameObject Instantiate(string path, Transform parent = null)
{
// 위에서 정의한 Load를 통해 가져옴
GameObject prefab = Load<GameObject>($"Prefabs/{path}");
// prefab 가져오기 실패 -> 문제가 있는 것
if(prefab == null)
{
// Log를 남김
Debug.Log($"Failed to load prefab : {path}");
return null;
}
GameObject go = Object.Instantiate(prefab, parent);
int index = go.name.IndexOf("(Clone)"); // "Clone" 문자열의 인덱스를 반환한다.
if (index > 0)
go.name = go.name.Substring(0, index); // Substring으로 0부터 index까지 문자열을 자른다.
return go;
}

3. UI_Base.cs에서 자주사용하는 함수 중 GameObject를 반환하는 함수가 없으므로 추가
*UI_Base.cs
protected GameObject GetObject(int idx) { return GET<GameObject>(idx); }
4. UI_Base.cs에서 추가한 AddUIEvent함수가 이름이 안붙어서 BindEvent로 바꾸고자한다.
ctrl + shift + F를 눌러서 AddUIEvent -> BindEvent로 바꾼다.

5. UI_Inven.cs에서 Prefab을 Instantiate한 부분을 UIManager을 통해 만들도록 한다. 사실 UI_Inven_Item은 Popup도 아니고 Scene도 아닌 UI_Inven에 기생하는 것이기 때문에 별도의 파일로 뽑아서 관리하도록 한다. 마찬가지로 Script도 SubItem 폴더를 만들어주어 옮긴다.

=> 이렇게 한 후 UIManager.cs에서 Popup과 Scene에서 했던 것과 같이 함수를 만들어 준다.
-> UIManager에서 자동으로 Prefab을 가져오고 Script를 연결해준다.
* UIManager.cs
public T MakeSubItem<T>(string name = null) where T: UI_Base
{
// 이름을 받지않으면 T의 이름으로
if (string.IsNullOrEmpty(name))
name = typeof(T).Name;
// UI Prefab을 가져옴
GameObject go = Managers.Resource.Instantiate($"UI/SubItem/{name}");
return go.GetOrAddComponent<T>();
}
-> 부모 연결도 한번에 해주기위해 UIManager.cs를 다음과 같이 바꾼다.
* UIManager.cs
public T MakeSubItem<T>(Transform parent = null, string name = null) where T: UI_Base
{
// 이름을 받지않으면 T의 이름으로
if (string.IsNullOrEmpty(name))
name = typeof(T).Name;
// UI Prefab을 가져옴
GameObject go = Managers.Resource.Instantiate($"UI/SubItem/{name}");
if (parent != null)
go.transform.SetParent(parent);
return go.GetOrAddComponent<T>();
}
* UI_Inven.cs
// 실제 인벤토리 정보를 참고해서 해야하지만 현재는 강제적으로
for (int i = 0; i < 8; i++)
{
GameObject item = Managers.UI.MakeSubItem<UI_Inven_Item>(gridPanel.transform).gameObject; // UIManager의 함수를 이용
// component 추가
UI_Inven_Item invenItem = item.GetOrAddComponent<UI_Inven_Item>();
invenItem.SetInfo($"집행검{i}번");
}