💻

エディタ拡張で特定のフォルダ以下のプレハブを編集・保存するまで

2023/01/24に公開

概要

特定のフォルダ以下のプレハブ全てをリネームしたり、プレハブ内の画像を置換するといった作業が発生して、その数がとても手作業ではできない……といったものがありました。

今回はそれをエディタ拡張で解決したので、まとめてみます。
ちなみに、今回実装したコードは ここ にまとめたので、合わせて見てもらえるとよりよいかなと思います。

環境

  • Unity 2021.3.16f1
  • Windows10

エディタ拡張で空のウィンドウを作る

エディタ拡張なので、Editor フォルダを作ったあと、任意の cs ファイルを作ります。
そして、以下のようなものを作りました。

using UnityEditor;

public class PrefabEditEditorWindow : EditorWindow
{
    [MenuItem("PrefabEdit/プレハブ変換")]
    private static void OpenWindow()
    {
        var window = GetWindow<PrefabEditEditorWindow>("プレハブ変換");
        window.Show();
    }
}

EditorWindow を継承することで、空のウィンドウを作成することができます。
また、ウィンドウをコンテキストメニューから開きたいので、MenuItem アトリビュートを付けた static のメソッドを用意します。

これで空のウィンドウが用意できたので、次に中身を作っていきます。

フォルダーをアタッチできる領域を作り、フォルダのパス情報を保存する

private DefaultAsset targetFolder;
private string targetFilePath;

private void OnGUI()
{
    var currentTargetFolder = (DefaultAsset)EditorGUILayout.ObjectField("対象フォルダ", targetFolder, typeof(DefaultAsset), false);

    if (currentTargetFolder != null)
    {
        targetFolder = currentTargetFolder;
        targetFilePath = AssetDatabase.GetAssetOrScenePath(targetFolder);
    }
}

この実装をすると、以下のような見た目のウィンドウが作れます。

OnGUI メソッドを定義して実装をしていきます。
OnGUI の中に EditorGUILayout.ObjectField を呼び出すことで、フォルダをアタッチできる領域が作られます。

第二引数に領域内に表示されるオブジェクトを設定し、第三引数に割り当てることができるオブジェクトの型を指定しています。
第三引数で設定した DefaultAsset を使うことで、フォルダのアタッチを実現しています。

アタッチしたとき EditorGUILayout.ObjectField が戻り値を返すので、
その戻り値を利用してパス情報を取得します。

これには、AssetDatabase.GetAssetOrScenePath メソッドを利用します。
これでプロジェクトルートからの相対パスが取得できるので、このパスを起点として処理を進めることとします。

AssetDatabase には他にもいろんなアセット操作メソッドがあるので、覗いてみると面白いかもしれません。
このあとでもいくつかのメソッドが登場します。

画像のアタッチできる領域を作る

同様に、画像のアタッチできる領域を作ります。

private Sprite convertSprite;

private void OnGUI()
{
    var currentConvertSprite = (Sprite)EditorGUILayout.ObjectField("置換用画像", convertSprite, typeof(Sprite), false);

    if (currentConvertSprite != null)
    {
        convertSprite = currentConvertSprite;
    }
}

同じく EditorGUILayout.ObjectField メソッドを使います。
が、今回は画像をアタッチしたいので、第三引数に Sprite 型を指定しています。
そして、画像がアタッチされたとき、つまり null でないときに変数として画像を保持しておきます。

実装すると以下のような見た目になります。

ボタンを押したら任意の関数を実行できるようにする

private void OnGUI()
{
    if (GUILayout.Button("Convert"))
    {
        ConvertAssets();
    }
}

private void ConvertAssets()
{
    Debug.Log("Start Convert Assets");
}

OnGUI の中で GUILayout.Button メソッドを呼び出し、ボタンを配置します。
ボタンを押したかはこのメソッドの戻り値で取得できるので、if 文の中に入れておきます。
ので、この実装だとボタンを押したときに ConvertAssets が実行されるようになります。

列挙の処理を作る

private void ConvertAssets()
{
    Debug.Log("Start Convert Assets");

    if (string.IsNullOrEmpty(targetFilePath))
    {
        return;
    }

    var assetGuids = AssetDatabase.FindAssets("t:prefab", new[] { targetFilePath });

    foreach (var assetGuid in assetGuids)
    {
        var path = AssetDatabase.GUIDToAssetPath(assetGuid);
        Debug.Log($"AssetPath: {path}");
    }
}

ボタンを押したときに呼び出されるメソッドの中身を実装していきます。

