C#
Unity
NCMB

【NCMB】【Unity】ファイルストアでセーブデータ(JSONファイル)のバックアップ

やりたいこと

セーブデータのバックアップをサーバーに保存し、復元する。

概要

事前準備

オートログイン機能を使ってユーザー登録を行い、ユーザー名はPlayerPrefsに保存。
こちらの方法でゲームのセーブデータをjsonファイルに保存している
・user情報とバックアップのファイル名を紐づたNCMBclass(今回はクラス名を"SaveData"とする)を作成しておく

バックアップの流れ

・NCMBのデータストアにjsonファイルのバックアップを保存する
・SaveDataの新しいレコードをサーバーに追加する

復元

・"username"をkeyにSaveDataクラスを取得する。
・SaveDataクラスの"filename"をkeyにデータストアからバックアップを取得する
・バックアップデータをクライアントのデータに上書きして、読み込み直す。

NCMBデータストアのclass設定

スクリーンショット 2018-06-03 23.21.35.png

Script

テスト用Script

FileSaveTest.cs
//データのセーブ、バックアップ、バックアップの取得、読み込みを行う
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using UnityEngine;
using NCMB; //忘れないように

public class FileSaveTest : MonoBehaviour {

    public Player player = new Player();

    private const string userNameKey = "_user_name_";
    private const string passwordKey = "_pass_word_";

    //パスとファイル名
    string path;
    string fileName;

    //セーブデータ
    List<NCMBObject> saveDataList;

    void Start()
    {
        SaveData.Clear();

        path = Application.persistentDataPath + "/";
        fileName = Application.companyName + "." + Application.productName + ".savedata.json";

        //テスト用に値書き換え,バックアップされる
        player.PlayerName = "アズマ1";
        Debug.Log("player.PlayerName = " + player.PlayerName);

        //セーブ
        SavePlayer();
        UploadSaveFile();

        //テスト用に値書き換え,バックアップされてない
        player.PlayerName = "アズマ2";
        Debug.Log("player.PlayerName = " + player.PlayerName);

        //バックアップのロード、savedata.jsonに上書き
        loadSaveData();

        //savedata.jsonからロード
        player = SaveData.GetClass<Player>("Player", new Player());
        //値変更確認,バックアップされた「アズマ1」が出力されれば成功
        Debug.Log("player.PlayerName = " + player.PlayerName);
    }

    //JsonUtilityでセーブ
    void SavePlayer(){
        SaveData.SetClass<Player>("Player", player);
        SaveData.Save();
    }

    /// <summary>
    /// セーブデータファイルをサーバーにアップロード
    /// </summary>
    void UploadSaveFile(){

        string str = "";

        //JSONファイルを読んでstringへ
        if (File.Exists(path + fileName))
        {
            using (StreamReader sr = new StreamReader(path + fileName, Encoding.GetEncoding("utf-8")))
            {
                str = sr.ReadToEnd();
            }
        }

        byte[] data = System.Text.Encoding.UTF8.GetBytes(str);

        //ファイル名重複を避けるためファイル名を日付+乱数で生成
        string NCMBFileName = Application.companyName + "." + Application.productName + ".savedata" + "-" + DateTime.Now.Year+DateTime.Now.Month+DateTime.Now.Day+DateTime.Now.Hour+DateTime.Now.Minute+DateTime.Now.Second;
        NCMBFileName += "-" + UnityEngine.Random.Range (100000,999999);
        NCMBFileName += ".json";

        NCMBFile file = new NCMBFile(NCMBFileName, data);
        file.SaveAsync((NCMBException error) => {
            if (error != null)
            {
                // 失敗
                Debug.Log("UploadPlayer : 失敗"); //追記
            }
            else
            {
                // 成功
                Debug.Log("UploadPlayer : 成功"); //追記
            }
        });

        //バックアップレコードを追加
        AddRecord(NCMBFileName);
    }

    //バックアップの更新
    void AddRecord(string filename)
    {
        NCMBObject obj = new NCMBObject("SaveData");
        obj.Add("username", PlayerPrefs.GetString(userNameKey, ""));
        obj.Add("savedatafile", filename);
        obj.Add("date", DateTime.Now.Date);

        obj.Save((NCMBException e) => {
            if (e != null)
            {
                Debug.Log("save data error");
                showError(e);
            }
            else
            {
                //成功時の処理
                Debug.Log("UpdateSaveData : 成功");
            }
        });
    }

    void showError(NCMBException e)
    {
        Debug.Log("UpdateSaveData : 失敗");
    }

