개발/개발일지

3D 게임 개발 공부 7 - 인벤토리, 아이템 구현

메피카타츠 2023. 4. 12. 23:58

어제와 오늘은 인벤토리와 아이템과 관련된 기능을 구현했다.

이전에 간단하게 RPG를 만들어 본 경험이 있어서 어렵지 않게 구현할 수 있을 것이라고 생각했는데, 인벤토리와 아이템도 제대로 된 구조를 짜기 위해서는 꽤나 많은 노력이 필요했다. 거기다 3D게임이다보니, 모든 게임이 그런 것은 아니지만 아이템을 장착할 때마다 캐릭터의 모델링을 바꿔주는 등의 작업이 필요했기 때문에 예상했던 것보다 굉장히 어려웠다. 모델링 관련 지식이 부족하기 때문인지 저번에 구현했던 전투 시스템을 구현하는 것보다 어려웠던 것 같다.

 

오늘은 어쩌다보니 시간도 꽤나 늦어져서... 이번에는 그림 없이 글로만 정리해야할 것 같다. 먼저 작성한 스크립트 17개에 대한 정리부터 시작하겠다.

 

ItemObjectDatabase: ItemObject들을 보관하는 데이터베이스이며 각각의 아이템에 id를 매겨줌(기본적으로 1개만 존재 가능하나 부위별로 나누어 id가 겹치지 않도록 해주면 여러 개도 가능)
ItemObject: 아이템을 찍어낼 수 있는 기본 틀
Item: ItemObject의 정보를 가지고 있는, 실질적으로 유저에게 드롭되고 보여지는 아이템이 가지게 되는 스크립트 (id, 이름, 효과와 같은 게임 내 처리에서 필요한 정보를 담고 있음(각각의 생성된 아이템을 처리할 때는 이 Item을 사용))
=> 여기서 id는 ItemObject, 즉 틀에 대한 id를 가지고 있는 것임.
ItemBuff: Item 내에 있는 효과에 대한 정보를 생성하고 저장(힘, 스태미너 등의 스탯)

 

 

InventoryObject: 가방, 장비창, 퀵슬롯, 상자 등 아이템이 들어가는 InventorySlot을 여러 개 가질 수 있는, 흔히 생각하는 인벤토리의 정보를 가지고 있는 ScriptableObject이다.
Inventory: InventorySlot들에 대한 정보를 가지고 있다.
InventorySlot: InventoryObject 내에서 아이템을 놓을 수 있는 각각의 한 칸에 대한 정보를 가지고 다.
InventoryUI: 시작할 때 각각의 InventorySlot을 초기화해줌(parent, 슬롯 내 아이템의 이미지, 개수 등 표시). 또한, 아이템을 드래그하는 이벤트 등도 구현되어있으며, 이 InventoryUI를 상속받아 구현하도록 하는 추상 클래스이다.

 

 

DynamicInventoryUI_New: InventoryUI를 상속받아 구현하며, 플레이어의 인벤토리나 혹은 상자와 같이 칸의 수를 변경하여 적용할 수 있는 인벤토리를 구현하는 데 사용한다.

StaticInventoryUI_New: InventoryUI를 상속받아 구현하며, 플레이어의 장비창처럼 정해진 개수의 정해진 위치에 인벤토리가 있는 경우에 사용하는 인벤토리를 구현하는 데 사용한다.

 

 

TestItem: ItemObjectDatabase에 있는 아이템을 랜덤으로 생성하고 전부 없애주는 등 기본적인 기능을 테스트하기 위한 스크립트이다.

 

 

EquipmentCombiner: 장비의 부위마다 bone들이 있고, 장비들마다 bone이 겹치는 부위가 있는데, 따로 장비를 추가하게 되면 중복되는 bone들이 많아지게 되기 때문에 효율적으로 bone을 추가하기 위해 사용되는 스크립트다. 이름을 HashCode로 변환하여 Dictionary에 저장하기 때문에 중복되는 bone이 있어도 하나만 저장된다. bone들의 최상위 부모인 rootGameObject가 있고, 본들의 이름을 Hash로 변환하여 int형 값을 통해 해당 bone의 transform 정보를 저장하는 Dictionary 데이터를 저장하고 있다. 이외에 처리에 필요한 각종 함수들이 구현되어 있다.

PlayerEquipment: 시작할 때, 혹은 아이템을 장착하거나 해제할 때 EquipmentCombiner의 기능을 사용하여 캐릭터의 모델링 관련 처리를 해주는 스크립트다. 캐릭터의 장비 등은 연결된 bone에 따라 움직이는 애니메이션이 다르기 때문에 SkinnedMeshRenderer를 사용하지만, 무기 등은 통째로 움직이기 때문에 MeshRenderer를 사용한다. 이런 것들을 분리하여 처리해주기도 한다. SkinnedMeshRenderer는 EquipmentCombiner를 통해 하나의 Transform에 bone들을 추가하여 해당 transform을 ItemInstances에 추가하여 저장한다.

