Unityでセーブデータをファイルに保存してみた

Unity

Unity初心者でカードゲームを作成している者です。

今回ゲームに必須とも言えるセーブ&ロードの機能を色々と調べながら実装をしてみました。

その方法について記載をしてみたいと思います。

PlayerPrefsは使いたくなかった

「Unity セーブ」などで検索を行うと、まずPlayerPrefsを使用する方法がヒットしてきます。

PlayerPrefsはUnity標準の機能ですし、非常に簡単に実装できるので、当初はこの機能を使ってセーブ&ロードを行おうと思いました。

public static void save(){
  //NameというKeyを使用して"太郎"という文字列をセーブする
  PlayerPrefs.SetString("Name", "太郎");
  PlayerPrefs.Save();
}
public static void load(){
  //NameというKeyを使用してデータをロード。nullの場合は花子を返却する。
  name = PlayerPrefs.GetString("Name", "花子");
}

public static void delete(){
  //セーブデータを削除
  PlayerPrefs.DeleteAll();
}

前述したとおり非常に簡単に実装が出来ますし、仮にセーブされていないデータがロードされてしまったときのnullの扱いについてもハンドリングされていて良い感じです。

しかし、「このPlayerPrefsを使用して作成されるセーブデータの保存場所は何処なんだろう」というのが気になりました。

調べてみると、PlayerPrefsはレジストリにセーブデータを登録するという仕様のようです。

自分のゲームごときがレジストリを使用したくないという思いがありましたし、以下のような懸念もありました。

  • PCを移す度にセーブデータがリセットされてしまう
  • バグが有った時にゲーム側からセーブデータにパッチを当てにくい
  • (僕がユーザーの立場なら)フリーゲームにレジストリ操作されるのは気持ち悪い
  • セーブファイルを複数作る場合に困る

更にPlayerPrefsでは「String」、「Int」、「Float」の3種類しか保存をすることが出来ないようです。

ゲームが複雑になるにつれて、自分の定義した構造体などでセーブデータを定義したいと思うようになりそうだったので、自作でファイルに保存するようにしました。

本当はセーブ&ロードのライブラリが公開されていたので、そちらを使いたかったのですが、Unity終了時にファイルサイズが0になる現象などが発生して解決できなかったという経緯もあります。

セーブデータをJsonに保存してみる

早速ですが、自分のゲームに実装したセーブ&ロードのコードを記載してみます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;

[Serializable]
public struct SaveData {
  public List<string> deck;
  public int money;
}

public static class SaveManager {

  public static SaveData sd;
  const string SAVE_FILE_PATH = "save.json";

  public static void saveDeck(List<string> _deck){
    sd.deck = _deck;
    save();
  }

  public static void saveMoney(int _money){
    sd.money = _money;
    save();
  }

  public static void save(){
      string json = JsonUtility.ToJson (sd);
      #if UNITY_EDITOR
        string path = Directory.GetCurrentDirectory();
      #else
        string path = AppDomain.CurrentDomain.BaseDirectory.TrimEnd('\\');
      #endif
      path +=  ("/" + SAVE_FILE_PATH);
      StreamWriter writer = new StreamWriter (path, false);
      writer.WriteLine (json);
      writer.Flush ();
      writer.Close ();
  }

  public static void load(){
    try
    {
      #if UNITY_EDITOR
        string path = Directory.GetCurrentDirectory();
      #else
        string path = AppDomain.CurrentDomain.BaseDirectory.TrimEnd('\\');
      #endif
      FileInfo info = new FileInfo(path + "/" + SAVE_FILE_PATH);
      StreamReader reader = new StreamReader (info.OpenRead ());
      string json = reader.ReadToEnd ();
      sd = JsonUtility.FromJson<SaveData>(json);
    }
    catch (Exception e)
    {
      sd = new SaveData();
    }
  }
}
void Start(){
    //セーブデータをロードする
    SaveManager.load();
}

public void save()
{
    //金額に100をセーブする
    SaveManager.saveMoney(100);
}

public int getMoney(){
    //セーブデータから金額を取得する
    return SaveManager.sd.money;
}

実装の方法に色々考え方はあると思うのですが、

  1. とりあえず最初に全部ロードして変数に格納
  2. セーブする変数に変更があったら都度全データをセーブ
  3. ロードした値はPublicな変数から取得

という形にしました。

RPGのセーブポイント的なものではなく、オートでセーブ&ロードが実行される形式です。

↓こんな感じでjsonファイルに保存されました。

StreamWriterの部分はusingとかを使えば更にスマートに書けそうです。

usingを使用することでファイルを安全にクローズすることが出来ます。途中でエラーが発生してもtry catchを使わずにクローズできる。

参考サイト:

