내일배움캠프 Unity 9일차 TIL - 개인 과제 (Sparta Dungeon)

Wooooo·2023년 11월 9일
0

내일배움캠프Unity

목록 보기
11/94

오늘의 키워드

오늘은 개인 과제의 마지막 별 6개짜리 챌린지인 게임 저장/불러오기 기능을 추가하는데 성공했다.
개발 과정에서 겪였던 에러, 고민, 해결한 방법 등등을 써보려고 한다.


class, property 역직렬화

public abstract class Item
{
	public int ID { get; protected set; }
    public string Name { get; protected set; }
    public int Price { get; protected set; }
    public string Descript { get; protected set; }
    public int Atk { get; protected set; }
    public int Def { get; protected set; }
}

오늘은 자리에 앉자마자 어제 나를 괴롭게 했던 abstract class인 Item 클래스를 먼저 역직렬화 하려고 해봤다.
JsonConvert.DeserializeObject<T>()을 이용해서 Item 클래스로 역직렬화 하면, Item 클래스는 추상 클래스라 인스턴스를 가질 수 없다며 오류가 나는 문제가 있었다.
그래서 그냥 에라 모르겠다~ Item에서 abstract를 지워봤다. 그런데 이번엔 에러는 안나는데 property에 값이 전부 기본값으로 들어가 있는 거다.
이제와서 생각해보면 너무 당연한 얘기인데, Item의 property들은 외부에서 변조할 수 없게 setter를 protected로 선언해놔서 그런거였다. 이 문제도 그냥 setter의 protected를 떼어버리면 간단하게 해결할 수는 있겠지만, Item의 옵션이나 가격등을 외부에서 건드릴 수 있게 되는건 원치 않아서, 다른 방법을 찾아보기로 했다.

JObject

그래서, Item 대신 JObject라는 형태로 역직렬화 하기로 했다. JObject와 그 친구들에 대해 잠깐 정리해보겠다.

  • JObjectstring 자료형의 keyJToken 자료형의 value 한 쌍으로 이루어져 있는 오브젝트이다. key값을 이용해 value에 접근한다.
  • JPropertykey, value 둘 다 필드로 제공해준다.
  • JArray는 간단하게 말하면, JObject의 배열이라고 생각하자.
  • 위의 친구들은 전부 JToken을 상속받는다.
    • [] 연산자를 통해서 인덱싱 할 수 있다.
      ex) item.Name = (string)itemJObject["Name"];
    • JObject의 value로 JArray가 있을 수 있고, JArray의 value로 JObject가 있을 수 있다.
      따라서, 복잡한 형태의 데이터 구조도 json으로 저장하고, 불러올 수 있다.
public static void InitItemDictionary()
{
	var jsonStr = AESManager.Decrypt(File.ReadAllText(@"ItemDictionary.json"));
    var dict = JsonConvert.DeserializeObject<Dictionary<int, JObject>>(jsonStr);
    foreach (var e in dict)
    	instance.Add(e.Key, Item.JsonParse(e.Value));
}

ItemDictionary에 모든 아이템의 정보를 불러오는 코드.

public static Item JsonParse(JObject obj)
{
	var id = (int)obj["ID"];
    var name = (string)obj["Name"];
    var price = (int)obj["Price"];
    var desc = (string)obj["Descript"];
    var atk = (int)obj["Atk"];
    var def = (int)obj["Def"];

	if (id < 10)
    	return new Weapon(id, name, price, atk, desc);
	else
    	return new Armor(id, name, price, def, desc);
}

JObject를 받아서 Weapon이나 Armor의 객체를 만들어 반환해주는 Item 클래스의 static 메서드.


Player Data Save/Load

아이템 데이터의 저장과 불러오기는 성공했으니, 이제 캐릭터의 데이터만 작업하면 된다.
내가 짠 Character 클래스는 자신의 Inventory 클래스를 필드로 가지고 있고, Invetory 클래스는 자신의 주인인 Character의 클래스를 필드로 가지고 있다. 따라서 Character 클래스를 그대로 JsonConvert.SerializeObject<T>()를 이용해 직렬화를 시도하면, 다음과 같은 오류가 발생했다.
Character 클래스를 직렬화 하다가 Invetory가 나와서 Inventory를 직렬화 했더니 Character가 또 나왔다.. 이런 무한 루프가 발견이 됐다고 예외가 발생한 것이다.

이것은 그냥 CharacterInventory에서 필요한 정보만 따로 빼서 처리해주는게 맞는 방향인 것 같았다.

public JArray ToJArray()
{
	JArray res = new JArray(items.Select(x => x.ID).ToList());
    return res;
}

Inventory 클래스가 가지고 있는 ItemList를 JArray로 만들어 반환하는 메서드.

public JObject ToJObject()
{
	JObject res = new JObject
    {
    	{ "Name", Name },
        { "Job", Job },
        { "Level", Level },
        { "Atk", _atk },
        { "Def", _def },
        { "HP", HP },
        { "Gold", Gold },
        { "Exp", Exp },
        { "equipWeapon", equipWeapon.ID },
        { "equipArmor", equipArmor.ID },
        { "Inventory", inventory.ToJArray() }
	};
    return res;
}

Character 클래스 중에서 필요한 정보만 JObject로 만들어 반환하는 메서드.

아이템 관련해선 Item의 ID만 따로 저장해두고, 초기화단계에서 ItemDictionary를 이용해 ID로 객체를 만들어내기로 했다.

이렇게, 게임의 데이터를 저장하고 불러오는 기능을 추가하는게 끝이 났다.


