유니티에서 세이브 데이터를 저장하는 방식은 여러가지가 있다. 대표적인 것이 JSON, XML, CSV 등의 파일형식으로 저장하거나 그것들을 바이너리화 하는 것이다. 특히 JSON방식은 자바스크립트의 인기에 힘입어 많이 쓰이는 편이다. 유니티에서는 Unity5 시절부터 JsonUtility라고 하여 엔진 내에서 Json관련 라이브러리를 제공한다.
이 글에서는 JsonUtility로 클래스 객체를 직렬화 한 뒤, System.Security.Cryptography를 이용해서 다른 사람이 세이브파일을 뜯어볼 수 없도록 암호화 해보도록 하겠다.
우선 JsonUtility가 기존에 많이 쓰였던 다른 Json 라이브러리에 비해 좋은 점은 아래와 같다.
1. Vector3 변수를 그대로 저장할 수 있다.
2. 가비지 컬렉터 메모리를 최소한만 이용한다.
3. 속도가 빠르다.
(출처 : https://docs.unity3d.com/kr/current/Manual/JSONSerialization.html)
일단 저장할 데이터를 담을 클래스를 아래와 같이 만들어보겠다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | using System.Collections.Generic; using UnityEngine; [System.Serializable] public class SaveData { //여러가지 타입이 가능하지만, Dictionary는 안된다. [SerializeField] private string characterName; [SerializeField] private int level; [SerializeField] private float exp; [SerializeField] private List<string> itemsName; //외부 Json 라이브러리가 아닌 JsonUtility를 쓰면 Vector3도 저장할 수 있다. [SerializeField] private Vector3 lastPosition; //생성자 public SaveData(string t_characterName, int t_level, float t_exp, List<string> t_itemsName, Vector3 t_lastPosition) { characterName = t_characterName; level = t_level; exp = t_exp; //일일이 값을 복사하는게 깔끔하다. itemsName = new List<string>(); foreach(var n in t_itemsName) { itemsName.Add(n); } lastPosition = new Vector3(t_lastPosition.x, t_lastPosition.y, t_lastPosition.z); } } | cs |
위의 코드는 저장할 변수들을 간추린 SaveData 클래스이다.
위의 코드에서 중요한 부분은, class 상단에 [System.Serializable] 어트리뷰트를 붙여줘야 한다는 것이다.
[System.Serializable] 어트리뷰트는 이 클래스를 직렬화 할 수 있게 표식을 남겨주는 의미이다.
직렬화라는 것은 데이터를 byte단위로 한가닥으로 길게 뽑아낸다고 생각하면 되는데,
인터넷 통신을 할 때나 파일을 읽고 쓸 때는 데이터가 byte단위로 이동하기 때문에 세이브 데이터를 만들려면 이 표식을 붙여줘야 한다.
그 다음에 위의 코드에서 눈여겨볼 것은 저장할 변수 앞에 붙은 [SerializeField] 어트리뷰트이다.
내가 저장하고 싶은 변수를 체크하는 방법은 크게 2가지가 있다.
1. 변수를 public 접근지정자로 선언한다.
2. 변수를 private 접근지정자로 선언한 후 [SerializeField]를 앞에 붙여준다.
나 같은 경우에는 플레이어의 정보를 은닉하고 싶기 때문에 private으로 선언했고, 그 때문에 앞에 [SerializeField]를 붙여주었다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | using System.Collections.Generic; using UnityEngine; using System.IO; using System.Security.Cryptography; public class SaveManager : MonoBehaviour { private static readonly string privateKey = "1718hy9dsf0jsdlfjds0pa9ids78ahgf81h32re"; public static void Save() { //원래라면 플레이어 정보나 인벤토리 등에서 긁어모아야 할 정보들. SaveData sd = new SaveData( "글릭", 10, 0.1f, new List<string> { "포션", "단검" }, new Vector3(1, 2, 3)); string jsonString = DataToJson(sd); string encryptString = Encrypt(jsonString); SaveFile(encryptString); } public static SaveData Load() { //파일이 존재하는지부터 체크. if(!File.Exists(GetPath())) { Debug.Log("세이브 파일이 존재하지 않음."); return null; } string encryptData = LoadFile(GetPath()); string decryptData = Decrypt(encryptData); Debug.Log(decryptData); SaveData sd = JsonToData(decryptData); return sd; } //////////////////////////////////////////////////////////////////////////////////////// //세이브 데이터를 json string으로 변환 static string DataToJson(SaveData sd) { string jsonData = JsonUtility.ToJson(sd); return jsonData; } //json string을 SaveData로 변환 static SaveData JsonToData(string jsonData) { SaveData sd = JsonUtility.FromJson<SaveData>(jsonData); return sd; } //////////////////////////////////////////////////////////////////////////////////////// //json string을 파일로 저장 static void SaveFile(string jsonData) { using (FileStream fs = new FileStream(GetPath(), FileMode.Create, FileAccess.Write)) { //파일로 저장할 수 있게 바이트화 byte[] bytes = System.Text.Encoding.UTF8.GetBytes(jsonData); //bytes의 내용물을 0 ~ max 길이까지 fs에 복사 fs.Write(bytes, 0, bytes.Length); } } //파일 불러오기 static string LoadFile(string path) { using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read)) { //파일을 바이트화 했을 때 담을 변수를 제작 byte[] bytes = new byte[(int)fs.Length]; //파일스트림으로 부터 바이트 추출 fs.Read(bytes, 0, (int)fs.Length); //추출한 바이트를 json string으로 인코딩 string jsonString = System.Text.Encoding.UTF8.GetString(bytes); return jsonString; } } //////////////////////////////////////////////////////////////////////////////////////// private static string Encrypt(string data) { byte[] bytes = System.Text.Encoding.UTF8.GetBytes(data); RijndaelManaged rm = CreateRijndaelManaged(); ICryptoTransform ct = rm.CreateEncryptor(); byte[] results = ct.TransformFinalBlock(bytes, 0, bytes.Length); return System.Convert.ToBase64String(results, 0, results.Length); } private static string Decrypt(string data) { byte[] bytes = System.Convert.FromBase64String(data); RijndaelManaged rm = CreateRijndaelManaged(); ICryptoTransform ct = rm.CreateDecryptor(); byte[] resultArray = ct.TransformFinalBlock(bytes, 0, bytes.Length); return System.Text.Encoding.UTF8.GetString(resultArray); } private static RijndaelManaged CreateRijndaelManaged() { byte[] keyArray = System.Text.Encoding.UTF8.GetBytes(privateKey); RijndaelManaged result = new RijndaelManaged(); byte[] newKeysArray = new byte[16]; System.Array.Copy(keyArray, 0, newKeysArray, 0, 16); result.Key = newKeysArray; result.Mode = CipherMode.ECB; result.Padding = PaddingMode.PKCS7; return result; } //////////////////////////////////////////////////////////////////////////////////////// //저장할 주소를 반환 static string GetPath() { return Path.Combine(Application.persistentDataPath, "save.abcd"); } } | cs |
참고로 암호화 부분은 https://gist.github.com/TarasOsiris/9020497 이곳을 참고해 만들었다.
외부에서 쓰는 public 함수는 Save와 Load밖에 없다.
그 밑의 함수들은 모두 Save와 Load 안에서 실행될 뿐인 함수들이다.
암호화는 많은 공부가 필요한데 간략히만 설명하도록 하겠다.
일단 RijndaelManaged 클래스가 뭔지부터 알아야 하는데, 암호화와 관련된 것들을 총괄해주는 클래스이다.
코드의 123번째 줄부터 보면 설정을 해주는데 각각의 의미를 아주 간단하게 말하자면 아래와 같다.
Key : 암호화와 해석에 필요한 키. 외부로 유출되면 안된다.
Mode : 암호화 방식. 여기서 ECB는 key를 이용한 가장 간단한 암호화 방식이라고 할 수 있다.
padding : 데이터가 전체 암호화에 필요한 바이트보다 짧을 때 남은 바이트를 채워주는 방식을 설정한다.
이 RijndaelManaged로부터 암호화를 시켜주는 Encryptor와 암호화를 풀어주는 Decryptor를 만들 수 있다.
참고로 Application.persistentDataPath는 윈도우10 기준으로 아래와 같다.
C:\Users\(사용자이름)\AppData\LocalLow\(회사이름)\(유니티 프로젝트 이름)
위의 암호화 방식 없이 파일로 저장하면, 그 파일을 메모장으로 열었을 때 저장한 데이터가 고스란히 나오지만,
암호화를 한 파일을 연다면 이상한 문자들이 나오는 것을 알 수 있을 것이다.
'유니티 > 제작기술' 카테고리의 다른 글
오브젝트 풀링 - 유니티로 공부하는 게임 제작기술 (Object Pooling) (0) | 2019.08.27 |
---|