ファイルにテキストを書き込むには?[C#/VB、.NET全バージョン]
StreamWriteクラスを用いたファイルへのテキストの書き込み、using構文によるファイルのクローズなど、.NETにおけるテキストファイル書き込みの基礎を説明する。

セーブ&ロードの基本的なロジックについては以下のサイトを参考にさせてもらいました。

【Unity】オブジェクトをJsonにシリアライズしてテキストに保存・読み込みする - Qiita
ゲームのセーブデータをJsonとしてテキストに保存して読み込むために、その方法を調べたメモとサンプルです。 SaveManager.cs using System.Collections; using System.Collect...

また今回はファイルの保存場所については、ゲームのexeがある場所と同じところを指定しています。

管理者権限が必要な場所に配置されるとエラーになる

ファイルの保存を行う際に気をつけたいのが保存時のエラーです。

「ディスク容量がない」、「ファイル保存中にゲームを終了」といったものはクリティカルなのですが、滅多に起こらないのであまり個人開発で考慮しなくても良いかもしれません。

一番起こりそうなのが、UACやウイルス対策ソフト関連のエラーですね。

勝手にファイルを作るわけなので、普通に防がれる可能性が高いです。

自分の場合は、「ゲームはProgramFilesやC:\直下には保存せず、デスクトップなどで遊んでください」と注意書きするつもりですが、守らえるかは不明瞭。

一般的なソフトウェアと同じで「ユーザーのAppData」であれば安全に保存をすることが出来ます。

C:/Users/<ユーザー名>/AppData/LocalLow/

上記フォルダは「Application.persistentDataPath」で取得することが出来ます。

ただ、同人のゲームをよくやっていると

  • セーブファイルはゲームと同じフォルダにある(RPGツクールがそういう仕様だから)
  • 勝手に別の場所に作るのは気持ち悪い

という固定観念があったため、自フォルダに保存することにしました。

あとPCを変えた時に、サルベージしないとセーブが引き継がれないのは寂しいことも多いので。

暗号化するならBase64とかを使用すれば良いんじゃないか

あとjsonだとデータが丸見えなので暗号化をしたい場合もあるかもしれません。

(自分はどうでもいいと思っていますが)

予めBase64で暗号化しておけば、まず復号化出来ないと思うし、簡単に実装が可能なのでおすすめします。

Base64で文字列を複合、暗号化するのは下記のサイトが参考になりそうです。

文字列をBase64でエンコード/デコードするには?[C#、VB]

セーブデータを複数に分ける

今までのソースは、セーブデータが1つだけの前提で作成していたのですが、複雑なゲームを作成する場合は複数のセーブデータを保持できるようにしたいです。

複数分けたデータをJsonで保存するソースも作成したので記載しておこうと思います。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;

[Serializable]
public struct GlobalData {
  public List<string> deck;
  public int money;
  public string updateTime;
  public string name;
  public int fileNo;
}
public static class Global
{
    public static GlobalData globalData = new GlobalData();
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;
using System.Text;

public static class SaveManager {

  // セーブデータの保存先ディレクトリ
  const string SAVE_DIRECTORY = "save";
  // セーブファイルの名前
  const string SAVE_FILE_NAME = "save";
  // セーブファイルの拡張子
  const string SAVE_FILE_TAIL = ".json";
  // セーブデータの一覧
  public static Dictionary<int, GlobalData> saveDatas
    = new Dictionary<int, GlobalData>();

  // クラス起動時にSaveファイルを読み取っておく
  static SaveManager(){
    // プロジェクトディレクトリを取得    
    #if UNITY_EDITOR
      string path = Directory.GetCurrentDirectory();
    #else
      string path = AppDomain.CurrentDomain.BaseDirectory.TrimEnd('\\');
    #endif

    // セーブデータの保存先ディレクトリを取得
    path +=  ("/" + SAVE_DIRECTORY + "/" );
    createDirectory(Path.GetDirectoryName(path));
    string[] names = Directory.GetFiles(path, SAVE_FILE_NAME + "*" + SAVE_FILE_TAIL);
    foreach (string name in names)
    {
      try
      {
        FileInfo info = new FileInfo(name);
        StreamReader reader = new StreamReader(info.OpenRead(), Encoding.GetEncoding("UTF-8"));
        string json = reader.ReadToEnd();
        reader.Close();
        GlobalData sd = JsonUtility.FromJson<GlobalData>(json);
        saveDatas.Add(sd.fileNo, sd);
      }
      catch (Exception e)
      {
#if UNITY_EDITOR
            UnityEditor.EditorApplication.isPlaying = false;
#elif UNITY_STANDALONE
            UnityEngine.Application.Quit();
#endif
      }
    }
  }

  public static void save(int index, GlobalData sd){
      sd.fileNo = index;
      sd.updateTime = DateTime.Now.ToString();
      sd.name = "セーブデータ" + index.ToString();
      string json = JsonUtility.ToJson(sd);
      #if UNITY_EDITOR
        string path = Directory.GetCurrentDirectory();
      #else
        string path = AppDomain.CurrentDomain.BaseDirectory.TrimEnd('\\');
      #endif
      path +=  ("/" + SAVE_DIRECTORY + "/" + SAVE_FILE_NAME + index.ToString() + SAVE_FILE_TAIL);
      createDirectory(Path.GetDirectoryName(path));
      StreamWriter writer = new StreamWriter (path, false, Encoding.GetEncoding("UTF-8"));
      writer.WriteLine(json);
      writer.Flush();
      writer.Close();
  }
  private static GlobalData setInitValue(GlobalData sd) {
    // セーブデータを使用しない場合の初期値
    sd.money = 50;
    sd.deck = new List<string>();
    sd.updateTime = DateTime.Now.ToString();
    sd.name = "";
    return sd;
  }

  public static GlobalData load(int index){
    GlobalData sd = new GlobalData();
    if(saveDatas.ContainsKey(index)){
       sd = saveDatas[index];
    } else {
       sd = setInitValue(new GlobalData());
    }
    return sd;
  }
}

  public static void createDirectory(string path){
    if (!Directory.Exists(path))
    {
      Directory.CreateDirectory(path);
    }
  }

GlobalData.csというクラスにセーブデータから取得したデータを保存しておくようにしておく仕様です。

セーブデータはsaveというディレクトリに、save1.json、save2.jsonという形で保存します。

ゲーム中ではGlobalData.csのデータを更新していき、セーブ用のメソッドでファイル出力します。

使用例は以下の通りです。

// save1からデータをロードする
Global.globalData = SaveManager.load(1);

// ゲームデータのmoneyを100加算する
Global.globalData.money += 100;

// save1にゲームデータを保存する
SaveManager.save(1, Global.globalData);

コメント

タイトルとURLをコピーしました