3. Save & load

개발하는 운동인·2025년 1월 15일

아래 내용들은 <오늘코딩> 유투버를 통해 정리함.

Save & load을 구현하기 위해 Json의 간단한 개념

건담을 멀리 있는 누군가에게 택배를 보내야 하는데 어떻게 하면 좋을까?

  • 건담을 내용물과 + 조립설명서로 분리해서 택배 상자에 담아 보낸다.

분리된 것들을 이용하여 건담을 조립할 때는?

-내용물 + 조립설명서를 통해 건담을 조립한다.

즉, 처음에 보냈던 건담의 상태가 받았을 때 건담의 상태가 동일할 것이다.

원하는 것을 저장하거나 통신할 때 아래와 같은 개념으로 이루어짐.

  • 클래스(코드) -> Json(택배) : 클래스를 Json으로 변환하는 과정이 필요.
  • Json(택배) -> 조립도 -> 클래스(코드) : Json을 클래스로 변환하는 과정이 필요.

변환하는 과정들은 유니티에서 기본적으로 지원하고 있다.

Json 사용 방법

    1. 데이터(코드 = 클래스)를 만들어야 함 -> 저장할 데이터 생성
    1. 그 데이터를 Json으로 변환(택배 상자로 포장)
    1. Json(택배)를 다시 원래의 데이터(코드 = 클래스)로 바꾸는 방법

1. 데이터(코드 = 클래스)를 만들기 -> 저장할 데이터 생성

    1. 임의로 TestItem.cs 스크립트를 생성한다. 그리고 Data라는 클래스를 만든다.
using UnityEngine;

class Data //⭐ 
{
    
}
public class TestItem : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
  • 그런 다음, Data 클래스 안에 저장하고 싶은데 데이터들을 작성하면 됨.
using UnityEngine;

class Data
{
    public string nickname; //⭐ 
    public int level; //⭐ 
    public int coin; //⭐ 
    public bool isSkill; //⭐ 
    //기타 등등
}
public class TestItem : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
  • 하지만, 지금 클래스(코드)를 만들었는데 , 엄밀히 말하면 데이터는 없는 상태다. 즉, 변수만 만들었지 어떤 값도 존재하지 않기 때문. 그래서 값을 넣어줘야 한다.
using UnityEngine;

class Data
{
    public string nickname;
    public int level;
    public int coin;
    public bool isSkill;
    //기타 등등
}
public class TestItem : MonoBehaviour
{

    Data player = new Data() //⭐
    {
        nickname = "김진", //⭐
        level = 50, //⭐
        coin = 200, //⭐
        isSkill = false //⭐
    };


    private void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

2. 그 데이터를 Json으로 변환(택배상자로 포장)

    1. 다음 코드를 추가한다. jSON 변환은 유니티에서 지원하고 있다. 그리고 변환할 때 반환타입은 string 이므로 string 타입 변수로 저장할 수 도 있다. 물론 출력도 가능.
using UnityEngine;

class Data
{
    public string nickname;
    public int level;
    public int coin;
    public bool isSkill;
    //기타 등등
}
public class TestItem : MonoBehaviour
{

    Data player = new Data()
    {
        nickname = "김진",
        level = 50,
        coin = 200,
        isSkill = false
    };


    private void Start()
    {
        string jsonData =  JsonUtility.ToJson(player); //⭐

        Debug.Log(jsonData); //⭐
    }

}
  • 실행 결과이다. 값을 넣어준 결과대로 정상 출력 되었다.

3. Json(택배)를 다시 원래의 데이터(코드 = 클래스)로 바꾸는 방법

    1. 다시 코드를 추가
using UnityEngine;

class Data
{
    public string nickname;
    public int level;
    public int coin;
    public bool isSkill;
    //기타 등등
}
public class TestItem : MonoBehaviour
{

    Data player = new Data()
    {
        nickname = "김진",
        level = 50,
        coin = 200,
        isSkill = false
    };


    private void Start()
    {
        // 1. 건담 조립도(클래스)을 보고 나서 택배 상자로 포장 -> Json으로 변환하는 과정.
        string jsonData =  JsonUtility.ToJson(player);

        // 2. 택배 상자에 건담 조립도(클래스)가 Data  클래스이므로 조립도를 보고 클래스로 변환
        Data player2 =  JsonUtility.FromJson<Data>(jsonData); //⭐ FromJson<데이터가 들어있는 클래스(조립도) 적으면 > , 
        Data 클래스로 변환 할 것이기 때문에 반환 타입이 Data이므로 Data 객체 선언
          
        Debug.Log(player2.nickname); //⭐
        Debug.Log(player2.level); //⭐
        Debug.Log(player2.coin); //⭐
        Debug.Log(player2.isSkill); //⭐
    }

}
  • 실행 결과. 1번에서 값을 넣어준대로 정상 출력되었다.

구현 시작

  • Scene을 2개 만들 것이다. 슬롯을 클릭하여 게임씬으로 넘어 갈 수 있게. 난 Select 씬이 아닌 MainMenu 씬으로 할 것임.
  • Script는 3개 만들 것이다. 각 씬에서 담당하는 Select, Game 데이터를 관리할 DataManager 스크립트를 만들 것
    1. 씬을 만들고 씬 리스트에 저장

