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

UnityのPrefab内部をCSV出力するEditor拡張:階層・コンポーネント一覧

Prefabの中にあるGameObjectとコンポーネントを一覧で確認するために、選択フォルダー以下の.prefabを検索し、内部の階層を5列のCSVへ書き出すEditor拡張を掲載します。Prefabの保存先フォルダーだけを一覧にしたい場合は、ファイル階層のツリー出力を使います。

仕様の参照はUnity 6.3 LTSです。本文のCSV整形部分はWindowsの.NET環境で確認しましたが、Unity Editorでのコンパイル、Prefab読込、Variant・入れ子Prefabの動作は未確認です。元記事の画像は旧版の資料で、今回の修正版を実行した画像ではありません。

目次

CSVに出力する5列と対象範囲

項目・症状内容・確認先
Prefab名Prefabファイルの拡張子を除いた名前。同名の別Prefabは次のPrefabパスで区別。
PrefabパスAssetsから始まるアセットパス。選択フォルダーと子フォルダーの.prefabが対象。
階層(兄弟番号付き)各GameObjectの名前と兄弟番号。例:Hero[0]/Body[0]。名前の/、%、[、]は%表記にして区切りと区別。
GameObject名GameObjectの元の名前。階層列で使った%表記へ置換しない。
コンポーネント各GameObjectのコンポーネント型を名前空間付きで列挙。nullは[Missing Script]として残す。各プロパティの設定値は取得しない。

選択できる対象はAssets内のフォルダーです。複数フォルダーや親子を同時に選んだ場合は、見つかったアセットパスを重複除外します。Scene内の配置、Packages内のアセット、FBXなどのModel Prefabは、この.prefab限定の例の対象に含めません。非Activeの子もTransformをたどる構成です。

次の画像は元記事の出力資料です。修正版では兄弟番号、名前空間付きの型、Missing Scriptの記録などを加えているため、列の内容が同じとは限りません。

指定したフォルダ内のPrefabから階層情報を抽出してCSVへ書き出すエディタ拡張ツールの概要を示す画面。

導入:EditorフォルダーへC#として保存する

  1. 最初はプロジェクトの複製で試します。Assets/Editorを作り、下のコード全体をPrefabHierarchyExporter.csとして保存します。GameObjectへ付けるコンポーネントではなく、Editorのメニューから呼ぶコードです。
  2. 旧ExportAllPrefabsWithPathのコードを置き換える場合は、同じメニューを持つ旧スクリプトを並行して残さないようにします。重複したクラスやメニューがないかConsoleで確認します。
  3. asmdefを使うプロジェクトではEditor専用のAssemblyへ置きます。UnityEditorをPlayer用Assemblyへ入れない構成にします。
  4. Unityへ戻り、Consoleのコンパイルエラーを確認します。Tools → Export Prefabs (with Path)が出ることを確認してから、対象フォルダーを選びます。

Editorフォルダーの役割はUnity公式の予約フォルダー名を参照します。旧配布先の01.phpは中身がC#で、PHPとして導入するファイルではありません。修正版はここに掲載した内容を.csで保存してください。

最小のPrefabで出力を確認する

  1. Assets内に確認用フォルダーを作り、ルートと子を持つ小さなPrefabを1つ保存します。最初は日本語やカンマなどを含まない名前で列と対象数を確認します。
  2. Projectでそのフォルダーを選択します。HierarchyのGameObjectや.prefabファイル自体を選ぶ操作とは区別します。
  3. Tools → Export Prefabs (with Path)を実行し、保存ダイアログでCSVの保存先とファイル名を指定します。キャンセルすると保存せず終了します。既存ファイルへの保存では上書きの選択を確認してください。
  4. Consoleの成功・失敗件数とCSVを確認します。ルートと子それぞれに1行ずつあり、ヘッダーを除いたGameObject行数がログと一致するか調べます。
  5. その後に複数Prefab、同名の兄弟、日本語、カンマ、引用符、Missing Script、非Activeの子、Variant・入れ子Prefabなどを加えて確認します。

元記事のUnity画面も旧資料です。修正版はウィンドウを開く方式ではなく、フォルダーを選んでメニューから保存ダイアログを開きます。

Prefab情報書き出しツールのパッケージやスクリプトをUnityに取り込んだ直後の状態を示す画面。

修正版コード全文

