2016/06/21
概要
※2016/7/5 コードを整備しました。List<T>型もセーブできます。
前回セーブ関連の記事を書きましたが、意外と読んでいただけているようで、Google先生で”Unity セーブ”と検索すると、上の方に出てくるようになってました。うれしいもんですね!(ページビューは大したことないけど。。)
前回記事を書いたときには、あれ以上良い実装が思いつかなかったので、そのまま運用してたけど、やっぱり使いづらいなぁということで実のところ、納得いってませんでした。
また、前回はLitJsonでシリアライズしてたけど、Unity 5.3でJson関連が標準機能になったらしいので
Unity標準機能を使い、使いやすくなったセーブ機能をご紹介します。
前回の問題点、今回の改良点は以下のとおりです。早く機能実装したい方は読み飛ばしてください。
問題点
・LitJson.dllが必要。→dllインポートが手間かかる。以下のとおりビルドをミスる可能性ありとのこと(コメントありがとうございました!)
・セーブしたい変数を増やすたびにGameDataクラスに追記しないといけない。→面倒。クラスが肥大化して気持ち悪い。
・GameObjectに貼り付けて使用しないといけない。→面倒
改善点
・Unity標準のJsonクラスを使用するように変更→LitJsonが不要になった。
・PlayerPrefsのkey, valueのような使い方を意識して作成。PlayerPrefsだとint, float, stringだけしかセーブできないところを、List,や独自定義クラスも簡単にセーブできるようにした。要はPlayerPrefsの拡張版みたいな感じ。
・GameObjectに貼り付けなくても、スクリプト上のどこからでも使用できる。これもPlayerPrefsと同じような使い方。
ソースコード
必要なcsファイルはSaveData.csのみです。暗号化が必要であれば、前回の記事を参考にしてください。jsonにしたstringをcriptに流し込むだけですので。※今回は未実装
セーブ機能の実装方法まとめ 使い勝手の良いセーブを実装する【Unity開発】
・Savedata.cs
・SerializableDictionary.cs
SerializableDictionary.csは7/5にソースコードを編集・修正した段階でSavedata.csのインナークラスにしました。Savedata.csのみコピーいただければ動作するかと思います。
Savedata.cs
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using System.IO; | |
using UnityEngine; | |
/// <summary> | |
/// Json形式でセーブできるクラスを提供します。 | |
/// </summary> | |
/// <remarks> | |
/// 最初に値を設定、取得するタイミングでファイル読み出します。 | |
/// </remarks> | |
public class SaveData | |
{ | |
/// <summary> | |
/// SingletonなSaveDatabaseクラス | |
/// </summary> | |
private static SaveDataBase savedatabase = null; | |
private static SaveDataBase Savedatabase { | |
get { | |
if (savedatabase == null) { | |
string path = Application.persistentDataPath + "/"; | |
string fileName = Application.companyName + "." + Application.productName + ".savedata.json"; | |
savedatabase = new SaveDataBase (path, fileName); | |
} | |
return savedatabase; | |
} | |
} | |
private SaveData () | |
{ | |
} | |
#region Public Static Methods | |
/// <summary> | |
/// 指定したキーとT型のクラスコレクションをセーブデータに追加します。 | |
/// </summary> | |
/// <typeparam name="T">ジェネリッククラス</typeparam> | |
/// <param name="key">キー</param> | |
/// <param name="list">T型のList</param> | |
/// <exception cref="System.ArgumentException"></exception> | |
/// <remarks>指定したキーとT型のクラスコレクションをセーブデータに追加します。</remarks> | |
public static void SetList<T> (string key, List<T> list) | |
{ | |
Savedatabase.SetList<T> (key, list); | |
} | |
/// <summary> | |
/// 指定したキーとT型のクラスコレクションをセーブデータから取得します。 | |
/// </summary> | |
/// <typeparam name="T">ジェネリッククラス</typeparam> | |
/// <param name="key">キー</param> | |
/// <param name="_default">デフォルトの値</param> | |
/// <exception cref="System.ArgumentException"></exception> | |
/// <returns></returns> | |
public static List<T> GetList<T> (string key, List<T> _default) | |
{ | |
return Savedatabase.GetList<T> (key, _default); | |
} | |
/// <summary> | |
/// 指定したキーとT型のクラスをセーブデータに追加します。 | |
/// </summary> | |
/// <typeparam name="T">ジェネリッククラス</typeparam> | |
/// <param name="key">キー</param> | |
/// <param name="_default">デフォルトの値</param> | |
/// <exception cref="System.ArgumentException"></exception> | |
/// <returns></returns> | |
public static T GetClass<T> (string key, T _default) where T : class, new() | |
{ | |
return Savedatabase.GetClass (key, _default); | |
} | |
/// <summary> | |
/// 指定したキーとT型のクラスコレクションをセーブデータから取得します。 | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <param name="key"></param> | |
/// <param name="obj"></param> | |
/// <exception cref="System.ArgumentException"></exception> | |
public static void SetClass<T> (string key, T obj) where T : class, new() | |
{ | |
Savedatabase.SetClass<T> (key, obj); | |
} | |
/// <summary> | |
/// 指定されたキーに関連付けられている値を取得します。 | |
/// </summary> | |
/// <param name="key">キー</param> | |
/// <param name="value">値</param> | |
/// <exception cref="System.ArgumentException"></exception> | |
public static void SetString (string key, string value) | |
{ | |
Savedatabase.SetString (key, value); | |
} | |
/// <summary> | |
/// 指定されたキーに関連付けられているString型の値を取得します。 | |
/// 値がない場合、_defaultの値を返します。省略した場合、空の文字列を返します。 | |
/// </summary> | |
/// <param name="key">キー</param> | |
/// <param name="_default">デフォルトの値</param> | |
/// <exception cref="System.ArgumentException"></exception> | |
/// <returns></returns> | |
public static string GetString (string key, string _default = "") | |
{ | |
return Savedatabase.GetString (key, _default); | |
} | |
/// <summary> | |
/// 指定されたキーに関連付けられているInt型の値を取得します。 | |
/// </summary> | |
/// <param name="key">キー</param> | |
/// <param name="value">デフォルトの値</param> | |
/// <exception cref="System.ArgumentException"></exception> | |
public static void SetInt (string key, int value) | |
{ | |
Savedatabase.SetInt (key, value); | |
} | |
/// <summary> | |
/// 指定されたキーに関連付けられているInt型の値を取得します。 | |
/// 値がない場合、_defaultの値を返します。省略した場合、0を返します。 | |
/// </summary> | |
/// <param name="key">キー</param> | |
/// <param name="_default">デフォルトの値</param> | |
/// <exception cref="System.ArgumentException"></exception> | |
/// <returns></returns> | |
public static int GetInt (string key, int _default = 0) | |
{ | |
return Savedatabase.GetInt (key, _default); | |
} | |
/// <summary> | |
/// 指定されたキーに関連付けられているfloat型の値を取得します。 | |
/// </summary> | |
/// <param name="key">キー</param> | |
/// <param name="value">デフォルトの値</param> | |
/// <exception cref="System.ArgumentException"></exception> | |
public static void SetFloat (string key, float value) | |
{ | |
Savedatabase.SetFloat (key, value); | |
} | |
/// <summary> | |
/// 指定されたキーに関連付けられているfloat型の値を取得します。 | |
/// 値がない場合、_defaultの値を返します。省略した場合、0.0fを返します。 | |
/// </summary> | |
/// <param name="key">キー</param> | |
/// <param name="_default">デフォルトの値</param> | |
/// <exception cref="System.ArgumentException"></exception> | |
/// <returns></returns> | |
public static float GetFloat (string key, float _default = 0.0f) | |
{ | |
return Savedatabase.GetFloat (key, _default); | |
} | |
/// <summary> | |
/// セーブデータからすべてのキーと値を削除します。 | |
/// </summary> | |
public static void Clear () | |
{ | |
Savedatabase.Clear (); | |
} | |
/// <summary> | |
/// 指定したキーを持つ値を セーブデータから削除します。 | |
/// </summary> | |
/// <param name="key">キー</param> | |
/// <exception cref="System.ArgumentException"></exception> | |
public static void Remove (string key) | |
{ | |
Savedatabase.Remove (key); | |
} | |
/// <summary> | |
/// セーブデータ内にキーが存在するかを取得します。 | |
/// </summary> | |
/// <param name="_key">キー</param> | |
/// <exception cref="System.ArgumentException"></exception> | |
/// <returns></returns> | |
public static bool ContainsKey (string _key) | |
{ | |
return Savedatabase.ContainsKey (_key); | |
} | |
/// <summary> | |
/// セーブデータに格納されたキーの一覧を取得します。 | |
/// </summary> | |
/// <exception cref="System.ArgumentException"></exception> | |
/// <returns></returns> | |
public static List<string> Keys () | |
{ | |
return Savedatabase.Keys (); | |
} | |
/// <summary> | |
/// 明示的にファイルに書き込みます。 | |
/// </summary> | |
public static void Save () | |
{ | |
Savedatabase.Save (); | |
} | |
#endregion | |
#region SaveDatabase Class | |
[Serializable] | |
private class SaveDataBase | |
{ | |
#region Fields | |
private string path; | |
//保存先 | |
public string Path { | |
get { return path; } | |
set { path = value; } | |
} | |
private string fileName; | |
//ファイル名 | |
public string FileName { | |
get { return fileName; } | |
set { fileName = value; } | |
} | |
private Dictionary<string, string> saveDictionary; | |
//keyとjson文字列を格納 | |
#endregion | |
#region Constructor&Destructor | |
public SaveDataBase (string _path, string _fileName) | |
{ | |
path = _path; | |
fileName = _fileName; | |
saveDictionary = new Dictionary<string, string> (); | |
Load (); | |
} | |
/// <summary> | |
/// クラスが破棄される時点でファイルに書き込みます。 | |
/// </summary> | |
~SaveDataBase () | |
{ | |
Save (); | |
} | |
#endregion | |
#region Public Methods | |
public void SetList<T> (string key, List<T> list) | |
{ | |
keyCheck (key); | |
var serializableList = new Serialization<T> (list); | |
string json = JsonUtility.ToJson (serializableList); | |
saveDictionary [key] = json; | |
} | |
public List<T> GetList<T> (string key, List<T> _default) | |
{ | |
keyCheck (key); | |
if (!saveDictionary.ContainsKey (key)) { | |
return _default; | |
} | |
string json = saveDictionary [key]; | |
Serialization<T> deserializeList = JsonUtility.FromJson<Serialization<T>> (json); | |
return deserializeList.ToList (); | |
} | |
public T GetClass<T> (string key, T _default) where T : class, new() | |
{ | |
keyCheck (key); | |
if (!saveDictionary.ContainsKey (key)) | |
return _default; | |
string json = saveDictionary [key]; | |
T obj = JsonUtility.FromJson<T> (json); | |
return obj; | |
} | |
public void SetClass<T> (string key, T obj) where T : class, new() | |
{ | |
keyCheck (key); | |
string json = JsonUtility.ToJson (obj); | |
saveDictionary [key] = json; | |
} | |
public void SetString (string key, string value) | |
{ | |
keyCheck (key); | |
saveDictionary [key] = value; | |
} | |
public string GetString (string key, string _default) | |
{ | |
keyCheck (key); | |
if (!saveDictionary.ContainsKey (key)) | |
return _default; | |
return saveDictionary [key]; | |
} | |
public void SetInt (string key, int value) | |
{ | |
keyCheck (key); | |
saveDictionary [key] = value.ToString (); | |
} | |
public int GetInt (string key, int _default) | |
{ | |
keyCheck (key); | |
if (!saveDictionary.ContainsKey (key)) | |
return _default; | |
int ret; | |
if (!int.TryParse (saveDictionary [key], out ret)) { | |
ret = 0; | |
} | |
return ret; | |
} | |
public void SetFloat (string key, float value) | |
{ | |
keyCheck (key); | |
saveDictionary [key] = value.ToString (); | |
} | |
public float GetFloat (string key, float _default) | |
{ | |
float ret; | |
keyCheck (key); | |
if (!saveDictionary.ContainsKey (key)) | |
ret = _default; | |
if (!float.TryParse (saveDictionary [key], out ret)) { | |
ret = 0.0f; | |
} | |
return ret; | |
} | |
public void Clear () | |
{ | |
saveDictionary.Clear (); | |
} | |
public void Remove (string key) | |
{ | |
keyCheck (key); | |
if (saveDictionary.ContainsKey (key)) { | |
saveDictionary.Remove (key); | |
} | |
} | |
public bool ContainsKey (string _key) | |
{ | |
return saveDictionary.ContainsKey (_key); | |
} | |
public List<string> Keys () | |
{ | |
return saveDictionary.Keys.ToList<string> (); | |
} | |
public void Save () | |
{ | |
using (StreamWriter writer = new StreamWriter (path + fileName, false, Encoding.GetEncoding ("utf-8"))) { | |
var serialDict = new Serialization<string, string> (saveDictionary); | |
serialDict.OnBeforeSerialize (); | |
string dictJsonString = JsonUtility.ToJson (serialDict); | |
writer.WriteLine (dictJsonString); | |
} | |
} | |
public void Load () | |
{ | |
if (File.Exists (path + fileName)) { | |
using (StreamReader sr = new StreamReader (path + fileName, Encoding.GetEncoding ("utf-8"))) { | |
if (saveDictionary != null) { | |
var sDict = JsonUtility.FromJson<Serialization<string, string>> (sr.ReadToEnd()); | |
sDict.OnAfterDeserialize (); | |
saveDictionary = sDict.ToDictionary (); | |
} | |
} | |
} | |
else{ saveDictionary = new Dictionary<string, string> (); } | |
} | |
public string GetJsonString (string key) | |
{ | |
keyCheck (key); | |
if (saveDictionary.ContainsKey (key)) { | |
return saveDictionary [key]; | |
} else { | |
return null; | |
} | |
} | |
#endregion | |
#region Private Methods | |
/// <summary> | |
/// キーに不正がないかチェックします。 | |
/// </summary> | |
private void keyCheck (string _key) | |
{ | |
if (string.IsNullOrEmpty (_key)) { | |
throw new ArgumentException ("invalid key!!"); | |
} | |
} | |
#endregion | |
} | |
#endregion | |
#region Serialization Class | |
// List<T> | |
[Serializable] | |
private class Serialization<T> | |
{ | |
public List<T> target; | |
public List<T> ToList () | |
{ | |
return target; | |
} | |
public Serialization () | |
{ | |
} | |
public Serialization (List<T> target) | |
{ | |
this.target = target; | |
} | |
} | |
// Dictionary<TKey, TValue> | |
[Serializable] | |
private class Serialization<TKey, TValue> | |
{ | |
public List<TKey> keys; | |
public List<TValue> values; | |
private Dictionary<TKey, TValue> target; | |
public Dictionary<TKey, TValue> ToDictionary () | |
{ | |
return target; | |
} | |
public Serialization () | |
{ | |
} | |
public Serialization (Dictionary<TKey, TValue> target) | |
{ | |
this.target = target; | |
} | |
public void OnBeforeSerialize () | |
{ | |
keys = new List<TKey> (target.Keys); | |
values = new List<TValue> (target.Values); | |
} | |
public void OnAfterDeserialize () | |
{ | |
int count = Math.Min (keys.Count, values.Count); | |
target = new Dictionary<TKey, TValue> (count); | |
Enumerable.Range (0, count).ToList ().ForEach (i => target.Add (keys [i], values [i])); | |
} | |
} | |
#endregion | |
} |
解説とポイント
・簡単に説明すると、Dictionary<string, string> saveDictionaryにstring型のkeyとそれに対応するstring型のvalue(json文字列)を保存するようにしてる。これをこのままシリアライズできればいいのだけど、Dictionary型はJsonにSerializeできないので、セーブする直前にSerializableDictionaryクラスでkey, valueそれぞれのListに変換し、それをシリアライズしてる。
SaveDataの外部公開関数は以下の通り。PlayerPrefsの外部関数をある程度踏襲してるつもり。
Static関数
Load() | 指定のパスからjsonデータを読み込みます。 |
Save() | 変更された値を指定ファイルに保存します。 |
SetInt(string key, int value) | int型の値を設定します。 |
SetFloat(string key, float value) | float型の値を設定します。 |
SetString(string key, string value) | string型の値を設定します。 |
SetClass<T>(string key, T value) | T型の値を設定します。 |
SetList<T>(string key, List<T> value) | List<T>型の値を設定します。 |
GetInt(string key, int default) | keyで指定されたint型の値を取得します。 |
GetFloat(string key, float default) | keyで指定されたint型の値を取得します。 |
GetString(string key, string default) | keyで指定されたstring型の値を取得します。 |
GetClass<T>(string key, T default) | keyで指定されたT型の値を取得します。 |
GetList<T>(string key, List<T> default) | keyで指定されたList<T>型の値を取得します。 |
Remove(string key) | keyで指定された値を削除します。 |
Clear() | 保存データをすべて削除します。 |
・PlayerPrefsはSetするたびにファイルアクセスするらしい?ので、処理が重いという話を聞きますが、この方法なら、SaveData.Save()を実行したときに初めてjson形式に保存するので、Setしてもそんなに重い処理ではない。(Dictionaryに追加するくらいの処理のみ)
・注意として、SaveClass.Get〇〇系の関数は呼び出しの度にjson文字列をデシリアライズするので、処理が重いかもしれない。シーン遷移時や、Start, Awake関数内で実行しておけばそんなに気にならないと思う。
・Get〇〇したときにkeyが存在しないとき、今はnullが返ってくるようにしてるけど、PlayerPrefsだと第2引数にデフォルト値を設定できるようになってるのね。確かにこの実装の方が汎用性高そう。まだ改善の余地ありかな。
・第2引数にデフォルト値を渡せるようにしました。
・PlayerPrefsだと中身がブラックボックスで拡張できない、かつ外部に公開されたメソッドも少ないので、物足りない感じがしていました。
上記スクリプトであれば「登録されたkey一覧を取得する」「value一覧を取得する」「キーが存在するかを確認する」etcなどなど機能拡張も可能です。
使い方
今回もテスト用スクリプト用意しました。
using UnityEngine; | |
using System.Collections; | |
using System.Collections.Generic; | |
public class Test : MonoBehaviour { | |
[SerializeField] | |
public class Player{ | |
[SerializeField] | |
public int hp; | |
public float atk; | |
public string name; | |
public List<string> items; | |
public Player(){ | |
items = new List<string>(); | |
items.Add("ポーション"); | |
items.Add("エーテル"); | |
items.Add("エリクサー"); | |
hp=10; | |
atk = 100f; | |
name = "クラウド"; | |
} | |
} | |
// Use this for initialization | |
void Start () { | |
//セーブデータの設定 | |
SaveData.SetInt ("i", 10); | |
SaveData.SetClass<Player> ("p1", new Player ()); | |
SaveData.Save (); | |
Player getPlayer = SaveData.GetClass<Player> ("p1", new Player ()); | |
Debug.Log ("取得したint値は" + SaveData.GetInt ("i")); | |
Debug.Log (getPlayer.name); | |
Debug.Log (getPlayer.items.Count + "こアイテムを持ってます"); | |
Debug.Log (getPlayer.items[0] + getPlayer.items[1] + getPlayer.items[2] ); | |
} | |
// Update is called once per frame | |
void Update () { | |
} | |
} |
使い方とポイント
・基本的にPlayerPrefsみたいな使い方でオッケーです。拡張機能としては、独自クラスをまるごと保存できることです。
※保存したいフィールドにはpublic, [Serializable Field]をつけるようにしてください。
・独自クラスを保存するときは
SaveData.SetClass<独自クラス> (クラスインスタンス);
・独自クラスを取得するときは
独自クラス インスタンス名 = SaveData.GetClass<独自クラス>(キー);
・ちなみにSaveData.SetClass<List<string>>(List型のオブジェクト)でList型もjsonとして保存できましたよ。
・List<T>型は正常に保存されなかったので、新たにGetList<T>, SetList<T>メソッドを作りました。
無事クラウドさんの持ってるアイテムが復活できました。独自クラス内部にList型があっても問題なく保存できてます。めでたしめでたし。
注意
・シリアライズしたい独自クラスには[SerializeField]を忘れずに書いておいてください。
・Load()とSave()は必要なタイミングで実施してください。あと、Scene遷移時の挙動をデバッグできてませんが、シーン読み込み時には忘れずにLoad()、シーン遷移直前にはSave()を実行しておいてください。
※デストラクタでセーブするようにしたので、ゲーム終了時には必ずセーブされます。
Unityではデストラクタが必ず呼ばれると保証されていないとの話も聞きましたので、ローディング、シーンの切り替え時などに定期的に自分でSave()を呼んだほうがいいと思います。
あとがき
- 前回のセーブ機能より確実に使い勝手がよくなったと思います。PlayerPrefsの機能拡張版みたいな感じで使えるのではないでしょうか。
- jsonで保存してますが、xmlでも、csvでもちゃんと管理できればどんな保存方法でもいいんですよねー。
- あと、Mobileだとjsonではなくもっと高速なFlatBuffersってのがあるらしいです。下記参考記事。ちょっと導入ハードルが高そうでしたが、大変勉強になりました。
Unityで永続化としてFlatBuffersを使用する (モバイル編)
コメント
元々jsonのファイルが存在しない状態で、出力されたjsonファイルは外部ファイルのどこに保存されるかっていうのはどのように指定します?
初心者の質問で大変申し訳ありません・・・。
by 通りすがり 2016年5月29日 4:07 PM
コメントありがとうございます!お返事が遅れ申し訳ありません!
スクリプトのどこからでも指定できます。
SaveData.Path = “保存したいフォルダのパス”;
SaveData.FilePath = SaveData.Path + /”ファイル名.json”;
で指定できるはずです!
by Magnagames 2016年6月19日 12:08 PM
Savedata.cs と書かれている場所にソースコードが本来あると思うのですが、
自分のブラウザでは何も表示されません…
ブラウザはChrome,IE,Firefox全て見られませんでした。
何か解決策はありますか?
使う以前の問題で申し訳ありません。
by Noname 2016年7月25日 11:28 AM
コメントいただき、ありがとうございます。
https://gist.github.com/Magnagames/96cf672868a48cac1412a4fe9bd63ef6
上記URLにアクセスできますでしょうか。ご確認をお願いいたします。
by Magnagames 2016年7月25日 11:10 PM
SaveData.csのみでセーブできること確認したのですが、SerializableDictionary.csはどのあたりで使っているのでしょうか
また、この記事中で言うSerializableDictionary.csはどれの事を指しているのでしょうか
手元で特にSerializableDictionary.csが無くても動いてしまったのできになりました
by つくね 2016年8月9日 10:57 AM
返信が遅れ、申し訳ございません。
コメントいただき、ありがとうございます。
SerializableDictionary.csは7/5にソースコードを編集・修正した段階で不要となりました。厳密にはSerializationというクラスに書き換え、
Savedata.csのサブクラスにしています。
Savedata.csのみコピーいただければ動作するかと思います。
ご指摘ありがとうございました。
by Magnagames 2016年8月17日 8:21 AM
説明通りに3つのスクリプトを入れたところ、test.csでエラーが発生しました。
error CS0117: `SaveData’ does not contain a definition for `○○’
○○の部分は、Load、SetData、GetData、の3つです。
上記の3つを含む行を消せば再生可能です。 SetInt、GetInt、は機能します。
それともう1つ必ずエラーがでて(こちらは出てても再生可能)
NullReferenceException: Object reference not set to an instance of an object
UnityEngine.UI.Graphic.OnRebuildRequested () (at /Users/builduser/buildslave/unity/build/Extensions/guisystem/UnityEngine.UI/UI/Core/Graphic.cs:480)
UnityEngine.UI.GraphicRebuildTracker.OnRebuildRequested () (at /Users/builduser/buildslave/unity/build/Extensions/guisystem/UnityEngine.UI/UI/Core/GraphicRebuildTracker.cs:33)
UnityEngine.CanvasRenderer.RequestRefresh ()
とエラーが出ますが関係あるのでしょうか?
紹介された機能を実装したくunity5.0から5.3.5にアップデートしたのが問題だったんでしょうか?
何卒宜しくお願いします。
by beginner 2016年8月14日 3:33 PM
コメントいただき、ありがとうございます。
7/5時点で一部ソースコードを改変したため、Load、SetData、GetDataのメソッドは以下の理由によりエラーが発生したものと思われます。
・Load 以前はpublicメソッドとしていましたが、現在は外から直接アクセスできないようprivateで使用しています。Savedataクラスに最初にアクセスした段階で
必ずLoadが呼ばれるようになっているので、この行は削除いただいて問題ありません。
・SetData, GetDataはメソッド名をSetClass, GetClassに書き換えています。名前を変えただけですので、DataをClassに書き換えていただければ、
動作するかと思います。test.csを書きかえますので、もう一度お試しください。
・NullReferenceExceptionにつきましては、エラー内容からは本コードと直接関係ないように思えます。。
もしかすると、Savedataを使用するタイミング(Awake, Startで呼出している順序など)に問題があり、null referenceが発生しているのかもしれません。
by Magnagames 2016年8月17日 8:35 AM
おかげさまでセーブ関係は上手く実装できました。とても分かりやすく使いやすいです。
そして最後の関門、暗号化で躓きました。
「セーブ 暗号化」の検索でこちらの前記事にたどり着き、より便利というこちらを利用している入門者です。
暗号化は前回の記事を参考に、とあり初心者ながら噛み砕いて熟考してはみたもののどうにも正解が分かりません。
前回の記事のCript.csをインポートして
SaveData.csのSaveメソッドに
string crypted = Crypt.Encrypt(dictJsonString);//追加
writer.WriteLine (crypted); //変更
で保存ファイル上は暗号化されていました。が本当にこれだけで大丈夫でしょうか?
前記事SavableSingleton.cs内の //復号化の際に〜 や、//フォルダーがない場合は〜などは手付かずです。
さらにLoadメソッドの復号化化などはさらに自信がありません。
(暗号化のみ実装してtest.csを回すとデバッグログ上は普通に表示されて、暗号化方法が正しいのか、復号化してないのにどうしてと疑問ばかりです。)
大変面倒事かと思いますがSaveData.csでの暗号化の正解を教えていただければ幸いです。本当に申し訳ありません。
by beginner 2016年8月20日 1:37 AM
コメントありがとうございます。
セーブ関係、うまく実装できたとのこと、良かったです。
暗号化は後日記事にまとめて掲載しますので、お待ちくださいませ。
by Magnagames 2016年8月23日 8:34 AM
初歩的な質問で申し訳ないのですが、このままではbool型はセーブできないんでしょうか?
by kindo 2016年10月18日 10:50 AM
ご連絡が大変遅れ、申し訳ございません。bool型はint型で0,1を保存すれば対応できるかと思います。
もともとboolはtrue= 1, false =0 を置き換えているようなものなので。
bool.ToIntなどを使用すれば、良い実装になるかと思います。
by Magnagames 2016年11月28日 8:06 PM
はじめまして。
セーブ機能を探していてこちらのサイトに辿りつきました。
便利に使わせていただいております。
List型なのですが、たとえば
public List testList;
としたstring型のリストを
SaveData.SetList(“test”,testList);
でセーブしようとすると、以下のようなエラーが出ます。
The type `string’ must be a reference type in order to use it as type parameter `T’ in the generic type or method
T型には参照型を使いなさいと書かれているのではないかと思うのですが、string型は参照型のような気がします。
解決方法等わかりましたら、ご回答いただけると幸いです。
by nekocat 2016年12月1日 8:06 PM
コメントいただき、ありがとうございます。
当方環境では同様のエラーを再現できませんでしたが、以下のコードで
ジェネリック制約(where)をつけていたのが原因かもしれません。
制約の意味は以下の通りなので、SetListには必要なさそうですね。
【Tはclassで、かつ、引数なしコンストラクタを呼び出し、T型のインスタンスを生成できる】
サイトに掲載しているコードは書き換えましたので、コピーいただければと思います。
(SetList抜粋) (string key, List list) where T: class, new() (string key, List list)
public static void SetList
↓以下の通り変更
public static void SetList
貴環境で動作するかご確認いただき、ご連絡いただけると幸いです。
by Magnagames 2016年12月2日 3:26 AM
ご返答ありがとうございます。
返信が遅くなり申し訳ありません。
変更されたコードにしたところ、無事に動作いたしました。
ご自身で再現できない状態でありながら、的確な解決方法を提示いただき感謝いたします。
ありがとうございいました。
by nekocat 2016年12月3日 11:50 AM
はじめまして。
記事を拝見しぜひ使いたいと試していますが、何点かわからない点がありました。
当方プログラム・Unityを始めてまだ間もなく基本的な部分で申し訳ありませんが、ご教授頂けませんでしょうか。
1.jsonファイルのパス設定していない場合の挙動につきまして
当方で試したところ、jsonファイルのパス設定をしなくても、データがセーブ出来ました(int型の変数にて)。
これはファイルの指定がない場合は、SaveData.cs側でパスの指定、jsonファイル生成を行っているという理解で宜しいでしょうか。
またこの場合、jsonファイルはどちらに生成されるのでしょうか。(PCで検索かけましたが見つけられませんでした)
2.jsonファイルのパス指定方法について
2016年6月19日のコメントを拝見したところ、下記記載されており、当方でSaveData.cs内で指定しようとしましたが、うまくいきませんでした。SaveData.cs内でどのように指定すれば宜しいでしょうか。あるいはSaveData.csとは別のクラスで指定するのでしょうか。また指定について、例えばSetJsonFilePath()といったメソッドとして設定するのでしょうか。
>スクリプトのどこからでも指定できます。
>
>SaveData.Path = “保存したいフォルダのパス”;
>SaveData.FilePath = SaveData.Path + /”ファイル名.json”;
>
>で指定できるはずです!
お手数お掛けしますが、よろしくお願いいたします!
by yasuyasu 2016年12月22日 4:17 PM
返信が遅くなり、申し訳ありません。
現状では外部から容易に保存先を変更できないよう、コードの中に埋め込んだ設計となっています。
パスの指定はコード内の24,25行目です。
string path = Application.persistentDataPath + “/”;
string fileName = Application.companyName + “.” + Application.productName + “.savedata.json”;
サイトの通りだと、上記パスで指定した場所に保存されます。
保存先を変更したい場合は、保存先のパスを直接変更いただく方が良いかと思います。
なお、上記のApplication.persistentDataPathはOSごとにファイルの保存先が異なりますので、
ご注意ください。Windows、iOS、Androidそれぞれで保存先が異なります。
このあたりのサイト様をご参考ください。
http://qiita.com/bokkuri_orz/items/c37b2fd543458a189d4d
ご参考いただければ幸いです。
by Magnagames 2016年12月26日 11:55 PM
とても分かりやすく解説頂き、感謝申し上げます。
お忙しところコメント頂きありがとうございました!
by yasuyasu 2016年12月29日 12:12 PM
最初にGetFloat()するとKeyNotFoundExceptionが発生しないでしょうか?
343行目辺りでデフォルト値を返さずに処理が進んで,345行目辺りで問題が発生しているように見受けられました。
by mmm 2017年7月7日 7:09 AM
はじめまして。
最近unityを触りはじめ、データをセーブしたいと思いこのサイトに辿りつきました。
一つ質問なのですが独自クラス型のリストを保存する事は出来ますでしょうか?
何度やっても上手く行かずでこんがらがっております。
お時間のあるときにで構いませんのでご教授いただければと思います。
by yo 2017年9月30日 12:15 AM
お世話になっております。
スクリプトをありがたく使わせて頂いております。
使っていて気になったのですが、GetFloatを使う際あらかじめSetFloatで値をいれておかないとエラーになります。
スクリプトを見ていて気になったのですが、343行目は[ret = _default;]ではなく[return _default;]ではないでしょうか。
by UesugiApp 2017年12月6日 1:24 PM
Savedata.csを使わせていただいております。大変便利で助かっております。
ただ、不明な点がありますので、質問いたします。
Load関数内に、下記の部分があります。
if (File.Exists (path + fileName)) {
using (StreamReader sr = new StreamReader (path + fileName, Encoding.GetEncoding (“utf-8”))) {
if (saveDictionary != null) {
var sDict = JsonUtility.FromJson<Serialization> (sr.ReadToEnd());
sDict.OnAfterDeserialize ();
ここで、Unityのエディタを起動して初回の実行時のみ、sDictがNullになってしまい、sDict.OnAfterDeserialize () の箇所でエラーが発生してしまうことがあります。
Debug.Log(sr);
Debug.Log(saveDictionary);
Debug.Log(sDict);
これを挟んで確認したところ、正常作動時は、
sr = System.IO.StreamReader
saveDictionary = System.Collections.Generic.Dictionary`2[System.String,System.String]
sDict = SaveData+Serialization`2[System.String,System.String]
となりましたが、エラー時のみ、
sDict = Null
になってしまいます。
実行を停止して再度実行すると、Unityを再度起動するまでは同様のエラーが発生しなくなります。
対策として、sDict.OnAfterDeserialize()を実行する前にsDictがNullでないかどうかを判定し、
Nullである場合はエラーメッセージを表示して読み込みを中断するようにしました。
また、その場合に空のセーブデータで上書きしてしまうことを防ぐため、デストラクタをコメントアウトしました。
しかし根本解決には至っていない状態です。
大変不可解なエラーですが、もし原因に心当たりがございましたらご助言をお願いします。
by 坂本龍 2018年2月12日 10:12 PM
こんにちは、素敵なコードだと思います。
SetClassのコメントが間違っていそうです。
SetXXXの引数overloadでdefault指定となしで用意されていると尚いいなと思いました:)
by m 2018年3月6日 8:29 AM
はじめまして、様々なサイトで調べたりしていたのですが実力不足でカバーしきれずメールした次第です。
端的に言いますと、こちらのアドレスのセーブデータクラスを使ってandroidアプリを制作しておりましたところ、
Editarでは正常にセーブロードが行われたのですが、
android実機ではセーブやロードが正常に働いていることを確認してから、
アプリを終了して再びアプリを開くとデータが初期状態で起動します。
このセーブデータクラスがとても使いやすいので、多少のアレンジでandroidでのアプリ終了時開始時の挙動を治したいのですが、どうしたらよろしいのでしょうか?
ちなみにunityのバージョンは2017 1.1f.1で試したandroid実機は4.4.1です
by perika 2018年5月9日 7:48 PM