[Unity] 아이템 인벤토리를 활용한 캐릭터 인벤토리 구현

송제백
∙∙조회 8

기존 아이템 인벤토리를 기반으로 캐릭터 인벤토리를 구현했다. 슬롯 생성, 선택, 삭제 등의 기본 구조는 동일하게 유지했지만, 캐릭터의 데이터 특성에 맞게 ID 관리, 정렬 및 필터링, JSON 직렬화 방식을 수정했다.

1. 아이템과 캐릭터 ID 관리 방식의 차이

기존 아이템은 ID를 string으로 관리했지만, 캐릭터는 CharacterId라는 enum을 사용했다.

아이템 데이터

csharp
public class ItemData
{
    public string Id { get; set; }
}

캐릭터 데이터

csharp
public enum CharacterId
{
    CHAR_1,
    CHAR_2,
    CHAR_3,
    CHAR_4,
}

public class CharacterData
{
    public CharacterId Id { get; set; }
}

아이템은 "ITEM_1"과 같은 문자열을 그대로 전달하지만, 캐릭터는 CharacterId.CHAR_1이라는 enum 값을 전달한다.

enum을 사용했을 때의 장점

1. 컴파일 단계에서 잘못된 ID 사용을 방지할 수 있다.

문자열은 오타가 발생해도 컴파일 단계에서 확인하기 어렵다.

csharp
DataTableManager.ItemTable.Get("ITME_1");

반면 enum은 정의되지 않은 값을 사용하면 컴파일 오류가 발생한다.

csharp
DataTableManager.CharacterTable.Get(CharacterId.CHAR_1);

따라서 정해진 캐릭터 종류를 사용할 때 문자열보다 안전하게 관리할 수 있다.

하지만 강제 형변환이나 외부 데이터에서 잘못된 숫자가 들어오는 경우까지 모두 방지하는 것은 아니였다.

2. 자동완성을 활용할 수 있다.

처음에는 캐릭터 ID를 string으로 전달했지만, 강사님께서 캐릭터 ID를 문자열로 관리하면 당장은 편해도 나중에 캐릭터를 사용하는 코드가 많아질수록 작성과 유지보수가 어려워질 수 있다고 말씀하셨다. 처음에는 못 느꼈는데 하다보니까 아 이래서 이랬구나 생각했다.

이를 enum으로 변경하면서 CharacterId.을 입력하면 정의된 캐릭터 ID 목록이 자동완성으로 표시되어, 매번 문자열을 직접 입력할 필요가 없어졌다. 또한 잘못된 ID 이름을 사용하면 컴파일 단계에서 오류를 확인할 수 있어 코드의 안정성도 높일 수 있었다.

3. 캐릭터 종류를 명확하게 제한할 수 있다.

현재 캐릭터는 네 종류로 정해져 있으므로 enum을 통해 사용할 수 있는 ID를 명시할 수 있다.

반대로 아이템처럼 종류가 많고 CSV에서 새로운 데이터를 자주 추가하는 경우에는 string이 더 유연하다.

enum을 사용하면 캐릭터를 추가할 때 CSV뿐만 아니라 enum 정의도 수정해야 하기 때문이다.

2. 데이터 테이블의 차이

ID 자료형이 달라지면서 데이터 테이블의 Dictionary 구조도 변경되었다.

ItemTable

csharp
private readonly Dictionary<string, ItemData> table
    = new Dictionary<string, ItemData>();

CharacterTable

csharp
private readonly Dictionary<CharacterId, CharacterData> table
    = new Dictionary<CharacterId, CharacterData>();

아이템은 문자열을 Key로 사용하고, 캐릭터는 enum을 Key로 사용한다.

CSV에서 데이터를 불러오는 과정은 동일하지만, 데이터를 조회할 때 요구하는 ID의 자료형이 다르다.

csharp
// 아이템 조회
ItemData item = DataTableManager.ItemTable.Get("ITEM_1");

//캐릭터 조회
CharacterData character =
    DataTableManager.CharacterTable.Get(CharacterId.CHAR_1);

enum은 내부적으로 정수 기반의 값 형식이므로 문자열을 Key로 사용할 때와 비교해 문자열 해시 계산 및 비교를 줄일 수 있다는 장점도 있다.

다만 일반적인 작은 규모의 데이터 테이블에서는 성능 차이보다 타입 안정성과 관리 편의성이 더 중요한 장점이라고 생각한다.

3. 정렬 및 필터링의 차이

아이템 인벤토리에서는 생성 날짜와 이름을 기준으로 정렬했지만, 캐릭터 인벤토리에서는 HP, ATK, DEF, SPEED를 기준으로 하는 정렬 기능을 추가했다.

csharp
(lhs, rhs) => lhs.CharacterData.Hp.CompareTo(rhs.CharacterData.Hp),
(lhs, rhs) => rhs.CharacterData.Hp.CompareTo(lhs.CharacterData.Hp),

