0. 들어가기에 앞서
사전 합반 프로젝트를 마무리하고 나서, 해당 프로젝트에서 해 왔던 작업과 그 결과물에 대해 다시금 정리하고 회고하는 시간을 가져보고자 한다.
프로젝트 명 : 진짜 로켓으로 배달합니다! 코팡 익스프레스
한 줄 소개 : 맵을 탐험하면서 자원을 파밍하며, 매일 에너지 할당량을 채워 탈출하는 생존 게임
개발 기간 : 6월 19일 ~ 7월 8일
개발 인원 : 기획팀 5인 / 프로그래밍 팀 5인 - 본인 프로그래밍 팀원
사용 툴 : Unity Engine 2022.3.61, Visual Studio 2022, Git, Github
기획팀에서 원하는 인벤토리의 기능은 다음과 같았다.
게임의 기획 의도상 자원의 수집 및 활용이 중요한 생존 게임으로서, 인벤토리의 중요성이 크다고 판단함. 따라서 인벤토리의 데이터를 싱글톤으로 관리하는 편이 좋다고 판단함.
인벤토리의 경우 UI와 연동되는 부분이 많으며, UI와 데이터의 일치 여부가 중요하므로 데이터의 변화와 View를 분리하는 MVC 패턴을 활용함.
Model이 되는 인벤토리의 데이터에 해당하는 부분 - InventoryManager를 싱글톤으로 선언하여 데이터의 관리만을 담당하고, UI의 표시로 해당하는 부분을 View로 설정하여 오로지 출력만을 담당할 수 있도록 함.
*초기 구상 클래스 다이어그램
InventoryManager - Model
해당 싱글톤 데이터 관리부에서 아이템의 획득, 이동, 사용 및 아이템 버리기 등을 구현하였으며, 특정 행동을 하는 조건에서 Instance로 호출하는 방식으로 데이터의 변동을 관리함.
1) ItemSlotUnit, InventorySlotUnit
해당 유닛은 인벤토리 슬롯 각각에 붙는 컴포넌트로, 아이템의 드래그, 클릭, 놓기 등의 행동이 일어났을 때 InventoryManager를 호출하여 해당 행동을 Model에 반영할 수 있도록 설정되어 있다.
해당 컴포넌트를 상속받은 슬롯의 종류를 늘려 다양한 상황에서의 데이터 변화 및 이동을 반영할 수 있다.
2) HotbarSlotUnit
기획의도에 따라 퀵슬롯의 경우, 사용할 수 있는 아이템의 등록만을 원했기 때문에 Inventory의 연동에 있어서도 기능의 분리가 필요했음. 따라서 ItemSlotUnit에서는 공통적인 기능만을 관리하고 해당 컴포넌트를 상속한 자식Unit의 경우, 그 고유한 기능만을 담는 상속의 방식을 활용함.
3) InventoryController, HotbarController 등 - View
컨트롤러는 최종적으로 인벤토리 내의 데이터 변동에 대한 UI출력만을 담당하는 부분이다. UI 출력에서의 효율성을 위해 Action을 활용했으며, Enable 시에 UpdateUISlot이 추가되고, OnDisable에서 UpdateUISlot이 제거된다.
4) DragSlot
아이템을 드래그하고 있는 동안 이미지를 표시할 수 있도록 하는 UI 요소.
드래그 중일 때 호출하여 마우스를 따라가는 이미지를 표시하도록 하며, 드래그가 끝났을 때 해당 이미지를 Clear한다.
인벤토리의 데이터와 실제 UI 출력을 오류 없이 반영할 수 있으며, 컨텐츠 추가에 따른 확장성을 고려한 아이템 이동 및 데이터 저장 등을 구현할 수 있음
아이템의 종류는 크게 세 가지가 있다.
다만 여기서 장비 아이템의 경우 이후 기획안 변경으로 사용시 제거되며 영구적인 스테이터스 증가 요소로 작용하여 소모품과 거의 유사한 형태로 전환됨.
아이템의 경우 그 종류가 많고, 이후에 추가될 상황 등을 고려해 데이터 목록으로 관리하면 좋겠다는 생각을 함. 또한 아이템의 특성상 아이템 자체의 속성이 변하는 경우는 기획 의도상 없을 거라고 판단했기 때문에 스크립터블 오브젝트로 생성하는 방식을 고려함.
스크립터블 오브젝트로 해당 아이템의 데이터를 가지고 있게 하며, 해당 아이템의 데이터를 가진 채로 특별한 조작이 있을 경우 컨트롤을 할 수 있는 방식으로 ItemData와 ItemController를 분리하여 설계함.
아이템의 속성으로 필요한 것은 아이템ID, 아이템 이름, 아이템 타입, 아이템의 에너지, 아이템의 무게, 스택 사이즈, 분해 가능 여부, (사용 가능한 아이템의 경우) 아이템의 스텟, 아이템의 아이콘, 아이템의 프리팹 등을 요소로 스크립터블 오브젝트의 데이터를 구성함
이후 아이템의 사용 기능이 필요할 경우에는 아이템의 타입에 따라 사용 가능 여부를 판정하고, 판정 여부에 따라 사용 가능한 아이템일 시 Switch문으로 각각의 아이템의 기능을 구현하여 실행시킨다.
아이템 프리펩을 생성할 때 각 아이템에 맞는 데이터를 삽입하는 방식으로 간단하게 아이템을 구성할 수 있고, 아이템의 사용 가능 여부를 ItemController를 통해 판단하고선 각 아이템의 기능에 맞춰 사용을 구현할 수 있음. 여기서 같은 기능이면서도 스테이터스가 다른 아이템의 사용을 구현할 시에 아이템 데이터에 있는 수치를 바탕으로 아이템의 사용을 구현할 수 있기 때문에, 각각의 아이템의 사용을 구현하는 번거로움 및 코드의 중복을 줄일 수 있을 것으로 기대됨.
기획 팀에서 다양한 밸런싱 및 데이터에 대한 부분을 구글 스프레드시트로 전달해 줄 예정이었기 때문에, 해당 부분을 일일히 작업하는 것은 손이 많이 가는 작업일 것이며 이후에 밸런싱 등으로 데이터 변동이 일어났을 때 대응하기가 번거로울 것이라고 생각함. 따라서 스프레드 시트의 데이터를 직접적으로 반영하고 데이터 변동 시에도 유연하게 대응할 수 있는 방법에 대해 고민하게 됨.
구글 스프레드시트의 데이터를 직접적으로 유니티에 반영하는 방식으로, 크게 두 가지 단계를 거쳐서 데이터 변환과정을 생각하였다.
여기서 1 - 구글 스프레드시트의 데이터를 데이터 테이블 형태로 연동하여 가져오는 부분과, 2 - 그 데이터를 바로 유니티에 사용할 수 있는 데이터로 변환하는 작업 이렇게 두 가지로 나눴다.
이러한 데이터 연동 방식은 두 사람이 각 역할을 맡아 진행했으며, 2 - 연동하여 가져온 데이터를 가공하여 유니티에서 사용할 수 있는 스크립터블 오브젝트로 전환하는 유틸리티의 개발 부분을 내가 맡았다.
우선적으로 다른 팀원이 작업한 TableManager는, Dictionary를 통해 테이블 타입과 테이블 클래스를 등록하고 각각의 Table에서의 정보를 string 데이터로 Load 코루틴을 순차적으로 실행하는 방식으로 데이터를 일괄적으로 가져온다.
팀원이 작업한 TableManager의 경우 싱글톤으로 선언되어 있기 때문에 Instance로 불러와서 테이블 정보 string 데이터의 리스트를 불러올 수 있으므로, 해당 데이터를 각각의 형태에 맞춰 ScriptableObject.CreateInstance로 스크립터블 오브젝트의 데이터로서 변환한다.
using UnityEditor로 해당 기능을 구현했다. 예시로, 아이템 데이터 생성 유틸리티의 경우 [MenuItem("Utilities/Generate Item")] 라는 이름으로 유틸리티 버튼을 만들었다.
이에 따라 예시로 int, float, ItemType, bool 등의 변수는 전부 TryParse로 변환하는 방식을 취하며, 직접적인 변환이 어려운 이미지나 프리팹의 경우 AssetDatabase.LoadAssetAtPath<데이터형식>(“경로”) 와 같은 방식으로 불러와서 스프레드 시트의 데이터를 바로 스크립터블 오브젝트로 변환하는 방식을 활용했다.
또한 UnityEditor의 기능을 사용하는 경우, 이는 빌드 파일에 들어가게 되면 빌드가 되지 않는 문제점이 생기므로 해당 생성 기능 자체는 유니티 특수 폴더 중 하나인 Editor라는 파일 내부에서 관리하도록 한다.(Editor 내부에 있는 스크립트는 빌드 시에 포함되지 않음)
아래는 실제로 제공되었던 스프레드 시트데이터와 함께 변환된 결과 스크립터블 오브젝트이며, 실제 변환과정을 GIF로서 첨부했다.
원본 스프레드시트 데이터
실제 생성된 스크립터블 오브젝트
변환과정
해당 방식의 경우 처음에는 데이터를 변환하는 스크립트를 작성해야 하는 작업을 초반에 진행해야 하는 번거로움이 있으나, 이후에 밸런싱 등으로 데이터에서의 변동이 일어났을 시에 해당 수치를 확인하고 일일히 수정할 필요가 없다는 장점이 있다.
이와 같은 방식을 이용하여 아이템 정보뿐만 아니라 레시피 정보, 수리 시스템 정보 및 다양한 아이템 확률 정보 등을 스크립터블 데이터로 생성할 수 있었으며, 밸런싱의 수치 또한 스크립터블 오브젝트를 직접 찾아 수정할 필요 없이 바로 반영되는 것을 확인했다.
실제로 기획팀의 요구로 아이템 중 ‘생존일지’라는 아이템이 삭제되었을 때 단순하게 유틸리티 버튼을 다시 한 번 눌러 스크립터블 오브젝트를 생성하는 것만으로 간단하게 생존일지 아이템을 더미 데이터로 바꿀 수 있었으며, 여기서 체계를 더 다듬으면 스프레드시트 데이터만으로 게임의 모든 데이터를 구성할 수 있는 체계를 기대할 수 있을 것으로 보인다.
아이템 제작 시스템 및 분해 시스템의 목적은, 기획팀의 기획 의도상 아래와 같은 목적을 가지고 있다.
다만 여기서 아이템 제작 시스템의 경우 상대적으로 최종 기획안이 늦게 나온 편이기 때문에 초기에 프로토타입으로 제작한 버전과 최종 적용 버전으로 나뉘어 있으며 해당 두 버전의 구상 및 설계 방법을 서술하고자 함.
2.1 아이템 분해 시스템
아이템은 분해 가능한 아이템과 분해 불가능한 아이템이 있으며, 이와 같은 컨텐츠 요소에 반영하기 위해 아이템 데이터에 분해 가능 여부를 결정하는 bool 변수를 추가함. 요구하는 UI디자인은 아래와 같았으며 일괄 이동 기능을 요구하였음.
이에 따라 일괄 이동 기능은 전체 슬롯에서 아이템의 존재 여부를 판단하고 분해 가능한 아이템만 별도의 데이터로 저장하여 UI로 출력하는 방식으로 출력하는 방식을 생각함. 또한 무게에 대한 개념도 있으니 분해대 위에 올라간 아이템의 무게만큼 인벤토리에서 빼는 방식도 필요하며, 분해를 했을 시에 (아이템의 에너지량 * 아이템의 개수)만큼의 에너지량을 획득해야 함
2.2 아이템 제작 시스템
아이템 제작 시스템의 경우 기획안의 변동이 잦았던 부분으로 초기 버전과 최종 버전으로 나누어 서술하고자 함.
초기 요구사항 : 제작 시스템은 재료를 소모하여 특정 아이템을 만들 수 있음. 제작 아이템 목록에서 레시피를 클릭할 시 제작 아이템의 상세 정보를 확인할 수 있으며, 각각의 제작 아이템은 제작 시간을 가짐. 각각의 제작 시간에 따라 제작을 한 후에 아이템이 완성되며, 해당 아이템은 인벤토리로 바로 들어가게 됨.
제작 시스템의 경우 필요한 기능은 아이템의 제작 가능 여부 판단, 제작 가능한 아이템을 제작할 시에 해당 재료를 소모, 제작 시간이 있는 경우이므로 아이템이 바로 제작되는 것이 아닌 Progress bar로 제작 시간을 표시하고 최종적으로 아이템이 제작되는 4가지 단계로 구성해야 함.
이에 따라 제작 레시피에서는 기본적으로 재료 아이템에 대한 정보, 결과 아이템에 대한 정보, 제작 시간 등의 정보가 필요함. 여기서 아이템의 경우 이미 아이템에 대한 Scriptable Object 데이터를 만들어 놓은 상태이므로, 이런 데이터 자체를 요구하는 변수를 추가하여 데이터에 이미 있는 아이템이름, 설명 및 아이콘, 프리팹 등을 직접적으로 활용하는 방식으로 레시피 Scriptable Object를 구성함. 따라서 레시피 데이터 자체는 재료 아이템 데이터, 결과 아이템 데이터, 제작 시간 float 변수 정도만을 요구하는 간결한 구성이 가능함.
아이템의 제작 가능 여부를 판단하는 경우, 해당 재료 아이템 및 수량이 인벤토리에 있는지 판단하는 방식으로 진행해야 한다. 여기서 효율성을 높이기 위한 방식에 대한 고민으로, ‘레시피를 선택‘하고 나서 아이템의 제작 가능 여부를 판단한다는 것에 주목을 했다. 이에 따라 레시피를 모두 저장해놓고 모든 레시피에 대해 제작 가능 여부를 대조해 놓는 방식보다, 특정 레시피가 선택되었을 때에만 해당 레시피의 정보에 따른 재료 아이템의 소지 여부를 판정하는 방식으로 효율성을 높일 수 있다고 판단함. 또한 재료 아이템의 소지 여부를 판정하는 방식으로 해싱의 방식을 활용하는 것으로 효율성을 높일 방식을 고민함.
최종 요구사항 : 제작 시스템은 재료 아이템의 소모 없이 오로지 에너지만을 소모하여 아이템을 만들 수 있으며, 제작 시간에 대한 요소는 삭제됨.
최종 요구사항에 따른 제작시스템에 따라 레시피에 요구되는 데이터는 소모되는 에너지량과 결과 아이템에 대한 정보만 필요한 간결한 구조가 됨. 현재 선택된 레시피에 따라 요구하는 에너지량을 충족하는지 판정하고, 아이템을 제작하여 인벤토리에 넣을 수 있도록 구성함
2.3 수리 시스템
수리 시스템의 경우 에너지를 소모하여 베이스캠프의 요소를 수리하는 방식으로, 필요한 정보는 수리하는 요소의 명칭, 설명, 아이콘베이스캠프의 요소를 수리하는 방식으로, 필요한 정보는 수리하는 요소의 명칭, 설명, 아이콘, 요구 에너지량 등이다. 이에 따라 선택되는 수리 요소를 선택했을 때 이름 및 상세정보, 요구 에너지량을 띄우고, 수리가 가능할 시에 수리를 한 후 해당 수리 요소의 완료 여부를 체크하는 방식이 필요함.
3.1 분해 시스템
분해 시스템에는 기본적으로 아이템의 정보를 기반으로 분해 여부를 판정하고, 분해 가능한 아이템의 경우 분해 슬롯으로 이동시킬 수 있는 방식으로 구성해야 함.
분해 시스템은 아이템의 개별 이동(드래그 앤 드롭)이 가능하고 아이템의 일괄 이동 기능도 필요함. 이를 위해선 우선적으로 분해 슬롯의 정보를 저장할 별도의 데이터 저장부 - DecompoisitionSystem 이 있어야 하며, 이는 인벤토리와 달리 싱글톤 요소는 필요 없이 데이터 저장만 하는 용도로 만들었다. 또한 인벤토리를 구성할 때 만들었던 ItemSlotUnit의 기능을 상속한 DecompositionSlotUnit을 별도로 구성하여, DecompoisitionSystem의 데이터에 대한 변동 사항을 이 Unit에서 관리하고, 최종적으로 UI의 출력만을 담당하는 DecompositionController로 Unit들을 관리하는 방식을 취함.
여기서 에너지라는 시스템은 다른 담당자가 만든 GameManager에서 GameData만을 관리하는 GameData 클래스에서 에너지에 대한 정보 및 에너지의 증감에 대한 함수를 저장하고, 분해할 시에 에너지가 증가하는 함수를 호출하는 방식으로 처리했음.
3.2 제작 시스템
3.2.1 재료 판별형 제작 시스템(초기)
초기 제작 시스템의 요구사항은 다음과 같았다.
제작에 필요한 재료
제작 결과물
제작 시간
재료 아이템과 결과물은 모두 아이템이므로, 아이템 스크립터블 오브젝트로 정보를 가져올 수 있으므로 별도로 이미지나 프리팹을 가져올 필요성이 없다는 간편함이 있었음.
1.1) 레시피 스크립터블 오브젝트의 데이터 구성
여기에서 고민했던 부분으로, 제작에 필요한 재료가 몇 개인지에 대한 부분이 있었다. 구체적으로 나온 기획안이 없을뿐더러 나중의 확장성을 고려하기 위해서 이 제작 재료의 개수를 제한 없이 레시피를 만들 수 있는 방식에 대해 고민이 있었다.
이에 따라 재료 아이템을 List로 받을 수 있게 설정하고, 위의 데이터 변환 과정에서는 해당 string 의 Length를 기준으로 특정 영역 사이의 데이터를 전부 재료로 판정하는 방식으로 레시피 데이터를 읽어오는 방식을 취하도록 했다.
(예시 : 제작시간 : 10, 재료 아이템 1, 재료 아이템 2, 재료 아이템 3, 결과 아이템 과 같이 입력하였을 때,
string 데이터 배열 [0]은 시간으로 가정하고 데이터를 등록,
[1] ~ [string Data의 길이 - 2] 까지가 재료 아이템이라고 가정하고 데이터를 등록,
마지막 [string Data의 길이 -1] 부분이 결과 아이템이라고 가정하고 등록)
이와 같은 방식으로 레시피는 재료 아이템의 종류 및 개수에 유동성을 준 채로 다양한 레시피가 나올 수 있는 다형성을 주었음.
1.2) 레시피의 제작여부 판정
레시피의 제작 여부의 경우, 인벤토리 내 모든 아이템을 검사하여 재료의 존재 여부를 판정할 수 있으나, 보다 효율적인 방법에 대해 고민함.
여기서 크래프팅 시스템의 경우 모든 데이터를 해당 테이블이 가지고 있는 방식을 취하는 것도 좋은 방법이 될 수 있겠으나, 기획안으로 나온 내용을 바탕으로 ‘선택된 레시피’에 대해서만 제작 여부를 판별하는 것도 효율성을 높이는 방안이라고 판단함.
따라서 모든 레시피에 대한 스크립터블 오브젝트 데이터를 테이블 자체에 부여하는 방법 대신, 제작 레시피를 선택하는 패널 각각에 레시피 데이터를 하나씩 넣는 방식을 취한 다음, 해당 버튼을 눌렀을 때 CraftingController의 current Recipe를 선택된 레시피로 데이터를 전달하는 방식으로 레시피의 제작여부를 판별하도록 함.
이와 같은 방식으로 current Recipe 가 null 이 아니게 되었을 때 아이템의 제작 가능 여부를 판정하는데, 필요한 건 가지고 있는 아이템의 종류와 아이템의 개수이다. 따라서 Dictionary<ItemSO, int> 로 해당 아이템이 아직 키로 등록되지 않았을 경우 키를 등록하면서 개수(stack)을 등록하고, 키로 이미 등록된 아이템의 경우 개수(stack)을 더하는 방식으로 가진 아이템의 종류 및 개수를 저장하였다.
이와 같이 인벤토리와 요구 재료의 Dictionary 정보를 각각 저장한 다음, 해당 두 Dictionary 정보를 foreach로 비교하여 인벤토리에서 요구 재료의 키가 없는지, 혹은 키에 해당하는 만큼의 충분한 개수(value) 가 없는지 판별하여 아이템의 제작 가능 여부를 판별하였다.
이에 따라 아이템이 제작 가능할 경우 제작 버튼을 활성화, 불가능할 시 제작 버튼을 비활성화 하는 방식을 취함.
1.3) 아이템 제작 시간
아이템이 제작 가능하다고 판정되었을 때 아이템 제작 시도를 하면, 아이템 제작 시간을 코루틴으로서 실행한다. 이때 코루틴 내에서 코루틴이 종료되는 시점에서 아이템이 출력되도록 설정하여 제작을 하는 시간 동안에는 아이템이 출력되지 못하도록 함.
3.2.2 에너지 소모형 제작 시스템(최종)
다만 위와 같이 재료의 소모를 통해 제작하는 시스템의 경우, 최종 프로젝트에는 적용하지 못했으므로 기획팀의 기획 의도에 따라 에너지를 소모하여 아이템을 생성하는 방식으로 전환함. 에너지의 소모만을 판정하는 경우, GameManager.Instance.GameData… 로 현재 에너지를 확인하는 방식으로 간단하게 판별할 수 있으므로, 해당 판별 과정을 거쳐 제작 여부를 판정하고 아이템을 바로 생성하는 방식으로 전환함.
3.2.3 수리 시스템
수리 시스템의 경우 제작 시스템과 유사하게 에너지를 소모하여 제작하는 방식은 비슷하나, 한 번 수리를 한 뒤로 똑같은 수리를 반복하면 안 된다는 요소와 엔딩에 영향을 미치는 요소이므로 수리 여부를 bool 배열로서 GameData에 저장함. 이에 따라 이미 수리가 완료된 항목은 다시 수리 시도를 할 수 없고, 해당 레시피 또한 비활성화하는 방식을 취함.
해당 시스템 등은 인벤토리를 이용한 기능을 확장한 사례로 게임의 핵심 컨텐츠를 구성한 시도이다.
또한 이 중에서도 크래프팅 시스템에 적용된 아이템 제작 레시피 및 아이템 제작 여부 판별 시스템의 경우, 재료 아이템의 종류 개수를 자유롭게 설정할 수 있다는 확장성을 고려한 설계를 하였음. 이에 따라 크래프팅 시스템이 필요한 다른 게임에서도 유연하게 적용할 수 있을 것이라 기대할 수 있으며, 더욱이 확장성을 고려하여 결과 아이템의 개수, 그리고 각 아이템의 수량까지도 담을 수 있는 방법으로서의 확장성까지 고려하면 유연한 시스템을 구현할 수 있을 것이라 생각함.
상자 시스템에 대한 기획팀에서 요구하는 사안은 다음과 같았다.
상자에는 두 가지 종류의 물건이 들어가야 한다. - 아이템과 컬렉션 아이템
여기서 컬렉션의 경우 아이템과는 별개의 형태이며, 획득 시 바로 컬렉션에 등록되는 방식이어야 한다. (인벤토리에 들어와서는 안 되는 형태)
상자 아이템에는 일괄 수령 버튼이 있어야 하며, 아이템과 컬렉션이 한 번에 인벤토리 및 컬렉션북에 등록되는 방식이어야 한다.
지금까지는 아이템에 대한 데이터만 저장하고 있었으나, 컬렉션에 대한 데이터를 저장하고 있는 별도의 슬롯이 필요함. 이에 따라 컬렉션만을 담을 수 있는 별도의 슬롯을 포함한 상자를 구성함.
상자를 구성하는 부분을 데이터만을 다루는 BoxSystem과 UI만을 다루는 BoxController로 기능을 나눔.
아이템을 담을 수 있는 BoxSlot의 경우, 기존 인벤토리에서 사용했던 ItemSlotUnit을 상속한 방식으로 사용. 따라서 아이템을 담을 수 있고 드래그 이동도 가능하고 다시 상자로 아이템을 담을 수도 있는 등의 방식이 됨.
컬렉션용 슬롯의 경우, 컬렉션만을 담아야하므로 상속 없이 별도로 BoxCollectionUnit을 등록하고, 컬렉션 시스템을 담당하는 분의 코드를 분석해 컬렉션을 등록하는 함수를 호출하는 방식으로 컬렉션을 바로 등록하고 데이터를 삭제하는 방식으로 구성함.
아이템 일괄 가져오기의 경우 아이템 슬롯과 컬렉션 슬롯이 별도로 분리되어 있으므로, 아이템 슬롯에 대해 모든 슬롯을 확인하고 가져오는 것과 컬렉션 슬롯에 대호 모든 슬롯을 확인하고 가져오는 방식으로 구성함.
이 부분에 대해서는 당시 개발 상황 상 좋은 구성 방법이 떠오르지 않아 다소 아쉬운 설계로 남았던 부분이다. 다만 여기에서 떠오른 방식으로 아래와 같은 구성 방법이 가능하지 않았을까 하는 생각이 남는다.
아이템과 컬렉션 자체는 별개의 오브젝트로 되어야 한다는 전제로, ItemSO와 CollectionSO가 있을 것이다. 다만 이 둘을 자식으로 두는 부모 클래스를 두어, 박스 슬롯 자체는 해당 데이터를 담을 수 있는 형태로 구성한다.
이에 따라 박스에만 한정해서 해당 부모 클래스의 데이터를 담아두고, 각각의 아이템 및 컬렉션을 상자에 등록한 다음 다운당 부모 클래스의 데이터를 담아두고, 각각의 아이템 및 컬렉션을 상자에 등록한 다음 다운캐스팅을 통해서 이를 인벤토리로 가져오기 혹은 컬렉션으로 등록하기와 같은 방식을 활용할 수 있을 것으로 생각된다.
1, 기획의도
상자에는 아이템과 컬렉션이 등장하는데, 유형을 총 네 가지로 나누어 다음과 같은 확률로 등장한다.
이와 같이 아이템에는 상자별 티어의 개념이 있고, 일자 시스템의 영향을 받는 요소도 있으며 해당 요소와 관계 없이 일정한 확률로 랜덤한 아이템이 등장하는 방식도 있다.
우선 해당 확률 테이블에 관해서도 Scriptable Object를 적용하여 데이터 테이블을 통한 스크립터블 오브젝트를 바로 생성하는 방식을 활용할 생각이었다. 다만 아이템을 생성했을 때처럼 티어별, 혹은 일자별 각각의 Scriptable Object를 생성하는 방식은 적합하지 않을 것이므로, 해당 확률표 자체를 하나의 Scriptable Object 데이터로 뽑아낼 수 있는 방식에 대한 고민이 필요했음.
또한 아이템 유형별로 확률표 및 적용 사항이 상이하고, 확률 조건이 많이 복잡하므로 이를 계산하기 위한 방식을 고민함. 확률 계산에 대한 리서치를 해 보면서, 이와 같은 확률을 적용하기 위해서는 가중치 확률 시스템에 대한 구현이 필요하다고 판단함.
확률표에 대한 스크립터블 오브젝트의 구성 방법
예시로 A 아이템에 대한 확률표를 기준으로, 스프레드시트의 데이터는 다음과 같이 정리되어 있다.
그렇다면 우선적으로 확률표에 대한 변수 자체는 ItemSo, 티어1의 확률, 티어2의 확률, 티어3의 확률, 아이템의 개수 이렇게 5개의 변수를 저장해야 한다.
이러한 5개의 변수를 담는 클래스를 우선 하나 만들고, System.Serializable로 직렬화를 시킨다. 그 다음, 해당 클래스의 리스트 묶음을 가지는 Scriptable Object를 만든다.
이 다음으로 테이블 데이터를 읽은 다음 해당 데이터를 생성해야 하는데, 이 부분은 Scriptable Object의 요소가 list이므로 각각의 데이터를 변환한 후 List.Add(new Data{})로 데이터를 입력해 주는 방식으로 foreach문으로 데이터를 입력한 다음 해당 반복문이 끝난 후에 데이터를 생성하는 방식으로 하나의 데이터로 묶어 변환했다.
해당 변환 과정을 통해 생성한 스크립터블 오브젝트는 다음과 같이 데이터가 등록된다.
가중치 확률 시스템은 각 항목에 특정 확률(가중치)을 부여하여 무작위로 선택할 때, 가중치가 높은 항목이 선택될 확률이 더 높도록 조절하는 시스템을 말한다. 이는 일반적인 무작위 선택과 달리, 특정 항목이 더 자주 등장하거나 덜 등장하도록 제어할 수 있게 한다.
가중치 확률 시스템은 다음과 같은 구조로 설계한다.
가중치 확률 시스템은 Dictionary를 활용하며, <T, int> 로 저장된 데이터에 따라 확률을 계산한다.
T에 원하는 데이터 형식을 저장하고, int에 해당하는 value 값으로 확률, 즉 뽑는 횟수를 저장한다.
예를 들어 아이템 A의 등장확률이 30%, 아이템 B의 등장확률이 50%, 아이템 C의 등장확률이 20% 라고 했을 때, Dictionary에는 각각 key - 아이템 A, value - 30, key - 아이템 B, value - 50, key - 아이템 C, value - 20 과 같이 Dictionary 데이터를 저장한다.
이와 같이 각각의 Dictionary 데이터를 저장한 후, 아이템을 뽑을 확률에 대해 계산을 한다.
아이템을 뽑을 확률에 대해서는 다음과 같이 계산한다
.
-int weight = 0 으로 지정하고, Dictionary의 아이템의 value값만큼 더하기 시작한다. 이때, weight가 pivot의 값을 넘어섰을 때의 item을 반환한다.
A 아이템의 경우가 가중치 확률 시스템을 반영해야 하는 대표적인 예시였다. 우선은 확률표로 구성한 스크립터블 데이터를 가져온 다음 해당 데이터를 박스의 티어에 따라 Switch문으로 가중치 랜덤 시스템에 대입한다. 이 과정까지를 Init으로 하여 Start() 단계에서 실행시키고, 상자가 활성화될 때 Select를 진행한다. 아이템을 뽑는 확률은 해당 확률에 따른 4가지 아이템을 확정적으로 뽑는 확률이기 때문에 아이템 뽑기를 4번 시행하고 나온 결과를 출력한다.
B 아이템의 경우 일지라는 아이템이 있으며, 해당 아이템은 1에서 10까지 있다. 다만 일지의 경우 그 순서대로 중복 없이 아이템이 출현하는 조건으로, 일지에 해당하는 데이터를 전부 Scriptable Object로 생성해 놓은 다음 데이터 List를 확률 생성기에 등록한다. 그리고 아이템이 순서대로 중복 없이 나와야 하므로 Queue를 사용하여 초기 등록을 한다.
그 다음, 일자와 확률을 Dictionary 형태로 key = 일자, value = 확률로 등록을 한 다음, 일자의 key 값을 기준으로 value 값과과 Random.Range로 나온 수를 비교하여 충족했는지 판정한다. 일자에 따라 일지의 등장확률이 변동하며 확률이 충족했을 경우 일지가 등장한다.
C 아이템의 경우 식량이라는 아이템 하나만의 등장 확률로서 B와 유사하게 key = 일자, value = 확률로 등록을 한다. 그 다음 일자의 key 값을 기준으로 value 값과과 Random.Range로 나온 수를 비교하여 충족했는지 판정한다. 일자에 따라 식량의 등장확률이 변동하며 확률이 충족했을 경우 식량이 등장한다.
D 아이템의 경우 10가지의 컬렉션 아이템이 동률로 등장하며, 아이템 자체가 등장할 확률은 70%, 10가지 아이템이 등장하는 확률이 동률이기에 각 7%의 확률로 등장한다. 동률의 경우 굳이 가중치 확률 시스템을 사용할 필요는 없으나, 해당 시스템에 동일한 값을 넣어서 확률을 간단하게 낼 수 있으므로 먼저 70%의 확률에 대한 판정을 먼저 한 후 가중치 확률 시스템에 동률의 값으로 넣어 아이템의 등장 확률을 결정했다.
가중치 랜덤 시스템은 특히나 아이템의 종류가 많고 각 아이템 별 등장확률이 상이할 때 유용하게 사용할 수 있는 방식이다. 제네릭으로 만든 가중치 랜덤 시스템은 이와 같이 단순한 아이템의 등장확률 뿐만이 아닌 게임 내에서 발생하는 여러가지 확률적 요소 등 다양한 방면에서 활용할 수 있을 것으로 기대된다. 또한 해당 확률 시스템을 좀 더 보강하는 방식으로, Random.Range의 시드 값을 계속해서 변경하는 방식으로 완전한 랜덤 확률을 만들 수도 있을 것으로 보인다.
원인 : 초기 디자인으로 설계되었던 인벤토리의 경우, 아이템 데이터를 관리하는 영역과 UI로 출력하는 영역에서의 명확한 구분이 되지 않아, UI의 정보가 업데이트 되는 과정에서 아이템 데이터도 날아가는 문제가 발생함.
해결 방법 : 지나치게 복잡하면서 각 컴포넌트의 역할이 모호한 구조를 조금 더 간결하고 명확한 방향으로 수정하여, 보다 안정적인 형태의 인벤토리 시스템으로 설계함.
원인 : 결과물 습득의 경우 데이터를 결과 슬롯에 추가로 저장하는 방식을 대신해 ‘현재 선택된 레시피’의 아이템을 그대로 획득하는 방식으로 설계했음. 하지만 이와 같은 경우 아이템을 수령하기 전 ‘현재 선택된 레시피’를 변경하면 변경된 아이템이 획득되는 버그가 발생함
해결 방법 : 결과물을 습득하기 전 ‘현재 선택된 레시피’의 변경을 할 수 없도록 레시피 목록 위로 투명한 Panel을 덮어 추가적인 조작을 하지 못하도록 함.
원인 : 아이템을 드래그하는 도중 인벤토리를 종료하면, EndDrag에서의 드래그 종료 및 아이템 이동 판정 기능이 작동하지 않아, 해당 드래그 이미지가 그대로 플레이어의 마우스를 맴도는 현상이 발생
해결 방법 : 슬롯 자체가 드래그 도중 비활성화가 된 경우, OnDisable() 을 통해 아이템을 이동할 수 없었다는 강제 판정을 내리고 DragSlot을 Clear 함.
원인 : InventoryManager의 경우에는 싱글톤으로 선언되어 씬 전환시에도 데이터가 저장되어 있으나. 그와 연관된 UI Controller 등은 씬이 전환됨과 동시에 InventoryManager와의 연동이 해제되고 파괴됨. 이에 따라 다시 씬을 실행할 시 UI Controller들은 정상적으로 InventoryManager와 연결된 상태가 아니고 아이템 사용 불가 및 복사 버그 등의 비정상 행동을 보임
해결 방법 : InventoryManager가 필수적으로 연동해야 하는 UI를 InventoryManager의 자식 오브젝트로 설정하여 씬 전환 시에도 연결이 해제되지 않도록 처리함. 또한 게임오버가 되고 나서 재시작하는 경우, 인벤토리를 초기화하는 함수를 추가하여 게임 재시작 시에 인벤토리에 아이템이 들어 있지 않도록 처리함.