3D 강의에서 아이템과 관련된 내용의 비중이 절반 이상을 차지하는 것 같다.
아이템 자체에 대한 데이터, 아이템과의 상호작용, 인벤토리 등 아이템 하나로 인해 이루어지는 작업이 많기 때문이다.
강의 길이가 짧은 것은 아니지만, 구조 설계와 각 코드들의 역할에 대해 상세하게 짚고 넘어가기에는 부족한 시간이었고, 압축적으로 방대한 양을 다루다 보니 추가적으로 공부하지 않으면 이해했다고 착각한 채로 넘어가게 될 것 같았다.
즉, 전체적인 아이템 시스템을 어떻게 설계했으며, 강의에서 작성된 스크립트들은 각각 무슨 역할을 하며, 어떻게 분리되어 있는지 확인할 필요성을 느꼈다.
이번 TIL은 기록이라기보다는 이해를 위한 필기
1. 아이템 (픽업 가능한 오브젝트)
ItemData
: 아이템 데이터 담당
- 스크립터블 오브젝트.
- 각 아이템이 가지고 있는 데이터.
- dropPrefab과 equipPrefab 등 각 아이템이 발현할 수 있는 모든 유형의 프리팹 데이터를 보유
- ItemObjects가 이 데이터를 가지고 있음
public enum ItemType
{
Resource,
Equipable,
Consumable
}
public enum ConsumableType
{
Hunger,
Health
}
[System.Serializable]
public class ItemDataConsumable
{
public ConsumableType type;
public float value;
}
public class ItemData : ScriptableObject
{
[Header("Info")]
// 기본 데이터
[Header("Stacking")]
public bool canStack;
public int maxStackAmount;
[Header("Consumable")]
public ItemDataConsumable[] consumables;
[Header("Equip")]
public GameObject equipPrefab;
}
- [System.Serializable] 어트리뷰트는 클래스나 구조체가 직렬화될 수 있음을 나타낸다.
- ItemData에서 ItemDataConsumable[] 타입의 데이터인 consumables를 갖고 있는데, ItemData는 모두 유니티의 인스펙터 창에서 값이 조정되어야 한다. consumables 또한 인스펙터에서 값이 할당이 되는데, 이 때 consumables의 요소 하나하나의 값을 설정해줄 필요가 있다. 즉, ItemDataConsumable의 데이터를 인스펙터창에서 직접 설정해야 하므로 [System.Serializable] 어트리뷰트가 필요하다.
ItemObejcts
: 아이템 프리팹이 다른 요소들과 상호작용하는 등의 상황에서 필요한 로직 보유.
- 아이템 데이터를 불러와서 메서드를 실행. (메서드의 실행 내용은 아이템에 관계 없이 동일하며, 여기서 쓰이는 아이템 데이터가 아이템마다 다르므로 서로 다른 실행을 하게 됨)
- 해당 아이템과 관련된 모든 로직을 담는 것이 아니라, 월드에 나와 있는 프리팹에 대한 로직만을 담당함. (월드에 나와 있는 아이템은 그 즉시 플레이어와 상호작용하는 것이 아니라 인벤토리에서 상호작용하게 되므로 비교적 단순한 구조임.)
- 스크립트가 아이템 프리팹에 붙어있기 때문에 이 스크립트를 통해서 오브젝트를 선택할 수 있으므로 ‘ItemObject’라는 이름을 붙인 것으로 추정
- IInteractable 구현: 플레이어가 월드에서 물체들과 상호작용하기 위해서는 InteractionManager를 활용함. 이 .때, 플레이어는 Iinteractable을 구현하는 스크립트를 가진 오브젝트와만 상호작용하므로, 월드에 나와 있는 아이템 프리팹은 이 인터페이스를 꼭 구현해야 함.
- 아이템 특유의 상호작용 문구 출력, InteractionManager가 자신의 OnInteractInput 메서드 내부에서 ItemObjects의 OnInteraction 메서드(아이템을 인벤토리 리스트에 담기) 호출
InteractionManager
: 플레이어와 오브젝트의 상호작용 담당
- 플레이어에 붙어 있음
- Ray를 통해 물체 감지(update), 감지된 물체 curInteractionGameObject로 설정 interaction text, 대상 오브젝트의 IInteractable 클래스에 접근하여 interaction 관련 메서드 호출 (ex. 아이템 수집해서 리스트에 추가)
- 현재 상호작용은 아이템을 대상으로만 이루어지지만, 후에 문 열기, NPC와 대화하기 등의 상호작용 시에도 활용 가능성이 있으므로 IInteractable 인터페이스를 활용함
2. 장착 (플레이어)
Equip
: 최상위 클래스. OnAttackInput() 필수 구현 설정 (EquipTool과 분리시킨 이유는 뭘까… )
EquipTool
: 장착 아이템 프리팹의 컴포넌트가 되는 스크립트.
(이 장착 아이템 프리팹은 월드에 나와 있는 아이템 프리팹과는 구분된다.)
- 주워지는 대상인 상태와 플레이어가 활용하는 상태의 프리팹, 스크립트(데이터 & 로직)를 모두 분리
- ‘장착템‘ 중 공격용인지 자원획득용인지 판별해서 attack input을 실행했을 때 다른 실행을 함 (이 판별 기준은 각 아이템 프리팹의 인스펙터창에서 설정)
- 인터페이스나 상속을 써서 아이템 별로 나눈다면, 데이터를 하나의 스크립트에서, 로직을 하나의 스크립트에서 관리할 수 있을 것 같은데 분리시킨 이유?
- 아이템의 역할이 큰 비중을 차지하지 않고(로직 및 특성 단순) 아이템 각각의 클래스를 따로 구현하는 것이 비효율적일 때 유용한 것 같음.
- 소모템과 장착템의 작동 방식이 너무 다르기 때문에, 인터페이스를 통해 로직을 분리시키기에는 동일한 메서드를 호출한다는 것 자체가 좀 애매?
- 장착의 비중이 크다?
- 아이템 데이터는 아이템들이 공통적으로 가져야 할 정보 카테고리를 설정하고, 그 외에 부차적으로 붙는 것들은 열거형(조건문을 위한)을 통해 서로 다른 로직을 실행하도록
- '장착템'의 특성상, 카메라 참조 필요 (대상 aim)
- OnHIt과 OnAttackInput()
- OnAttackInput으로 공격 애니메이션 발동 + 공격 주기를 반영해서 공격 가능 상태 설정
- OnHit에서 실제 데미지를 대상에게 적용 (애니메이션 발동 타이밍과 맞아야 하므로 메서드를 분리시켜서 애니메이션 이벤트의 콜백으로 걸어준 것으로 이해)
EquipManager
: 장비 장착, input에 따른 장착 아이템 관련 동작 호출을 담당
- 플레이어의 컨디션을 참조해서 EuipTool.OnAttackInput(condition) → 스테미나 반영
- Input Actions의 이벤트 콜백으로 걸어주기 위해 플레이어 오브젝트에 붙여서 쓴다고 이해
- 아이템 장착 및 attack 행위를 호출하는 담당. attack을 실제로 행하는 것은 현재 장착된 아이템의 Equip에서
- 사실상 메서드 실행은 Equip을 상속받는 EquipTool에서 하는데 진짜 Equip을 상위클래스로 만들고 EquipTool이 상속받아서 구현하도록 구조를 설계한 이유가 너무 궁금함. EquipTool 외에 Equip을 상속받는 다른 클래스가 생길 가능성을 고려한 건지 (그렇다면 이름이 왜 EquipTool...)
- 장착 시 기존의 오브젝트를 파괴하고 프리팹을 인스턴스화하도록 되어 있는데, 활성/비활성화 하는 방법과 비교했을 때 어떤 게 유리한지
Destory / Instantiate
- 필요한 아이템만 메모리에 로드되므로 불필요한 메모리 사용을 줄일 수 있음.
- 아이템을 업그레이드하거나 수정하는 경우에 유용할 수 있음.
- 상대적으로 오버헤드가 큰 연산임. 특히 많은 수의 아이템을 빠르게 교체할 때 성능 문제가 발생할 수 있음.
- 자주 오브젝트를 생성하고 파괴하면 가비지 컬렉터가 더 자주 작동하여 프레임 드랍을 유발할 수 있음.
Activate / Deactivate
- 빠른 연산 → 성능 문제 최소화
- 아이템의 상태(예: 내구도, 업그레이드 상태 등)를 유지 가능. (아이템 재활성화 시 상태를 다시 설정할 필요 없음)
- 모든 가능한 아이템을 메모리에 유지해야 함 → 더 많은 메모리 사용
- 많은 아이템이 활성화되거나 비활성화되면 관리하는 데 복잡성이 증가할 수 있음
메모리 사용 최적화가 중요한 상황이거나 오브젝트의 다양성이 필요한 경우 (예: 많은 종류의 장착 아이템이 있고, 사용자가 이 중 일부만 사용하는 경우)에는 Instantiate와 Destroy 방법이 적합하다.
성능이 중요한 상황 (예: 실시간 전투 게임에서 아이템을 빠르게 교체해야 하는 경우)에서는 오브젝트를 활성화/비활성화하는 방법이 유리하다.
3. 인벤토리 (플레이어)
ItemSlot
: 아이템 정보를 저장
- 아이템 데이터와 수량 보유
- 인벤토리의 Start 부분에서 배열의 요소로 객체화됨. 이후 아이템이 추가되거나 사라지는 등의 변경 사항이 있을 때, 각 ItemSlot의 정보가 업데이트 됨
Inventory
: 주운 아이템을 슬롯에 저장, UI 업데이트, 아이템 소모 및 착용 로직 호출
*코드 자체가 어렵지는 않지만 메서드가 많아서 이해하는 데 시간이 걸리는 편이라 메서드의 역할 위주로 정리
- 인벤토리 자체를 세팅
- Start : 인벤토리 초기 세팅. 슬롯 객체 생성 및 UI 초기화
- OnInventoryButton : Input에 따른 Toggle 호출
- UnityEvent : 인스펙터에서 시각적으로 연결 및 관리가 가능
- Toggle : 인벤토리 패널 활성 / 비활성화
- IsOpen : 인벤토리 패널이 활성화되어 있는지 bool값 return
- 아이템 추가
- AddItem : 아이템을 슬롯에 추가
- 아이템이 canStack인 경우 GetItemStack 호출
- 위의 결과가 null이 아니면 이미 인벤토리에 해당 아이템 종류가 있다는 뜻이므로 이미 아이템이 배치된 슬롯의 quantity만 올려줌 & return
- return되지 않은 경우 (canStack == false이거나 인벤토리에 해당 아이템 종류가 없는 경우) GetEmptySlot 호출
- 위의 결과가 null이 아니라면 인벤토리에 자리가 있다는 뜻이므로 빈 슬롯에 아이템 데이터가 업데이트 됨 (index가 낮은 슬롯부터 탐색하므로 앞에서부터 배치됨) & return
- return되지 않은 경우 (인벤토리에 자리가 없는 경우) ThrowItem 호출
- GetItemStack : ItemSlot 객체가 담긴 배열을 순회하면서 선택된 아이템과 동일한 타입의 아이템이 이미 슬롯 중에 담겨 있는지 검사
- GetEmptySlot : ItemSlot 객체가 담긴 배열을 순회하면서 빈 슬롯이 있는지 검사
- ThrowItem : 아이템 데이터에 포함된 dropPrefab을 인스턴스화
- AddItem : 아이템을 슬롯에 추가
- 아이템 선택 (선택 정보 UI 관리)
- UpdateUI : 인벤토리에 변동이 있을 때마다 호출되어 UI를 업데이트. 각 슬롯UI이 가지고 있는 메서드에 슬롯의 데이터가 반영되어 호출됨
- SelectItem :
- 플레이어가 슬롯 UI를 선택하면 인벤토리의 이 메서드가 호출됨.
- 이 때 슬롯이 가지고 있는 index 정보가 전달되어 현재 선택된 아이템의 정보를 업데이트하고, UI에 선택된 아이템과 관련된 정보가 노출될 수 있도록 UI를 업데이트 하고 있음
- 선택된 아이템 종류에 따라 버튼도 활성/비활성화
- 아이템 장착 및 소모
- OnUseButton : 소모템일 경우 PlayerConditions를 통해 효과를 적용시킴, RemoveSelectItem 호출
- OnEquipButton : 현재 장착 중인 아이템이 있을 경우 UnEquip을 호출하고 (UI에 반영되어야 하기 때문), EquipManager를 통해 장착 아이템 교체
- OnUnEquipButton : UnEquip 호출
- UnEquip : 아이템 장착 해제 및 UI 업데이트
- OnDropButton : ThrowItem과 RemoveSelectItem 호출
- RemoveSelectedItem : 아이템 슬롯의 quantity를 낮추고, quantity가 0이 될 경우, 해당 아이템 관련 정보를 초기화
- ClearSelectedItemWindow : 선택된 아이템과 관련된 정보를 보여주는 UI 초기화
ItemSlotUI
: 각 슬롯에 해당하는 UI요소에 붙어서 아이템의 정보를 UI상으로 보여줄 수 있도록 함
- 가지고 있는 정보
- ItemSlot(아이템 & 수량 데이터)
- Image (UI상에 보여질 아이콘)
- TextMeshProUGUI(수량 텍스트로 표시)
- Outline(장착된 아이템을 구분하기 위한 아웃라인)
- Button (해당 UI요소가 클릭될 경우 Inventory의 SelectItem 메서드가 호출될 수 있도록 연결하기 위함)
- int index : SelectItem을 호출할 때 자신의 정보를 넘겨주기 위함
- bool equipped : 장착템의 장착 여부를 확인하기 위한 변수
- Set(ItemSlot slot)
- slot이 가진 데이터를 바탕으로 UI요소 업데이트
- equipped 변수를 바탕으로 Outline enable 혹은 disable ( outline.enabled = equipped; )
- Clear() : 슬롯 UI 초기화
기억할 것
아이템 프리팹 분리
생각해보면 당연한 건데, 아이템을 장착한다는 것이 논리상으로는 픽업한 아이템을 장착하는 것이므로 하나의 아이템을 계속 활용하는 것으로 인식이 됨. 따라서 프리팹을 분리시킨다는 것에 대해 생각하지 못함.
오브젝트의 동작 그런데 각 상황에서 오브젝트가 행할 기능이 너무 다르고 애니메이션까지 추가된다는 점을 고려하면, 하나의 프리팹을 활용하기는 어렵다는 점
인벤토리에 아이템 저장하는 방식
인벤토리 슬롯 UI와 연결하고 UI의 슬롯 개수 크기의 ItemSlot[]을 만들어서 데이터를 저장하는 것. 아이템 정보, 개수 등을 묶어서 ItemSlot 클래스를 만드는 것은 상당히 기본적인 로직인 것 같은데 신기하게도 이때까지 그렇게 써본 적이 없다.
이때까지는 아이템 데이터 자체에 quantity를 포함시켜서 이 데이터를 플레이어가 가지고 있도록 하고, 아이템 데이터 자체를 리스트에 담는 형식으로 관리해왔다. 이 경우
- 아이템 데이터가 정적인 정보와 동적인 정보를 함께 포함하게 된다는 문제점. → 아이템의 정의 (예: 공격력, 설명)와 현재 상태 (예: 보유 개수)를 혼합하는 결과를 초래
- 동일한 아이템을 여러 플레이어나 시스템이 사용할 경우, 보유 개수와 같은 동적인 정보를 별도로 관리해야 할 수도 있으므로 문제가 됨
Bool 타입 데이터의 활용
조건식이나 bool 변수(혹은 프로퍼티)에 값을 할당할 때 메서드의 반환값을 쓰거나, 다른 bool 변수를 쓰는 등의 방법을 통해 코드를 간결화한 것
나의 경우 매번 정직하게 true나 false를 쓰고 있음. 필요한 경우 이렇게 쓰는 게 맞지만, 이상하게 bool타입의 데이터에 다른 bool 데이터를 할당하는 것이 익숙하지가 않다. 몇 번 써보고 친해지기...
'👾 내일배움캠프 > 🎮 TIL & WIL' 카테고리의 다른 글
내일배움캠프 36일차 TIL - 알고리즘 풀다가 C# 공부하기 (DateTime) (1) | 2023.10.06 |
---|---|
내일배움캠프 35일차 TIL - 적 생성 및 로직 (Unity 네비게이션 시스템) (0) | 2023.10.06 |
내일배움캠프 7주차 WIL - 개인 과제 완성 (0) | 2023.10.06 |
내일배움캠프 (휴일) TIL - 플레이어의 움직임 추가 공부 (0) | 2023.09.24 |
내일배움캠프 33일차 TIL - 초기화와의 싸움 (0) | 2023.09.23 |