ItemInstances: bone들로 구성된 SkinnedMeshRenderer 혹은 MeshRenderer를 컴포넌트로 가지고 있는 transform의 정보를 부위별로 저장한다.

 

 

IInteractable: 상호작용할 수 있도록 구현할 때 상속받아 구현하도록 사용하는 인터페이스이다.

PickupItem: 우클릭으로 상호작용하여 주울 수 있는 아이템에 등록하는 스크립트로, 어떤 아이템으로 설정할 것인지, 주울 수 있는 거리 내에 있는지 등을 판별하는 기능을 제공한다.
GroundItem: 근처에 가면 주울 수 있는 아이템에 등록하는 스크립트로, 어떤 아이템을 등록할 것인지의 정보가 등록되어 있다.

 

 

오늘은 대부분의 기능을 직접 씬 내에서 등록해보고 사용해봤기 때문에 새로 알게 된 내용, 어려웠던 내용에 대해서만 정리해보겠다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "New Item Database_New", menuName = "Inventory System/Items/DataBase_New")]
public class ItemObjectDatabase_New : ScriptableObject
{
    public ItemObject_New[] _itemObjects;

    public void OnValidate()
    {
        for (int i = 0; i < _itemObjects.Length; ++i)
        {
            _itemObjects[i]._data._id = i;
        }
    }
}

먼저 ScriptableObject와 CreateAssetMenu에 대해서 알게 되었다. ScriptableObject는 적은 양의 데이터를 유니티에서 효율적으로 처리할 수 있도록 해주는 것 같다. CreateAssetMenu를 통해 우클릭으로 해당 오브젝트를 생성할 수도 있다. 여기서는 아이템이라든지, 아이템을 관리해주는 데이터베이스 등을 프로젝트 내에 오브젝트로 생성해서 편리하게 등록하고 관리할 수 있었다.

 

public InventorySlot_New GetEmptySlot()
    {
        return Slots.FirstOrDefault(i => i._item._id < 0);
    }

FirstOrDefault라는 것에 대해서도 배웠다. for문을 활용하지 않고 한 줄로 간단하게 for문을 사용하는 것과 같은 효과를 내는 것 같다. 위의 코드는 Slots[i]._item._id를 순회하면서 0보다 작은 것이 있는지를 찾는 느낌이다. 한 눈에 보기에 명료한 코드는 아니라고 느껴지지만 간단하고 직관적으로 이해할 수 있는 코드인 것 같다.

이것과 비슷하게 SingleOrDefault도 있는데, 이 안에 해당하는 값이 딱 1개만 있는지를 찾아오는 함수라고 한다. 중복되는 값이 없어야 하는 경우에 사용하면 좋을 것 같은데, 하나만 있는지 확인하기 위해서는 전체를 순회해야 할테니까 굉장히 비효율적일 것이라는 생각이 들었다. 다만 그만큼 안전성을 확보할 수 있는 것 같다.

 

public Action<InventorySlot_New> _OnPostUpdate;

Action에 대해서도 알게 되었는데, delegate와 비슷하지만 약간 다른 부분이 있는 것 같다. 반환값이 없는 메소드를 추가할 수 있다고 하는데... 등록해놓고 외부에서 해당 Action에 함수들을 추가하여 여러 기능을 수행할 수 있도록 해주는 것 같다.

 

[Min(1), SerializeField] protected int _numberOfColumn = 4;

이런 식으로 Min(1)과 같이 최소값을 지정해줄 수 있는 것도 새로 알았다.

 

protected void AddEvent(GameObject go, EventTriggerType type, UnityAction<BaseEventData> action)
    {
        EventTrigger trigger = go.GetComponent<EventTrigger>();
        if (!trigger)
        {
            Debug.LogWarning("No EventTrigget component found!");
            return;
        }

        EventTrigger.Entry eventTrigger = new EventTrigger.Entry { eventID = type };
        eventTrigger.callback.AddListener(action);
        trigger.triggers.Add(eventTrigger);
    }

그리고 이벤트를 추가하는 방법에 대해서도 알게 되었는데, C#에서는 이런식으로 등록하는 방식이 조금 복잡했다. 커서가 들어오거나, 나가거나, 드래그를 하거나 등의 이벤트를 등록할 때 필요한 기능이다.

 

bool isOnUI = EventSystem.current.IsPointerOverGameObject();

그리고 현재 마우스 포인터가 UI 위에 있는지를 확인해주는 함수에 대해서도 알게 되었다.

 

 

