유니티/유니티 공부

JsonUtility

DOlpa_ 2025. 8. 19. 02:03
728x90

게임에서 저장과 불러오기라는 기능을 구현하기 위해서는 여러가지 방법들이 있지만 크게 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 텍스트로 번역하고 파일에 쓰고 읽는 '실무자' 역할을 담당한다.

 

728x90
반응형