- 게임을 만들다 보면 한 객체를 여러 곳에서 참조하는 구조가 자주 등장한다.
- 특히 장착 아이템과 인벤토리가 같은 아이템 객체를 공유하는 구조에서는, 이 참조가 세이브/로드 과정에서 깨지는 문제가 발생할 수 있다.
- 이번 포스트에서는 이 문제를 어떻게 Guid를 기반으로 해결했는지 기록해보려고 한다.
💥 문제 상황: 저장 후 장착 아이템 참조가 풀려버린다?
- C# Text RPG 게임에서 Player는 다음 두 개의 아이템 저장소를 가지고 있다:
- Inventory: 현재 보유 중인 아이템 리스트
- EquippedItems: 실제로 착용 중인 장비 (무기, 방어구 등)
- 이때 EquippedItems는 Inventory에 존재하는 아이템을 참조하는 구조였다.
예를 들어 무기를 장착하면 해당 무기 객체는 인벤토리와 장비 슬롯에서 동일한 인스턴스로 존재했다.
- 그런데 JSON으로 저장한 후 다시 불러오면, 장비 슬롯이 참조하던 객체는 더 이상 Inventory에 있는 아이템이 아니었다.\
// 저장 전: 참조 공유 상태
Inventory[0] == Equipments[ItemType.Weapon]
// 저장 후 로드하면:
Inventory[0] != Equipments[ItemType.Weapon] // 참조 깨짐
- JSON 역직렬화는 데이터를 "값"으로 복원하기 때문에, 객체 참조 관계가 사라진 것이다.
- 결과적으로 장비를 강화해도 인벤토리에는 반영되지 않고, 장비 해제 시 인벤토리와 따로 노는 현상이 발생했다.
✅ 해결 방법: 고유 ID(Guid)를 활용해 참조 복원하기
- 이 문제를 해결하기 위해, 아이템에 고유 식별자(Guid) 를 부여하고 장비창에는 참조 대신 ID만 저장하는 구조로 변경했다.
- 로드할 때는 인벤토리를 기준으로 ID → Item 매핑을 만든 뒤, 다시 참조를 연결해주는 방식이다.
- 저장 전: 장착 아이템의 참조 대신, ID만 따로 저장
📌 구조 요약
- Item 클래스에 Guid Id 필드를 추가
- Equipments는 여전히 Dictionary<ItemType, Item> 구조 유지
- 저장용으로 EquippedItemIds : Dictionary<ItemType, Guid> 를 따로 둠
- 저장 시: 장비창의 키와 Item.Id만 추출하여 EquippedItemIds에 저장
public void PrepareForSave()
{
EquippedItemIds = Equipments.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Id);
}
- 로드 시: Inventory를 ID 기준으로 매핑, Equipments는 EquippedItemIds를 통해 다시 Inventory 아이템 참조
// 장비창 복원
var dict = Inventory.ToDictionary(item => item.Id, item => item);
// 기존 장비 창
Equipments = new Dictionary<Item.ItemType, Item>();
foreach (var kvp in EquippedItemIds)
{
if (dict.TryGetValue(kvp.Value, out var item))
{
Equipments[kvp.Key] = item;
}
}
- 이 방식은 단순하면서도 확장성이 좋고, 추후 데이터베이스나 서버 저장 방식으로 전환할 때도 유용하게 쓸 수 있다고 한다.
❗ 뜻밖의 문제: this는 여전히 나였다
- Guid 기반 장 구조를 도입하고 “이제 다 됐다”고 생각한 순간, 생각지도 못한 문제에 부딪혔다.
- 바로 LoadPlayer() 메서드 안에서 this와 player의 개념이 꼬여버린 것이다.
// LoadScene 클래스
Player? player = SaveManager.Load(stringName);
if (player == null) // 불러오지 못했다면
{
Console.Write("해당 플레이어의 데이터를 찾을 수 없습니다.");
Thread.Sleep(2000);
}
else // 불러왔다면
{
Player.Instance.LoadPlayer(player); // 불러온 플레이어 세팅
}
- 위 코드는 JSON에서 복원한 player 객체를 싱글턴 Player.Instance에 반영하려는 의도였다.
- 그래서 Player 클래스에 LoadPlayer() 메서드를 만들어 다음과 같이 구현했다:
// Player 클래스
public void LoadPlayer(Player player)
{
instance = player;
// 장비창 복원
var dict = instance.Inventory.ToDictionary(item => item.Id, item => item);
instance.Equipments = new Dictionary<Item.ItemType, Item>();
foreach (var kvp in instance.EquippedItemIds)
{
if (dict.TryGetValue(kvp.Value, out var item))
{
instance.Equipments[kvp.Key] = item;
}
}
// 장착된 아이템에 따라 능력치 세팅
instance.SetAbilityByEquipment();
}
🤯 내가 빠진 착각
- 처음엔 instance = player;라고 했으니, 이제 this도 player일 거라고 착각했다.
- 하지만 C#(C++)에서 this는 항상 현재 메서드를 호출한 인스턴스를 가리킨다.
- 즉, this.instance = player;는 내 내부 필드(instance) 를 player로 바꾼 것일 뿐, this 자체가 player가 된 게 아니다.
- 해결하고 나니 구조를 바꿔야 하지 않나 싶었지만, 교훈이 되도록 이대로 두기로 했다.
📝 마무리하며
- 포인터(참조) 개념은 익숙하다고 생각했는데, 여전히 부족함을 느낀 하루였다.
'Programming > C#' 카테고리의 다른 글
| 🎯 C# 델리게이트와 이벤트 차이 - Action vs event Action (1) | 2025.08.14 |
|---|---|
| C# 미니 프로젝트 - TextRPG (1) | 2025.07.11 |
| C# 콘솔에서 멀티 스레드로 키 입력 처리하기 (2) | 2025.07.10 |
| C# static 함수의 구조와 그 의미 (2) | 2025.07.09 |
| C# object: 참조 타입의 뿌리 (1) | 2025.07.07 |