    1. Selet 스크립트가 아닌 SceneSelct 스크립트, Game 스크립트가 아닌 SceneInGame 스크립트, 그리고 DataManager 스크립트 생성
    1. MainMenu 씬에서 DataManager라는 빈 객체 만들고 DataManager 스크립트 추가.
    1. DataManager.cs 을 싱글톤으로. 왜? 게임 내에 항상 존재해야 하므로 항상 접근가능하게.
using UnityEngine;

public class DataManager : MonoBehaviour
{
    //게임 내에 항상 존재하면 좋으므로 싱글톤
    private static DataManager instance;
    public static DataManager Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject go = new GameObject("DataManager"); //EventBus라는 빈 객체를 만들고
                instance = go.AddComponent<DataManager>(); //EventBus 빈 객체에 EventBus 스크립트(컴포넌트)을 추가
            }
            return instance;
        }



    }

    private void Awake()
    {
        if(instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(instance.gameObject);
        }
        
  		DontDestroyOnLoad(go);
    }
}
  • 실행 결과. 파괴 되지 않음.

데이터 저장하는 방법 1: 저장할 데이터가 존재 - 저장할 때 Json 사용

using UnityEngine;


public class PlayerData //⭐
{
    //이름, 레벨 , 코인, 착용중인 무기 들을 저장
    public string name; /⭐
    public int level; /⭐
    public int coin; /⭐
    public int item; /⭐ int로 한 이유는 이 값이 1번이라면 1번에 대응되는 무기. 2번이라면 2번에 대응되는 무기.
}
public class DataManager : MonoBehaviour
{
    //게임 내에 항상 존재하면 좋으므로 싱글톤
    private static DataManager instance;
    public static DataManager Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject go = new GameObject("DataManager"); //EventBus라는 빈 객체를 만들고
                instance = go.AddComponent<DataManager>(); //EventBus 빈 객체에 EventBus 스크립트(컴포넌트)을 추가
            }
            return instance;
        }



    }

    PlayerData nowPlayer = new PlayerData(); 
 
    private void Awake()
    {
        #region 싱글톤
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(instance.gameObject);
        }
        DontDestroyOnLoad(this.gameObject);
        #endregion

    }

    private void Start()
    {
        string data = JsonUtility.ToJson(nowPlayer);
    }
}

데이터 저장하는 방법 2: 데이터를 Json으로 변환

using UnityEngine;


public class PlayerData
{
    //이름, 레벨 , 코인, 착용중인 무기 들을 저장
    public string name;
    public int level;
    public int coin;
    public int item; //int로 한 이유는 이 값이 1번이라면 1번에 대응되는 무기. 2번이라면 2번에 대응되는 무기.
}
public class DataManager : MonoBehaviour
{
    //게임 내에 항상 존재하면 좋으므로 싱글톤
    private static DataManager instance;
    public static DataManager Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject go = new GameObject("DataManager"); //EventBus라는 빈 객체를 만들고
                instance = go.AddComponent<DataManager>(); //EventBus 빈 객체에 EventBus 스크립트(컴포넌트)을 추가
            }
            return instance;
        }



    }

    PlayerData nowPlayer = new PlayerData(); //⭐
 
    private void Awake()
    {
        #region 싱글톤
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(instance.gameObject);
        }
        DontDestroyOnLoad(this.gameObject);
        #endregion

    }

    private void Start()
    {
        string data = JsonUtility.ToJson(nowPlayer); //⭐
    }
}

데이터 저장하는 방법 3: Json을 외부에 저장

  • 지시어를 아래와 같이 작성. 이 지시어는 Input,Output 즉, 어딘가로 데이터를 빼거나 가져올 때 사용.
    1. 일단 다음과 같이 작성
using UnityEngine;
using System.IO;

public class PlayerData
{
    //이름, 레벨 , 코인, 착용중인 무기 들을 저장
    public string name;
    public int level;
    public int coin;
    public int item; //int로 한 이유는 이 값이 1번이라면 1번에 대응되는 무기. 2번이라면 2번에 대응되는 무기.
}
public class DataManager : MonoBehaviour
{
    //게임 내에 항상 존재하면 좋으므로 싱글톤
    private static DataManager instance;
    public static DataManager Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject go = new GameObject("DataManager"); //EventBus라는 빈 객체를 만들고
                instance = go.AddComponent<DataManager>(); //EventBus 빈 객체에 EventBus 스크립트(컴포넌트)을 추가
            }
            return instance;
        }



    }

    PlayerData nowPlayer = new PlayerData();

    string path; //⭐
    
    private void Awake()
    {
        #region 싱글톤
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(instance.gameObject);
        }
        DontDestroyOnLoad(this.gameObject);
        #endregion

        path = Application.persistentDataPath;  //⭐
    }

    private void Start()
    {
        string data = JsonUtility.ToJson(nowPlayer);  //⭐
		Debug.Log(path); //⭐
       // File.WriteAllText(path,data); //WriteAllText.(경로,어떤걸 저장할지);
    }
}
  • 실행 결과. path가 무엇인지 너무 궁금해서 출력 해보려함.
  • 이 프로젝트 경로를 따라가니 아래 사진처럼 텅 비어있다.

즉, 이 경로는 유니티에서 모바일 게임이나 PC게임을 만들 때 유니티가 알아서 폴더를 생성해주는데 생성 폴더 경로를 의미한다.

    1. 다시 코드 작성. 저장할 때는 WriteAllText()을 사용
using UnityEngine;
using System.IO; //⭐ 저장하기 위해 File.WriteAllText() 사용해야 함.