各Prefabの行は一時的にまとめ、読取と解放が成功したものだけCSVへ追加します。読込や走査の途中で失敗したPrefabの部分的な行を、成功結果に混ぜない構成です。深さ256を超える階層はエラーとして記録します。

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

public static class PrefabHierarchyExporter
{
    [MenuItem("Tools/Export Prefabs (with Path)")]
    public static void ExportPrefabs()
    {
        UnityEngine.Object[] selected = Selection.objects;
        if (selected.Length == 0)
        {
            Debug.LogWarning("ProjectでAssets内のフォルダーを選択してください。");
            return;
        }
        var folders = new HashSet<string>(StringComparer.Ordinal);
        foreach (UnityEngine.Object item in selected)
        {
            string path = AssetDatabase.GetAssetPath(item);
            if (!AssetDatabase.IsValidFolder(path) || !(path == "Assets" || path.StartsWith("Assets/", StringComparison.Ordinal)))
            {
                Debug.LogWarning("選択対象はAssets内のフォルダーだけにしてください。");
                return;
            }
            folders.Add(path);
        }
        var uniquePaths = new SortedSet<string>(StringComparer.Ordinal);
        foreach (string folder in folders)
        {
            foreach (string guid in AssetDatabase.FindAssets("t:Prefab", new[] { folder }))
            {
                string path = AssetDatabase.GUIDToAssetPath(guid);
                if (path.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) uniquePaths.Add(path);
            }
        }
        if (uniquePaths.Count == 0)
        {
            Debug.LogWarning("選択フォルダー以下に.prefabが見つかりませんでした。");
            return;
        }
        string savePath = EditorUtility.SaveFilePanel("CSVとして保存", "", "prefab_structure_" + DateTime.Now.ToString("yyyyMMdd_HHmmss"), "csv");
        if (string.IsNullOrEmpty(savePath)) return;

        var csv = new StringBuilder();
        csv.Append(PrefabHierarchyCsvFormatting.Row("Prefab名", "Prefabパス", "階層(兄弟番号付き)", "GameObject名", "コンポーネント"));
        int success = 0;
        int failed = 0;
        int rowCount = 0;
        foreach (string path in uniquePaths)
        {
            GameObject root = null;
            var perPrefab = new StringBuilder();
            int perPrefabRows = 0;
            bool loaded = false;
            try
            {
                root = PrefabUtility.LoadPrefabContents(path);
                if (root == null) throw new InvalidOperationException("Prefabのルートを取得できませんでした。");
                Traverse(root.transform, Path.GetFileNameWithoutExtension(path), path,
                    PrefabHierarchyCsvFormatting.Segment(root.name, 0), perPrefab, 0, ref perPrefabRows);
                loaded = true;
            }
            catch (Exception e)
            {
                Debug.LogError("Prefabの読取失敗: " + path + "\n" + e);
            }
            finally
            {
                if (root != null)
                {
                    try { PrefabUtility.UnloadPrefabContents(root); }
                    catch (Exception e)
                    {
                        loaded = false;
                        Debug.LogError("Prefabの解放失敗: " + path + "\n" + e);
                    }
                }
            }
            if (loaded)
            {
                csv.Append(perPrefab);
                rowCount += perPrefabRows;
                success++;
            }
            else failed++;
        }
        if (success == 0)
        {
            Debug.LogWarning("全Prefabの読取に失敗したため、CSVは保存していません。");
            return;
        }
        try
        {
            File.WriteAllText(savePath, csv.ToString(), new UTF8Encoding(true));
            Debug.Log("CSV保存完了: 候補 " + uniquePaths.Count + " / 成功 " + success + " / 失敗 " + failed
                + " / GameObject行 " + rowCount + "\n" + savePath);
        }
        catch (Exception e) { Debug.LogError("CSV保存失敗: " + savePath + "\n" + e); }
    }

    private static void Traverse(Transform node, string prefabName, string assetPath, string hierarchy,
        StringBuilder csv, int depth, ref int rowCount)
    {
        if (depth > 256) throw new InvalidOperationException("階層が深さ256を超えました。");
        var components = new List<string>();
        foreach (Component component in node.GetComponents<Component>())
            components.Add(component == null ? "[Missing Script]" : component.GetType().FullName);
        csv.Append(PrefabHierarchyCsvFormatting.Row(prefabName, assetPath, hierarchy, node.name, string.Join(", ", components)));
        rowCount++;
        foreach (Transform child in node)
            Traverse(child, prefabName, assetPath, hierarchy + "/" + PrefabHierarchyCsvFormatting.Segment(child.name, child.GetSiblingIndex()),
                csv, depth + 1, ref rowCount);
    }
}