まず、フォルダがアタッチされてないときは中身を実行されたくないので、string.IsNullOrEmpty(targetFilePath) でチェックしています。

次に、アタッチされたフォルダを基準に検索をかけていきます。
これには、AssetDatabase.FindAssets を利用します。

第一引数には検索対象のフィルター文字列を指定します。
今回はプレハブのみを操作したいので 検索フィルターのページ を参考に、
"t:prefab" というフィルターを入れています。

第二引数に検索対象となるパスの配列を渡します。
今回は取得済みのパスを一つ配列にして渡しています。

AssetDatabase.FindAssets メソッドでは GUID として取得できますが、このままだと分かりづらいことや、後述のリネーム処理でも利用したいのでパスを取得します。

パスの取得は AssetDatabase.GUIDToAssetPath メソッドを使い、文字通り GUID からパスに変換します。
ここで取得できるパスは、Project フォルダーからの相対パスとなります。

以下は実装後にボタンを押して実行した結果です。

Prefabs フォルダー内の RenderTexture アセットは除外されて、プレハブアセットのみがログとして出力されるのがわかります。

操作・保存の処理を作る

ここまでで列挙までできたので、あとは操作と保存処理です。
今回は、画像の変更リネーム 操作をして保存します。

画像の変更

private void ConvertAssets()
{
    Debug.Log("Start Convert Assets");

    if (string.IsNullOrEmpty(targetFilePath))
    {
        return;
    }

    var assetGuids = AssetDatabase.FindAssets("t:prefab", new[] { targetFilePath });

    foreach (var assetGuid in assetGuids)
    {
        var path = AssetDatabase.GUIDToAssetPath(assetGuid);
        Debug.Log($"EditAssetPath: {path}");

        EditAsset(path);
    }
}

private void EditAsset(string path)
{
    // 画像変更処理
    var prefabObject = PrefabUtility.LoadPrefabContents(path);
    prefabObject.GetComponent<Image>().sprite = convertSprite;
    PrefabUtility.UnloadPrefabContents(prefabObject);
}

ConvertAssets 内で 個別のアセットのパスを取得したあと、それを利用して画像を変更する処理が上のソースです。

まず、PrefabUtility.LoadPrefabContents メソッドを利用して、プレハブアセットを専用のシーンに読み込みます。

戻り値として GameObject が取得できるので、後は好きなように操作可能です。
今回は例として画像の変更をするので、Image コンポーネントを取得して Sprite を変更しています。

操作が終わった後は、PrefabUtility.UnloadPrefabContents を呼び出してシーンからプレハブオブジェクトを破棄します。
数個なら問題なさそうですが、数千数万で破棄しない場合メモリを圧迫することになる予感がします(未検証)

リネーム

private void EditAsset(string path)
{
    // リネーム処理
    var nameOld = Path.GetFileNameWithoutExtension(path);
    var nameNew = $"{nameOld}_edit";
    AssetDatabase.RenameAsset(path, nameNew);
}

編集済みのプレハブに _edit とリネームをします。

リネームには AssetDatabase.RenameAsset を利用します。

第一引数には Project フォルダーからの相対パス を入れます。
これは前述した AssetDatabase.GUIDToAssetPath で取得したパスがそのまま使えます。

第二引数にはリネーム後の名前を入れます。
今回は元の名前に対して _edit と付けたいので、
Path.GetFileNameWithoutExtension メソッドで拡張子を除いたファイル名を取得してあげて、新しい名前を定義するようにしてみました。

保存

private void ConvertAssets()
{
    Debug.Log("Start Convert Assets");

    if (string.IsNullOrEmpty(targetFilePath))
    {
        return;
    }

    var assetGuids = AssetDatabase.FindAssets("t:prefab", new[] { targetFilePath });

    foreach (var assetGuid in assetGuids)
    {
        var path = AssetDatabase.GUIDToAssetPath(assetGuid);
        Debug.Log($"EditAssetPath: {path}");

        EditAsset(path);
    }

    AssetDatabase.SaveAssets();
}

保存には、AssetDatabase.SaveAssets メソッドを利用します。

全ての操作が終わった後にこれを呼び出せば保存処理が勝手に走ります。

余談ですが、個別のプレハブアセットを保存したい場合、
PrefabUtility.SaveAsPrefabAsset メソッドも役立ちます。

まとめ

以上ウィンドウを作ってプレハブを操作・保存する方法でした。
これを基本として UI を作り込めば、より凝ったことも実現できそうな予感がします。
ぜひ試してみてください。

Discussion