public class PlayerData
{
    //이름, 레벨 , 코인, 착용중인 무기 들을 저장
    public string name;
    public int level;
    public int coin;
    public int item; //int로 한 이유는 이 값이 1번이라면 1번에 대응되는 무기. 2번이라면 2번에 대응되는 무기.
}
public class DataManager : MonoBehaviour
{
    //게임 내에 항상 존재하면 좋으므로 싱글톤
    private static DataManager instance;
    public static DataManager Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject go = new GameObject("DataManager"); //EventBus라는 빈 객체를 만들고
                instance = go.AddComponent<DataManager>(); //EventBus 빈 객체에 EventBus 스크립트(컴포넌트)을 추가
            }
            return instance;
        }



    }

    PlayerData nowPlayer = new PlayerData();

    string path;

    string filename = "Save";         //⭐ 파일 명까지 정해주는게 나중에 파일 관리할 때 편함. 파일명은 Save
    private void Awake()
    {
        #region 싱글톤
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(instance.gameObject);
        }
        DontDestroyOnLoad(this.gameObject);
        #endregion

        path = Application.persistentDataPath + "/";  //⭐"/" 를 경로 맨 뒤에 붙여줘야 오류가 없음. 
    }

    private void Start()
    {
        string data = JsonUtility.ToJson(nowPlayer);


        File.WriteAllText(path + filename, data); //⭐ WriteAllText.(경로,어떤걸 저장할지);
    }
}
  • 실행 결과
  • Save라는 파일이 생겼다.

저장하는 기능은 필요할 때마다 자주 쓰는 기능이므로 별도의 메서드 생성

    1. 다시 코드 작성
using UnityEngine;
using System.IO;

public class PlayerData
{
    //이름, 레벨 , 코인, 착용중인 무기 들을 저장
    public string name;
    public int level;
    public int coin;
    public int item; //int로 한 이유는 이 값이 1번이라면 1번에 대응되는 무기. 2번이라면 2번에 대응되는 무기.
}
public class DataManager : MonoBehaviour
{
    //게임 내에 항상 존재하면 좋으므로 싱글톤
    private static DataManager instance;
    public static DataManager Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject go = new GameObject("DataManager"); //EventBus라는 빈 객체를 만들고
                instance = go.AddComponent<DataManager>(); //EventBus 빈 객체에 EventBus 스크립트(컴포넌트)을 추가
            }
            return instance;
        }



    }

    PlayerData nowPlayer = new PlayerData();

    string path;

    string filename = "Save";         //파일 명까지 정해주는게 나중에 파일 관리할 때 편함.
    private void Awake()
    {
        #region 싱글톤
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(instance.gameObject);
        }
        DontDestroyOnLoad(this.gameObject);
        #endregion

        path = Application.persistentDataPath + "/"; 
    }

    private void Start()
    {

    }

    public void SaveData() //⭐
    {
        string data = JsonUtility.ToJson(nowPlayer); //⭐

        //저장하기 - WriteAllText() 사용
        File.WriteAllText(path + filename, data); //⭐ WriteAllText.(경로,어떤걸 저장할지);
    }

}

불러오는 방법 1: 외부에 저장된 Json을 가져옴

using UnityEngine;
using System.IO;

public class PlayerData
{
    //이름, 레벨 , 코인, 착용중인 무기 들을 저장
    public string name;
    public int level;
    public int coin;
    public int item; //int로 한 이유는 이 값이 1번이라면 1번에 대응되는 무기. 2번이라면 2번에 대응되는 무기.
}
public class DataManager : MonoBehaviour
{
    //게임 내에 항상 존재하면 좋으므로 싱글톤
    private static DataManager instance;
    public static DataManager Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject go = new GameObject("DataManager"); //EventBus라는 빈 객체를 만들고
                instance = go.AddComponent<DataManager>(); //EventBus 빈 객체에 EventBus 스크립트(컴포넌트)을 추가
            }
            return instance;
        }



    }

    PlayerData nowPlayer = new PlayerData();

    string path;

    string filename = "Save";         //파일 명까지 정해주는게 나중에 파일 관리할 때 편함.
    private void Awake()
    {
        #region 싱글톤
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(instance.gameObject);
        }
        DontDestroyOnLoad(this.gameObject);
        #endregion

        path = Application.persistentDataPath + "/"; 
    }

    private void Start()
    {

    }

    public void SaveData()
    {
        string data = JsonUtility.ToJson(nowPlayer);

        //저장하기 - WriteAllText() 사용
        File.WriteAllText(path + filename, data); //WriteAllText.(경로,어떤걸 저장할지);
    }

    public void LoadData() //⭐
    {
        //불러오기 - ReadAllText() 사용
        string data = File.ReadAllText(path + filename); //⭐    
    }
}

불러오는 방법 2: Json을 데이터 형태로 변환

using UnityEngine;
using System.IO;

public class PlayerData
{
    //이름, 레벨 , 코인, 착용중인 무기 들을 저장
    public string name;
    public int level;
    public int coin;
    public int item; //int로 한 이유는 이 값이 1번이라면 1번에 대응되는 무기. 2번이라면 2번에 대응되는 무기.
}
public class DataManager : MonoBehaviour
{
    //게임 내에 항상 존재하면 좋으므로 싱글톤
    private static DataManager instance;
    public static DataManager Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject go = new GameObject("DataManager"); //EventBus라는 빈 객체를 만들고
                instance = go.AddComponent<DataManager>(); //EventBus 빈 객체에 EventBus 스크립트(컴포넌트)을 추가
            }
            return instance;
        }



    }

    PlayerData nowPlayer = new PlayerData();

    string path;

    string filename = "Save";         //파일 명까지 정해주는게 나중에 파일 관리할 때 편함.
    private void Awake()
    {
        #region 싱글톤
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(instance.gameObject);
        }
        DontDestroyOnLoad(this.gameObject);
        #endregion

        path = Application.persistentDataPath + "/"; 
    }

    private void Start()
    {

    }

    public void SaveData()
    {
        string data = JsonUtility.ToJson(nowPlayer);

        //저장하기 - WriteAllText() 사용
        File.WriteAllText(path + filename, data); //WriteAllText.(경로,어떤걸 저장할지);
    }

    public void LoadData()
    {
        //불러오기 - ReadAllText() 사용
        string data = File.ReadAllText(path + filename);

        nowPlayer = JsonUtility.FromJson<PlayerData>(data); //⭐ 기존에 선언했던 nowPlayer을 불러오고 나서 덮어 씌우게 됨
    }
}

