Programming Serendipity

プログラミングを中心に種々雑多に書き留めます

特定のプレハブを全てのシーンにコピーするエディタ拡張(ならびにHideFlagsの使い方)

続きからどうぞ。

Tools > Copy To All Scenesで出てきたウインドウにプレハブをDDしてボタンを押せば、BuildSettingに入ってるシーン全てにプレハブをコピーします。

using UnityEngine;
using UnityEditor;
using UnityEngine.UI;
using UnityEditor.SceneManagement;

public class ScriptableGameObject : ScriptableObject
{
    public GameObject prefab;
}

[ExecuteInEditMode]
public class CopyToAllScenes : EditorWindow
{
    static SerializedProperty sp;

    static GUIStyle style = new GUIStyle();

    [MenuItem("Tools/Copy To All Scenes #%&c")]
    public static void ShowWindow()
    {
        var window = GetWindow(typeof(CopyToAllScenes), true, "Copy to all scenes");
        window.minSize = new Vector2(400, 100);

        var obj = CreateInstance<ScriptableGameObject>();
        obj.hideFlags = HideFlags.DontSave; // 追記:シーン切り替えで破棄されないように設定
        var serializedObject = new SerializedObject(obj);

        sp = serializedObject.FindProperty("prefab");

        // set gui style
        style.alignment = TextAnchor.MiddleCenter;
        style.fontStyle = FontStyle.Bold;
        style.border = new RectOffset(5, 5, 5, 5);
        style.normal.textColor = Color.green;
    }

    /// <summary>
    /// プレハブかどうかを判定。インスタンスかされていないプロジェクトビュー上のプレハブはこれで検知できる
    /// </summary>
    bool IsPrefab(Object obj)
    {
        return PrefabUtility.GetPrefabParent(obj) == null && PrefabUtility.GetPrefabObject(obj) != null;
    }

    void OnGUI()
    {
        GUILayout.Space(10);
        GUILayout.Label("Select the prefab you want to copy to all scenes", style);
        GUILayout.Space(10);
        EditorGUILayout.PropertyField(sp);
        GUILayout.Space(10);
        if (GUILayout.Button("Copy this prefab to all scenes"))
        {
            // プレハブでなければコピーしない
            if (!IsPrefab(sp.objectReferenceValue))
            {
                Debug.LogFormat("'{0}' is not a prefab. No copying performed.", sp.objectReferenceValue.name);
                return;
            }

            // 今のシーンが編集中の場合、他のシーンを開くと変更が破棄されてしまうため、セーブしておく
            if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
            {
                Debug.Log("Process cancelled.");
                return;
            }

            // 今開いているシーンを覚えておく
            var currentScenePath = EditorSceneManager.GetActiveScene().path;

            // 全てのシーンに対応
            var scenes = EditorBuildSettings.scenes;
            var copiedCount = 0;
            var skippedCount = 0;
            foreach (var scene in scenes)
            {
                EditorSceneManager.OpenScene(scene.path);

                // 存在していれば生成しない
                if (GameObject.Find(sp.objectReferenceValue.name) != null)
                {
                    Debug.LogWarningFormat("'{0}' already exists in the scene '{1}'.", sp.objectReferenceValue.name, scene.path);
                    skippedCount++;
                    continue;
                }

                // 生成してセーブ
                PrefabUtility.InstantiatePrefab(sp.objectReferenceValue);
                copiedCount++;
                EditorSceneManager.MarkAllScenesDirty();
                EditorSceneManager.SaveOpenScenes();
            }

            Debug.LogFormat("'{0}' was copied:{1}, already exists:{2}, total:{3}", sp.objectReferenceValue.name, copiedCount, skippedCount, scenes.Length);

            // 最後に、実行前に開いていたシーンを開く
            EditorSceneManager.OpenScene(currentScenePath);
        }
    }
}

こんな感じです。特に書くこともなくコードのままですが、ちなみに。

エディタメニューのショートカットの制御キーの覚え方

#%&がShift,Ctrl,Altに対応しているので、日本語キーボードなら左から順番に356と覚えれば忘れません。

追記:Type Mismatchエラーについて

このスクリプト、追記部分のhideFlagsの設定をしていないと、実行し終わったあとにType Mismatchと表示されてプレハブを受け付けなくなってしまいます。 これは、SctiptableObjectGameObject同様、シーンの切り替えで破棄されてしまうためです。 実はこれ、バグとしてUnityに報告していたのですが、"By Design"という返事が返ってきて、事情を教えていただきました。そのとき紹介してもらった記事も張っておきます。

blogs.unity3d.com

そこで、HideFlagsDontSaveを登録することでシーンが切り替わっても破棄されないようにすることでエラーが出なくなります。 リファレンスを見る限り、なんとも名前付けを失敗している気がしますが…HideFlagsというより、HideDestroyFlagsDontSaveというよりDontDestroyOnSceneLoadingという気がしますが…。 ちなみに、一般的にはScriptableObjectにはHideAndDontSaveを指定するのがセオリーらしいですが、このケースではそうやるとドラッグアンドドロップを受け付けなくなってしまうので、DontSaveでよさそうです。