のっぴきならない事情によりUnity5で独自データ保存したくなった。
json形式でもいいかと思ったが速度&サイズを考えてMsgPackでバイナリ保存してみる。
Unityバージョンは5.3.5f1以上を想定。
作成プログラム概略
- マウス左右ボタン押下でマウスカーソル位置にGameObject生成
- マウス中ボタンで生成したGameObject全削除
- プログラム終了時に状態保存し、次回起動時にその状態を復元する
1.Unity新規プロジェクト作成
適当にUnityDataSaveTestなど名前つけて新規作成する。
一応出来上がったプロジェクトは**ここ**に置いておいた。
2.「.NET 2.0」使うように設定
- Unityデフォルトだと**「.NET 2.0 subset」**使う設定になっているがこれだとMsgPack使用時エラー出るので変更する必要がある。
- メニューの**「Edit」⇒「Project Settings」⇒「Player」を選択すると画面右側「Inspector」内が切り替わるのでそこの「Api Compatibility Level」で「.NET 2.0」**に変更する。
3.Pluginsフォルダ作成して外部DLL受け入れ準備
**「Assets」フォルダ直下に「Plugins」**フォルダを作成する。
Unityはこの中に入れたDLLを自動的に参照追加するらしい。
4.MsgPackダウンロードしてPluginsフォルダにコピー
-
https://github.com/msgpack/msgpack-cli/releases/
から現時点での安定バージョン**「MsgPack.Cli.0.6.8.zip」**をダウンロード。 -
解凍したフォルダ内の**「unity3d-full/MsgPack.dll」をさっき作った「Plugins」**フォルダへコピーする。
これでとりあえずMsgPack使う準備が整った。
5.基本的なMsgPack使い方について
サンプルを見ると以下の様な感じで使うらしい。
まずクラス定義。
public class PhotoEntry {
public long Id { get; set; }
public string Title { get; set; }
public DateTime Date { get; set; }
public string Comment { get; set; }
public byte[] Image { get; set; }
private readonly List _tags = new List();
public IList Tags { get { return this._tags; } }
}
そして、シリアライズ&デシリアライズ。
>```csharp
// They are object for just description.
var targetObject =
new PhotoEntry {
Id = 123,
Title = "My photo",
Date = DateTime.Now,
Image = new byte[] { 1, 2, 3, 4 },
Comment = "This is test object to be serialize/deserialize using MsgPack."
};
targetObject.Tags.Add("Sample");
targetObject.Tags.Add("Excellent");
var stream = new MemoryStream();
// 1. Create serializer instance.
var serializer = MessagePackSerializer.Get<PhotoEntry>();
// 2. Serialize object to the specified stream.
serializer.Pack(stream, targetObject);
// Set position to head of the stream to demonstrate deserialization.
stream.Position = 0;
// 3. Deserialize object from the specified stream.
// 1. Create serializer instance.
var serializer = MessagePackSerializer.Get<PhotoEntry>();
// 2. Serialize object to the specified stream.
serializer.Pack(stream, targetObject);
// Set position to head of the stream to demonstrate deserialization.
stream.Position = 0;
// 3. Deserialize object from the specified stream.
var deserializedObject = serializer.Unpack(stream);
上記の様にコンパイル時にわかってる型ならとても簡単。
6.MsgPackでの派生クラスの扱いについて
以下の様に派生させたクラスを扱う場合Data.ValuesにはBまたはCのオブジェクトを入れることができる。
が、実際にシリアライズされたのはA.ValueAだけだった。
どうやら直接指定したクラスのメンバのみシリアライズされる様だ。
public class A {
public int ValueA;
}
public class B : A {
public int ValueB;
}
public class C : A {
public int ValueC;
}
public class Data {
public List<A> Values;
}
7.派生クラスのシリアライズを自分でなんとかする
- 以下の様に基本クラス(BaseClass)作成し、派生先の自作クラスID一覧とそれに対応する派生クラスを作成する。
/// <summary>
/// MessagePackでシリアライズするクラスID一覧
/// </summary>
public enum ClassId {
Unknown,
/// <summary>
/// ひよこデータクラスID
/// </summary>
HiyokoData,
}
/// <summary>
/// MessagePackでシリアライズする基本クラス
/// </summary>
public abstract class BaseClass {
/// <summary>
/// フィールド数、派生先クラスで上書きする
/// </summary>
protected const int FieldCount = 1;
/// <summary>
/// クラスID
/// </summary>
public ClassId ClassId;
/// <summary>
/// デフォルトコンストラクタ、MessagePackがこいつを必要としている
/// </summary>
BaseClass() {
}
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="id">派生先クラスID</param>
public BaseClass(ClassId id) {
this.ClassId = id;
}
/// <summary>
/// パッキング
/// </summary>
/// <param name="packer">パッキング先</param>
public abstract void PackToCore(Packer packer);
/// <summary>
/// アンパッキング
/// </summary>
/// <param name="unpacker">アンパッキング元</param>
public abstract void UnpackFromCore(Unpacker unpacker);
/// <summary>
/// シリアライズする合計フィールド数の取得
/// </summary>
public abstract int GetFieldCount();
}
/// <summary>
/// ひよこ保存用データ
/// </summary>
public class HiyokoData : BaseClass {
protected new const int FieldCount = BaseClass.FieldCount + 4; // 合計フィールド数をオーバーロード
public int Kind;
public float X, Y, Angle;
public HiyokoData() : base(ClassId.HiyokoData) {
}
public override void PackToCore(Packer packer) {
packer.Pack(this.Kind);
packer.Pack(this.X);
packer.Pack(this.Y);
packer.Pack(this.Angle);
}
public override void UnpackFromCore(Unpacker unpacker) {
unpacker.ReadInt32(out this.Kind);
unpacker.ReadSingle(out this.X);
unpacker.ReadSingle(out this.Y);
unpacker.ReadSingle(out this.Angle);
}
public override int GetFieldCount() {
return FieldCount;
}
}
- そして以下の様にBaseClass用の処理をオーバーライドさせるシリアライザクラスも作成。
- やってることはMsgPack配列の先頭要素をクラスIDとして扱い、IDに対応したオブジェクト生成してる。
- 使う時はこのクラス内のメソッドClassSerializer.Get()でやってる様に登録してcontextに渡してMsgPackシリアライザ取得。
/// <summary>
/// 独自クラス処理オーバーライドさせるシリアライザ
/// </summary>
public class ClassSerializer : MessagePackSerializer<BaseClass> {
static Dictionary<ClassId, Type> _Types; // シリアライズ可能型一覧
/// <summary>
/// 静的コンストラクタでシリアライズ可能な型一覧を初期化する
/// </summary>
static ClassSerializer() {
_Types = new Dictionary<ClassId, Type>();
_Types[ClassId.HiyokoData] = typeof(HiyokoData);
}
/// <summary>
/// コンストラクタ
/// </summary>
public ClassSerializer(SerializationContext ownerContext)
: base(ownerContext) {
}
/// <summary>
/// 独自クラス処理オーバーライドされたシリアライザを取得する
/// </summary>
/// <typeparam name="T">ルートクラスデータ型</typeparam>
/// <returns>シリアライザ</returns>
public static MessagePackSerializer<T> Get<T>() {
var context = new SerializationContext();
context.Serializers.RegisterOverride(new ClassSerializer(context));
return MessagePackSerializer.Get<T>(context);
}
/// <summary>
/// パッキング処理をオーバーライド、自作クラスをMsgPackの配列型としてパッキングする
/// </summary>
/// <param name="packer">パッキング先</param>
/// <param name="objectTree">パッキングしたいオブジェクト</param>
protected override void PackToCore(Packer packer, BaseClass objectTree) {
packer.PackArrayHeader(objectTree.GetFieldCount()); // 配列の要素数セット
packer.Pack((UInt32)objectTree.ClassId); // 先頭要素としてクラスIDセット
objectTree.PackToCore(packer); // 後は派生先クラスに任せる
}
/// <summary>
/// アンパッキング処理をオーバーライド、自作クラスをMsgPackの配列型としてアンパッキングする
/// </summary>
/// <param name="unpacker">アンパッキング元</param>
/// <returns>生成したオブジェクト</returns>
protected override BaseClass UnpackFromCore(Unpacker unpacker) {
// 現在位置は配列の先頭でなければならない
if (!unpacker.IsArrayHeader)
throw SerializationExceptions.NewIsNotArrayHeader();
// 既に読み込まれている配列要素数取得
var length = unpacker.LastReadData.AsInt32();
// 配列の先頭要素をクラスIDとして扱う
UInt32 i;
unpacker.ReadUInt32(out i);
var id = (ClassId)i;
// クラスIDに対応する型情報取得
Type type;
if (!_Types.TryGetValue(id, out type))
throw new MsgPack.MessageTypeException(i.ToString() + " is not ClassId");
// 型情報からオブジェクト生成
BaseClass o = Activator.CreateInstance(type) as BaseClass;
if (o.GetFieldCount() != length)
throw new MsgPack.UnpackException(o.GetType() + " field count mismatch " + length);
// 後は生成されたオブジェクトに任せる
o.UnpackFromCore(unpacker);
return o;
}
8.保存のルートとなるデータクラス作成
以下の様なクラスを作成する。
やってることは以下の2つだけ。
- シーン内オブジェクトを自分に取り込む。
- 取り込まれているのをシーンに復元する。
/// <summary>
/// 永続化されるゲームデータ
/// </summary>
public class GameData {
static string _filePath = Application.persistentDataPath + "/data.dat";
/// <summary>
/// ゲームデータ保存先パス名、実行環境用のパスが返る
/// </summary>
public static string FilePath {
get {
return _filePath;
}
}
/// <summary>
/// ひよこデータ一覧 保存&読み込み時のみ使用する
/// </summary>
public List<HiyokoData> Hiyokos;
/// <summary>
/// ゲームオブジェクトのデータを内部に取り込む
/// </summary>
public void StoreGameObjects() {
this.Hiyokos = new List<HiyokoData>();
foreach (var obj in GameObject.FindObjectsOfType<GameObject>()) {
var h = obj.GetComponent<Hiyoko>();
if (h != null) {
this.Hiyokos.Add(h.Store());
}
}
}
/// <summary>
/// ゲームオブジェクトを復元する
/// </summary>
public void RestoreGameObjects() {
foreach (var hd in this.Hiyokos) {
Hiyoko.Restore(hd);
}
this.Hiyokos = null;
}
}
9.ゲーム全体の管理クラス作成
以下の様なゲーム全体管理するクラスを作成する、こいつが保存と読み込みを行う。
これをシーン内の適当なオブジェクトにアタッチする。
/// <summary>
/// ゲーム全体の管理を行う
/// </summary>
public class GameManager : MonoBehaviour {
static GameObject _hiyo, _matsuhiyo;
/// <summary>
/// ひよこプレファブ取得
/// </summary>
public static GameObject Hiyo {
get {
return _hiyo = _hiyo ?? (GameObject)Resources.Load("Prefabs/Hiyoko");
}
}
/// <summary>
/// 祭りひよこプレファブ取得
/// </summary>
public static GameObject MatsuHiyo {
get {
return _matsuhiyo = _matsuhiyo ?? (GameObject)Resources.Load("Prefabs/MatsuHiyo");
}
}
/// <summary>
/// 初期化時に保存してあるゲームデータから復元
/// </summary>
void Awake() {
Debug.Log("Start load game data from " + GameData.FilePath);
var gd = ClassSerializer.LoadObject<GameData>(GameData.FilePath, new GameData());
gd.RestoreGameObjects();
Debug.Log("End load game data");
}
/// <summary>
/// アプリ終了時に保存
/// </summary>
void OnApplicationQuit() {
Debug.Log("Start save game data to " + GameData.FilePath);
var gd = new GameData();
gd.StoreGameObjects();
ClassSerializer.SaveObject<GameData>(GameData.FilePath, gd);
Debug.Log("End save game data");
}
}
10.ゲームオブジェクト⇔保存データのやり取りクラス作成
以下の様に一旦保存用データを介してから永続化する様にしてみる。
これをゲームオブジェクトにアタッチすることで必要データのみが永続化される。
/// <summary>
/// ひよこちゃん
/// </summary>
public class Hiyoko : MonoBehaviour {
/// <summary>
/// ゲームオブジェクトから保存するデータを取得する
/// </summary>
public HiyokoData Store() {
var t = this.transform;
var p = t.position;
HiyokoData hd = new HiyokoData();
hd.Kind = t.tag == "Hiyoko" ? 0 : 1;
hd.X = p.x;
hd.Y = p.y;
hd.Angle = t.localEulerAngles.z;
return hd;
}
/// <summary>
/// 保存してあるデータからゲームオブジェクトを復元する
/// </summary>
public static void Restore(HiyokoData hd) {
GameObject.Instantiate(
hd.Kind == 0 ? GameManager.Hiyo : GameManager.MatsuHiyo,
new Vector3(hd.X, hd.Y, 0), Quaternion.Euler(0, 0, hd.Angle));
}
}
11.マウス操作でゲームオブジェクト生成するクラス
ペイントするようにゲームオブジェクトを生成してみる。
/// <summary>
/// ペイントする様にゲームオブジェクトを生成する
/// </summary>
public class Painter : MonoBehaviour {
static float _angle;
void FixedUpdate() {
var mpos = Input.mousePosition;
var position = Camera.main.ScreenToWorldPoint(mpos);
position.z = 0;
// マウス左ボタン押したらひよこ生成
if (Input.GetMouseButton(0)) {
Instantiate(GameManager.Hiyo, position, Quaternion.Euler(0, 0, _angle));
_angle += 2;
}
// マウス右ボタン押したら祭りひよこ生成
if (Input.GetMouseButton(1)) {
Instantiate(GameManager.MatsuHiyo, position, Quaternion.Euler(0, 0, _angle));
_angle += 2;
}
// マウス中ボタン押されたら全ひよこ削除
if (Input.GetMouseButtonDown(2)) {
foreach (var obj in GameObject.FindObjectsOfType<GameObject>()) {
var h = obj.GetComponent<Hiyoko>();
if (h != null) {
GameObject.Destroy(obj);
}
}
}
}
}
終了して再起動すると復元される。
起動する度に描画順が逆になるけどまぁよしとしよう。
12.最後にサンプルプログラムの後始末について
自分のWindows環境だと**「C:\Users\ユーザー名\AppData\LocalLow\xiden」**に保存されたファイルができる。
このフォルダ消せば後始末完了。
但しこれは環境によって異なるらしいのでログに出力されたパスで判断してください。