불러오는 방법 3: 불러온 데이터를 사용

  • 위 코드에서 불러온 데이터이자 Json을 덮어 씌운 nowPlayer을 사용하면 된다.

슬롯별로 다르게 저장해야 한다.

씬 UI 구성

    1. 다음과 같이 배치한다.

  • 이름 적기 위해 아래 처럼 세팅

  • 아래 이미지는 비활성화 한 상태로 한다.

어떤 식으로 구현 할 것이냐면

    1. [새로운 게임 시작] 버튼을 타이틀 화면에서 클릭 하면 아래 처럼 뜨게 할 것이다.
    1. 그런 다음 슬롯 별로 각각 데이터들을 저장할 것이다. 만약에 해당 슬롯에 데이터가 없다면 "비어 있음"으로 보이게 할 것이고 데이터가 존재 한다면 저장했던 플레이어의 이름이 나타나게 할 것이다.
      - 즉, 데이터가 있는 슬롯을 클릭하면 인 게임으로 이동하고 저장된 데이터 슬롯을 클릭하면 아래 이름을 적는 UI가 보이게 할 것이다.

구현 시작

    1. SceneSelect.cs 작성
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneSelect : MonoBehaviour
{

    public GameObject creat; //비어있는 슬롯을 눌렀을 때 뜨는 창 (플레이어 이름 적는 UI)
    public TextMeshProUGUI[] slotText; //슬롯의 텍스트

    public void Slot()
    {
        //1.저장된 데이터가 없을 때
        Creat(); //(플레이어 이름 적는 UI) 활성화 하는 메서드


        //2. 저장된 데이터가 있을 때 => 불러오기 해서 게임 씬으로 넘어가게.
        DataManager.Instance.LoadData(); //불러오기
        GoGame(); //게임 씬으로 넘어가게.
    }
    public void Creat()
    {
        creat.gameObject.SetActive(true); //(플레이어 이름 적는 UI) 활성화
    }

    public void GoGame()
    {
        SceneManager.LoadScene(1); //인 게임씬
    }
}

문제 1: 근데 슬롯을 어떻게 알맞게 불러올까?

    1. DataManager.cs 코드 수정
using UnityEngine;
using System.IO;

public class PlayerData
{
    //이름, 레벨 , 코인, 착용중인 무기 들을 저장
    public string name;
    public int level;
    public int coin;
    public int item; //int로 한 이유는 이 값이 1번이라면 1번에 대응되는 무기. 2번이라면 2번에 대응되는 무기.
}
public class DataManager : MonoBehaviour
{
    //게임 내에 항상 존재하면 좋으므로 싱글톤
    private static DataManager instance;
    public static DataManager Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject go = new GameObject("DataManager"); //EventBus라는 빈 객체를 만들고
                instance = go.AddComponent<DataManager>(); //EventBus 빈 객체에 EventBus 스크립트(컴포넌트)을 추가
            }
            return instance;
        }



    }

    public PlayerData nowPlayer = new PlayerData();

    string path;

    string filename = "Save";         //파일 명까지 정해주는게 나중에 파일 관리할 때 편함.

    public int nowSlot; //⭐

    private void Awake()
    {
        #region 싱글톤
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(instance.gameObject);
        }
        DontDestroyOnLoad(this.gameObject);
        #endregion

        path = Application.persistentDataPath + "/"; 
    }

    private void Start()
    {

    }

    public void SaveData()
    {
        string data = JsonUtility.ToJson(nowPlayer);

        //저장하기 - WriteAllText() 사용
        File.WriteAllText(path + filename + nowSlot.ToString(), data); //⭐경로의 파일이름을 지정한 후 데이터를 저장하고 파일 이름 뒤에 슬롯의 번호까지 추가. Save0,Save1...
    }

    public void LoadData()
    {
        //불러오기 - ReadAllText() 사용
        string data = File.ReadAllText(path + filename + nowSlot.ToString()); //⭐ 경로의 파일이름을 지정한 후 데이터를 저장하고 파일 이름 뒤에 슬롯의 번호까지 추가. Save0,Save1...

        nowPlayer = JsonUtility.FromJson<PlayerData>(data); //기존에 선언했던 nowPlayer을 불러오고 나서 덮어 씌우게 됨
    }
}
    1. SceneSelect.cs 코드 추가
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneSelect : MonoBehaviour
{

    public GameObject creat; //비어있는 슬롯을 눌렀을 때 뜨는 창 
    public TextMeshProUGUI[] slotText; //슬롯의 텍스트

    public void Slot(int number) //⭐ 버튼 클릭 이벤트 호출, int number을 매개변수로
    {
        DataManager.Instance.nowSlot = number; //⭐ 호출했을 때 매개변수로 받은 숫자가 슬롯의 번호가 되는 개념.

        //1.저장된 데이터가 없을 때
        Creat();


        //2. 저장된 데이터가 있을 때 => 불러오기 해서 게임 씬으로 넘어가게.
        DataManager.Instance.LoadData(); //불러오기
        GoGame(); //게임 씬으로 넘어가게.
    }
    public void Creat() 
    {
        creat.gameObject.SetActive(true);
    }

    public void GoGame()
    {
        SceneManager.LoadScene(1);//인 게임씬
    }
}
    1. SceneSelect 스크립트를 Canvas에 할당
    1. SceneSelect 컴포넌트에 인스펙터 할당
  • 게임 씬으로 넘어가는 확인 버튼에 OnClick 이벤트 추가

  • 슬롯 버튼의 OnClick 이벤트에서 Slot메서드로 지정하면 숫자를 입력하는 것이 뜬다.(Slot 메서드에 매개변수로 int타입이므로) 할당 해보자.

  • 0으로 설정

  • 1로 설정

  • 2로 설정

