[방치형게임 만들기] 게임 데이터 저장/불러오기
📕학습 개요
오늘은 유니티 게임 데이터 저장/로드 시스템을 구현하며 마주친 클래스 상속 관계(다형성)의 직렬화 문제를 깊이 있게 학습했다. 두 가지 해결 방안의 장단점을 비교하며 어떤 방식이 더 유지보수하기 좋은 구조인지 명확하게 이해할 수 있었다.
📖학습 내용
1. 유니티 생명주기와 초기화 순서의 이해
- 문제점:
MonoBehaviour가 아닌 일반 C# 클래스의 생성자에서Application.persistentDataPath같은 유니티 API를 호출하면UnityException이 발생했다. - 원인: C# 클래스의 생성자는 유니티 엔진이 완전히 준비되기 전에 호출된다. 따라서 이 시점에는 파일 경로 같은 플랫폼 종속적인 정보에 접근할 수 없다.
- 핵심 원칙: 유니티 API 호출은 반드시 엔진이 준비된 후인
Awake()나Start()메서드 내에서 이루어져야 한다. - 설계 패턴: 일반 C# 클래스(
SaveLoadManager)가 유니티 API에 직접 의존하는 대신,MonoBehaviour를 상속하는 상위 관리자(Managers)가Awake()에서 경로를 생성하여,SaveLoadManager의 생성자에 매개변수로 전달(의존성 주입)하는 방식으로 문제를 해결했다. 이로써 클래스 간의 의존성이 분리되고 코드가 더 명확해졌다.
2. 데이터 직렬화 기법과 선택
JsonUtility의 한계: 유니티 내장JsonUtility는 빠르지만Dictionary타입을 직접 직렬화하지 못하는 명백한 한계가 있다.Newtonsoft.Json의 활용: 외부 패키지인Newtonsoft.Json은 딕셔너리를 포함한 거의 모든 C# 객체를 손쉽게 직렬화할 수 있어 매우 강력하다.Package Manager를 통해 쉽게 설치할 수 있다.BigInteger저장:long타입을 초과하는 매우 큰 숫자를 다루는BigInteger는 JSON의 숫자 타입 한계를 넘을 수 있다. 데이터 손실을 방지하기 위해ToString()으로 변환하여 문자열(string)로 저장하고, 불러올 때BigInteger.Parse()로 복원하는 것이 가장 안전하다.[JsonIgnore]와 대리 속성(Surrogate Property)을 사용하면 이 과정을 깔끔하게 자동화할 수 있다.
[JsonIgnore] public BigInteger Gold { get; private set;}
// 안전하게 저장하기 위해 문자열로 변환해서 저장
public string Gold_str
{
get{return Gold.ToString();}
set
{
if (!string.IsNullOrEmpty(value))
{
Gold = BigInteger.Parse(value);
}
else
{
Gold = BigInteger.Zero;
}
}
}
3. 다형성(Polymorphism) 데이터 처리
- 문제점:
Dictionary<int, ItemState>처럼 부모 클래스 타입으로 저장된 데이터를 불러올 때,Newtonsoft.Json은 각 항목이 어떤 자식 클래스(GearState,SkillState등)인지 알지 못해 모두 부모 클래스로 생성했다. 이로 인해is GearState같은 타입 캐스팅이 실패했다. - 해결 방안 1:
TypeNameHandling설정 (추천)JsonSerializerSettings에서TypeNameHandling = TypeNameHandling.Objects옵션을 설정했다.- 이 옵션은 직렬화 시 JSON에
$type이라는 메타데이터를 추가하여 각 객체의 정확한 타입을 기록한다. - 역직렬화 시 이
$type정보를 읽어 정확한 자식 클래스로 객체를 생성해주므로, 타입 캐스팅 문제가 완벽히 해결된다. - 장점: 확장성이 매우 뛰어나다. 나중에 새로운 아이템 타입이 추가돼도 저장/로드 관련 코드를 전혀 수정할 필요가 없다.
- 해결 방안 2: 타입별 컨테이너 분리
Dictionary<int, GearState>,Dictionary<int, SkillState>처럼 각 타입별로 별개의 딕셔너리에 나눠서 저장하는 방식.- 단점: 새로운 타입이 추가될 때마다
SaveData클래스, 저장 로직, 로드 로직 등 여러 곳을 수정해야 해서 유지보수가 어렵다.
4. 안정적인 데이터 로딩 로직 설계
- “게임을 불러오면 인벤토리가 비는” 버그를 통해 체계적인 디버깅의 중요성을 배웠다.
- 데이터 파이프라인 확인: 문제가 발생했을 때, 저장 → 파일 기록 → 파일 읽기 → 역직렬화 → 데이터 적용의 전 과정을 단계별로 의심하고 확인해야 한다.
- 디버깅 체크리스트:
- 저장 파일 원본 확인:
Application.persistentDataPath경로의.json파일에 데이터가 제대로 기록되었는지 직접 열어서 확인한다. - 역직렬화 결과 확인: 데이터를 불러온 직후,
SaveData객체 내의 컬렉션(itemStates등)에 데이터가 들어있는지Debug.Log로 개수를 찍어본다. - 초기화 순서 확인:
InventoryManager.Init()이ItemDatabase의 초기화보다 먼저 실행되는 등의 순서 문제를 확인한다.Script Execution Order설정이 필요할 수 있다. - 데이터 재연결(Re-linking) 로직 확인: 불러온 상태 데이터(
ItemState)에 원본 데이터(ItemData)를 다시 연결해주는 로직이 누락되지 않았는지 확인한다. (주석 처리되어 있던 부분) - 방어 코드:
as연산자 사용 후에는 항상null체크를 하여 예기치 않은NullReferenceException을 방지한다.
- 저장 파일 원본 확인:
댓글남기기