RuntimeでPrefabを保存したい

## はじめに

UnityでのEditorを作る際にRuntime中でのPrefabを保存する必要のあるEditorを作ることがあったので、その方法を紹介したいと思います。

## Prefabのパスを取得する

実は、Runtime中のGameObjectからPrefabのパスを取得する方法は存在しません。なので本来はPrefabのパスを直接指定してあげる必要がありますが、「Prefabの名前がAssetの中で一意である」限りは取得することができます。

```cs
AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets(filter).First())
```

これで取得することができます。

## Prefabとして保存する

Prefabを保存する方法にもいくつか注意が必要です。

```cs
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()`として生成する必要があります。

```cs
var instance = PrefabUtility.LoadPrefabContents(PrefabPath);

// Prefabの入れ子
PrefabUtility.InstantiatePrefab(_cubePrefab, instance.transform);

PrefabUtility.SaveAsPrefabAsset(instance, PrefabPath);
PrefabUtility.UnloadPrefabContents(instance);
```

これでRuntimeでのPrefabの保存機能が実装できました。次に使いやすいクラスを作成していきます。

## RuntimePrefabUtility

### コード全体

<details><summary>畳んであります</summary>

```cs:TestEditor.cs
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;
    }
}

```

```cs:RuntimePrefabUtility.cs
#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

```

</details>

ここではいくつかのポイントを説明します。

まずコンストラクタについて、このプロジェクト内で`_baseObj`のオブジェクト名はAsset内で一意であることが確定しているので、オブジェクト名から前述した方法でパスを取得しています。

次にSaveTargetがPrefabとGameObjectで定義されている理由ですが、Unityの仕様上Scene上に置かれたPrefabはRuntimeになった瞬間Prefabとの接続が切れるので、Runtime中に元のPrefabを変更してもScene上のオブジェクトには影響しません。なのでSceneのObject用とPrefab用として2回呼び出す必要があります。

:::note info
このテクニックを使い、逆に保存するタイミングを任意で変更できるようになります。
例えば、GameViewで色々変更(SaveTarget.GameObject)したけど、変更を破棄したいとなれば、そのままRuntimeをExitすれば変更は破棄されます。
:::

あとは`Save()`の中に処理を追加していく感じになります。

## おわりに

通常ならPrefabを編集したい場合はEditorで編集できるのが一番ですが、プロジェクトの技術的制約からRuntimeでのPrefabEditorが必要になる場合があります。Editorで編集することが難しい場合はRuntimeでもこういった手法で保存可能なので、この記事をご覧の方は参考にしてください。