해당 값이 슬롯마다 nowSlot의 번호로 저장될 것이다. 문제 해결!

문제 2: 빈 슬롯을 눌렀을 때 플레이어의 이름을 적는 부분이 있었다. 나중에 불러올 때 이름을 불러와야 하므로 같이 저장해야 한다.

    1. SceneSelect.cs 코드 수정
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneSelect : MonoBehaviour
{

    public GameObject creat; //비어있는 슬롯을 눌렀을 때 뜨는 창 
    public TextMeshProUGUI[] slotText; //슬롯의 텍스트
    public TextMeshProUGUI newPlayerName; //⭐ 플레이어 이름 적는 ui - 인풋 필드 텍스트

    private void Start()
    {
        //슬롯별로 저장된 데이터가 존재하는지 판단.
    }
    public void Slot(int number) //버튼 클릭 이벤트 호출
    {
        DataManager.Instance.nowSlot = number; //호출했을 때 매개변수로 받은 숫자가 슬롯의 번호가 되는 개념.

        //1.저장된 데이터가 없을 때
        Creat();


        //2. 저장된 데이터가 있을 때 => 불러오기 해서 게임 씬으로 넘어가게.
        DataManager.Instance.LoadData(); //불러오기
       // ⭐ DataManager.Instance.nowPlayer.name = newPlayerName.text; //⭐ PlayerData 클래스 객체인 name에 새로 적은 플레이어에 이름으로 저장.
        GoGame(); //게임 씬으로 넘어가게.
    }
    public void Creat() 
    {
        creat.gameObject.SetActive(true);
    }

    public void GoGame() //이름 확인 버튼 누르면 실행
    {
        //⭐  DataManager.Instance.nowPlayer.name = newPlayerName.text; -> 버그 요소임. 불러오고 나서 이름을 지정하면 빈 문자열이 될 것임.
        SceneManager.LoadScene(1);//인 게임씬 
    }
}
    1. 인스펙터 할당 까지 ㄱㄱ
  • 위 추가한 코드를 주석처리 하는 이유는 버그 때문이다. 첫번째 부분을로 주석한 이유는 GoGame메서드가 실행되기 전에 저장하면. 즉, 확인을 누르기도 전에 새로 적은 플레이어 이름을 저장하면 안된다. 그럼 두번째 부분인 확인 버튼을 클릭하고 나서 하면 되지 않나? 했는데 불러오고 나서 이름을 지정하면 빈 문자열이 될 것이다.

어떻게 해야 할까? 저장된 데이터가 있을 때 없을 때 부분 먼저 구현해보자

    1. Json을 클래스 형식으로 저장된 데이터의 존재 유무를 파악하는 File.Exist()을 사용해보자.
using System.IO;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneSelect : MonoBehaviour
{

    public GameObject creat; //비어있는 슬롯을 눌렀을 때 뜨는 창 
    public TextMeshProUGUI[] slotText; //슬롯의 텍스트
    public TextMeshProUGUI newPlayerName;

    bool[] savefile; //⭐ for문에서 파일이 존재하는 지 안하는지 bool 값으로 나옴. bool 배열 사용
    private void Start()
    {
        //⭐ 슬롯별로 저장된 데이터가 존재하는지 판단.
        for(int i =0; i <3; i++) //⭐ 슬롯이 현재 3까지 존재 하므로, 0,1,2 범위 사용
        {
            if (File.Exists(DataManager.Instance.path + i)) //⭐ 즉, Save0~Save2 중에 파일이 존재한다면.ex) Save0 파일 존재
            {
                savefile[i] = true; //⭐ 해당 Save0~Save2 중 존재한 파일을 true로. ex) Save0 파일을 true로 
                DataManager.Instance.nowSlot = i; //⭐ 슬롯넘버 할당.  ex) Save0 파일의 넘버가 0이므로 0을 슬롯0번으로 할당.
                DataManager.Instance.LoadData(); //⭐불러오기
                slotText[i].text = DataManager.Instance.nowPlayer.name; //⭐해당 슬롯0번의 텍스트가 PlayerData의 name으로 저장

            }
        }
    }
    public void Slot(int number) //버튼 클릭 이벤트 호출
    {
        DataManager.Instance.nowSlot = number; //호출했을 때 매개변수로 받은 숫자가 슬롯의 번호가 되는 개념.

        //1.저장된 데이터가 없을 때
        Creat();


        //2. 저장된 데이터가 있을 때 => 불러오기 해서 게임 씬으로 넘어가게.
        DataManager.Instance.LoadData(); //불러오기
       
        GoGame(); //게임 씬으로 넘어가게.
    }
    public void Creat() 
    {
        creat.gameObject.SetActive(true);
    }

    public void GoGame()
    {
        //DataManager.Instance.nowPlayer.name = newPlayerName.text; -> 버그 요소임. 불러오고 나서 이름을 지정하면 빈 문자열이 될 것임.
        SceneManager.LoadScene(1);//인 게임씬
    }
}
  • SceneSelect.cs에서 DataManager.cs에 path을 사용하기 위해 path을 public으로.

