はじめに
UnityでのEditorを作る際にRuntime中でのPrefabを保存する必要のあるEditorを作ることがあったので、その方法を紹介したいと思います。
Prefabのパスを取得する
実は、Runtime中のGameObjectからPrefabのパスを取得する方法は存在しません。なので本来はPrefabのパスを直接指定してあげる必要がありますが、「Prefabの名前がAssetの中で一意である」限りは取得することができます。
AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets(filter).First())
これで取得することができます。
Prefabとして保存する
Prefabを保存する方法にもいくつか注意が必要です。
var instance = PrefabUtility.LoadPrefabContents(PrefabPath);
// 値の変更
instance.transform.position = Vector3.zero;
PrefabUtility.SaveAsPrefabAsset(instance, PrefabPath);
PrefabUtility.UnloadPrefabContents(instance);
上記が基本的なPrefabの保存方法になります。この手順はPrefabをダブルクリックしたときの画面(Prefabモード)に入っているイメージです。最後に忘れずUnloadPrefabContents()も必要です。
また、Prefabの入れ子にしたい場合、通常のInstantiate()ではGameObjectとして生成されてしまうため、InstantiatePrefab()として生成する必要があります。
var instance = PrefabUtility.LoadPrefabContents(PrefabPath);
// Prefabの入れ子
PrefabUtility.InstantiatePrefab(_cubePrefab, instance.transform);
PrefabUtility.SaveAsPrefabAsset(instance, PrefabPath);
PrefabUtility.UnloadPrefabContents(instance);
これでRuntimeでのPrefabの保存機能が実装できました。次に使いやすいクラスを作成していきます。
RuntimePrefabUtility
コード全体
畳んであります
using System.Threading;
using UnityEngine;
public class TestEditor : MonoBehaviour
{
[SerializeField]
private GameObject _baseObj;
[SerializeField]
private GameObject _cubePrefab;
[SerializeField]
private GameObject _spherePrefab;
private void Start()
{
var token = destroyCancellationToken;
var rpu = new RuntimePrefabUtility(_baseObj);
rpu.Save(token, RuntimePrefabUtility.SaveTarget.GameObject | RuntimePrefabUtility.SaveTarget.Prefab, Save);
}
private bool Save(CancellationToken ct, RuntimePrefabUtility.SaveTarget saveTarget, GameObject instance)
{
var cube = UnityEditor.PrefabUtility.InstantiatePrefab(_cubePrefab, instance.transform) as GameObject;
cube.transform.position = new Vector3(0, -5, 0);
return true;
}
}
#if UNITY_EDITOR
using Cysharp.Threading.Tasks;
using System;
using System.Linq;
using System.Threading;
using UnityEditor;
using UnityEngine;
using UnityEngine.AddressableAssets;
public class RuntimePrefabUtility
{
public enum SaveTarget
{
None = 0 << 0,
GameObject = 1 << 0,
Prefab = 1 << 1,
}
public GameObject SceneObj { get; private set; }
public string PrefabPath { get; private set; }
public RuntimePrefabUtility(GameObject obj, string path)
{
SceneObj = obj;
PrefabPath = path;
}
public RuntimePrefabUtility(GameObject obj)
{
SceneObj = obj;
PrefabPath = FindAssetFromFilter(obj.name);
}
public static string FindAssetFromFilter(string filter)
=> AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets(filter).First());
public void Save(CancellationToken ct, SaveTarget saveTarget, Func<CancellationToken, SaveTarget, GameObject, bool> save)
{
if (saveTarget.HasFlag(SaveTarget.GameObject))
{
save(ct, saveTarget, SceneObj);
}
if (saveTarget.HasFlag(SaveTarget.Prefab))
{
var instance = PrefabUtility.LoadPrefabContents(PrefabPath);
if (save(ct, saveTarget, instance))
{
PrefabUtility.SaveAsPrefabAsset(instance, PrefabPath);
}
PrefabUtility.UnloadPrefabContents(instance);
}
}
}
#endif //UNITY_EDITOR
ここではいくつかのポイントを説明します。
まずコンストラクタについて、このプロジェクト内で_baseObjのオブジェクト名はAsset内で一意であることが確定しているので、オブジェクト名から前述した方法でパスを取得しています。
次にSaveTargetがPrefabとGameObjectで定義されている理由ですが、Unityの仕様上Scene上に置かれたPrefabはRuntimeになった瞬間Prefabとの接続が切れるので、Runtime中に元のPrefabを変更してもScene上のオブジェクトには影響しません。なのでSceneのObject用とPrefab用として2回呼び出す必要があります。
このテクニックを使い、逆に保存するタイミングを任意で変更できるようになります。
例えば、GameViewで色々変更(SaveTarget.GameObject)したけど、変更を破棄したいとなれば、そのままRuntimeをExitすれば変更は破棄されます。
あとはSave()の中に処理を追加していく感じになります。
おわりに
通常ならPrefabを編集したい場合はEditorで編集できるのが一番ですが、プロジェクトの技術的制約からRuntimeでのPrefabEditorが必要になる場合があります。Editorで編集することが難しい場合はRuntimeでもこういった手法で保存可能なので、この記事をご覧の方は参考にしてください。