AES 암호화 알고리즘

json 파일을 뽑고 보니, 그냥 메모장으로 열어서 수치만 바꿔줘도 캐릭터가 무식하게 강해지거나 부자가 될 수 있다. 그래서 구글링으로 AES 암호화 알고리즘을 이용해서 Json txt 파일을 암호화 해봤다.

DqDJyke/yFzLUNwCL/GjOqrZq5URqzKSZ1u9CsVV18mT+YssG5cXCESJz020Bz8t+mWYqZeNxUTyCrFp3ZEP+s7LIx7xc7+PvDmvfCcnyWZgBIpW9VYpnJw4qlfQkA71s8ny7bE/AzI31bpoUfyMxISqIrOa7t5kfq/mP0mvA/dfZpAhKvVA6TMJH2ue0CrA

이게 AES 알고리즘을 이용해 암호화 한 내 캐릭터 정보이다. 원래는 이렇게 생겼다.

{"Name":"JSON LOVER","Job":"전사","Level":3,"Atk":14,"Def":9,"HP":100,"Gold":3500,"Exp":0,"equipWeapon":0,"equipArmor":10,"Inventory":[10,0]}

AES는 대칭키 암호화 알고리즘으로, 하나의 키로 암호화와 복호화가 가능하다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Security.Cryptography;
using System.IO;

namespace source
{
    public class AESManager
    {
        static string aesKey = "Y2Tc3un1H7RRLMJ0lhTvii0mS4v4q6XjPT5+96PT8O0=";
        static string aesIv = "NBCChapter2SpartaDunge==";
        public static string Encrypt(string originText)
        {
            byte[] encrypted;

            using (AesCryptoServiceProvider aes = new AesCryptoServiceProvider())
            {
                aes.KeySize = 256;
                aes.BlockSize = 128;
                aes.Key = Convert.FromBase64String(aesKey);
                aes.IV = Convert.FromBase64String(aesIv);
                aes.Mode = CipherMode.CBC;
                aes.Padding = PaddingMode.PKCS7;

                ICryptoTransform enc = aes.CreateEncryptor(aes.Key, aes.IV);

                using (MemoryStream ms = new MemoryStream())
                {
                    using (CryptoStream cs = new CryptoStream(ms, enc, CryptoStreamMode.Write))
                    {
                        using (StreamWriter sw = new StreamWriter(cs))
                        {
                            sw.Write(originText);
                        }
                        encrypted = ms.ToArray();
                    }
                }
            }

            return Convert.ToBase64String(encrypted);
        }

        public static string Decrypt(string encryptedText)
        {
            string decryted = null;
            byte[] cipher = Convert.FromBase64String(encryptedText);

            using (AesCryptoServiceProvider aes = new AesCryptoServiceProvider())
            {
                aes.KeySize = 256;
                aes.BlockSize = 128;
                aes.Key = Convert.FromBase64String(aesKey);
                aes.IV = Convert.FromBase64String(aesIv);
                aes.Mode = CipherMode.CBC;
                aes.Padding = PaddingMode.PKCS7;

                ICryptoTransform dec = aes.CreateDecryptor(aes.Key, aes.IV);

                using (MemoryStream ms = new MemoryStream(cipher))
                {
                    using (CryptoStream cs = new CryptoStream(ms, dec, CryptoStreamMode.Read))
                    {
                        using (StreamReader sr = new StreamReader(cs))
                        {
                            decryted = sr.ReadToEnd();
                        }
                    }
                }
            }

            return decryted;
        }
    }
}

구글링으로 찾아보니, C#으로 구현한 AES 알고리즘 코드가 git에 있어서, 그대로 가져다 써 봤다.

AES Key를 난수로 생성해주는 사이트

AES에 꼭 필요한 4가지

  1. Key : 대칭키로, 암호화 복호화 모두 이 키를 가지고 진행한다. AES-128 / AES-256 등등 알고리즘 뒤에 오는 숫자들은 이 키의 bit 수를 의미한다고 한다.
  2. IV : 초기화 벡터 (Initialize Vector)라고 한다. 이후 소개할 CBC 방식에 사용할 벡터라고 이해하면 될 것 같다. 이 IV를 매번 다르게 만들어주면 같은 키를 사용하더라도 매번 다른 암호문을 만들어 낼 수 있다고 한다.
    또, key와는 다르게 굳이 비밀일 필요도 없다고 함.
  3. Cipher Mode : 암호화 과정에서 데이터 처리 방법을 지정한다. CBC, ECB, CTR, OFB, CFB 등이 있는데, 가장 대표적으로 많이 사용되는 방식은 CBC(Cipher Block Chaining)라고 한다. CBC 방식은 암호화 과정에서 이전에 암호화된 byte와 계속해서 XOR연산을 진행하는데, 첫 암호화 단계에선 XOR연산을 할 byte가 아직 만들어지지 않았으니, 이 때 IV를 이용한다.
  4. Padding Mode : padding은 특정 공간 안에 부족한 부분을 메워주는 것을 말한다. 암호화에서 padding은 부족한 부분을 메울 때 의미없는 문자들로 채운다고 하는데, PKCS5, PKCS7 등이 있다.

출처

https://gist.github.com/RichardHan/0848a25d9466a21f1f38
https://ts2ree.tistory.com/333
https://generate-random.org/encryption-key-generator?count=1&bytes=16&cipher=aes-256-cbc&string=&password=
https://aspdotnet.tistory.com/2836

profile
game developer

0개의 댓글