프로젝트의 기존 아이템 시스템 구조가 워낙 안 좋아서 개선하게 됐다. Lyra 샘플 프로젝트의 인벤토리 시스템을 참고해 만들었고, 분석한 내용을 요약한 글이다.
1. InventoryManagerComponent
액터에 붙일 수 있는 인벤토리 컴포넌트다. 헤더파일에 여러 구조체가 선언되어 있다. 그리고 Component에서는 CanAddItem, AddItem, RemoveItem, UseItem(서버에 요청) 등의 함수를 제공한다. 즉, 인벤토리를 한 번 감싸는 클래스이며, 서버 혹은 다른 클래스와의 통신을 담당하는 클래스라고 볼 수 있다.
1-1. FInventoryList
InventoryComponent를 보면 가장 먼저 볼 수 있는 구조체는 FInventoryList다. 내부에 OwnerComponent라는 멤버변수와 함께 FInventoryEntry 배열이 있다. 실제 인벤토리 역할을 하는 구조체다. Component에서 제공하는 함수를 호출하면, 이 구조체의 AddEntry, RemoveEntry 등으로 접근하게 된다.
1-2. FInventoryEntry
FInventoryEntry는 슬롯 한 칸을 의미하는 구조체로, InventoryItemInstance를 가리키는 포인터와 함께 수량을 의미하는 Quantity가 선언되어있다. 함수는 따로 선언되어있지 않다.
2. InventoryItemInstance
FInventoryEntry의 멤버변수 포인터가 가리키는 객체다. Quantity는 '슬롯의 상태'를 나타내는 값이기 때문에 Entry에서 관리했다. 여기엔 장착 여부, 가격 등이 변수로 선언되어있다. 이 값을은 '아이템의 상태'를 나타내기 때문이다. 그리고 멤버변수로 선언된 TSubclassOf가 있고, InventoryItemDefinition 클래스를 가리키고 있다.
3. InventoryItemDefinition
실제 아이템 역할을 하는 클래스다. 먼저 매크로부터 살펴보면 UCLASS(Blueprintable, Const, Abstract)를 갖고 있다. 즉, 블루프린트로 파생 가능하며 값이 변하지 않는 추상 클래스다. 변하지 않는 값의 예를 들면 아이템 이름, 설명, 아이콘, 카테고리 등이 있다. 순수 데이터로 활용하는 클래스기 때문에 Class Default Object로 가져와 사용하는 클래스다. DefaultObject는 클래스마다 하나씩 있고, TSubclassOf에서 접근 가능하며, 초기값으로 할당된 데이터를 갖고 있는 오브젝트를 말한다. 이 클래스의 사용법은 블루프린트로 파생 후, 위에서 말한 아이템 이름이나 아이콘 등을 할당해 사용한다. 마지막으로 이 클래스는 InventoryItemFragment 배열을 갖고 있다. 이 또한 블루프린트 클래스에서 직접 추가한다.
4. InventoryItemFragment
InventoryItemDefinition에서 배열로 갖고 있는 클래스다. 매크로부터 살펴보면 UCLASS(DefaultToInstanced, EditInlineNew, Abstract)로 선언되어있다. 해당 클래스를 UPROPERTY로 사용할 때, 자동으로 인스턴스를 생성해준다는 뜻이며, 인라인으로 생성/편집할 수 있게 해주고, 추상 클래스임을 나타낸다. C++상에서 상속시켜 정의, Definition의 블루프린트 클래스에서 이를 할당해 편집해주면 된다. '아이템 기능'이라고 오해하기 쉬운데, 기능보단 '어댑터'에 가깝다. 내 프로젝트엔 탈것이 있어 RideableFragment를 선언해 사용했으니, 이를 예시로 들겠다.
5. 사용법
RideableFragment엔 AActor 클래스를 가리키는 멤버변수 TSoftClassPtr, 'VehicleClass'밖에 없다. 탈것 아이템인 Definition에서 RideableFragment를 하나 할당해주고, 원하는 VehicleClass를 초기값으로 할당해주면 세팅은 끝이다.
클라이언트가 탈것 아이템을 인벤토리에서 사용하면 해당 아이템 슬롯(UI)가 참조하고 있는 ItemInstance에 접근, Definition을 가져오고, 다시 거기서 ItemID를 가져와 등록한다. 그 후 클라이언트가 탑승을 서버에 요청하면 탈것으로 등록된 ItemID가 모든 클라이언트에게 뿌려지게 된다. 해당 패킷을 받은 클라이언트들은 ItemID를 통해 ItemDefinition에 접근(ItemManager에 의해 게임 시작 시 미리 TMap으로 캐싱되어있다.), 다시 거기서 RideableFragment에 접근해 VehicleClass를 가져온다. 그리고 스폰 후 탑승 로직을 작동하면 된다.
즉, ItemFragment는 기능 역할이 아니라 어댑터 역할이다. ItemFragment가 직접 아이템으로서의 기능을 수행한 게 아니라, 다른 클래스가 Fragment에 접근해서 관련 정보를 읽어온 뒤 원하는 로직을 동작했기 때문이다.
6. 장점
아이템 기능 확장이 기존 기능을 전혀 해치지 않고 가능하다. 예를 들어 이런 기획이 나왔다고 해보자.
'포션 아이템을 투척해 사용할 수 있게 해주세요.'
기존에 '사용하기'밖에 없던 포션 아이템을 투척 아이템으로도 사용할 수 있게 해달라는 기획이다. 그럼 나는 UsableFragment만 갖고 있던 포션 아이템에게 ThrowableFragment를 작성해서 붙여주기만 하면 끝이다. 포션 사용 시 그냥 마실 건지 아니면 던질 건지 결정하는 로직은 기획이 또 나와야겠지만, 그걸 결정하는 부분에서 둘 중 하나에 접근해 데이터를 꺼내 쓰기만 하면 된다.