public static class PrefabHierarchyCsvFormatting
{
    public static string Csv(string value)
    {
        return "\"" + (value ?? "").Replace("\"", "\"\"") + "\"";
    }
    public static string Row(params string[] values)
    {
        var fields = new string[values.Length];
        for (int i = 0; i < values.Length; i++) fields[i] = Csv(values[i]);
        return string.Join(",", fields) + "\r\n";
    }
    public static string Segment(string name, int siblingIndex)
    {
        return (name ?? "").Replace("%", "%25").Replace("/", "%2F").Replace("[", "%5B").Replace("]", "%5D")
            + "[" + siblingIndex + "]";
    }
}

読込・解放・失敗件数の扱い

LoadPrefabContentsはPrefabを隔離したSceneへ読み込みます。この例はPrefabを保存するAPIを呼ばず、UnloadPrefabContentsをfinallyで呼んで解放を試みます。解放に失敗した場合も対象パスと例外をConsoleへ記録します。

候補件数、成功件数、失敗件数、GameObject行数は別の数です。失敗があればCSVは成功したPrefabだけの結果になります。全件失敗ならCSVを保存しません。保存処理そのものが失敗した場合も、Consoleの例外と保存先を確認してください。

Prefab内の独自スクリプトやEditor処理を含む実環境の動作まで確認した例ではありません。最初の複製で、処理後のScene・Prefab・Consoleを確認します。自動保存を行うEditPrefabContentsScopeへ置き換えると処理の性質が変わるため、この読取例では使いません。

カンマ・引用符・改行とCSVの読み込み

全フィールドを二重引用符で囲み、中の二重引用符は2つへ重ねる処理を入れています。これにより、名前にカンマや改行があっても一つのCSVフィールドとして扱えます。記法はRFC 4180のCSV形式を参考にしています。ファイルはUTF-8のBOM付きで保存するコードです。

確認した範囲は、掲載コードから取り出した整形クラスを.NETでコンパイルし、空文字、日本語、カンマ、引用符、改行など10種類の構成した名前をCSVへ変換し、PythonのCSV読取で元の5列に戻ることです。Prefabの抽出やExcel上の操作を実行した結果ではありません。

表計算ソフトではCSVの取込機能を使い、必要な列を文字列として指定します。二重引用符で囲むことだけでは、数字・日付への変換や先頭の=などを式と解釈する処理は防げません。表示が変わったときは保存内容と取込条件を分けて確認してください。

メニューや出力が期待どおりにならない場合

項目・症状内容・確認先
メニューが見つからないConsoleのエラー、.cs拡張子、EditorフォルダーまたはEditor専用asmdef、旧コードとの重複を確認。
フォルダー選択の警告ProjectでAssets内のフォルダーだけを選択。Sceneやファイル、Packagesを混ぜない。
Prefabが見つからない検索範囲と.prefab拡張子を確認。FindAssetsの仕様では指定フォルダーと子を検索する。
一部のPrefabが出力されないConsoleの失敗パス、読込・階層・解放の例外を確認。候補数だけで全件成功と判断しない。
同じ名前の行を区別できないPrefabパスと兄弟番号付き階層を使う。兄弟の順序が変われば番号も変わるため、永続IDとは扱わない。
文字化け・列ずれUTF-8とCSVの取込条件、引用符を保持しているか確認。単純なカンマSplitで読み込まない。

コンポーネントの型は一覧にしますが、設定不備を自動で判定する機能は含めていません。プロパティ検査へ広げる場合は、必要な項目と判定条件を追加する別の実装が必要です。

既存の元コードの配布先には原版の01.phpがあり、本文の修正版とは異なります。ここでは外部リポジトリを更新していません。ファイル配置のツリーや撮影など関連するEditor拡張は次の記事で確認できます。

Unityのエディタ拡張に関する他の解説記事もあわせて参考にしてください。

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

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

Unityのおすすめ書籍

開発PCの選び方

周辺機器の優先度

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