【Unity】Message Pack for C# v2でセーブ機能を実装する

どうも、ゆみねこです。

これは小学生の頃からの悩みなんですが、Unityでゲームを作るにあたり、セーブ機能の実装は初心者にとってあまりに鬼門です。

PlayerPrefsはゴミすぎて使い物にならず、JsonUtilityは制約がありすぎと、Unity標準で用意されたものはどちらも限定的です。

そこで今回は、セーブ機能の実装で苦労している人向けに、個人開発のオフラインゲームであればだいたい通用するであろう実装の仕方を解説します。

Message Pack for C# v2

セーブ機能の実装にあたり、「Message Pack for C#」を使います。

Message Packとは

データを扱いやすくまとめた、ファイル形式のひとつです。
UnityやC#に限ったものではなく、様々な言語で使われている一般的なフォーマットです。

https://msgpack.org/ja.html

Unity使いには馴染みがないかもしれませんね。
Jsonはともかく、Message Packは存在すら知りませんでした。

この形式でセーブデータを作成してファイルとして保存・読み込みをしようというのが、今回やろうとしていることです。

Jsonでやる方法は過去に記事にまとめたので、そちらが気になる場合は併せてご覧ください。

なぜMessage Packを使うのか

ゆみねこ(管理人)
ゆみねこ(管理人)

「Message Pack? なにそれ。Jsonでよくない?」

そう思っていた時期が、ぼくにもありました。
結論から言うと、別にJsonでもいいです。
ただ、Message PackはJsonより容量が少なく、高速です。


C#でMessage Packを扱うためのライブラリとして「Message Pack for C#」というものを使います。

これが、元々はモバイル向けMMOの通信用として作られていただけあってめちゃくちゃ最適化されており、速度がべらぼうに速いです。

どれぐらい速いかというと、1000個のオブジェクトを10000回 Message Packにシリアライズしても、90msしかかからないぐらいには速いです。

https://github.com/neuecc/MessagePack-CSharp
公式による速度の比較。一般的なJsonライブラリであるJson.NETよりかなり速いことが分かります。

ゲーム開発において、速度はかなり重要です。

実際は一度に10000回もセーブなんかしたらHDD(SSD)が悲鳴を上げるのでそんな状況は起こりえませんが、それぐらい最適化されているので、安心して使えるというわけです。

最近になってバージョンが2に更新され、Visual StudioやASP.NETで使われるぐらいには最強のライブラリと化しました。

今後、間違いなくC#シリアライザとしてメジャーな存在になると思われますので、今のうちに使い方を勉強しておきましょう!

Message Pack for C#を導入する

Message Pack for C#の魅力がわかったところで、さっそく導入です。

公式のReadMeが丁寧なのでそちらを読んでね、という記事が多いですが、ぼくのように外国語になると途端に幼児退行化してしまう方もいらっしゃるので、ここで解説していきます。

必要なファイルを入手する

Releases · neuecc/MessagePack-CSharp

GitHubのReleaseページへアクセスし、unitypackageファイルをダウンロードします。
また、IL2CPPやWebGLで動かす場合には事前コード生成が必要なため、mpc.zipもダウンロードします。

Message Pack for C#をインポートする

ダウンロードしたら、unitypackageファイルをUnityのProjectビューに放り込んでImportします。

特に難しい操作は必要ありません。いつものようにアセットを入れるだけです。

コード生成ツールを導入する

先ほどmpc.zipをダウンロードした方は、zipファイルを展開し、Assetsフォルダと同じ階層に「GeneratorTools」などの適当な名前をつけ、展開されたフォルダをそのまま突っ込みましょう。
(別にどこに作ってもいいですが、解説の都合上分かりやすくしています)

以上で、導入は完了です。

スポンサードサーチ

使ってみる

基本的な流れ

さっそく使ってみましょう。Message Pack for C#の使い方は以下のとおりです。

  • シリアライズしたいclassに [MessagePackObject] 属性をつける
  • シリアライズしたいフィールドに [Key] 属性をつけ、番号をふる
  • MessagePackSerializer.Serialize() でシリアライズする
  • MessagePackSerializer.Desirialize() で、デシリアライズする

順に見ていきましょう

MessagePackObject属性と、Key属性

シリアライズしたいclassやstructに [MessagePackObject] アトリビュートをつけると、そのオブジェクトがシリアライズ対象(今回の場合はセーブ対象)になります。

また、MessagePackObject属性に指定したオブジェクトのフィールド(変数)に [Key] 属性で番号を振っていくことで、そのフィールドがシリアライズ対象になります。

例を示します。

当然ですが、using MessagePackを忘れないようにしましょう。

Serializable属性をつけているのは、UnityのInspectorビューで表示されるようにするためで、必須ではありません。
Keyの番号は、同じclass内で重複してはいけません(多分)

