Amazonで商品を見るセール会場へ

UnityのPrefabファイルをツリーCSV出力するEditor拡張:フォルダー階層

Projectで選択したフォルダー以下の.prefabファイルと保存フォルダーの構造を、ツリーとしてCSVへ書き出すEditor拡張です。Prefab内部のGameObjectやコンポーネントを調べたい場合は、内部構造のCSV出力を使います。

仕様の参照はUnity 6.3 LTSです。本文のCSV整形とツリー構築だけをWindowsの.NET環境で確認しました。Unity Editorでのコンパイル、Project選択、AssetDatabase検索、保存ダイアログ、実アセットの出力は未確認です。掲載画像は元記事の旧資料で、修正版の実行結果ではありません。

目次

出力の仕様:1列CSVとPrefabを含むフォルダー

項目・症状内容・確認先
対象Projectで選ぶAssets内のフォルダー1つと、その子フォルダーにある.prefab。PackagesやModel Prefabは対象外。
1列のCSVヘッダーはツリー。選択したアセットパスをルート行にし、フォルダーとPrefabを枝記号で表示。
表示するフォルダー見つかったPrefabの保存階層に必要なフォルダー。Prefabがない空フォルダーは省略。
並び順フォルダーを先、Prefabを後とし、それぞれOrdinalの文字列順。自然順やExplorerと同じ並びを保証しない。
取得しない情報Prefab内部のGameObject、コンポーネント、ファイルサイズ、更新日時、参照関係は出力しない。

次の画像は旧版の表示例です。修正版では選択フォルダーのアセットパスをルート行へ入れ、Prefabに関係するフォルダーだけを残します。旧画像と同じ行が出るとは限りません。

選択したフォルダ以下のPrefab構造をツリー形式のCSVとして出力するUnityエディタ拡張ツールの概要を示す図。

Assets/Editorへコードを保存する

  1. プロジェクトを複製し、Assets/Editor/PrefabTreeCsvExporter.csへ下のコード全体を保存します。GameObjectへ追加するコンポーネントではありません。
  2. 旧PrefabTreeCsvExporterがある場合は置き換え、同じクラスやメニューを2つ残さないようにします。
  3. asmdefを使う構成ではEditor専用Assemblyへ入れ、UnityEditorをPlayer用へ含めない設定を確認します。Editorフォルダーの公式説明も参照してください。
  4. Unityへ戻りConsoleのコンパイルエラーを確認します。Tools → 選択フォルダ以下のPrefabツリー構造をCSV出力のメニューが出るか確認します。

フォルダーを選び、保存結果を確認する

  1. 最初はAssets内の小さなフォルダーを用意し、直下と子フォルダーに1つずつPrefabを保存します。Prefabがないフォルダーも作ると、省略される条件を確認できます。
  2. Projectでそのフォルダー1つだけを選択します。HierarchyのGameObject、Prefabファイル、複数フォルダーの選択は、この例の入力にしません。
  3. メニューを実行し、保存ダイアログでCSVの保存先とファイル名を指定します。旧版の保存先フォルダー選択から、ファイル保存ダイアログへ変更しています。キャンセルすると保存せず終了します。
  4. ConsoleのPrefab件数と、CSVのPrefabファイル行を照合します。CSVにはヘッダーやフォルダー行もあるため、全行数をPrefab件数として数えません。
  5. 日本語やカンマを含む名前、複数の枝、同名Prefabの異なる保存先などを加えて、取込後も1列のツリーとして読めるか確認します。

例えばAssets/PrefabsにA.prefabとSub/B.prefabがある場合、修正版はAssets/Prefabsをルートにして、Subの下にB.prefab、直下にA.prefabを配置する構成です。これは構成したパスを用いた整形確認の例で、実PrefabをUnityから出力した検証画像ではありません。

旧版の出力資料との違い

次のコードブロックは、元記事にあった旧ツリー例を保存したものです。旧版は最上位を/として選択フォルダー名を省略し、Prefabのないフォルダーも走査していました。修正版の正確な出力例としては使いません。

/
└─ Prefabs
    └─ sample
        ├─ sample 1.prefab
        ├─ sample 2.prefab
        └─ sample 3.prefab

元記事の画像も旧版の資料として残しています。今回の修正版では保存ダイアログとルート行、空フォルダーの扱いを変更しています。

ツールの実行によって指定先の保存先フォルダに日付入りのCSVファイルが出力された様子。

修正版コード全文

PrefabのパスはAssetDatabaseから取得します。Application.dataPath全体からAssetsという文字を削除する処理や、物理ディレクトリの再帰探索は使いません。Prefabの内部も読み込まず、見つかったアセットパスからツリーを構築します。

using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;

public static class PrefabTreeCsvExporter
{
    [MenuItem("Tools/選択フォルダ以下のPrefabツリー構造をCSV出力")]
    public static void Export()
    {
        UnityEngine.Object[] selected = Selection.objects;
        if (selected.Length != 1)
        {
            Debug.LogWarning("ProjectでAssets内のフォルダーを1つだけ選択してください。");
            return;
        }
        string folder = AssetDatabase.GetAssetPath(selected[0]);
        if (!AssetDatabase.IsValidFolder(folder) || !(folder == "Assets" || folder.StartsWith("Assets/", StringComparison.Ordinal)))
        {
            Debug.LogWarning("Assets内のフォルダーを1つ選択してください。");
            return;
        }
        var paths = new SortedSet<string>(StringComparer.Ordinal);
        foreach (string guid in AssetDatabase.FindAssets("t:Prefab", new[] { folder }))
        {
            string path = AssetDatabase.GUIDToAssetPath(guid);
            if (path.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) paths.Add(path);
        }
        if (paths.Count == 0)
        {
            Debug.LogWarning("選択フォルダー以下に.prefabが見つかりませんでした。");
            return;
        }
        string savePath = EditorUtility.SaveFilePanel("PrefabツリーのCSVを保存", "", "Prefab_Tree_" + DateTime.Now.ToString("yyyyMMdd_HHmmss"), "csv");
        if (string.IsNullOrEmpty(savePath)) return;
        try
        {
            List<string> lines = PrefabTreeCsvFormatting.BuildLines(folder, paths);
            var csv = new StringBuilder();
            csv.Append(PrefabTreeCsvFormatting.Csv("ツリー") + "\r\n");
            foreach (string line in lines) csv.Append(PrefabTreeCsvFormatting.Csv(line) + "\r\n");
            File.WriteAllText(savePath, csv.ToString(), new UTF8Encoding(true));
            Debug.Log("PrefabツリーCSV保存完了: Prefab " + paths.Count + "件\n" + savePath);
        }
        catch (Exception e) { Debug.LogError("PrefabツリーCSV保存失敗: " + savePath + "\n" + e); }
    }
}