    //ファイル名を取得, ファイルを取得, 読み込む
    void loadSaveData()
    {
        saveDataList = new List<NCMBObject>();

        //QueryTestを検索するクラスを作成
        NCMBQuery<NCMBObject> query = new NCMBQuery<NCMBObject>("SaveData");
        //ユーザー名で抽出
        query.WhereEqualTo("username", PlayerPrefs.GetString(userNameKey, ""));
        //ソートkeyを設定
        query.OrderByDescending("createDate");
        //検索結果を取得
        query.FindAsync((List<NCMBObject> objList, NCMBException e) => {
            if (e != null)
            {
                //検索失敗時の処理
                Debug.Log("loadSaveData : 失敗");
            }
            else
            {
                Debug.Log("loadSaveData : 成功");
                //最新のファイルを読み込む
                Debug.Log(objList[0]["username"] + " " + objList[0]["savedatafile"]);
                LoadSaveFile(objList[0]["savedatafile"].ToString());
            }
        });
    }

    //SaveDataファイルを取得
    void LoadSaveFile(string Name)
    {
        NCMBFile file = new NCMBFile(Name);
        file.FetchAsync((byte[] fileData, NCMBException error) => {
            if (error != null)
            {
                // 失敗
                Debug.Log("LoadSaveFile : 失敗");
            }
            else
            {
                //成功
                Debug.Log("LoadSaveFile : 成功");
                SaveBytesTo(fileData);
            }
        });
    }

    //Byteデータをsavedata.jsonに書き込む
    void SaveBytesTo(byte[] b)
    {
        Debug.Log("SaveBytesTo");
        //UTF8 エンコードでbyteからstringへ
        string text = System.Text.Encoding.UTF8.GetString(b);
        //上書き
        StreamWriter writer = new StreamWriter(path + fileName, false, Encoding.GetEncoding("utf-8"));
        writer.WriteLine(text);
        writer.Close();
    }

}

テスト用Class

TestClasses.cs
//テスト用ClassをまとめたScript
//class構成が大きな影響を与えることはないと思われるので、任意に書き換えてOK
using System.Collections.Generic;

[System.Serializable]
public class Player
{
    public string PlayerName = "冒険者";

    public int Coin = 100;

    public List<Item> ItemList = new List<Item>{
        new Item("potion", 0),
        new Item("Ditoxic",1),
        new Item("MonsterBall",2)
    };

    public Skill[] SkillArray = new Skill[3]
    {
        new Fire("Fire",0),
        new Ice("Ice",1),
        new Poison("Poison",2)
    };
}

[System.Serializable]
public class Item
{
    string Name = string.Empty;
    int Id = 0;
    //コンストラクタ
    public Item(string name, int id)
    {
        Name = name;
        Id = id;
    }
}

//※Skillクラスは各skillに継承される
[System.Serializable]
public class Skill
{
    public string Name = string.Empty;
    public int Id = 0;
    //コンストラクタ
    public Skill(string name, int id)
    {
        Name = name;
        Id = id;
    }
}

[System.Serializable]
public class Fire : Skill
{
    //継承クラスのコンストラクタはこう書く
    public Fire(string name, int id) : base(name, id)
    {
        Name = name;
        Id = id;
    }
}

[System.Serializable]
public class Ice : Skill
{
    //継承クラスのコンストラクタはこう書く
    public Ice(string name, int id) : base(name, id)
    {
        Name = name;
        Id = id;
    }
}

[System.Serializable]
public class Poison : Skill
{
    //継承クラスのコンストラクタはこう書く
    public Poison(string name, int id) : base(name, id)
    {
        Name = name;
        Id = id;
    }
}

Log(一部抜粋)

player.PlayerName = アズマ1
player.PlayerName = アズマ2
player.PlayerName = アズマ1

データストアの様子

スクリーンショット 2018-06-03 23.34.25.png

ファイルストアの様子

スクリーンショット 2018-06-03 23.34.50.png

備考

・Macでのsavedata.jsonの格納場所
/Users/azuma/Library/Application Support/"CompanyName"/"ApplicationName"/"CompanyName"."ApplicationName".savedata.cs
・player.PlayerNameはSerializeされているので、ゲームを再生中にInspectorからも確認できます。
・このまま導入するとアプリを立ち上げる度にバックアップファイルがサーバーに増え続けるので、バックアップファイル作成頻度の制限や、ファイルの削除機能も実装が必要です。

宣伝:開発中のゲームPV

ビデオが開けなかった場合に表示されるテキスト