[Unity] 아이템 및 인벤토리 구현
첫 구상
- 클래스를 구현하기 전에 러프하게 생각해본 변수와 기능들이다.
- 아이템 데이터 클래스
- 아이템들에 대한 데이터
- 아이템 클래스
- 아이템 이름
- 아이템 유형 - enum
- 아이템 이미지
- 아이템 프리팹
- 아이템 개수
-
아이템 효과(기능)
- 아이템 획득
- 가까이 다가가면 키 활성화 + 획득
- (콜라는 충돌하면 획득 가능..?)
- 아이템 사용
- 키를 누르면 상황에 맞는 아이템이 자동으로 빠져나감
- 콜라30개 모아 페인트로 변환한다고 할때, 어떤 방식으로 변환시킬지. 예를 들면 더블클릭 뭐 오른쪽 버튼 눌러서 바디페인팅으로 변환 등
- 아이템 인식 영역(Raycast)
- 아이템 드래그
- 아이템 클릭시
- 아이템 툴팁(설명)
- 인벤토리 내에서 마우스 위로 올리면 설명 볼 수 있음
- 인벤토리 클래스
- 아이템 추가
- 이이템 사용(감소 및 삭제)
- 아이템 드래그 및 위치변경
- 슬롯
- 아이템 데이터 클래스
ActivatingKeyTrigger
-
상호작용하는 오브젝트 가까이 다가가면 활성화 됨
-
아이템을 키를 누르면 획득하도록 함
💻전체 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
[ExecuteInEditMode]
[RequireComponent(typeof(SphereCollider))]
public class ActivatingKeyTrigger : MonoBehaviour
{
#region VARIABLES & PROPERTIES
private SphereCollider _trigger;
private CanvasGroup _ui;
[Range(0.1f, 100.0f)] public float radius = 1.0f;
public KeyTriggerType type;
public List<KeyCode> pressKey;
public bool isDone = false;
public bool isActive = false;
public Action callback = null;
public ItemObject itemObj = null;
#endregion
#region UNITY_EVENTS
private void Awake() {
_trigger = this.GetComponent<SphereCollider>();
_trigger.isTrigger=true;
if(!itemObj) itemObj = GetComponent<ItemObject>();
}
private void Start() {
}
private void Update() {
_trigger.radius = radius;
if(!isDone&&isActive&&Input.GetKey(pressKey[0])) {
isDone = true;
isActive = false;
switch(type) {
case KeyTriggerType.GetItem:
{
ItemManager.Inst.itemInventory.AddItem(itemObj.penguin.Id);
Destroy(itemObj.gameObject);
break;
}
}
if(callback!=null) {
callback();
}
}
}
private void OnTriggerEnter(Collider other) {
if(!isDone&&other.gameObject!=this) {
switch(type) {
case KeyTriggerType.GetItem:
{
isActive=true;
break;
}
}
}
}
private void OnTriggerExit(Collider other) {
if(other.gameObject!=this) {
switch(type) {
case KeyTriggerType.GetItem:
{
isActive=false;
break;
}
}
}
}
#endregion
}
[ExecuteInEditMode]
[RequireComponent(typeof(SphereCollider))]
- 편집 모드(Edit Mode): 게임에 변경을 가한다
- 플레이 모드(Play Mode): 게임을 플레이함으로써 해당 변경을 테스트한다
-
일반적으로, 유니티에서 스크립트를 구성하는 객체들은 플레이 모드에서만 실행되지만, [ExecuteInEditMode]는 편집모드에서도 Monobehaviour 스크립트의 각 이벤트 함수가 호출되도록 해준다
- [RequireComponent(typeof())]을 사용하는 스크립트를 오브젝트에 추가하면 필요하느 컴포넌트가 자동으로 오브젝트에 추가된다. 따라서 아이템 오브젝트에 이 스크립트를 추가하면 자동으로 SphereCollider 컴포넌트가 추가된다
📝VARIABLES & PROPERTIES
private SphereCollider _trigger;
private CanvasGroup _ui;
[Range(0.1f, 100.0f)] public float radius = 1.0f;
public KeyTriggerType type;
public List<KeyCode> pressKey;
public bool isDone = false;
public bool isActive = false;
public Action callback = null;
public ItemObject itemObj = null;
SphereCollider
: 트리거로 쓰이게 됨. isTrigger. 아이템이 범위 내에 들어오면 주울 수 있도록 함CanvasGroup
[Range(0.1f, 100.0f)] public float radius
: 트리거 범위-
public KeyTriggerType type;
: enum형으로cs public enum KeyTriggerType { GetItem, TalkAction, InterAction, StopSlowAction, }
아이템 얻는데 쓰이는지, 엔피시와 대화할때 쓰이는지 등 구분한다 -
public List<KeyCode> pressKey
: 아이템 먹을 키 - public Action callback : Action은 미리 선언된 델리게이트 변수이다.
- Func은 반환값이 있는 메소드를 참조하는 델리게이트 변수
- Action은 반환값이 없고, 매개변수가 없는 메소드를 참조하는 델리게이트 변수
📝UNITY_EVENTS
✏️Awake()
private void Awake() {
_trigger = this.GetComponent<SphereCollider>();
_trigger.isTrigger=true;
if(!itemObj) itemObj = GetComponent<ItemObject>();
}
- Awake()는 Start보다 더 먼저 호출되는 함수로 초기화할때 쓰임
- 트리거는 플레이어가 아이템 주울 수 있는 범위 안에 들어왔는지 확인하는 용도
- 오브젝트의 콜라이더 컴포넌트를 가져오고, isTrigger를 체크해줌
- 만약 아이템이 없으면 오브젝트의 ItemObject 컴포넌트를 가져옴
✏️Update()
private void Update() {
_trigger.radius = radius;
if(!isDone&&isActive&&Input.GetKey(pressKey[0])) {
isDone = true;
isActive = false;
switch(type) {
case KeyTriggerType.GetItem:
{
ItemManager.Inst.itemInventory.AddItem(itemObj.penguin.Id);
Destroy(itemObj.gameObject);
break;
}
}
if(callback!=null) {
callback();
}
}
}
- 매 프레임마다 호출
- 지금 이 스크립트는 편집모드에서 변경사항을 반영해야하기 때문에 radius도 새롭게 받아준다
- 만약 아이템이 범위내에 있고, 먹지 않았고 해당 키를 누른다면, 상호작용을 시작한다
- 키 용도가 아이템을 얻는 것이라면 인벤토리에 아이템을 추가하고, 해당 아이템은 씬에서 삭제시킨다
✏️onTriggerEnter / onTrigerExit
private void OnTriggerEnter(Collider other) {
if(!isDone&&other.gameObject!=this) {
switch(type) {
case KeyTriggerType.GetItem:
{
isActive=true;
break;
}
}
}
}
private void OnTriggerExit(Collider other) {
if(other.gameObject!=this) {
switch(type) {
case KeyTriggerType.GetItem:
{
isActive=false;
break;
}
}
}
}
- 유니티 이벤트 함수
-
이 함수를 통해 충돌을 감지한다. 여기선 아이템 인식 범위 내에 플레이어가 들어왔는지 안왔는지의 용도로 쓰인다
- OnTriggerEnter
- 두 개의 게임오브젝트가 충돌할 때 호출
- 만약 먹지 않았고, 충돌체가 다른 아이템이 아닌경우, 즉 플레이어와 충돌된 경우, 키트리거타입이 아이템을 얻는 용도이면 아이템을 먹을 수 있도록 isActive를 활성화시킨다
- OnTriggerExit
- 두 개의 게임오브젝트가 충돌을 멈춘 후 호출
- 플레이어와 아이템간의 충돌이 사라지면 isActive를 꺼서 줍지못하도록 한다
Inventory
💻전체 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Inventory : MonoBehaviour
{
#region VARIABLES & PROPERTIES
GameObject _inventoryPanel;
GameObject _slotPanel;
public GameObject itemSlot;
private int _slotSize;
public List<Item> itemList = new List<Item>();
public List<GameObject> slotObjList = new List<GameObject>();
public List<Slot> slotList = new List<Slot>();
#endregion
#region UNITY_EVENTS
private void Start() {
_slotSize = DataManager.SLOT_SIZE;
_inventoryPanel = GameObject.Find("InventoryPopup");
_slotPanel = GameObject.Find("Content").gameObject;
for (int i = 0; i < _slotSize; i++) {
GameObject slotPrefab = Instantiate(itemSlot);
slotObjList.Add(slotPrefab);
var slot = slotObjList[i].GetComponent<Slot>();
slot.Id = i;
slotList.Add(slot);
slotObjList[i].transform.SetParent(_slotPanel.transform, false);
}
}
private void Update() {
}
#endregion
#region MAIN_FUNCTIONS
public void AddItem(int id) {
Item itemToAdd = ItemManager.Inst.GetItemById(id);
// 아이템이 인벤토리에 있고, 쌓을 수 있는 경우
if (itemToAdd.isStackable && CheckIfItemIsInInventory(itemToAdd)) {
for (int i = 0; i < slotList.Count; i++) {
if (itemToAdd.Id == slotList[i].item.Id) {
ItemUIObject data = slotObjList[i].transform.GetChild(0).GetComponent<ItemUIObject>();
data.Amount += itemToAdd.value;
break;
}
}
}
// 아이템이 인벤토리에 없는 경우
else {
for (int i = 0; i < slotList.Count; i++) {
if(slotList[i].item == null) {
var itemUI = Instantiate(Resources.Load<ItemUIObject>("Prefabs/ItemUIObject"), slotList[i].transform);
itemUI.Amount = itemToAdd.value;
itemUI.itemImage.sprite = itemToAdd.sprite;
itemUI.item = itemToAdd;
itemUI.slotId = i;
slotList[i].item = itemToAdd;
itemList.Add(itemToAdd);
break;
}
}
}
}
public bool CheckIfItemIsInInventory(Item item) {
for (int i = 0; i < itemList.Count; i++) {
if (item.Id == itemList[i].Id) {
return true;
}
}
return false;
}
#endregion
}
📝VARIABLES & PROPERTIES
GameObject _inventoryPanel;
GameObject _slotPanel;
public GameObject itemSlot;
private int _slotSize;
public List<Item> itemList = new List<Item>();
public List<GameObject> slotObjList = new List<GameObject>();
public List<Slot> slotList = new List<Slot>();
GameObject _inventoryPanel;
: 인벤토리 패널, 여기선 변수를 만들어 놓긴 했지만 쓰이진 않음.. 지워야하나GameObject _slotPanel;
: 슬롯 패널-
public GameObject itemSlot;
: 슬롯 변수 private int _slotSize;
: 슬롯 사이즈 변수public List<Item> itemList = new List<Item>();
: 인벤토리에 들어있는 아이템 리스트public List<GameObject> slotObjList = new List<GameObject>();
: 슬롯 프리팹 가지고 있는 슬롯 오브젝트 리스트public List<Slot> slotList = new List<Slot>();
: 슬롯 리스트
📝UNITY_EVENTS
private void Start() {
_slotSize = DataManager.SLOT_SIZE;
_inventoryPanel = GameObject.Find("InventoryPopup");
_slotPanel = GameObject.Find("Content").gameObject;
for (int i = 0; i < _slotSize; i++) {
GameObject slotPrefab = Instantiate(itemSlot);
slotObjList.Add(slotPrefab);
var slot = slotObjList[i].GetComponent<Slot>();
slot.Id = i;
slotList.Add(slot);
slotObjList[i].transform.SetParent(_slotPanel.transform, false);
}
}
- 슬롯 사이즈는 DataManager에 상수로 만들어둠. 거기서 바꿀 수 있음
- 게임 오브젝트중 인벤토리 패널을 찾아와 인벤토리패널 변수에 저장(여기선 안쓰임)
-
Content가 슬롯이 붙을 바로 위 부모 오브젝트로 슬롯패널 변수에 저장
- 정해둔 슬롯 사이즈만큼 패널에 슬롯을 붙여주어야 함
- itemSlot은 슬롯의 UI프리팹이다. 이것을 생성하여 슬롯 오브젝트 리스트에 넣어준다.
- itemSlot 프리팹에는 Slot클래스가 붙어있는데 이것이 슬롯이다. 이 슬롯이 가지고 있는 아이디에 번호를 붙여주고 슬롯리스트에도 넣어준다.
- 만들어준 슬롯 프리팹(slotObjList[i])을 패널에 붙여준다.
- 정리하면, 슬롯 프리팹을 slotObjList에 넣어주고, 프리팹 안에 있는 slot에 아이디 부여, slot만 따로 리스트에 또 추가해준 것
📝MAIN_FUNCTIONS
✏️AddItem
public void AddItem(int id) {
Item itemToAdd = ItemManager.Inst.GetItemById(id); // 아이디로 이 아이템이 콜라인지, 펭귄인지 뭔지 알아온다
// 아이템이 인벤토리에 있고, 쌓을 수 있는 경우
if (itemToAdd.isStackable && CheckIfItemIsInInventory(itemToAdd)) {
for (int i = 0; i < slotList.Count; i++) { // 슬롯 개수만큼 돌면서 해당 아이템을 찾는다
if (itemToAdd.Id == slotList[i].item.Id) { // 찾을 땐 슬롯리스트의 아이디 이용
ItemUIObject data = slotObjList[i].transform.GetChild(0).GetComponent<ItemUIObject>(); // 해당 슬롯UI를 가져옴
data.Amount += itemToAdd.value; // 개수를 나타내는 amount를 value만큼 증가시켜줌
break;
}
}
}
// 아이템이 인벤토리에 없는 경우
else {
for (int i = 0; i < slotList.Count; i++) {
if(slotList[i].item == null) { // 비어있는 슬롯을 찾는다
var itemUI = Instantiate(Resources.Load<ItemUIObject>("Prefabs/ItemUIObject"), slotList[i].transform); // 슬롯 프리팹을 가져와 붙인다
itemUI.Amount = itemToAdd.value; // 이때 프리팹의 변수를 초기화 시켜준다. 개수는 value만큼
itemUI.itemImage.sprite = itemToAdd.sprite; // 이미지는 아이템 이미지 가져오고
itemUI.item = itemToAdd;
itemUI.slotId = i;
slotList[i].item = itemToAdd; // 슬롯 리스트에 아이템도 할당
itemList.Add(itemToAdd); // 인벤토리에 들어있는 아이템을 담은 리스트에도 추가
break;
}
}
}
}
✏️CheckIfItemIsInInventory
- 인벤토리에 아이템이 있는지 검사하는 함수
public bool CheckIfItemIsInInventory(Item item) {
for (int i = 0; i < itemList.Count; i++) { // 인벤토리에 들어있는 아이템을 담은 리스트를 검사한다
if (item.Id == itemList[i].Id) { // 아이디로 아이템 검사하는데, 아이디 일치 즉 아이템 있으면 true 리턴
return true;
}
}
return false;
}
😃결과
임시 캐릭터지만 귀여운 북극곰
같이 일하는 상사같은 오빠가 만든 인벤토리
Tab을 누르면 주변의 아이템을 먹을 수 있다. 임시로 생선과 펭귄을 아이템으로 설정해놓았다. 펭귄은 한번 먹을 때 2개씩 자원이 쌓이고, 생선은 하나씩 쌓이도록 값(item.value)을 설정했다.
이제 오브젝트 풀링으로 아이템을 생성하고 쓰고 없애는 것으로 다시 바꿔볼 예정이다