public static class PrefabTreeCsvFormatting
{
    private sealed class Node
    {
        public readonly SortedDictionary<string, Node> Folders = new SortedDictionary<string, Node>(StringComparer.Ordinal);
        public readonly SortedSet<string> Files = new SortedSet<string>(StringComparer.Ordinal);
    }
    public static string Csv(string value)
    {
        return "\"" + (value ?? "").Replace("\"", "\"\"") + "\"";
    }
    public static List<string> BuildLines(string rootPath, IEnumerable<string> paths)
    {
        if (string.IsNullOrEmpty(rootPath)) throw new ArgumentException("ルートを指定してください。");
        rootPath = rootPath.TrimEnd('/');
        var root = new Node();
        foreach (string path in paths)
        {
            if (string.IsNullOrEmpty(path) || !path.StartsWith(rootPath + "/", StringComparison.Ordinal)
                || !path.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) continue;
            string[] parts = path.Substring(rootPath.Length + 1).Split('/');
            if (parts.Length > 257) throw new InvalidOperationException("保存階層が深さ256を超えました。");
            Node node = root;
            for (int i = 0; i < parts.Length - 1; i++)
            {
                Node next;
                if (!node.Folders.TryGetValue(parts[i], out next))
                {
                    next = new Node();
                    node.Folders.Add(parts[i], next);
                }
                node = next;
            }
            node.Files.Add(parts[parts.Length - 1]);
        }
        var lines = new List<string> { rootPath };
        Render(root, "", lines);
        return lines;
    }
    private static void Render(Node node, string indent, List<string> lines)
    {
        int count = node.Folders.Count + node.Files.Count;
        int index = 0;
        foreach (KeyValuePair<string, Node> folder in node.Folders)
        {
            bool last = ++index == count;
            lines.Add(indent + (last ? "└─ " : "├─ ") + folder.Key);
            Render(folder.Value, indent + (last ? "    " : "│   "), lines);
        }
        foreach (string file in node.Files)
            lines.Add(indent + (++index == count ? "└─ " : "├─ ") + file);
    }
}

ツリー構築とCSV整形で確認した範囲

掲載した整形クラスを.NETでコンパイルし、直下のソート・重複除外、子と兄弟、フォルダーの連鎖、範囲外と非Prefabの除外、日本語・カンマ、空入力の6ケースを確認しました。深さの制限を超える構成したパスがエラーになることも確認しています。

CSVは1行を一つのフィールドとして引用します。名前にカンマ、引用符、改行がある構成した10入力で、C#が作ったフィールドをPythonのCSV読取で元へ戻せることを確認しました。この確認にUnityの検索・出力操作や表計算ソフトの画面操作は含めていません。

ファイルはUTF-8のBOM付きで保存するコードです。表計算ソフトではCSV取込を使い、文字コードと列の型を確認します。CSVは表計算のブック形式ではなく、ツリー文字列を1列に置いた形式です。名前や枝記号が勝手に変わる場合は取込時の自動変換を調べます。

メニュー・検索・保存でつまずく場合

項目・症状内容・確認先
メニューが見つからないConsoleのコンパイルエラー、Editor専用の配置、.cs拡張子、旧コードの重複を確認。
選択の警告が出るProjectでAssets内のフォルダーを1つだけ選ぶ。ファイルや複数フォルダー、Packagesを混ぜない。
Prefabが見つからない選択範囲に.prefabがあるか、Importされているかを確認。FindAssetsの検索範囲は指定フォルダーと子。
フォルダーが出力にないPrefabがない空フォルダーは省略する仕様。全フォルダーの棚卸しとは分ける。
保存できないConsoleの保存失敗と例外、保存先の権限・ファイル利用状態を確認。保存失敗を成功として表示しない。
CSVが複数列になる全フィールドの引用を保って読む。単純なカンマSplitではなくCSVの読取・取込機能を使う。
内部のGameObjectが見えないこの例は保存階層の出力。Prefab内部CSVの記事へ進む。

検索後にアセットを移動・削除した場合の再取得、非常に大きい一覧の進捗・キャンセル、JSONや参照関係への拡張は含めていません。目的がファイル階層の確認を超える場合は、必要な列と動作を追加する別の設計にします。

既存の元コードの配布先では01.txtが旧例、02.csが旧版のコードです。本文の修正版とは異なり、ここでは外部リポジトリを更新していません。Prefabの内部構造や撮影を扱うEditor拡張は次の記事で確認できます。

エディタ拡張の記事は、ほかにもあります。

次に学ぶ・作業環境を選ぶ

学習を続けたい方や、作業環境を整えたい方は、目的に合うガイドをご覧ください。

Unityのおすすめ書籍

開発PCの選び方

周辺機器の優先度

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次