Unityでコンポーネントを連携させたり再利用したりする際にはprefabやGameObjectが便利だ。
その上でMVCパターンを採用する場合には、M層の情報がinspectorから見えなくなるのを防ぐために、MVCすべてのクラスをMonobehaviourの継承から作るというアプローチがありえる。
その結果、ある規模のMonobehaviourをセーブ/ロードする必要が出てくる。
以下のようにフィールドに建物を置くようなゲームを例にすると
Hierarchy上では以下のようになる
inspectorは以下のような感じ
この状態をセーブ/ロードしたい。
設計
- 利便性のため「継承すればセーブ/ロード可能となる」ようなMonobehaviourの派生クラスを作成する
- 最初の配置(インスタンス化)はprefabをinstantiateする
- 派生クラスではDictionaryを使って追加情報のセーブ/ロードを実装できる
- transform.parentもセーブ/ロードする
実装
「継承すればセーブ/ロード可能となる」クラス
- InstantiatePrefab()で最初のインスタンスを作成
- GetSaveData()でセーブデータ(string)を得る
- InstantiateSaveDataでロードする
prefabのパスとtransform.parentを文字列で保持するのがポイント。
セーブデータはkey:value形式の情報をcsvにしたもので、この1列が1インスタンスに該当する。
このせいでSave()/Load()のDictionaryではコロンとコンマが利用不可になったりしているあたりダサいが、とりあえず使う上では問題ないので今回はこれにした。Jsonやyaml化するアプローチもあるんだろうけど、別に一般的なフォーマットである必要はなく、Dictoinaryを使って気軽にアクセスできればそれでいいのでこのような実装になった。
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
public abstract class SerializableMonoBehaviour : MonoBehaviour
{
private string _prefab_path;
/// <summary>
/// prefabをインスタンス化する
/// </summary>
/// <param name="path">prefabへのパス</param>
/// <returns>インスタンス</returns>
public static SerializableMonoBehaviour InstantiatePrefab(string path) {
var ins = Instantiate<SerializableMonoBehaviour>(Resources.Load<SerializableMonoBehaviour>(path));
ins._prefab_path = path;
return ins;
}
/// <summary>
/// このインスタンスのセーブデータを得る
/// </summary>
/// <returns>セーブデータ</returns>
public string GetSaveData() {
/// セーブデータを用意する
var dic = new Dictionary<string, string>();
dic["_parent"] = transform.parent.name;
dic["_prefab"] = _prefab_path;
Save(dic);
/// Dictionary -> セーブデータ (1行の文字列)
var kv = from key in dic.Keys select key + ":" + dic[key];
return string.Join(",", kv.ToArray());
}
/// <summary>
/// セーブデータからインスタンスを作成する
/// </summary>
/// <param name="data"></param>
/// <returns>インスタンス</returns>
public static SerializableMonoBehaviour InstantiateSaveData(string data) {
/// セーブデータ -> Dictionary
string[] csv = data.Split(',');
Dictionary<string, string> dic = new Dictionary<string, string>();
foreach (string v in csv) {
string[] kv = v.Split(':');
dic[kv[0]] = kv[1];
}
/// インスタンス作成
var ins = Instantiate<SerializableMonoBehaviour>(Resources.Load<SerializableMonoBehaviour>(dic["_prefab"]));
/// 値の設定
GameObject parent = GameObject.Find(dic["_parent"]);
ins.transform.SetParent(parent.transform);
ins._prefab_path = dic["_prefab"];
ins.Load(dic);
return ins;
}
protected abstract void Save(Dictionary<string, string> dic);
protected abstract void Load(Dictionary<string, string> dic);
}
派生クラス
using System.Collections.Generic;
using UnityEngine.Tilemaps;
public class TilemapObject : SerializableMonoBehaviour{
public int X;
public int Y;
public int W;
public int H;
public Tile Tile;
protected override void Save(Dictionary<string, string> dic) {
dic["X"] = X.ToString();
dic["Y"] = Y.ToString();
dic["W"] = W.ToString();
dic["H"] = H.ToString();
}
protected override void Load(Dictionary<string, string> dic) {
X = int.Parse(dic["X"]);
Y = int.Parse(dic["Y"]);
W = int.Parse(dic["W"]);
H = int.Parse(dic["H"]);
}
}
セーブとロード
以下ではクリップボードを介してセーブ/ロードしている。
using UnityEngine;
using System.Linq;
public class SerializeManager : MonoBehaviour
{
void Update() {
if (Input.GetKeyDown(KeyCode.S)) {
GUIUtility.systemCopyBuffer = Serialize();
}
if (Input.GetKeyDown(KeyCode.L)) {
Deserialize(GUIUtility.systemCopyBuffer);
}
}
public string Serialize() {
/// Hierarchy上の全SerializableMonoBehaviourを収集
var objs = UnityEngine.Object.FindObjectsOfType<SerializableMonoBehaviour>();
var data = from smb in objs select smb.GetSaveData();
/// 得られたセーブデータを改行で連結
return string.Join("\n", data.ToArray());
}
public void Deserialize(string data) {
string[] esv = data.Split('\n');
foreach (string v in esv) {
SerializableMonoBehaviour.InstantiateSaveData(v);
}
}
}
あとはSerializableMonoBehaviourを継承した独自クラスを作ってprefab化して使えばよい。
前記のMiniHouseTilemapObjectの例。
using UnityEngine;
using UnityEditor;
public class MiniHouseTilemapObject : TilemapObject
{
}