LoginSignup
10
16

More than 5 years have passed since last update.

Unityでユーザーデータをローカルにファイルとして保存する

Posted at

試作ゲームでユーザーデータをローカルにファイル保存することにしたため、そのためのクラスを作ってみました。暗号化なし版と暗号化あり版です。
インスタンスをシリアライズし → 圧縮し → (暗号化し → )ファイルに保存 します。

ちなみに公式のPlayerPrefsを使わなかったのは、オブジェクトをまるっと保存する用途には向かなさそうに思えたためです。

事前準備

GZip圧縮用に、DotNetZipのライブラリIonic.Zlib.CF.dll を、Asset以下の適当なディレクトリに保存しておきます。

今回は \DotNetZipLib-DevKit-v1.9\zlib-v1.9-CompactFramework\Release\Ionic.Zlib.CF.dll を利用しました。

暗号化なし版

まずは暗号化なし版から記載します。
インスタンスのシリアライズ・圧縮・保存を行うクラスです。

DataFilePlain.cs
using System;
using System.Text;
using System.IO;
using UnityEngine;
using Ionic.Zlib;

namespace Example
{
    public class DataFilePlain : IDataFile
    {
        // constant
        readonly string PathFormat = Application.persistentDataPath + "/{0}.json.gz";
        readonly int BufferSize = 1024;

        // 保存
        public void Save<T>(T instance, string dataName)
        {
            var filepath = String.Format(PathFormat, dataName);

            // instance → JSON
            var json = JsonUtility.ToJson(instance);

            using (var outputStream = new FileStream(filepath, FileMode.Create, FileAccess.Write, FileShare.None)) // 圧縮データ → バイナリファイル
            using (var compressStream = new GZipStream(outputStream, CompressionMode.Compress)) // MemoryStream → 圧縮データ
            using (var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(json)))  // Json → MemoryStream
            {
                int byteCount;
                byte[] buffer = new byte[BufferSize];
                while ((byteCount = inputStream.Read(buffer, 0, buffer.Length)) > 0)
                    compressStream.Write(buffer, 0, byteCount);
            }
        }


        // 読み込み
        public T Load<T>(string dataName)
        {
            var filepath = String.Format(PathFormat, dataName);
            if (!File.Exists(filepath))
                return default(T);

            using (var inputStream = new FileStream(filepath, FileMode.Open, FileAccess.Read, FileShare.None)) // バイナリファイル → 圧縮データ
            using (var compressStream = new GZipStream(inputStream, CompressionMode.Decompress))
            using (var outputStream = new MemoryStream())
            {
                // 圧縮データ → MemoryStream
                int byteCount;
                byte[] buffer = new byte[BufferSize];
                while ((byteCount = compressStream.Read(buffer, 0, buffer.Length)) > 0)
                    outputStream.Write(buffer, 0, byteCount);

                // MemoryStream → Json
                var json = Encoding.UTF8.GetString(outputStream.ToArray());

                // JSON → instance
                T instance = JsonUtility.FromJson<T>(json);

                return instance;
            }
        }
    }
}

チートしたくば存分にするがよいと言わんばかりのファイル名となりますが、開発中はデバッグしやすいこちらのほうが楽と思われます。
また、あとで差し替えるため、インターフェイスを切っています。それがこちら。

IDataFile.cs
namespace Example
{
    public interface IDataFile
    {
        void Save<T>(T instance, string dataName);
        T Load<T>(string dataName);
    }
}

暗号化あり版

次に、上述のコードに暗号化を加えたバージョンがこちらです。

DataFileEncrypting.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using UnityEngine;
using Ionic.Zlib;
using System.Security.Cryptography;

namespace Example
{
    public class DataFileEncrypting : IDataFile
    {
        // constant
        readonly string PathFormat = Application.persistentDataPath + "/{0}.dat";
        readonly int BufferSize = 1024;
        readonly string Password = "password"; // TODO 本番では変更する
        readonly int AesBlockSize = 128;
        readonly int AesKeySize = 128;
        readonly int KeySize = 16;

