Unityにおけるプレハブ変更の検出と反映(拡張エディタ)

― PrefabUtility.ApplyPrefabInstance() の活用 ―


1. 概要

Unityでは、プレハブ(Prefab)は再利用可能なオブジェクトとして多用されます。

しかし、シーン上のプレハブインスタンスに加えた変更は、元のプレハブアセットには自動的に反映されません

PrefabUtility.ApplyPrefabInstance() は、これらの変更を元のプレハブに反映(上書き保存)するために使用します。

この機能は、エディタ拡張の中で使用されるEditor専用APIです。


2. 使用タイミングと目的

使用タイミング:

  • シーン内のプレハブに手動またはスクリプトで変更を加えた後
  • その変更をプレハブアセットへ反映したいとき

目的:

  • プレハブアセットを手動編集せずに最新状態に保つ
  • エディタツールから一括でプレハブを更新
  • 作業効率化やミス防止(編集漏れ防止)

3. サンプルコード(Editor拡張)

using UnityEditor;
using UnityEngine;

public class PrefabApplyExample
{
    [MenuItem("Tools/Apply Selected Prefab Changes")]
    public static void ApplySelectedPrefabChanges()
    {
        GameObject selected = Selection.activeGameObject;

        if (selected == null)
        {
            Debug.LogWarning("オブジェクトが選択されていません。");
            return;
        }

        GameObject prefabRoot = PrefabUtility.GetOutermostPrefabInstanceRoot(selected);
        if (prefabRoot != null)
        {
            PrefabUtility.ApplyPrefabInstance(prefabRoot, InteractionMode.UserAction);
            Debug.Log("プレハブに変更を適用しました。");
        }
        else
        {
            Debug.LogWarning("選択されたオブジェクトはプレハブのインスタンスではありません。");
        }
    }
}

4. 他APIとの違い

機能名用途プレハブ反映との関係
PrefabUtility.ApplyPrefabInstance()インスタンスの変更をプレハブアセットに反映〇:本命
PrefabUtility.ApplyModifiedProperties()シリアライズプロパティの反映(SerializedObject)×:プレハブ操作には不向き
GameObject.Instantiate()プレハブの複製(インスタンス生成)×:反映ではなく生成
AssetDatabase.ImportAsset()アセットの再読み込み×:プレハブ反映と無関係

5. 注意点

  • このAPIはUnityEditor 名前空間に属するため、ビルド後のアプリでは使用できません。
  • 複数人で作業する場合、意図しない反映が競合の原因になるため、変更内容の確認やバージョン管理と組み合わせて使うことが重要です。
  • 処理前に Undo.RecordObject() を使用すると、変更を元に戻すことができます。

6. まとめ

  • PrefabUtility.ApplyPrefabInstance() は、シーン上で変更したプレハブをアセットに反映させるためのエディタ専用API
  • 自動ツールやエディタメニューからの一括処理に向いており、変更管理の効率化に役立つ。
  • 使用にはプレハブインスタンスの選択・確認が必要。

以下に、拡張機能付きの GUIエディタウィンドウの完全な実装例を提示します。

本ツールでは、複数選択したプレハブインスタンスを一覧表示し、「適用」「リセット(Revert)」ボタンで操作できるようにしています。


ファイル名例

ExtendedPrefabEditorWindow.cs

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

public class ExtendedPrefabEditorWindow : EditorWindow
{
    private Vector2 scrollPos;
    private List<GameObject> prefabInstances = new List<GameObject>();

    [MenuItem("Tools/Prefab Utility/Advanced Apply & Revert")]
    public static void ShowWindow()
    {
        GetWindow<ExtendedPrefabEditorWindow>("Prefab Apply/Revert");
    }

    private void OnGUI()
    {
        GUILayout.Label("プレハブ変更の一括管理", EditorStyles.boldLabel);

        if (GUILayout.Button("現在の選択からプレハブインスタンスを取得"))
        {
            LoadSelectedPrefabInstances();
        }

        if (prefabInstances.Count == 0)
        {
            EditorGUILayout.HelpBox("プレハブインスタンスが検出されていません。", MessageType.Info);
            return;
        }

        scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
        foreach (var obj in prefabInstances)
        {
            if (obj == null) continue;

            EditorGUILayout.BeginVertical("box");
            EditorGUILayout.LabelField("オブジェクト名:", obj.name);
            EditorGUILayout.ObjectField("参照:", obj, typeof(GameObject), true);

            EditorGUILayout.BeginHorizontal();
            if (GUILayout.Button("適用"))
            {
                ApplyPrefab(obj);
            }
            if (GUILayout.Button("リセット"))
            {
                RevertPrefab(obj);
            }
            EditorGUILayout.EndHorizontal();
            EditorGUILayout.EndVertical();
        }
        EditorGUILayout.EndScrollView();
    }

    private void LoadSelectedPrefabInstances()
    {
        prefabInstances.Clear();

        foreach (var obj in Selection.gameObjects)
        {
            if (PrefabUtility.IsPartOfPrefabInstance(obj))
            {
                var root = PrefabUtility.GetOutermostPrefabInstanceRoot(obj);
                if (!prefabInstances.Contains(root))
                {
                    prefabInstances.Add(root);
                }
            }
        }

        if (prefabInstances.Count == 0)
        {
            EditorUtility.DisplayDialog("情報", "プレハブインスタンスが選択されていません。", "OK");
        }
    }

    private void ApplyPrefab(GameObject obj)
    {
        Undo.RegisterFullObjectHierarchyUndo(obj, "Apply Prefab");
        PrefabUtility.ApplyPrefabInstance(obj, InteractionMode.UserAction);
        Debug.Log($"'{obj.name}' をプレハブに適用しました。");
    }

    private void RevertPrefab(GameObject obj)
    {
        Undo.RegisterFullObjectHierarchyUndo(obj, "Revert Prefab");
        PrefabUtility.RevertPrefabInstance(obj);
        Debug.Log($"'{obj.name}' をプレハブの状態に戻しました。");
    }
}

機能一覧

機能説明
Selection.gameObjects複数選択されたGameObjectの取得
PrefabUtility.IsPartOfPrefabInstance()プレハブかどうかを判定
ApplyPrefab()プレハブインスタンスの変更をアセットに適用
RevertPrefab()プレハブインスタンスを元の状態に戻す
Undo.RegisterFullObjectHierarchyUndo()Undo(元に戻す)機能に対応

使用手順

  1. Hierarchyで複数のプレハブインスタンスを選択
  2. メニューから「Tools > Prefab Utility > Advanced Apply & Revert」を開く
  3. 「現在の選択からプレハブインスタンスを取得」ボタンを押す
  4. 各オブジェクトごとに「適用」または「リセット」ボタンを押す

今後の拡張候補

  • 差分の検出と警告(未適用変更があるかどうか)
  • AssetDatabase.SaveAssets() との連携
  • 選択中のプレハブの情報ログ出力(アセットパスなど)
  • 「全適用」や「全リセット」ボタン

C#

Posted by hidepon