이외에도 static을 사용해왔는지 정확하게 무엇이라고 설명하기 어려워서 찾아보았다. static은 하나만 가질 수 있는 것이다. 예를 들어 static을 사용해서 변수를 생성하면, 해당 변수를 가지고 있는 클래스가 여러 개 있더라도 그 변수는 공용으로 사용된다는 것이다. 아! 그리고 이전에 배웠던 것이 생각났는데, 클래스를 생성하지 않아도 static 변수는 사용이 가능하다.

 

그리고 const와 readonly의 차이점도 알아보았다. const는 컴파일 타임 때 값이 고정되는 것이고 readonly는 런타임 때 메모리에 등록되는 시점에 값이 고정되는 것이라는 차이가 있다고 한다. 때문에 성능 상의 이점을 보기 위해서는 const를 사용하는 것이 좋다고 한다. 다만 readonly는 생성자에서 초기화를 하여 값을 정할 수 있기 때문에 훨씬 유동적으로 프로그래밍이 가능하다는 것 같다.

 

이외에는 변수 명명에 대해서도 고민을 했다. 여태까지 전역 변수 앞에는 무조건 _를 붙였는데, 프로퍼티에는 붙이면 안되기도 하고, 헷갈리는 경우가 종종 있기도 해서, 규칙을 어떻게 정할지가 상당히 고민스럽다. 보통은 어떻게 하는지가 상당히 궁금하다.

 

 

이렇게 정리를 해보았는데, 새로 알게된 기능보다, 구조에 대해서 새로 알게 된 것이 굉장히 많은 것 같다. 이전에 아이템 관련 기능을 구현했을 때는, 벌써 4년이나 된 일인 것 같지만, 이런 구조를 신경쓰지 않고 주먹구구식으로 구현했는데, 이런 식으로 구조를 확실하게 정해놓고 프로그래밍을 하는 것이 훨씬 좋은 것 같다. 상당히 어렵고 복잡하고 난해했지만 실무에서 구현하는 방법에 대해서 자세히 배우게 된 것 같다. 다만 내가 생각한 게임에서는 사용되지 않는 기능들이 제법 있어서 필요 이상으로 배운 부분도 있는 것 같은데, 그래도 한 번이라도 구현해 본 경험이 있으면 좋을 것 같다고 생각한다.

 

 

 

어제 오늘 강의를 들으며 굉장히 힘들었는데, 그래도 어떻게든 듣고 구현을 하고 정리까지 완료했다. 이제 강의가 5개 남았기 때문에 내일이면 RPG 구현에 대한 기초적인 공부를 마칠 수 있을 것 같다. UI와 다이얼로그, 퀘스트, 레벨 디자인 등인데... 오늘만큼 구조가 복잡하지는 않았으면 하는 바람이다. 아무튼, 내일까지 강의를 다 듣고, 모레부터 3D개발에 착수할 생각이다. 물론 게임의 시작부터 끝까지는 아니고, 특정 게임의 특정 기능을 구현할 생각이다. 캐릭터의 움직임과, 적 캐릭터와 전투, 스킬, 아이템 획득, 장착, 판매 등 여태까지 배운 내용들을 활용하여 구현해볼 생각이다. 그리고 이것을 포트폴리오로 다시 취업에 도전할 생각이다. 1주일 전만 해도 앞으로 얼마나 성장해있을지 기대된다고 글을 썼었는데, 1주일만에 상당히 많은 것을 배운 것 같다. 지금이라면 리소스와 시간만 있으면 내가 플레이했던 게임들 대부분의 기능을 구현할 수 있을 것 같다. 물론 여전히 어려움은 있겠지만, 예전같았으면 어떻게 저런 걸 구현했지? 라는 생각을 했던 기능들을 지금은 아 이런이런 기능을 사용하면 구현할 수 있겠다 라는 지식정도는 생긴 것 같다. 최근에 즐길만한 게임이 없어서 며칠 전 디아블로3를 구매했는데, 상당히 재미있었다. 디아블로2와도 상당히 비슷한 느낌인데, 확실히 디아블로2에 비하면 최근 게임이라는 느낌이 강하게 들었다. 그리고 이 디아블로3를 플레이하면서 배운 내용들이 계속 생각났다. 이런 부분은 이렇게 구현했겠구나, 이런 부분은 이렇게 구현할 수 있겠구나... 하는 것들 말이다. 덕분에 게임에 조금은 집중할 수 없게 되었지만, 이런 생각들을 하면서도 꽤나 재미있었다. 내일 강의를 마치고 빨리 내가 배운 것들을 활용해서 포트폴리오를 만들고, 내가 이런 것들을 할 수 있다는 것을 어필하고 싶은 마음이 넘친다. 그리고 실무를 하면서 게임 개발에 기여하고, 그 과정에서 많은 난관들을 만나 극복하면서 성장하고 싶다. 빠르면 다음 주 안에도 개발을 완료해서 지원이 가능할 것 같다. 힘내야겠다!