UnityでセーブデータをSerialize保存する 〜現状〜
↑ 現状をまとめた
UnityでセーブデータをSerialize化して保存したい!
・・・楽したいので(笑)
- PlayerPrefsは手軽だけど大容量のデータには不向き
- Streamクラスでちまちま1つずつ保存するのはダルい。。
- XmlSerializerは便利だけど中身もろばれ_| ̄|○
- BinaryFormatterはなんかiOSでは動かないとか何とか・・・orz
ということで、作ってみた(^^)/
方法としてはObjectをJsonに変換して、それを暗号化して端末に保存/読み込み。
Jsonなら共通の形式に変換出来るし、デバッグ時も分かりやすい。単純な文字列だから暗号化も楽。
プログラム
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Security.Cryptography;
using LitJson;
public class SaveData
{
public int Id;
public string Name;
}
public class SceneSample : MonoBehaviour
{
private static readonly string EncryptKey = "c6eahbq9sjuawhvdr9kvhpsm5qv393ga";
private static readonly int EncryptPasswordCount = 16;
private static readonly string PasswordChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static readonly int PasswordCharsLength = PasswordChars.Length;
private static readonly string SavePath = Application.persistentDataPath + "/save.bytes";
void Start()
{
///// 保存 /////////////////////////////////////
{
// セーブデータ作成
SaveData obj = new SaveData();
obj.Id = 1;
obj.Name = "tempura";
Debug.Log("[Encrypt]Id:" + obj.Id);
Debug.Log("[Encrypt]Name:" + obj.Name);
// 暗号化
string json = JsonMapper.ToJson(obj);
string iv;
string base64;
EncryptAesBase64(json, out iv, out base64);
Debug.Log("[Encrypt]json:" + json);
Debug.Log("[Encrypt]base64:" + base64);
// 保存
byte[] ivBytes = Encoding.UTF8.GetBytes(iv);
byte[] base64Bytes = Encoding.UTF8.GetBytes(base64);
using (FileStream fs = new FileStream(SavePath, FileMode.Create, FileAccess.Write))
using (BinaryWriter bw = new BinaryWriter(fs))
{
bw.Write(ivBytes.Length);
bw.Write(ivBytes);
bw.Write(base64Bytes.Length);
bw.Write(base64Bytes);
}
}
///// 読み込み /////////////////////////////////////
{
// 読み込み
byte[] ivBytes = null;
byte[] base64Bytes = null;
using (FileStream fs = new FileStream(SavePath, FileMode.Open, FileAccess.Read))
using (BinaryReader br = new BinaryReader(fs))
{
int length = br.ReadInt32();
ivBytes = br.ReadBytes(length);
length = br.ReadInt32();
base64Bytes = br.ReadBytes(length);
}
// 複合化
string json;
string iv = Encoding.UTF8.GetString(ivBytes);
string base64 = Encoding.UTF8.GetString(base64Bytes);
DecryptAesBase64(base64, iv, out json);
Debug.Log("[Decrypt]json:" + json);
// セーブデータ復元
SaveData obj = JsonMapper.ToObject<SaveData>(json);
Debug.Log("[Decrypt]Id:" + obj.Id);
Debug.Log("[Decrypt]Name:" + obj.Name);
}
}
/// <summary>
/// AES暗号化(Base64形式)
/// </summary>
public static void EncryptAesBase64(string json, out string iv, out string base64)
{
byte[] src = Encoding.UTF8.GetBytes(json);
byte[] dst;
EncryptAes(src, out iv, out dst);
base64 = Convert.ToBase64String(dst);
}
/// <summary>
/// AES複合化(Base64形式)
/// </summary>
public static void DecryptAesBase64(string base64, string iv, out string json)
{
byte[] src = Convert.FromBase64String(base64);
byte[] dst;
DecryptAes(src, iv, out dst);
json = Encoding.UTF8.GetString(dst).Trim('\0');
}
/// <summary>
/// AES暗号化
/// </summary>
public static void EncryptAes(byte[] src, out string iv, out byte[] dst)
{
iv = CreatePassword(EncryptPasswordCount);
dst = null;
using (RijndaelManaged rijndael = new RijndaelManaged())
{
rijndael.Padding = PaddingMode.PKCS7;
rijndael.Mode = CipherMode.CBC;
rijndael.KeySize = 256;
rijndael.BlockSize = 128;
byte[] key = Encoding.UTF8.GetBytes(EncryptKey);
byte[] vec = Encoding.UTF8.GetBytes(iv);
using (ICryptoTransform encryptor = rijndael.CreateEncryptor(key, vec))
using (MemoryStream ms = new MemoryStream())
using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
{
cs.Write(src, 0, src.Length);
cs.FlushFinalBlock();
dst = ms.ToArray();
}
}
}
/// <summary>
/// AES複合化
/// </summary>
public static void DecryptAes(byte[] src, string iv, out byte[] dst)
{
dst = new byte[src.Length];
using (RijndaelManaged rijndael = new RijndaelManaged())
{
rijndael.Padding = PaddingMode.PKCS7;
rijndael.Mode = CipherMode.CBC;
rijndael.KeySize = 256;
rijndael.BlockSize = 128;
byte[] key = Encoding.UTF8.GetBytes(EncryptKey);
byte[] vec = Encoding.UTF8.GetBytes(iv);
using (ICryptoTransform decryptor = rijndael.CreateDecryptor(key, vec))
using (MemoryStream ms = new MemoryStream(src))
using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
{
cs.Read(dst, 0, dst.Length);
}
}
}
/// <summary>
/// パスワード生成
/// </summary>
/// <param name="count">文字列数</param>
/// <returns>パスワード</returns>
public static string CreatePassword(int count)
{
StringBuilder sb = new StringBuilder(count);
for (int i = count - 1; i >= 0; i--)
{
char c = PasswordChars[UnityEngine.Random.Range(0, PasswordCharsLength)];
sb.Append(c);
}
return sb.ToString();
}
}
内容
JsonパーサーはLitJsonを使用してる。別にMiniJsonとか何でもいい。
Base64の形式に変換をかけているが、特に意味はない。途中まで作ってたソースがそうなってたのでついでにそういう風にしといた。
暗号化時のパスワードは自動で16文字をセーブ処理毎に変化するように処理している。ここらへんは場合によっては変えてもいいかも(サーバ側からパスワード情報をもらう等)。
暗号化方法はAES。C#なら別のやり方もあるかもだけど、とりあえず手軽だったのでこれで。
データ保存は、Json→暗号化→Base64形式→byte情報、を保存している。簡単な暗号化形式なので、出来る限り分かりづらくしてる。あと暗号化/複合化で使用するキー情報も一緒に保存してるが、これは危険なので、基本的には別で管理した方がいいかも。まぁ、めんどくさかったらこのままでも(;^_^A
注意点
これで簡単にセーブデータを保存できる!
・・・とそんな単純な話しではない(笑)
-
Json変換のオーバヘッド
まぁ、周知の事実かと思うが、Json変換のオーバヘッドは大きい。データ量多くなるし、端末保存時のコストも上がる。
-
Jsonで扱えるデータ制限
Jsonで扱えるデータには制限がある。それでなくてもLitJsonとかはデフォルトでlongが使えなかったり。セーブデータを完全に意識しない、ということは出来ず、むしろセーブデータにするようなオブジェクトは専用に意識してデータ構造を作らないといけない。
-
暗号化のコスト
Json同様、暗号化にもコストがかかる。ただでさえデータ量の多いJson文字列を暗号化してるので、セーブするデータが増えれば増えるほどコストが多大にかかる。実際、実環境でまだ試してないので、大丈夫かどうか不安ではある。負荷とか速度とか。
最後に
てか特によく調べず作ったけど、今なら何か別の方法があるのかな?便利なAssetとか。
途中まで作ってたから完成させたけど、もう少し何かスマートな形がいいな~