저장된 데이터 확인 작업(불러오는 작업)이 끝나면 불러오는 것들을 리셋 해줘야 한다.

  • 아래 빨간색 박스.
    1. DataManager.cs에서 DataClear()메서드 추가 및 작성
using UnityEngine;
using System.IO;

public class PlayerData
{
    //이름, 레벨 , 코인, 착용중인 무기 들을 저장
    public string name;
    public int level;
    public int coin;
    public int item; //int로 한 이유는 이 값이 1번이라면 1번에 대응되는 무기. 2번이라면 2번에 대응되는 무기.
}
public class DataManager : MonoBehaviour
{
    //게임 내에 항상 존재하면 좋으므로 싱글톤
    private static DataManager instance;
    public static DataManager Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject go = new GameObject("DataManager"); //EventBus라는 빈 객체를 만들고
                instance = go.AddComponent<DataManager>(); //EventBus 빈 객체에 EventBus 스크립트(컴포넌트)을 추가
            }
            return instance;
        }



    }

    public PlayerData nowPlayer = new PlayerData();

    public string path;

    string filename = "Save";         //파일 명까지 정해주는게 나중에 파일 관리할 때 편함.

    public int nowSlot;

    private void Awake()
    {
        #region 싱글톤
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(instance.gameObject);
        }
        DontDestroyOnLoad(this.gameObject);
        #endregion

        path = Application.persistentDataPath + "/"; 
    }

    private void Start()
    {

    }

    public void SaveData()
    {
        string data = JsonUtility.ToJson(nowPlayer);

        //저장하기 - WriteAllText() 사용
        File.WriteAllText(path + filename + nowSlot.ToString(), data); // 경로의 파일이름을 지정한 후 데이터를 저장하고 파일 이름 뒤에 슬롯의 번호까지 추가. Save0,Save1...
    }

    public void LoadData()
    {
        //불러오기 - ReadAllText() 사용
        string data = File.ReadAllText(path + filename + nowSlot.ToString()); 

        nowPlayer = JsonUtility.FromJson<PlayerData>(data); //기존에 선언했던 nowPlayer을 불러오고 나서 덮어 씌우게 됨
    }

    public void DataClear() //⭐
    {
        nowSlot = -1; //⭐슬롯 번호가 -1로 안 갈거니까 -1로 설정.
        nowPlayer = new PlayerData(); //⭐초기값으로 다시 초기화
    }
}
    1. 불러오는 작업 끝나고 나서 for문 탈출 하고 나면 DataManager.cs에 있는 DataClear() 호출.

데이터 저장 유무 판별(계속)

using System.IO;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneSelect : MonoBehaviour
{

    public GameObject creat; //비어있는 슬롯을 눌렀을 때 뜨는 창 
    public TextMeshProUGUI[] slotText; //슬롯의 텍스트
    public TextMeshProUGUI newPlayerName;

    bool[] savefile = new bool[3]; //⭐ 배열이기 때문에 초기화 해줘야 작동함.
    private void Start()
    {
        //슬롯별로 저장된 데이터가 존재하는지 판단.
        for(int i =0; i <3; i++) //슬롯이 현재 3까지 존재 하므로, 0,1,2 범위 사용
        {
            if (File.Exists(DataManager.Instance.path + i)) //즉, Save0~Save2 중에 파일이 존재한다면.ex) Save0 파일 존재
            {
                savefile[i] = true; //해당 Save0~Save2 중 존재한 파일을 true로. ex) Save0 파일을 true로 
                DataManager.Instance.nowSlot = i; //슬롯넘버 할당.  ex) Save0 파일의 넘버가 0이므로 0을 슬롯0번으로 할당.
                DataManager.Instance.LoadData(); //불러오기
                slotText[i].text = DataManager.Instance.nowPlayer.name; //해당 슬롯0번의 텍스트가 PlayerData의 name으로 저장
                
            }
        }
        DataManager.Instance.DataClear();
    }
    public void Slot(int number) //버튼 클릭 이벤트 호출
    {
        DataManager.Instance.nowSlot = number; //호출했을 때 매개변수로 받은 숫자가 슬롯의 번호가 되는 개념.

        
        if (savefile[number]) //⭐ 1. 저장된 데이터가 있다면. 즉, 파일이 존재했다면 -> Start문 참고.
        {
            DataManager.Instance.LoadData(); //⭐불러오기
            GoGame(); //⭐ 
        }
        else //⭐ 2.저장된 데이터가 없을 때
        {
            Creat(); //⭐
        }
            
        //⭐ GoGame(); //Creat()가 호출되면 창이 나타나는데 창이 나타나자 마자 바로 게임씬으로 이동하면 안되므로 주석처리
    }
    public void Creat() 
    {
        creat.gameObject.SetActive(true);
    }

    public void GoGame()
    {
        //저장된 데이터가 없을 때 -> 빈 슬롯이였다면
        if (savefile[DataManager.Instance.nowSlot] == false) //⭐ Slot 메서드에서 DataManager.Instance.nowSlot을 업데이트 해줬기 때문에 사용 가능.
        {
            DataManager.Instance.nowPlayer.name = newPlayerName.text; //⭐
            DataManager.Instance.SaveData(); //⭐
        }
        SceneManager.LoadScene(1);//⭐ 인 게임씬
    }
}

만약에 파일이 존재하지 않았다면?

  • 아래 처럼 슬롯의 이름을 "비어 있음"으로 한다.

