📂

[Unity]Hierarchyのパスへのコピー/展開対応をしました

2024/06/03に公開

はじめに

Unityでの開発で、特定のオブジェクトの修正をデザイナーさんから依頼されることがありました。
オブジェクトのパスをコピー/展開できるようにして開発を少し楽にできるようにしました。

動作環境

Unity 2022.3.14f1
UnityCsReferenceの内部メソッドを使っているので、バージョンが変われば動かなくなる可能性があります。

対応のきっかけ


画像のLeftButtonのラベルのサイズを大きくして欲しいという依頼があったとします。

自分の探し方としては、アプリを実行してInspectorでチェックを外して、どこにあるかを探しています。

以前はHierarchy上で場所が特定できたら画面のスクリーンショットを撮って、アプリを停止させてからパスを開いてました。

しかしスクリーンショットと見比べてパスを開いていくのは大変でした。
そこでパスをコピーできないかと考えました。

パスのコピー

https://hacchi-man.hatenablog.com/entry/2020/11/10/220000
こちらを参考にさせていただき対応しました。ありがとうございます。

先頭にシーン名を追加するようにして使っています。

public static class CopyHierarchyPath
{
    // コンテキストメニューの先頭にでるように
    [MenuItem("GameObject/Copy Path", false, int.MinValue)]
    private static void CopyPath()
    {
        // アクティブなGameObjectを取得
        var active = Selection.activeGameObject;
        if (active == null) return;
        
        // 親を辿っていく
        var builder = new StringBuilder(active.transform.name);
        var current = active.transform.parent;

        while (current != null)
        {
            builder.Insert(0, $"{current.name}/");
            current = current.parent;
        }
        // シーン名もパスの先頭にいれておく
        builder.Insert(0, $"{active.scene.name}/");

        // クリップボードへ入れる
        GUIUtility.systemCopyBuffer = builder.ToString();
        
        Debug.Log($"パスをコピーしました:{GUIUtility.systemCopyBuffer}");
    }
}

パスのペースト

パスのコピーができたので、今度はそれをHierarchy上でペーストしたらパスが開いてくれたら楽なんじゃないかと考えて対応しました。
こちらはUnityCsReferenceの以下のコードを読んで参考にしました。

https://github.com/Unity-Technologies/UnityCsReference/blob/2022.3/Editor/Mono/SceneHierarchy.cs

public static class ExpandHierarchyPath
{
    // Copy Pathの次にでるように
    [MenuItem("GameObject/Expand Path", false, int.MinValue + 1)]
    static void ExpandPath()
    {
        // クリップボードからパスを取得
        var path = GUIUtility.systemCopyBuffer;
        if (string.IsNullOrEmpty(path)) return;

        Debug.Log($"パスから展開します:{path}");

        // 先頭にはシーン名が入っているので取得して開く
        var pathQueue = new Queue<string>(path.Split("/").ToList());
        var sceneName = pathQueue.Dequeue();
        var scene     = SceneManager.GetSceneByName(sceneName);

        // シーンを開く
        scene = OpenScene(scene, sceneName);
        ExpandScene(scene.name);

        // パスを展開する
        var transforms = scene.GetRootGameObjects().Select(gameObject => gameObject.transform).ToList();
        ExpandObject(transforms, pathQueue);
    }

    private static Scene OpenScene(Scene scene, string sceneName)
    {
        if (!scene.IsValid())
        {
            // Hierarchy上にシーンがなければ、Assets/Scenes/の下からシーンを探して追加モードで開く
            scene = EditorSceneManager.OpenScene($"Assets/Scenes/{sceneName}.unity", OpenSceneMode.Additive);
        }

        return scene;
    }

    private static void ExpandScene(string name)
    {
        var sceneHierarchy = GetSceneHierarchy();
        var methodInfo     = sceneHierarchy.GetType().GetMethod("SetScenesExpanded", BindingFlags.NonPublic | BindingFlags.Instance);
        var sceneNames     = new List<string>{ name };
        methodInfo.Invoke(sceneHierarchy, new object[] { sceneNames });
    }

    private static void ExpandObject(IEnumerable<Transform> transforms, Queue<string> pathQueue)
    {
        if (!pathQueue.TryDequeue(out var childObjectName)) return;
        foreach (var transform in transforms.Where(transform => childObjectName == transform.name))
        {
            Selection.activeGameObject = transform.gameObject;
            ExpandTreeViewItem(transform);
            // 非アクティブなコンポーネントも取得するように
            ExpandObject(transform.GetComponentsInChildren<Transform>(true).ToList(), pathQueue);
        }
    }

    private static void ExpandTreeViewItem(Component transform)
    {
        var sceneHierarchy = GetSceneHierarchy();
        var methodInfo     = sceneHierarchy.GetType().GetMethod("ExpandTreeViewItem", BindingFlags.NonPublic | BindingFlags.Instance);
        methodInfo.Invoke(sceneHierarchy, new object[] { transform.gameObject.GetInstanceID(), true });
    }

    private static object GetSceneHierarchy()
    {
        var type = typeof(EditorWindow).Assembly.GetType("UnityEditor.SceneHierarchyWindow");
        var property = type.GetProperty("sceneHierarchy");
        return property.GetValue(GetHierarchyWindow());
    }

    private static EditorWindow GetHierarchyWindow ()
    {
        EditorApplication.ExecuteMenuItem("Window/General/Hierarchy");
        return EditorWindow.focusedWindow;
    }
}

動作確認


Sample/Canvas/SafeArea/RightArea/Buttons/Layout/LeftButton/Label をアプリ実行中コピーして、アプリを停止させてから開きました。

まとめ

マルチシーンでの環境を想定しています。
また「Assets/Scenes/{sceneName}.unity」にシーンがあることを前提にしていたり、OpenSceneMode.Additiveで追加モード固定になっていたりします。

同じ階層に同じ名前のオブジェクトがあると誤作動するのですが、運用でカバーしています。
本当はインスタンスIDでパスを作ると良いのだと思いますが、文字列がわけがわからなくなるのでやめました。

パスを
Sample_(インスタンスID)/Canvas_(インスタンスID)/SafeArea_(インスタンスID)/...
のような形にしてインスタンスIDを含めることも検討したのですが、現状で使用に困ることはなかったのでそのままとしています。

開発時の対応が少し楽になったので作って良かったと思います。

Happy Elements

Discussion