24-2의 동글콩콩에 이어, 게임제작 동아리 ExpMakE의 25-1 학기 프로젝트에 참여하게 되었다. 이번엔 2D 액션 플랫포머와 공포 쯔꾸르의 형식이 합쳐진 게임을 개발하게 되었는데, 우선 인벤토리 시스템 전반의 개발을 맡게 되었다.
ScriptableObject란?
우선 이번 인벤토리 구현에 사용하게 될 ScriptableObject란, 클래스 인스턴스와는 별도로 대량의 오브젝트 데이터들을 관리하는 데 유용한 데이터 컨테이너로 프로젝트의 메모리 최적화에 도움을 준다. 프리팹을 인스턴스화할 때마다 데이터의 Clone을 생성하여 중복 데이터를 만드는 대신에, 데이터를 애셋 형태로 저장하여 런타임 중 참조를 통해 접근 가능하다.
더 자세한 내용은 유니티 공식 문서를 참조.
아이템 구현
일단 인벤토리에 들어갈 아이템을 만드는 것부터 시작한다.
이 게임에서 아이템은 소비 아이템, 장비 아이템(무기, 방어구 등), 퀘스트 아이템(열쇠 등), 엔딩 분기에 쓰이는 아이템으로 크게 나뉠 것으로 예상된다. 이에 맞게 아이템의 타입을 설정한다.
// ItemType.cs
public enum ItemType {
Consumable,
Equipment,
Quest,
Ending,
ETC
}
그리고 이 아이템의 정보를 저장할 ItemData 클래스를 작성한다.
3번 라인의 CreateAssetMenu가 바로 ScriptableObject를 이용하는 것으로, 이런 식으로 작성하면 이후 프로젝트 창에서 Create 메뉴를 통해 아이템 데이터를 가지는 애셋을 생성할 수 있다.
// ItemData.cs
using UnityEngine;
[CreateAssetMenu(fileName = "NewItem", menuName = "Inventory/Item")]
public class ItemData : ScriptableObject
{
public int id;
public string itemName;
[TextArea] public string description;
public Sprite icon;
public ItemType itemType;
// Equipment
public int attackPower;
public int defensePower;
// Consumable
public int healAmount;
}
또한 아이템들을 로드하고 관리할 데이터베이스 클래스를 작성한다. 이 또한 ScriptableObject를 활용해 간단히 관리 가능하도록 한다.
// ItemDatabase.cs
using UnityEngine;
using System.Collections.Generic;
[CreateAssetMenu(fileName = "ItemDatabase", menuName = "Inventory/ItemDatabase")]
public class ItemDatabase : ScriptableObject
{
public List<ItemData> items;
public ItemData GetItemById(int id)
{
return items.Find(item => item.id == id);
}
public ItemData GetItemByName(string name)
{
return items.Find(item => item.itemName == name);
}
public void AddItem(ItemData item)
{
if (!items.Contains(item))
{
items.Add(item);
}
}
public void RemoveItem(ItemData item)
{
if (items.Contains(item))
{
items.Remove(item);
}
}
}
아이템 획득 구현
우선 캐릭터가 아이템을 획득하는 상황을 만들기 위해서, 땅과 조작할 수 있는 플레이어 오브젝트를 임시로 만들었다. 플레이어 조작 부분은 내 담당 파트가 아니니 다른 쪽에서 완성되면 대체할 예정이라 대충 구현하였다. 중요한 게 아니므로 코드는 접어 두었다.
PlayerMovement.cs
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
bool isGround = true;
[SerializeField] float jumpPower = 5.0f; // 점프 힘
[SerializeField] float moveSpeed = 5.0f; // 이동 속도
private Rigidbody2D rb; // Rigidbody2D 컴포넌트
void Start()
{
rb = GetComponent<Rigidbody2D>(); // Rigidbody2D 컴포넌트 가져오기
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space) && isGround) // 스페이스바를 눌렀고 바닥에 있을 때 점프
{
Jump();
}
if (Input.GetKey(KeyCode.A))
{
rb.linearVelocity = new Vector2(-moveSpeed, rb.linearVelocity.y); // 왼쪽으로 이동
}
else if (Input.GetKey(KeyCode.D))
{
rb.linearVelocity = new Vector2(moveSpeed, rb.linearVelocity.y); // 오른쪽으로 이동
}
else // 아무 키도 누르지 않을 때
{
rb.linearVelocity = new Vector2(0, rb.linearVelocity.y);
}
}
void Jump() {
rb.AddForce(Vector2.up * jumpPower, ForceMode2D.Impulse);
isGround = false; // 중복 점프 방지
}
void OnCollisionEnter2D(Collision2D collision) {
if (collision.gameObject.CompareTag("Ground"))
{
isGround = true;
}
}
}
해당 플레이어 오브젝트에는 인벤토리를 연결하여, 아이템과 상호작용할 수 있도록 한다.
// Inventory.cs
using UnityEngine;
using System.Collections.Generic;
public class Inventory : MonoBehaviour {
// public ItemDatabase itemDatabase; // 아이템 데이터베이스
public List<InventoryItem> inventoryItems; // 인벤토리 아이템 리스트
private void Start()
{
inventoryItems = new List<InventoryItem>();
}
public void AddItem(ItemData itemData, int quantity = 1)
{
InventoryItem existingItem = inventoryItems.Find(item => item.itemData.id == itemData.id);
if (existingItem != null)
{
existingItem.quantity += quantity;
}
else
{
InventoryItem newItem = new InventoryItem(itemData, quantity);
inventoryItems.Add(newItem);
}
Debug.Log("현재 인벤토리 아이템 수: " + inventoryItems.Count);
}
public void RemoveItem(ItemData itemData, int quantity = 1)
{
InventoryItem existingItem = inventoryItems.Find(item => item.itemData.id == itemData.id);
if (existingItem != null)
{
existingItem.quantity -= quantity;
if (existingItem.quantity <= 0)
{
inventoryItems.Remove(existingItem);
}
}
}
}
아이템 쪽에는 아이템 획득에 관한 스크립트를 작성한다. 캐릭터가 접근하면 외곽선으로 하이라이트 표시가 되고, 그 상태에서 F키 입력 시 아이템이 획득 처리되고 소멸한다.
// ItemObject.cs
using UnityEngine;
[RequireComponent(typeof(Collider2D))]
public class ItemObject : MonoBehaviour
{
public ItemData itemData; // 아이템 데이터
public int quantity = 1; // 아이템 수량
public Material outlineMaterial; // 하이라이트용 머티리얼
private Material defaultMaterial; // 원래 머티리얼
private SpriteRenderer spriteRenderer;
private void Start()
{
spriteRenderer = GetComponent<SpriteRenderer>();
Debug.Log(spriteRenderer);
// Collider2D 컴포넌트가 없으면 추가
if (GetComponent<Collider2D>() == null)
{
gameObject.AddComponent<BoxCollider2D>();
}
if (spriteRenderer != null)
{
spriteRenderer.sprite = itemData.icon;
defaultMaterial = spriteRenderer.material;
}
}
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Player"))
{
spriteRenderer.material = outlineMaterial; // 외곽선 표시
}
}
private void OnTriggerStay2D(Collider2D other)
{
Inventory inventory = other.GetComponent<Inventory>();
if (other.CompareTag("Player") && inventory != null && itemData != null && Input.GetKeyDown(KeyCode.F))
{
inventory.AddItem(itemData, quantity);
Debug.Log($"아이템 획득: {itemData.itemName} x{quantity}");
Destroy(gameObject); // 아이템을 줍고 나면 오브젝트 삭제
}
}
private void OnTriggerExit2D(Collider2D other)
{
if (other.CompareTag("Player"))
{
spriteRenderer.material = defaultMaterial; // 원래 머티리얼로 복원
}
}
}
테스트
- Project 창에서 우클릭 -> Create -> Inventory -> Item과 ItemDatabase를 생성한다. 해당 메뉴는 위의 스크립트들에서 추가한 바 있다.
- 인스펙터 상에서 Item 애셋의 정보를 입력하고, ItemDatabase 애셋에 해당 아이템을 등록한다.
아이템을 배치하고, 스크립트들을 플레이어와 아이템에 적용한 뒤 테스트해 본다.
잘 되는 것 같다. 이제 UI를 구현할 차례다.