중간 구현 결과

  • 실행 전 경로에 있는 파일 모두 삭제
  • 실행 결과 - 제대로 저장이 안 되어있길래 DataManager.cs을 보니 버그가 있었다.
  • 비정상 코드
using UnityEngine;
using System.IO;

public class PlayerData
{
    //이름, 레벨 , 코인, 착용중인 무기 들을 저장
    public string name;
    public int level;
    public int coin;
    public int item; //int로 한 이유는 이 값이 1번이라면 1번에 대응되는 무기. 2번이라면 2번에 대응되는 무기.
}
public class DataManager : MonoBehaviour
{
    //게임 내에 항상 존재하면 좋으므로 싱글톤
    private static DataManager instance;
    public static DataManager Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject go = new GameObject("DataManager"); //EventBus라는 빈 객체를 만들고
                instance = go.AddComponent<DataManager>(); //EventBus 빈 객체에 EventBus 스크립트(컴포넌트)을 추가
            }
            return instance;
        }



    }

    public PlayerData nowPlayer = new PlayerData();

    public string path;

    string filename = "Save";         //파일 명까지 정해주는게 나중에 파일 관리할 때 편함.

    public int nowSlot;

    private void Awake()
    {
        #region 싱글톤
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(instance.gameObject);
        }
        DontDestroyOnLoad(this.gameObject);
        #endregion

        path = Application.persistentDataPath + "/"; //❗ 프로젝트 경로 + "/"을 해버리면 제대로 찾지 못한다. 
    }

    private void Start()
    {

    }

    public void SaveData()
    {
        string data = JsonUtility.ToJson(nowPlayer);

        //저장하기 - WriteAllText() 사용
        File.WriteAllText(path + "Save" + nowSlot.ToString(), data); //❗path + "Save" + nowSlot.ToString() 가 아닌 path + nowSlot.ToString() 로 수정
    }

    public void LoadData()
    {
        //불러오기 - ReadAllText() 사용
        string data = File.ReadAllText(path + "Save" + nowSlot.ToString()); //❗path + "Save" + nowSlot.ToString() 가 아닌 path + nowSlot.ToString() 로 수정

        nowPlayer = JsonUtility.FromJson<PlayerData>(data); //기존에 선언했던 nowPlayer을 불러오고 나서 덮어 씌우게 됨
    }

    public void DataClear()
    {
        nowSlot = -1; //슬롯 번호가 -1로 안 갈거니까 -1로 설정.
        nowPlayer = new PlayerData(); //초기값으로 다시 초기화
    }
}
  • 정상 코드
using UnityEngine;
using System.IO;

public class PlayerData
{
    //이름, 레벨 , 코인, 착용중인 무기 들을 저장
    public string name;
    public int level;
    public int coin;
    public int item; //int로 한 이유는 이 값이 1번이라면 1번에 대응되는 무기. 2번이라면 2번에 대응되는 무기.
}
public class DataManager : MonoBehaviour
{
    //게임 내에 항상 존재하면 좋으므로 싱글톤
    private static DataManager instance;
    public static DataManager Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject go = new GameObject("DataManager"); //EventBus라는 빈 객체를 만들고
                instance = go.AddComponent<DataManager>(); //EventBus 빈 객체에 EventBus 스크립트(컴포넌트)을 추가
            }
            return instance;
        }



    }

    public PlayerData nowPlayer = new PlayerData();

    public string path;

    public int nowSlot;

    private void Awake()
    {
        #region 싱글톤
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(instance.gameObject);
        }
        DontDestroyOnLoad(this.gameObject);
        #endregion

        path = Application.persistentDataPath + "/Save"; //👌
    }

    private void Start()
    {

    }

    public void SaveData()
    {
        string data = JsonUtility.ToJson(nowPlayer);

        //저장하기 - WriteAllText() 사용
        File.WriteAllText(path  + nowSlot.ToString(), data); //👌 경로의 파일이름을 지정한 후 데이터를 저장하고 파일 이름 뒤에 슬롯의 번호까지 추가. Save0,Save1...
    }

    public void LoadData()
    {
        //불러오기 - ReadAllText() 사용
        string data = File.ReadAllText(path + nowSlot.ToString());  //👌

        nowPlayer = JsonUtility.FromJson<PlayerData>(data); //기존에 선언했던 nowPlayer을 불러오고 나서 덮어 씌우게 됨
    }

    public void DataClear()
    {
        nowSlot = -1; //슬롯 번호가 -1로 안 갈거니까 -1로 설정.
        nowPlayer = new PlayerData(); //초기값으로 다시 초기화
    }
}
  • 실행 결과. 빈 슬롯1,2,3 중에 빈 슬롯 2번에 플레이어 이름을 입력하자. Save1 파일이 생겼고, 파일 안에 내가 적은 플레이어의 이름이 저장되었다. 그리고, 다시 게임 시작한 결과 빈 슬롯 1번에 내가 적은 플레이어의 이름이 적혀있다.

  • 아까 빈 슬롯 1 버튼을 클릭하면 0을 Slot 메서드에 인자로 넘겼었다.

  • LoadData() 메서드를 호출하게 되면,
  • Save0 파일이 생기는 것이다.

플레이어의 정보(이름,레벨,코인) 저장

    1. 게임 씬에 UI를 꾸며본다. Canvas 생성 후 아래와 같이 세팅
  • 왼쪽 상단에는 PlayerData 클래스에 있는 정보들인 이름,레벨,코인에 대한 정보가 있고, 오른쪽 하단에는 PlayerData클래스에 있는 item 값에 대응 되는 무기들에 대한 정보다.
  • 저장 버튼을 누르면 현재 변화된 상태를 저장.
    1. 플레이어 자식으로 바주카,소드,폭탄을 갖는 무기를 만들어본다.

  • 무기 버튼을 누르면 활성화 되게 끔 하기 위해 처음 플레이어의 자식들은 모두 비활성화 처리

    1. SceneInGame.cs 작성