        // 保存
        public void Save<T>(T instance, string dataName)
        {
            var filepath = String.Format(PathFormat, dataName);

            // instance → JSON
            var json = JsonUtility.ToJson(instance);


            // @see http://qiita.com/hibara/items/c9096376b1d7b5c8e2ae
            // @see https://github.com/hibara/FileEncryptSample/blob/master/FileEncryptSample/Form1.cs
            using (var aes = new AesManaged())
            {
                aes.BlockSize = AesBlockSize;
                aes.KeySize = AesKeySize;
                aes.Mode = CipherMode.CBC;
                aes.Padding = PaddingMode.PKCS7;
                Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes(Password, 16);
                byte[] salt = new byte[KeySize];
                salt = deriveBytes.Salt;
                byte[] bufferKey = deriveBytes.GetBytes(KeySize);
                aes.Key = bufferKey;
                aes.GenerateIV();
                ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);

                using (var outputStream = new FileStream(filepath, FileMode.Create, FileAccess.Write, FileShare.None)) // 暗号化データ → バイナリファイル
                using (var cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write)) // 圧縮データ → 暗号化データ
                using (var compressStream = new GZipStream(cryptoStream, CompressionMode.Compress)) // MemoryStream → 圧縮データ
                using (var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(json))) // Json → MemoryStream
                {
                    outputStream.Write(salt, 0, KeySize);
                    outputStream.Write(aes.IV, 0, KeySize);

                    // MemoryStream → GZipファイル
                    int byteCount;
                    byte[] buffer = new byte[BufferSize];
                    while ((byteCount = inputStream.Read(buffer, 0, buffer.Length)) > 0)
                        compressStream.Write(buffer, 0, byteCount);
                }
            }
        }

        // 読み込み
        public T Load<T>(string dataName)
        {
            var filepath = String.Format(PathFormat, dataName);
            if (!File.Exists(filepath))
                return default(T);

            // @see http://qiita.com/hibara/items/c9096376b1d7b5c8e2ae
            // @see https://github.com/hibara/FileEncryptSample/blob/master/FileEncryptSample/Form1.cs
            using (var aes = new AesManaged())
            // バイナリファイル → 暗号化データ
            using (var inputStream = new FileStream(filepath, FileMode.Open, FileAccess.Read, FileShare.None))
            {
                aes.BlockSize = AesBlockSize;
                aes.KeySize = AesKeySize;
                aes.Mode = CipherMode.CBC;
                aes.Padding = PaddingMode.PKCS7;

                byte[] salt = new byte[KeySize];
                inputStream.Read(salt, 0, KeySize);

                byte[] iv = new byte[KeySize];
                inputStream.Read(iv, 0, KeySize);
                aes.IV = iv;
                Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes(Password, salt);
                byte[] bufferKey = deriveBytes.GetBytes(KeySize);
                aes.Key = bufferKey;
                ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);

                using (var cryptoStream = new CryptoStream(inputStream, decryptor, CryptoStreamMode.Read)) // 暗号化データ → 圧縮データ
                using (var compressStream = new GZipStream(cryptoStream, CompressionMode.Decompress))
                using (var outputStream = new MemoryStream())
                {
                    // 圧縮データ → MemoryStream
                    int byteCount;
                    byte[] buffer = new byte[BufferSize];
                    while ((byteCount = compressStream.Read(buffer, 0, buffer.Length)) > 0)
                        outputStream.Write(buffer, 0, byteCount);

                    // MemoryStream → Json
                    var json = Encoding.UTF8.GetString(outputStream.ToArray());

                    // JSON → instance
                    T instance = JsonUtility.FromJson<T>(json);

                    return instance;
                }
            }
        }
    }
}

尚、暗号化の箇所は下記の記事とサンプルを参考にさせて頂きました。

テストコード

最後に、動作確認に使ったテストコードがこちらとなります。
Windows上のUnityで、Editor Tests Runnerから動作させました。

DataFileTest.cs
using NUnit.Framework;
using Example;
using System;
using System.Collections.Generic;

public class DataFileTest {

    [Serializable]
    public class TestObject
    {
        public int Id;
        public string Name;
        public float Rate;

        public List<TestChild> ChildList;
    }

    [Serializable]
    public class TestChild
    {
        public int ChildId;
    }


    [Test]
    public void EditorTest() {

        var child1 = new TestChild();
        child1.ChildId = 10;
        var child2 = new TestChild();
        child2.ChildId = 101;


        var instance = new TestObject();
        instance.Id = 1;
        instance.Name = "テスト";
        instance.Rate = 10.5f;
        instance.ChildList = new List<TestChild>() { child1, child2 };


        var file = new DataFilePlain();
        file.Save(instance, "test");

        TestObject testObject;
        testObject = file.Load<TestObject>("test");

        Assert.AreEqual(testObject.Id, instance.Id);
        Assert.AreEqual(testObject.Name, instance.Name);
        Assert.AreEqual(testObject.Rate, instance.Rate);
        Assert.AreEqual(testObject.ChildList[0].ChildId, instance.ChildList[0].ChildId);
        Assert.AreEqual(testObject.ChildList[1].ChildId, instance.ChildList[1].ChildId);


        var fileEncrypting = new DataFileEncrypting();
        fileEncrypting.Save(instance, "test");

        testObject = fileEncrypting.Load<TestObject>("test");

        Assert.AreEqual(testObject.Id, instance.Id);
        Assert.AreEqual(testObject.Name, instance.Name);
        Assert.AreEqual(testObject.Rate, instance.Rate);
        Assert.AreEqual(testObject.ChildList[0].ChildId, instance.ChildList[0].ChildId);
        Assert.AreEqual(testObject.ChildList[1].ChildId, instance.ChildList[1].ChildId);
    }
}

ちゃんとファイルが残っています。
image.png

以上。

10
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
16