게임에서 저장과 불러오기라는 기능을 구현하기 위해서는 여러가지 방법들이 있지만 크게 3가지로 나뉜다
1. PlayerPrefs (가장 간단)
- 키-값 쌍으로 데이터를 저장 (int, float, string 지원).
PlayerPrefs.SetInt("HighScore", 100);
PlayerPrefs.Save(); // 저장
int score = PlayerPrefs.GetInt("HighScore", 0); // 불러오기 (기본값 0)
- 장점: 간단, 소규모 데이터에 적합.
- 단점: 구조화된 데이터 저장 어려움, 보안 취약(플레이어가 파일 직접 수정 가능).
2. JSON 저장
- JsonUtility 또는 Newtonsoft.Json(외부 라이브러리)로 객체를 직렬화/역직렬화.
- 파일로 저장했다가 다시 불러올 수 있음.
[System.Serializable]
public class PlayerData {
public int money;
public string weapon;
public int ammo;
}
// 저장
PlayerData data = new PlayerData(){ money=100, weapon="Sword", ammo=30 };
string json = JsonUtility.ToJson(data);
System.IO.File.WriteAllText(Application.persistentDataPath + "/save.json", json);
// 불러오기
string jsonLoad = System.IO.File.ReadAllText(Application.persistentDataPath + "/save.json");
PlayerData loadedData = JsonUtility.FromJson<PlayerData>(jsonLoad);
- 장점: 구조적 데이터 저장 가능, 사람이 직접 읽고 수정 가능.
- 단점: 대용량에는 비효율적, 보안 없음 → 암호화 필요할 수 있음.
3. BinaryFormatter (바이너리 직렬화)
- 데이터를 이진 파일로 직렬화.
- JSON보다 용량 절약 가능, 사람이 직접 수정하기 어려움(보안 ↑).
더 있긴 하지만 인디게임 규모에서 쓰는건 이렇게 셋
갤볼에서는 1번 방식만 사용했었지만 조금더 저장해야할 데이터가 복잡해지면서 2번인 JsonUtility 방식도 사용하기로 했다
그렇기에 오늘은 이 중 2번! JsonUtility를 공부해보도록 해보자
< JsonUtility >
1. Json과 JsonUtility란?
게임을 껐다 켜도 내 돈과 아이템이 그대로 있으려면 어딘가에 기록을 해둬야한다.
JSON은 그 기록을 위한 '메모 형식(포맷)' 중 하나로, 사람이 읽고 쓰기 편한 텍스트로 되어있다.
JsonUtility는 Unity에 내장된 '번역가' 이다. 우리가 C# 스크립트에서 사용하는 데이터(클래스 객체)를
컴퓨터가 파일로 저장할 수 있는 JSON 텍스트 형식으로, 또는 그 반대로 번역해주는 역할을 한다.
JsonUtility.ToJson(data): C# 데이터 객체 -> JSON 텍스트로 번역 (저장용)
JsonUtility.FromJson<T>(json): JSON 텍스트 -> C# 데이터 객체로 번역 (불러오기용)
2. 데이터 설계도 : 무엇을 저장하는가?
번역가에게 일을 시키려면, 어떤 데이터를 번역할지 명확히 알려줘야 한다.
이것이 바로 '데이터 설계도', 즉 데이터를 담을 클래스이다.
[System.Serializable]
public class JokerData
{
public string objectName;
public int slot; // JokerZone 내에서의 슬롯 위치 (1부터 시작)
}
...
[Serializable]
public class PlayerData
{
public int handcount;
public int trashcount;
public int money;
public int ante;
public int round;
}
[System.Serializable] 속성: 클래스 선언 바로 위에 [System.Serializable]이라고 붙어있는데
이건 Unity의 JsonUtility 번역가에게 "이 클래스는 번역 대상이니 잘 봐둬!"라고 알려주는 중요한 표시이다.
이 표시가 없으면 JsonUtility가 해당 클래스를 번역할 수 없다.
구매한 조커 아이템 하나의 정보를 담는다. 이름(objectName)과 슬롯 위치(slot)를 가지고 있음.
[System.Serializable]
public class BuyJokersData
{
public List<JokerData> clickedObjects = new List<JokerData>();
}
BuyJokersData 클래스 : 구매한 모든 조커 아이템 목록(List<JokerData>)을 담는 더 큰 상자.
즉, JokerData 여러 개를 리스트로 관리한다.
이처럼 저장하고 싶은 데이터의 종류에 맞게 '설계도' 클래스를 만들어두는 것이 첫걸음이다.
3. 저장 과정 파헤치기 - 언제, 어떻게 저장될까?
이제 실제로 데이터가 저장되는 과정을 따라가 보자
3-1. 저장 시점 (Trigger): 주로 데이터에 변화가 생겼을 때 저장을 호출한다.
if (gameManager != null && price > 0 && gameManager.playerData.money - price >= 0)
{
gameManager.BuyItem(price);
아이템 구매/판매:
DragItem.cs 에서 아이템을 구매 영역(BuyZone)에 놓으면(OnMouseUp 메소드),
GameManager.BuyItem() 이나 GameManager.SellItem()을 호출한다.
public void BuyItem(int price)
{
//gameSaveData.money -= price;
playerData.money -= price;
saveManager.Save(playerData);
UpdateUI();
}
public void SellItem(int price)
{
//gameSaveData.money += price;
playerData.money += price;
saveManager.Save(playerData);
UpdateUI();
}
public void Reroll(int RerollCost)
{
//gameSaveData.money -= RerollCost;
playerData.money -= RerollCost;
saveManager.Save(playerData);
UpdateUI();
}
3-2. 저장 실행: GameManager.cs의 BuyItem, SellItem, Reroll, PlusRound 메서드를 보자.
이들은 모두 playerData의 값을 변경한 뒤, 마지막에 saveManager.Save(playerData); 를 호출한다
혹은 itemData.SaveObjectData를 호출한다
Debug.Log($"{itemType} 아이템 구매 성공! - 프리팹 이름: {gameObject.name}, 배정된 슬롯: {slotNumber}번");
ItemData itemData = FindObjectOfType<ItemData>();
itemData.SaveObjectData(gameObject.name);
...
public void SaveObjectData(string objectName)
{
// JokerZone 찾기
GameObject jokerZone = GameObject.FindGameObjectWithTag("JokerZone");
if (jokerZone == null)
{
Debug.LogError("JokerZone을 찾을 수 없습니다!");
return;
}
// 기존 데이터 초기화
buyJokersData.clickedObjects.Clear();
....
3-3. 실제 저장 로직
saveManager.Save(playerData);
.....
public void Save(PlayerData data)
{
string json = JsonUtility.ToJson(data, true); // true = 보기 좋은 형식
File.WriteAllText(savePath, json);
Debug.Log("저장 완료: " + savePath);
}
string json = JsonUtility.ToJson(data, true);:
GameManager 로부터 받은 playerData 객체를 JSON 텍스트로 번역한다.
true 옵션은 사람이 보기 좋게 줄바꿈을 넣어준다
File.WriteAllText(savePath, json); 번역된 JSON 텍스트를 save.json이라는 파일에 통째로 덮어쓴다.
public void SaveObjectData(string objectName)
{
// JokerZone 찾기
GameObject jokerZone = GameObject.FindGameObjectWithTag("JokerZone");
// 기존 데이터 초기화
buyJokersData.clickedObjects.Clear();
// JokerZone의 자식들을 순회하며 저장
int index = 0;
foreach (Transform child in jokerZone.transform)
{
if (child.CompareTag("BuyJoker"))
{
// 프리팹 이름에서 (Clone) 제거
string prefabName = child.name;
if (prefabName.Contains("(Clone)"))
{
prefabName = prefabName.Replace("(Clone)", "").Trim();
}
buyJokersData.clickedObjects.Add(new JokerData
{
objectName = prefabName,
slot = index++
});
}
}
// JSON으로 변환하여 저장
string jsonData = JsonUtility.ToJson(buyJokersData, true);
// savePath가 null이면 기본값 설정
if (string.IsNullOrEmpty(savePath))
{
savePath = "./Saves/JokerZone";
Debug.LogWarning($"[ItemData] 저장 경로가 비어있어 기본값으로 설정합니다: {savePath}");
}
string filePath = Path.Combine(savePath, "JokerZoneData.json");
Debug.Log($"[ItemData] 데이터 파일 경로: {filePath}");
File.WriteAllText(filePath, jsonData);
Debug.Log("JokerZone 데이터 저장 완료");
}
JokerZone에 있는 모든 아이템들을 순회하며 saveData.clickedObjects 리스트에 추가한다.
마찬가지로 JsonUtility.ToJson()으로 JSON 텍스트로 번역한 뒤, JokerZoneData.json 파일에 저장한다.
4. 불러오기 - 게임 시작시 데이터 복원
4-1. 불러오기 시점
private void Start()
{
gameSaveData = FindAnyObjectByType<GameSaveData>();
saveManager = FindAnyObjectByType<SaveManager>();
// 데이터 리셋은 여기서!!!!
//ResetData();
var itemData = FindObjectOfType<ItemData>();
if (itemData != null)
{
itemData.LoadAndPlaceJokerItems();
}
playerData = saveManager.Load();
불러오기 시점은 주로 게임이 시작되는 Start() 지점에서 이뤄진다
4-2. 불러오기 실행
public PlayerData Load()
{
if (!File.Exists(savePath))
{
Debug.LogWarning("저장 파일이 없음. 기본값 반환.");
return new PlayerData(); // 기본값
}
string json = File.ReadAllText(savePath);
PlayerData data = JsonUtility.FromJson<PlayerData>(json);
Debug.Log("불러오기 완료!");
return data;
}
File.Exists(savePath): 먼저 저장 파일(save.json)이 있는지 확인한다.
없다면 새로 시작하는 것이므로 기본 데이터를 반환한다.
string json = File.ReadAllText(savePath); : 파일에 있는 JSON 텍스트를 전부 읽어온다.
PlayerData data = JsonUtility.FromJson<PlayerData>(json);
읽어온 JSON 텍스트를 PlayerData C# 객체로 번역한다. 어떤 설계도(PlayerData)로 번역할지 알려주는 것이 중요하다.
return data; : 번역된 data 객체를 GameManager에게 돌려준다.
public void LoadAndPlaceJokerItems()
{
// savePath가 null이면 기본값 설정
if (string.IsNullOrEmpty(savePath))
{
savePath = "./Saves/JokerZone";
Debug.LogWarning($"[ItemData] 저장 경로가 비어있어 기본값으로 설정합니다: {savePath}");
}
string filePath = Path.Combine(savePath, "JokerZoneData.json");
Debug.Log($"[ItemData] 데이터 파일 경로: {filePath}");
if (!File.Exists(filePath))
{
Debug.Log("저장된 JokerZone 데이터가 없습니다.");
return;
}
// 저장된 데이터 로드
string jsonData = File.ReadAllText(filePath);
buyJokersData = JsonUtility.FromJson<BuyJokersData>(jsonData);
}
위 방법과 거의 동일한 과정으로 JokerZoneData.json 파일을 읽고
JsonUtility.FromJson<BuyJokersData>()를 통해 BuyJokersData 객체로 번역한다.
번역된 데이터를 바탕으로 for문을 돌며 Resources.Load 로 아이템 프리팹을 불러오고,
Instantiate로 게임 월드에 실제로 생성하여 배치한다.
5. 총정리
ItemData.cs & GameManager.cs
무엇을 저장할지 정의하는 '데이터 설계도'(PlayerData, BuyJokersData 등)를 가지고 있다.
DragItem.cs : 플레이어의 행동(드래그 앤 드롭)을 감지하여 데이터 변경과 **'저장 명령'**을 내리는 역할을 한다.
SaveManager.cs & ItemData.cs
JsonUtility라는 **'번역가'**를 이용해 실제 데이터를 JSON 텍스트로 번역하고 파일에 쓰고 읽는 '실무자' 역할을 담당한다.
'유니티 > 유니티 공부' 카테고리의 다른 글
| 도트 필터 씌우기 - 렌더 텍스처(Render Texture) (1) | 2026.01.21 |
|---|---|
| OnMouse 와 OnPointer, Raycast에 대하여 (1) | 2025.11.26 |
| #3 C# 프로그래밍 (메서드, 간단한 코딩예시, 클래스,인스턴스,접근제한자,참조타입) (2) | 2025.03.17 |
| #2 유니티 엔진 동작 원리 (클래스,메서드,상속,컴포넌트, 브로드캐스팅, MonoBehavior) (0) | 2025.02.28 |
| #1 유니티 인터페이스 (Edit bounding volume, Flythrough) (1) | 2025.02.26 |