인벤토리를 만들기 위한 기본 구조를 살펴보겠습니다.
인벤토리가 어떻게 작동하는지 알아보기 위한 아주 간단한 프로그램입니다.
사용된 아이콘 및 슬롯 이미지
https://assetstore.unity.com/packages/2d/gui/icons/rpg-inventory-icons-56687
https://www.kenney.nl/assets/ui-pack-rpg-expansion
인벤토리 기본 구조
Inventory 설정 (빈 GameObject 나 UI > Panel 중 하나로)
Bag 설정 (UI > Panel => Image에 배경 넣고 Grid Layout Group Component 추가)
Slot 설정 (UI > Image => Image에 슬롯이 되는 배경 이미지 넣기)
SlotItem 설정 (UI > Image)
구조나 모양은 크게 중요하지 않습니다. 이런 모양으로 만들어진다 정도일 뿐 정답은 없습니다.
각 설정의 Anchor의 위치만 잘 맞춰 주시면 됩니다.
아이템이 인벤토리에 저장되는 과정은
플레이어가 아이템을 얻으면 인벤토리에 등록된 슬롯에 빈자리가 있는지 검사하여 들어갈 자리가
남아 있다면 item 리스트에 획득한 아이템 정보를 저장하며 또한 빈 슬롯에 아이템 정보를 넘겨
아이템의 이미지를 슬롯에 표시하는 방식입니다.
using UnityEngine;
[CreateAssetMenu]
public class Item : ScriptableObject
{
public string itemName;
public Sprite itemImage;
}
아이템 정보를 담을 Item Model입니다.
Item.cs를 저장 후 유니티의 Project창에서 마우스 오른쪽 클릭하시면 Item 파일을 생성하실 수 있습니다.
생성된 Item 정보
Item 이름 및 이미지를 등록해 주시면 됩니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Slot : MonoBehaviour
{
[SerializeField] Image image;
private Item _item;
public Item item {
get { return _item; }
set {
_item = value;
if (_item != null) {
image.sprite = item.itemImage;
image.color = new Color(1, 1, 1, 1);
} else {
image.color = new Color(1, 1, 1, 0);
}
}
}
}
Slot.cs 는 SlotItem에 넣어 주시면 됩니다.
위에 그림과 같이 Image를 드래그해서 Slot Script의 Image에 넣어 주세요.
[SerializeField] Image image;
위에 이미지처럼 Image Component를 담을 곳
get { return _item; }
슬롯의 item 정보를 넘겨줄 때 사용됩니다.
set {
_item = value;
}
item에 들어오는 정보의 값은 _item에 저장됩니다.
if (_item != null) {
image.sprite = item.itemImage;
image.color = new Color(1, 1, 1, 1);
} else {
image.color = new Color(1, 1, 1, 0);
}
/* 이 부분은 바로 밑 코드의 AddItem()과 FreshSlot() 함수에서 사용됩니다. */
밑의 Inventory.cs 의 List<Item> items에 등록된 아이템이 있다면
itemImage를 image에 저장 그리고 Image의 알파 값을 1로 하여 이미지를 표시합니다.
만약 item이 null 이면 (빈슬롯 이면) Image의 알파 값 0을 주어 화면에 표시하지 않습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Inventory : MonoBehaviour
{
public List<Item> items;
[SerializeField]
private Transform slotParent;
[SerializeField]
private Slot[] slots;
#if UNITY_EDITOR
private void OnValidate() {
slots = slotParent.GetComponentsInChildren<Slot>();
}
#endif
void Awake() {
FreshSlot();
}
public void FreshSlot() {
int i = 0;
for (; i < items.Count && i < slots.Length; i++) {
slots[i].item = items[i];
}
for (; i < slots.Length; i++) {
slots[i].item = null;
}
}
public void AddItem(Item _item) {
if (items.Count < slots.Length) {
items.Add(_item);
FreshSlot();
} else {
print("슬롯이 가득 차 있습니다.");
}
}
}
Inventory.cs를 등록 후 이미지처럼 Bag을 넣어 주시면 Slots이 자동으로 채워집니다.
public List<Item> items;
아이템을 담을 리스트
private Transform slotParent;
위의 이미지처럼 Slot의 부모가 되는 Bag을 담을 곳
private Slot[] slots;
Bag의 하위에 등록된 Slot을 담을 곳
private void OnValidate() {
slots = slotParent.GetComponentsInChildren<Slot>();
}
OnValidate()의 기능은 유니티 에디터에서 바로 작동을 하는 역할을 합니다.
처음 인벤토리에 소스를 등록하시면 Console창에 에러가 뜨지만 Bag을 넣어 주시면
slots에 Slot들이 자동 등록됩니다.
void Awake() {
FreshSlot();
}
게임이 시작되면 items에 들어 있는 아이템을 인벤토리에 넣어 줍니다.
public void FreshSlot() {
int i = 0;
for (; i < items.Count && i < slots.Length; i++) {
slots[i].item = items[i];
}
for (; i < slots.Length; i++) {
slots[i].item = null;
}
}
FreshSlot의 역할은 아이템이 들어오거나 나가면 Slot의 내용을 다시 정리하여 화면에 보여 주는 기능을 합니다.
int i = 0을 외부에 선언한 건 두 개의 For 문에 같은 i의 값을 사용하기 위해서입니다.
첫 for문은 items에 들어 있는 수만큼 slots에 차례대로 item을 넣어 줍니다.
slot에 item이 들어가면 Slot.cs에 선언된 item의 set 안의 내용이 실행되어 해당 슬롯에 이미지를 표시하게 됩니다.
"i < items.Count && i < slots.Length" i의 값이 items와 slots 두 개의 값 보다 작아야만 돌아가는 구조입니다.
혹여 items의 수가 slots 수보다 많으면 안 되니깐요...
slot에 아이템을 다 저장하고 난 후에도 slot이 남아 있다면 다음 for문이 실행되어 빈 슬롯들은 모두 null 처리하게
됩니다.
/* 위쪽 Slot.cs에 보시면 Item이 Null 일경우 Image 알파 값 0을 주어 Image를 숨겨 버립니다 */
두 번째 for문이 돌 땐 i의 값은 첫 번째 for문에서 사용되고 남은 값부터 시작하게 됩니다. 즉 여기서 i의 값은 슬롯의 순서이며 아이템을 차례대로 i번째 슬롯에 넣고 나머지 i번째 슬롯은 null을 주어 빈 슬롯으로 표시되게 합니다.
public void AddItem(Item _item) {
if (items.Count < slots.Length) {
items.Add(_item);
FreshSlot();
} else {
print("슬롯이 가득 차 있습니다.");
}
}
아이템을 획득할 경우 AddItem을 불러와 넣어 주시면 됩니다.
여기에선 별도의 아이템 클릭을 통한 아이템 획득 코드는 없습니다. 직접 만들어 테스트 해보시기 바라며
인벤토리의 테스트는 위에서 만든 Item
예로 Axe라는 Item을 Inventory items 리스트에 넣어 주시면 됩니다.
그리고 Play 시켜 주시면
인벤토리에 아이템이 표시됩니다.
이상 인벤토리의 기본이 되는 구조를 만들어 봤습니다.
다른분들이 만들어 둔 좋은 인벤토리 소스도 많이 있겠지만 그냥 가져다 사용하시면 자신의 입맛에 맞는 인벤토리는
만들기 힘들 것입니다.
이런 최소한의 기본 구조를 가지고 조금씩 확장해 나가면 자신만의 인벤토리를 완성해 나갈 수 있을 겁니다.
% 아이템 클릭 시 인벤토리로 가져오는 코드입니다. %
public interface IObjectItem {
Item ClickItem();
}
IObjectItem이라는 인터페이스를 하나 생성 합니다.
이 인터페이스의 기능은 아이템을 클릭 시 그 아이템이 가지고 있는 정보를 넘겨주는 역할을 합니다.
이제 빈 오브젝트를 하나 만들어 위 이미지 처럼 각각의 Component를 넣어 주세요.
이 오브젝트는 아이템 하나의 역할을 합니다. (위 이미지 예처럼 이 오브젝트는 "물약"의 역할을 할 것입니다)
ObjectItem.cs는 아래 코드 입니다.
public class ObjectItem : MonoBehaviour, IObjectItem {
[Header("아이템")]
public Item item;
[Header("아이템 이미지")]
public SpriteRenderer itemImage;
void Start() {
itemImage.sprite = item.itemImage;
}
public Item ClickItem() {
return this.item;
}
}
위에서 만들어 둔 인터페이스를 포함하고 있습니다.
모든 아이템의 구성은 이렇게 만들어 주시면 됩니다.
위 코드 중 ClickItem() 함수는 인터페이스입니다. 기억해 두세요..
이제 화면에 아이템을 배치 후 마우스 왼쪽 버튼 또는 스마트폰의 경우 화면 터치 시 아이템을 인벤토리에 넣어 보겠습니다.
public class TestPlayer : MonoBehaviour {
[Header("인벤토리")]
public Inventory inventory;
void Update() {
if (Input.GetMouseButtonDown(0)) {
Vector2 pos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
RaycastHit2D hit = Physics2D.Raycast(pos, Vector2.zero);
if (hit.collider != null) {
HitCheckObject(hit);
}
}
}
void HitCheckObject(RaycastHit2D hit) {
IObjectItem clickInterface = hit.transform.gameObject.GetComponent<IObjectItem>();
if (clickInterface != null) {
Item item = clickInterface.ClickItem();
print($"{item.itemName}");
inventory.AddItem(item);
}
}
}
빈 오브젝트(Player)를 하나 만들어 코드를 넣어 주시면 됩니다. 아래 이미지 참조
위 이미지처럼 Player 오브젝트의 인벤토리 부분에 만들어 둔 이벤토리를 넣어 주세요. (Inventory.cs 가 있는 오브젝트를 넣어 주셔야 합니다)
RaycastHit2D hit = Physics2D.Raycast(pos, Vector2.zero);
if (hit.collider != null) {
HitCheckObject(hit);
}
게임을 플레이하면 RaycaseHit2D가 클릭된 곳에 오브젝트가 있나 체크합니다.
만약 오브젝트를 클릭했다면 HitCheckObject(hit) 함수로 hit 정보를 넘겨줍니다.
void HitCheckObject(RaycastHit2D hit) {
IObjectItem clickInterface = hit.transform.gameObject.GetComponent<IObjectItem>();
if (clickInterface != null) {
Item item = clickInterface.ClickItem();
print($"{item.itemName}");
inventory.AddItem(item);
}
}
IObjectItem clickInterface = hit.transform.gameObject.GetComponent<IObjectItem>();
클릭된 오브젝트의 IObjectItem 인터페이스를 clickInterface에 넘겨줍니다.
물론 클릭된 오브젝트가 아이템이 아닐 수 있기에 null 일 수도 있습니다.
if (clickInterface != null) {
Item item = clickInterface.ClickItem();
print($"{item.itemName}");
inventory.AddItem(item);
}
만약 clickInterface가 null 이 아니면 (인터페이스를 가지고 있다면)
Item item = clickInterface.ClickItem();
item에 클릭된 오브젝트의 아이템 정보를 넘겨줍니다.
위쪽 ObjectItem.cs의 ClickItem()을 참고하세요.
@ 인터페이스를 사용하는 이유는 가령 100개의 아이템이 있다면 클릭된 아이템이 무엇인지 확인할 필요 없이 그 아이템이 가지고 있는 인터페이스만 가져와 아이템의 정보를 넘겨주면 되기 때문입니다. @
inventory.AddItem(item);
마지막으로 만들어 둔 인벤토리에 아이템을 넣어 주시면 됩니다.
아이템을 클릭 시 정상적으로 작동하는 걸 알 수 있습니다.
이 글을 작성하면서 테스트용 인벤토리는 만들지 않아 클릭 시 인벤토리 적용 이미지는 없습니다.
이상 클릭 후 아이템을 인벤토리에 넣는 부분 있었습니다. 도움이 되었으면 합니다.
'Unity' 카테고리의 다른 글
유니티 C# - Interface 게임에 적용해 보기 (10) | 2024.07.09 |
---|---|
유니티 c# - 델리게이트(Delegate) Action 사용해 보기 (0) | 2024.07.09 |
유니티 - 게임 데이터 JSON으로 저장하기 (4) | 2024.07.09 |
유니티 - 오브젝트 풀링(Object pooling) (9) | 2024.07.09 |
유니티 (Unity) - 파일 데이터 비동기 방식으로 읽어 오기 (0) | 2024.07.09 |