using TMPro;
using UnityEngine;

public class SceneInGame : MonoBehaviour
{
    public TextMeshProUGUI name;
    public TextMeshProUGUI level;
    public TextMeshProUGUI coin;
    void Start()
    {
        //게임 씬에 넘어오면 플레이어의 정보(이름,레벨,코인)이 알맞게 나타나야 함.
        name.text += DataManager.Instance.nowPlayer.name; //name 은 현재 이름: 이므로 name += 형태로 진행.
        level.text += DataManager.Instance.nowPlayer.level.ToString(); //text string이고 level은 int이므로 ToString()
        coin.text += DataManager.Instance.nowPlayer.coin.ToString();
    }

    public void LevelUp()
    {
        DataManager.Instance.nowPlayer.level++; //값 바꾸기
        level.text = "레벨 : " + DataManager.Instance.nowPlayer.level.ToString(); //UI변화를 위함
    }

    public void CoinUp()
    {
        DataManager.Instance.nowPlayer.coin += 100;
        coin.text = "코인 : " + DataManager.Instance.nowPlayer.coin.ToString(); //UI변화를 위함
    }

    public void Save()
    {
        DataManager.Instance.SaveData();
    }
}

  • 레벨하고 코인은 기본 값을 정해주지 않았었다. 기본 값 정해주자.

    1. SceneInGame 스크립트를 캔버스에 할당
  • 버튼 이벤트 할당 까지 ㄱㄱ


실행 결과 - 세이브 정상 작동

  • 이름,레벨,코인 모두 정상적으로 증가되고, 저장 버튼 누른 뒤 다시 게임을 시작하면 마지막 저장했던 값들이 나타난다.

플레이어의 착용 무기 저장

    1. SceneInGame.cs 에 코드를 추가한다.
using TMPro;
using UnityEngine;

public class SceneInGame : MonoBehaviour
{
    public TextMeshProUGUI name;
    public TextMeshProUGUI level;
    public TextMeshProUGUI coin;

    public GameObject[] WeaponItem; //⭐

    void Start()
    {
        //게임 씬에 넘어오면 플레이어의 정보(이름,레벨,코인)이 알맞게 나타나야 함.
        name.text += DataManager.Instance.nowPlayer.name; //name 은 현재 이름: 이므로 name += 형태로 진행.
        level.text += DataManager.Instance.nowPlayer.level.ToString(); //text string이고 level은 int이므로 ToString()
        coin.text += DataManager.Instance.nowPlayer.coin.ToString();
    }

    public void LevelUp()
    {
        DataManager.Instance.nowPlayer.level++; //값 바꾸기
        level.text = "레벨 : " + DataManager.Instance.nowPlayer.level.ToString(); //UI변화를 위함
    }

    public void CoinUp()
    {
        DataManager.Instance.nowPlayer.coin += 100;
        coin.text = "코인 : " + DataManager.Instance.nowPlayer.coin.ToString(); //UI변화를 위함
    }

    public void Save()
    {
        DataManager.Instance.SaveData();
    }

    public void ItemSetting(int number) //⭐
    {
        for(int i = 0; i< WeaponItem.Length; i++) //⭐
        {
            if(number == i) //⭐ 선택한 무기(0)가 첫번째(0) 라면 
            { 
                WeaponItem[i].SetActive(true); //⭐첫번째 무기만 활성화
                DataManager.Instance.nowPlayer.item = number; //⭐활성화된 무기의 번호가 PlayerData의 item 변수에 저장됨. 즉, 대응 됨.
            }
            else //⭐ 아니라면
            {
                WeaponItem[i].SetActive(false); //⭐비활성화
            }
        }
    }
}
    1. 인스펙터 할당
  • 소드 버튼은 0번으로 ItemSetting 메서드를 호출할 때 값을 넘긴다.

  • 바주카 버튼은 1번으로 ItemSetting 메서드를 호출할 때 값을 넘긴다.

  • 폭탄 버튼은 2번으로 ItemSetting 메서드를 호출할 때 값을 넘긴다.

    1. 처음 무기를 아무것도 무기가 없는 코드를 작성 하길 원한다. 소드 버튼을 0번으로 했었다. 즉, item이 0이라면 Sword가 활성화 될 것이므로 item의 초기값을 -1로 지정.
    1. SceneInGame.cs에 Start문에서 ItemSettings 메서드를 호출한다. item이 -1이므로 시작할 때 아무런 무기도 없음.

실행 결과 - 정상 작동

  • 각 버튼에 따른 무기 활성화가 정상적으로 작동되고, 저장 기능도 물론 다시 시작하게 되면 이전의 착용했던 무기가 나타나는 것을 볼 수 있다.

하지만, 플레이어의 위치는 저장되지 않았다. 이것도 구현해보자.

  • 위에서 배웠던 내용과 비슷한 방식이다. 플레이어의 포지션 값을 저장했다가 게임을 시작할 때 불러와서 이전 포지션 값을 대입하면 된다.

슬롯 삭제

  • File.delete() 여기다가 삭제할 파일 경로를 넣어주면 됩니다.
  • 삭제버튼을 따로 만든 다음에 삭제하는 함수를 만들어서 연결해주면 되겠죠?? 영상에서 각 슬롯에 저장하는 거 만드는 부분 참고해서 만든뒤에 delete로만 바꾸면 됩니다.

0개의 댓글