기존에 사용했던 Comparison<T> 배열을 그대로 확장하여 별도의 정렬 알고리즘 없이 능력치별 오름차순과 내림차순을 구현했다.

필터링 방식에도 차이가 있다.

아이템은 ItemTypes를 비교하고, 캐릭터는 CharacterId를 비교한다.

csharp
// 아이템 필터링
(x) => x.ItemData.Type == ItemTypes.Weapon

// 캐릭터 필터링
(x) => x.CharacterData.Id == CharacterId.CHAR_1

두 기능 모두 Predicate<T>를 사용하지만, 아이템은 분류 기준인 Type을 비교하고 캐릭터는 개별 종류를 나타내는 ID를 비교한다는 차이가 있다.

4. JSON 직렬화 방식의 차이

기존 아이템은 string ID를 사용하므로 JSON으로 저장할 때 ID를 그대로 기록했다.

csharp
// ItemDataConverter
writer.WriteValue(value.Id);

반면 캐릭터는 enum을 사용하므로 CharacterDataConverter를 별도로 구현했다.

csharp
// CharacterDataConverter
writer.WriteValue(value.Id.ToString());

여기서 중요한 점은 프로그램 내부에서는 enum을 그대로 사용하지만, JSON에 저장할 때만 문자열로 변환한다는 것이다.

JSON 파일에서 읽은 문자열은 다시 enum으로 변환하여 사용한다.

csharp
var id = reader.Value as string;

if(!Enum.TryParse<CharacterId>(id, out var characterId))
{
    throw new JsonSerializationException($"잘못된 캐릭터 ID: {id}");
}

return DataTableManager.CharacterTable.Get(characterId);

즉, 캐릭터 ID는 다음 과정을 거친다.

CharacterId.CHAR_1 → "CHAR_1"(JSON 저장) → CharacterId.CHAR_1(복원)

이렇게 하면 코드 내부에서는 enum의 타입 안정성을 활용하면서 JSON에는 사람이 읽을 수 있는 ID를 저장할 수 있다.

또한 enum을 정수로 저장하는 방식과 달리, 문자열 이름으로 저장하면 enum 선언 순서가 변경되더라도 같은 이름을 유지하는 한 기존 ID를 복원할 수 있다.

다만 enum의 이름 자체를 변경하면 기존 JSON과 호환되지 않을 수 있다는 점은 주의해야 한다.

5. 캐릭터 선택 및 저장 구조 확장

기존 아이템 인벤토리는 SaveItemData를 전달했지만, 캐릭터 인벤토리는 SaveCharacterData를 전달하도록 수정했다.

csharp
public UnityEvent<SaveCharacterData> OnSelectSlot;

CharacterData는 캐릭터 종류별 기본 능력치를 관리하고, SaveCharacterData는 생성된 개별 캐릭터의 고유 ID와 생성 시각을 관리한다.

또한 기존 SaveDataV5에 CharacterList를 추가하여 아이템과 캐릭터 목록을 함께 저장하도록 구성했다.

csharp
public List<SaveItemData> ItemList = new List<SaveItemData>();
public List<SaveCharacterData> CharacterList = new List<SaveCharacterData>();

기존 저장 시스템을 유지하면서 새로운 데이터 종류를 추가하는 방식으로 구현했다.

마치며

이번 작업을 통해 기존 아이템 인벤토리의 구조를 재활용하면서도 데이터의 특성에 따라 구현 방식이 달라질 수 있다는 점을 확인했다.

특히 string과 enum을 사용한 ID 관리 방식의 차이를 이해할 수 있었다.

아이템처럼 데이터가 자주 추가되는 경우에는 string이 유연하지만 캐릭터처럼 정해진 종류를 코드에서 명확하게 구분해야 하는 경우에는 enum이 타입 안정성과 자동완성 측면에서 유리하다.

또한 JSON에 저장할 때는 enum을 문자열로 변환하고 불러올 때 다시 enum으로 복원하는 과정을 직접 구현해 볼 수 있었다. 물론 이 부분은 GPT선생님이 추천한 방법이다.

오늘 수업 한 30% 아래로 이해한 것 같은데 그래도 복습하면서 50%는 이해한 느낌이다. 오늘 머리에서 쥐 나는 줄 알았다. 강사님이 잘 설명해주지만 내가 속도를 못 따라간다. 생각 조금 하고 정신 차리면 갑자기 휘리릭 넘어가 있다. 걔다가 뒷자리라 유니티 인스펙터창은 왜 이리 글자가 작은건지 허허.... 가끔 이렇게 머리가 렉 걸린것처럼 되다보니까 어이없는 실수하고 강사님을 찾게된다. 그러고 나는 괜히 좀 뻘줌해진다 ꉂꉂ(ᵔᗜᵔ*)