フィールドが全てpublic変数なのも同様で、プロパティにした場合でも問題なく動作します。privateの場合は別途設定が必要になります。詳細は公式のRead Meに書いてありますが、よくわからないようならpublicにしておくのが無難です。

参考:Object Serialization – Mesage Pack for C#

シリアライズ・デシリアライズする

シリアライズ・デシリアライズは超簡単です。

先ほどのPlayerDataクラスのシリアライズ・デシリアライズをするSampleです。MessagePackSerializerのSerialize / Desirializeメソッドを使うだけです。簡単ですね。

ちなみに、byte配列で受け取るとメモリアロケーションが発生し、速度性の観点から見るとコストになります(参考:開発者さんのブログ記事)。
Message Pack for C#にはそれを抑えるための仕組みがあるようですが、まだ使い方が分かっていません。ごめんなさい。分かったら記事にします。

ファイルへ操作

Message Packに限らないC#全般のファイル操作の話ですが、苦手な方もいると思うので解説します。

ファイルへ保存する

Message Packはバイナリファイルなので、バイナリ用のメソッドを使います。
幸い、C#にはFile.WriteAllBytesという、非常に使いやすいメソッドが用意されています。

ファイルを読み込む

こちらも書き込みと同様に、File.ReadAllBytesというメソッドが用意されています。

サンプル

以上をふまえたサンプルがこちら。

Read / WriteAllBytesは、例外が発生しようと必ずファイルを閉じてくれますが、例外そのものは投げるのでtry / catchで囲ってあげましょう。

Jsonのときとなにも変わりませんね。むしろusing節すら不要な分、こちらのほうが簡単です。

事前コード生成をする

ゆみねこ(管理人)
ゆみねこ(管理人)

これにて解決 めでたしめでたし!
……そうは問屋がおろさない。

これにて一件落着、といけばいいのですが、残念ながらほとんどのケースでそうはいきません。

UnityにはIL2CPPという概念があるのは、ご存知の方も多いと思います。
(詳しくは説明しないので、わからない方はググってください)。

2020年現在、ほとんどの場合においてMonoではなくIL2CPPを選ぶでしょう。また、Unity1週間GameJamの盛況もあり、unityroomにWebGLでビルドしたい人も多いはずです。

冒頭でもお伝えしましたが、IL2CPPやWeb GL環境下で動かすには、事前に専用のコードを生成しなければなりません。

ここでつまずく人が非常に多く、事実ぼくも何度もくじけてやっと乗り越えてきたので、解説していきます。

パラメータを設定する

当該記事のとおりに導入していれば、GeneratorToolsフォルダに各OSごとの生成ツールが入っているはずです。Windowsならmpc.exeですね。

これを呼び出してコードを生成します。
呼び出す際に、コマンドライン上からパラメータを指定してあげる必要があります。

うげぇ~コマンドライン……と思ったあなた。ご安心を。ちゃんと公式でEditor拡張が用意されています。

ゆみねこ(管理人)
ゆみねこ(管理人)

と思ったけど、使い方わかんねえ。

仕方がないので、以下の記事を参考にEditor拡張を用意しました。

UnityのマスタデータにMasterMemoryを使ってみる

上記のScriptを、Editorフォルダの中に入れてください。
Tools -> CodeGenerator Lite でウィンドウが開きます。

入力欄に、生成コードをどこに出力するかを選択し、「生成」ボタンを押すことでコードが生成されます。

もしConsoleにエラーが出る場合は、mpc.exeがこの記事のとおりに配置されているかを今一度確認してください。

リゾルバーを登録する

コードは生成しただけではダメで、リゾルバーと呼ばれるものに登録してやる必要があります。

適当なクラスのメソッドに RuntimeInitializeOnLoadMethod 属性をつけ、ゲーム開始時にリゾルバーが登録されるように設定しましょう。

以下に例を示します。ほぼ、公式のReadMeそのままです。

RuntimeInitializeOnLoad属性をつけることで、ゲーム開始時、Awakeメソッドより前の段階で指定したメソッドが呼ばれるようになります。詳しくは以下の記事をどうぞ。

まとめ

お疲れ様でした。これで無事、セーブ機能の基本が実装できました。

あとは各自、static classなりSingletonMonoBehaviourなりで、セーブ・ロード機能を提供するクラスを実装しましょう(今回の記事では省略します)。

ゆみねこ(管理人)
ゆみねこ(管理人)

挫折をくり返して辿り着いたものなので、もしかしたら間違っているかもしれません。その際は、Twitterで教えていただければ幸いです。

補足

Message Pack for C#では、interfaceもシリアライズできます。以下の記事に詳しく書かれています。

これにより、ISaveData などのinterface(空でOK)を作れば、SaveManagerを作る際の受け渡しが非常に簡単になります。

それらを使ったScript群をGitHubに上げましたので、時間のある方は参考までにご活用ください。主にRPGの利用を想定した、Scene間をまたぐデータの保持機能を提供します。

SaveManager

記事をシェアする