2016/06/21
概要
前回セーブ関連の記事を書きましたが、一定数の関心がある技術領域のようで、記事を公開して数年がたった今でもちょくちょく訪問いただいています。
前回記事で紹介したセーブクラスは使いづらい部分もあり、個人的には納得がいってませんでした。
今回はUnity標準のPlayerPrefsクラスを参考に、同じような使い勝手で使用できるコードを意識して実装しました。
またUnity 5.3でJson関連がUnity標準機能になったので、前回まで使用していたLitJsonはお役御免としました。
前回の問題点、今回の改良点は以下のとおりです。早く機能実装したい方は読み飛ばしてください。
問題点
・LitJson.dllが必要。→dllインポートが手間かかる。以下のとおりビルドをミスる可能性ありとのこと(コメントありがとうございました!)
・セーブしたい変数を増やすたびにGameDataクラスに追記しないといけないのが面倒でクラスも肥大化していくので使いづらい。
・GameObjectに貼り付けて使用しないといけない。→面倒
改善点
・Unity標準のJsonUtilityを使用するようにしました。
・PlayerPrefsのkey, valueのような使い方を意識して作成しました。
・PlayerPrefsではint, float, stringしかセーブできませんが、Listや自作classも簡単にセーブできるようにしました。PlayerPrefsの拡張版みたいな感じです。
・GameObjectに貼り付けなくても、スクリプト上のどこからでも使用できます。これもPlayerPrefsを意識してコーディングしました。
ソースコード
必要なcsファイルはSaveData.csのみです。暗号化が必要であれば、前回の記事を参考にしてください。jsonにしたstringをcriptに流し込んで暗号化するだけです。
セーブ機能の実装方法まとめ 使い勝手の良いセーブを実装する【Unity開発】
Savedata.cs
解説とポイント
・Dictionary<string, string> saveDictionaryにstring型のkeyとそれに対応するstring型のvalue(json文字列)を保存するようにしています。
Dictionary型はJsonにSerializeできないので、セーブする直前にSerializableDictionaryクラスでkey, valueそれぞれのListに変換し、それをシリアライズしています。
SaveDataのメソッドは下表のとおりです。PlayerPrefsのメソッドを踏襲しています。
メソッド一覧
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() | 保存データをすべて削除します。 |
Save()を呼び出したときのみファイルアクセスするようにしています。
注意として、SaveClass.Get〇〇系の関数は呼び出しの度にjson文字列をデシリアライズするコストがかかります。
どれだけのコストがかかるのかまでは検証できていません。
シーン遷移時やStart, Awake関数内で実行しておけばそんなに気にならないと思います。
Unity標準のPlayerPrefsは中身がブラックボックスで拡張できない、かつ外部に公開されたメソッドも少ないですが、このコードを使用すれば独自に拡張も進められると思います。
上記スクリプトであれば「登録されたkey一覧を取得する」「value一覧を取得する」「キーが存在するかを確認する」etcなどなど機能拡張も可能です。
使い方
今回もテスト用スクリプト用意しました。
使い方とポイント
基本的にPlayerPrefsを使う感覚で使用してください。目玉機能としては、独自クラスをまるごと保存できることです。
注意点として、保存したいクラス(シリアライズしたいクラス)には[System.Serializable]の属性を書き加えてください。
・独自クラスを保存するときは
SaveData.SetClass<独自クラス> (クラスインスタンス);
・独自クラスを取得するときは
独自クラス インスタンス名 = SaveData.GetClass<独自クラス>(キー);
・List<T>型は正常に保存されなかったので、新たにGetList<T>, SetList<T>メソッドを作りました。
無事クラウドさんの持ってるアイテムが復活できました。独自クラス内部にList型があっても問題なく保存できてます。めでたしめでたし。
注意
シリアライズしたい独自クラスには[System.Serializable]を忘れずに書いておいてください。
あとがき
- 前回のセーブ機能より確実に使い勝手がよくなったと思います。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
はじめまして、素晴らしいコードをありがとうございます。
非常に便利に使わせていただいてるのですが、一つエラー?が起こっていたのでお伝えします。
他の方もおっしゃっていましたが、Unityエディタ初回起動時のみ
Load関数内のsDictがNullになってしまう問題がありました。
いろいろ確認した結果、SaveDataBaseのデストラクタでSave()を呼ぶことで(理由はわかりませんが)jsonファイルの中身がUnityエディタ終了時に初期化され空になってしまうことが原因であるようです。
試しにSaveDataBaseのデストラクタ内のSave()をコメントアウトしたところ、問題なくファイルに書き込まれ、Unityエディタ終了後も内容が維持できるようになりました。
by H 2019年7月29日 2:38 PM