Unityでクラフトゲーとかのオブジェクト情報のセーブ・ロード
Unity用3Dアセット「古や村」のモデルを、クラフトゲーム感覚で簡単にパーツ配置して建物を建築できるアプリを作りました。
古や村のパーツ構造は水平方向はグリッドで対処できても、垂直方向が特殊で既存の建築系アセットでは扱いづらいものでした。
かつ重心ミスってたりな作りの甘いプレハブがあったりで自分でも作りづらいと思ってまして、いつかプログラム技術を身に着けたら、最適化した建築ツールを作ってバンドルしたいと思っていたのですが、かなった時にはリリースから5年も経過してました…
しかもバンドルするにはコードが汚くてサードパーティ製アセットも使わざるを得なくて、無理そう
アプリとエディタ拡張のペアという内容になっています。
アプリで建築して設計図ファイル(中身はjson)を書き出し、エディタ拡張でUnityプロジェクト上でファイルを読み込みプレハブを並べる、という仕様になってます。
unityエディタを使わずアプリ単品でデータ作成できるようにすることで、unityの知識が全くない人にもシーンビルドの作業を任せられるという利点が生まれます。(unityの経験全くない人にエディタ使わせようとしたら、100%使い物にならないデータ作られるからね…プレハブとfbxモデル間違えて並べたり)
建築システム自体は、高等なことはしてません。
建築できるクラフトゲーのアセットは既にそこそこあり、グリッド式よりパーツが近接するとジョイント部分にスナップする仕様の方がかっこいいと思うので、オブジェクトの並べ方については解説しないとして、
並べたオブジェクトの情報、配置位置をセーブデータにしてファイルを作ってセーブ・ロードする部分だけ解説したいと思います。
まず、配置するオブジェクトは全てプレハブです。実行中にメッシュ生成とかで生み出したユニークオブジェクトは使いません。
事前に建材パーツリストのデータベースを作っておき、プレハブはIDやカテゴリIDと紐つけて登録しておき、生成時にIDを照合して対応したプレハブを生成するという仕様にしてます。(生成時にはプレハブにIDを控える用のコンポーネントをアタッチします)
建材パーツリストをインスペクタで見るとこう↓
設計図のクラスDataBaseBuildBluePrint は、こうなってます。
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
namespace FUNSET
{
[CreateAssetMenu(menuName = "Funset/BuildBluePrint")]
public class DataBaseBuildBluePrint : ScriptableObject
{
//設計図の名前
public string BuildBluePrintName;
[System.Serializable]
public class BuildingAssenblyData
{
[SerializeField, Header("建材カテゴリ,ID"), Tooltip("建材の種類ID=DataBaseBuildPrefabInfoのBPID")]
public int BPCategory,BPID;
[SerializeField, Header("建材配置レイヤーID"), Tooltip("建物の透視図、階層分けなどの用途を想定した、数字を割り当てられる")]
public int AsseLayerID;
[SerializeField, Header("建材のトランスフォーム")]
public Vector3 AssePosition, AsseRotation, AsseScale;
}
public List<BuildingAssenblyData> AssenblyData;
public void Reset()
{
BuildBluePrintName = "BuildBluePrint";
AssenblyData = new List<BuildingAssenblyData>();
}
}
}
クラスと言うかScriptableObjectです。
インスペクタで見るとこう↓
このDataBaseBuildBluePrintなら、セーブの際にトラブルなく丸ごとjsonにすることができ、設計図のとおりに並べた建物を再現させられました。
ファイルに書き出すコードは、こんな感じ↓
public DataBaseBuildBluePrint _dataBaseBuildBluePrint;
public string FilePath;
void SaveNowBluePrint()
{
//設計図をカラにします
_dataBaseBuildBluePrint.Reset();
_dataBaseBuildBluePrint.BuildBluePrintName = "BluePrint01";
//シーンに並べた建材パーツをカウントして、
//_dataBaseBuildBluePrintに記入する処理を書きます
.............
// _dataBaseBuildBluePrintをJSON形式にシリアライズします
var json = JsonUtility.ToJson(_dataBaseBuildBluePrint, true);
// JSONデータをファイルに保存します
File.WriteAllText(FilePath, json);
}
※このコードだけで動くわけではありません。解説用の端折ったやつです
※JsonUtility.ToJson();を使うにはusing UnityEngine;が必要です。
※File.WriteAllText();を使うにはusing System.IO;が必要です。
_dataBaseBuildBluePrintはシーン上の全建材パーツの情報を収めるためのDataBaseBuildBluePrintです。
FilePathはセーブ・ロードのファイルパスを控えるためのstring変数です。
FilePathには、セーブ・ロードの前にフォルダパス+ファイル名+拡張子で収めておきます。
_dataBaseBuildBluePrintには、事前に作成したScriptableObject=ファイルをセットしておきます。これは常に記録させる用でなくセーブ・ロードの時だけ使うキャッシュ用です。
jsonにできる情報は、シリアライズできるもの=インスペクタに表示して値を入力できるような変数だけですが、
ScriptableObjectは、必ずキャッシュ用のファイルを作って扱わないとシリアライズできません(=セーブ・ロードできない)。
記録させるだけならScriptableObjectでない普通の[System.Serializable]で作ったクラスでやればわざわざキャッシュファイルを作る手間が要らないのではと、あとから気づくのですが…そのかわりScriptableObjectは中身をインスペクタで常時視認できるという利点があるから、まいっかと…
jsonファイルをScriptableObjectに読みこませるコードはこう
var json = File.ReadAllText(FilePath);
JsonUtility.FromJsonOverwrite(json, _dataBaseBuildBluePrint);
jsonにできる単純な構造のクラスやScriptableObjectなら、ファイルの読み書き自体はあっさり簡単にできます。Unityエディタ上からでもビルドしたアプリ上からでも、ローカルPC上のどこにでも書き込めます。
難関なのは、FilePathをストレスなく入力する方法だけです。
インプットUIから手入力でもファイルパスは機能しますが、ファイルブラウザでファイル名を打ち込むかファイルリストをダブルクリックして読み書きとなると、自作は無理ゲーです。ファイルブラウザのUIから入力操作まで全て、完全自作の手間になるからです。
アセットで済ますのが一番。
無料で使えるアセットを探して使いましたが、
セーブ用のパス取得コードはこんな感じになります。
public bool SaveFileReady;
public void SetFileSavePath()
{
StartCoroutine(ShowSaveDialogCoroutine());
}
private IEnumerator ShowSaveDialogCoroutine()
{
// フィルタを設定します
FileBrowser.SetFilters(true, new FileBrowser.Filter("設計図ファイル", ".bbp"), new FileBrowser.Filter(".jsonファイル", ".json"));
// ダイアログが表示されたときに選択されるデフォルトフィルタを設定します
FileBrowser.SetDefaultFilter(".bbp");
// /除外する拡張子を設定します
FileBrowser.SetExcludedExtensions(".lnk", ".tmp", ".zip", ".rar", ".exe");
// 新しいクイックリンクを追加します
FileBrowser.AddQuickLink("Users", "C:\\Users", null);
// ファイル保存ダイアログを表示します
FileBrowser.ShowSaveDialog(null, null, FileBrowser.PickMode.Files, false, null, "BuildBluePrint.bbp", "Save As", "Save");
// ファイル読み込みダイアログを表示してユーザーからの応答を待ちます
//引数は、FileBrowser.PickMode=ファイルかフォルダーを選択、複数選択可能か、初期パス、デフォルトファイル名、窓のタイトル名、openボタンのテキスト
yield return FileBrowser.WaitForSaveDialog(FileBrowser.PickMode.Files, false, null, null, "Select Files", "Save");
if (FileBrowser.Success)
{
var Paths = FileBrowser.Result;
//取得できる文字列は、複数ファイル読み込みに対応するため配列になってます。単一ファイルならパスは[0]に入ってます
FilePath= Paths[0];
//セーブ用ファイルパス取得したフラグです
SaveFileReady = true;
}
else
{
Debug.Log("FileOpen Canceled");
}
}
このSimpleFileBrowserは、ファイルブラウザが開くのがコルーチンを使った非同期処理でFilePathが入力されるまで時間が止まらない仕様のため、FilePathが入力された判定フラグを作って毎フレーム監視して、成ったらセーブ・ロード処理を実行させる、という作り方をすることになります。
(アセットによって同期処理できる仕様のものもあるみたいです)
FilePathにパスが書き込まれたらフラグSaveFileReadyをtrueにし、
Update()でSaveFileReadyを監視し、trueになったら上記のメソッドSaveNowBluePrint()を実行、(終わったらSaveFileReadyはfalseに)とやれば
見事jsonファイルが作成されてセーブ完了です。
なお拡張子は自由に設定できます。.jsonでは用途の見分けがつかないので、「.bbp」としてあります。SimpleFileBrowserはセーブ時にファイルブラウザで拡張子を指定するフィルター設定が充実しています、
ちなみにロード用もコードはほぼ同じですが、ロード専用のメソッドを用います。
public bool LoadFileReady ;
public void SetLoadFileLPath()
{
StartCoroutine(ShowLoadDialogCoroutine());
}
private IEnumerator ShowLoadDialogCoroutine()
{
FileBrowser.SetFilters(true, new FileBrowser.Filter("設計図ファイル", ".bbp"), new FileBrowser.Filter(".jsonファイル", ".json"));
FileBrowser.SetDefaultFilter(".bbp");
FileBrowser.SetExcludedExtensions(".lnk", ".tmp", ".zip", ".rar", ".exe");
FileBrowser.AddQuickLink("Users", "C:\\Users", null);
// フォルダ選択ダイアログを表示します
FileBrowser.ShowLoadDialog((paths) => { Debug.Log("Selected: " + paths[0]); },
() => { Debug.Log("Canceled"); },
FileBrowser.PickMode.Files, false, null, null, "Select Files", "Select");
yield return FileBrowser.WaitForLoadDialog(FileBrowser.PickMode.Files, false, null, null, "Select Files", "Load");
if (FileBrowser.Success)
{
var FilePath = FileBrowser.Result;
FullPath = FilePath[0];
LoadFileReady = true;
}
else
{
Debug.Log("FileOpen Canceled");
}
}
※拡張子や中身が違ってたら無効にする処理はしてません。
私のモデリングとゲーム開発の最新情報は、Discoadを御覧ください。
https://discord.gg/Ur2pmF7ptw