LoginSignup
1
1

More than 1 year has passed since last update.

RectTransformをコピペするEditor拡張作ってみた

Last updated at Posted at 2022-10-05

はじめに

Unity2021から、下図のように CopyComponent や PasteComponentValue が階層化され、実行までの手間が増えている状況。
開発作業時に RectTransform のコピーを頻繁に実行するため、楽にコピペ出来るようにEditor拡張を作ってみた。
Unity2021 Editorスクショ

詳細

作業環境

  • Unity 2021.3.6f1
  • Unity 2020.3.20f1

操作手順

  1. コピー元(From)とコピー先(To)の RectTransform がアタッチされている GameObject を2つ選択
  2. キーボードの Ctrl + 右クリック にて、動作項目が表示されたウインドウを表示
    ※選択数が2つという条件がウインドウ表示の条件となっている
  3. 選択した GameObject の From と To を確認して、実行したい動作項目をクリックすることで RectTransform のコピペ実行
    実行完了後に Debug ログを出力
  4. 元に戻したい場合は、キーボードの Ctrl + Zキー で Undo

動作イメージ

Image(1) の RectTransform を Image(2) の RectTransform へ コピペする動作イメージ
動作イメージ

コード

コードを表示
CopyRectTransformHierarchyWindowCustomMenu.cs
using System.Collections.Generic;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;

namespace Furyu.Framework.Editor
{
    /// <summary>
    /// HierarchyWindowCustomMenuでのRectTransformのコピー
    /// <para>
    /// RectTransformをコピーしたい対象のGameObjectをHierarchyから選択し
    /// Ctrl+右クリックで実行したい操作のメニューを選択する
    /// FromのGameObjectのRectTransformをToのGameObjectへ適用する
    /// (つまりRectTransformが更新される側はToのGameObjectとなる)
    /// </para>
    /// </summary>
    public class CopyRectTransformHierarchyWindowCustomMenu
    {
        /// <summary>
        /// カスタムメニュー名フォーマット
        /// </summary>
        private const string CustomMenuNameFormat = "Copy RectTransform From [ {0} ] To [ {1} ]";

        /// <summary>
        /// 処理対象のGameObject数
        /// </summary>
        private const int TargetGameObjectsCount = 2;

        /// <summary>
        /// 実行対象のGameObjectリスト
        /// </summary>
        private static List<GameObject> targetGameObjects = new List<GameObject>();

        /// <summary>
        /// カスタムメニューの項目が選択された時のコールバック
        /// </summary>
        /// <param name="userData"></param>
        /// <param name="options"></param>
        /// <param name="selected"></param>
        private static void Callback(object userData, string[] options, int selected)
        {
            var (rectTransform1, rectTransform2) = GetSelectedGameObjectRectTransforms();

            switch (selected)
            {
                case 0:
                    // from Selection.gameObjects[0] to Selection.gameObjects[1]
                    CopyRectTransform(rectTransform1, rectTransform2);
                    break;
                case 1:
                    // from Selection.gameObjects[1] to Selection.gameObjects[0]
                    CopyRectTransform(rectTransform2, rectTransform1);
                    break;
            }

            Debug.Log($"Execute - {options[selected]}.");
        }

        /// <summary>
        /// 指定RectTransformのコピー
        /// </summary>
        /// <param name="copyFrom"></param>
        /// <param name="copyTo"></param>
        private static void CopyRectTransform(RectTransform copyFrom, RectTransform copyTo)
        {
            ReplaceComponentsIfDifferent<RectTransform>(copyFrom.gameObject, copyTo.gameObject);
        }

        /// <summary>
        /// 選択されたGameObjectのRectTransformの名前を取得する
        /// 同名の場合はParentの名前をアンダーバーで結合した名前となる
        /// </summary>
        /// <returns></returns>
        private static (string, string) GetSelectedGameObjectRectTransformNames()
        {
            var (rectTransform1, rectTransform2) = GetSelectedGameObjectRectTransforms();

            var rectTransform1Name = rectTransform1.name;
            var rectTransform2Name = rectTransform2.name;
            var rectTransform1Parent = rectTransform1.parent;
            var rectTransform2Parent = rectTransform2.parent;

            // 同名の場合
            while (rectTransform1Name == rectTransform2Name)
            {
                // rootまで到達してしまったら終了
                if (rectTransform1Parent == null || rectTransform2Parent == null)
                {
                    break;
                }

                rectTransform1Name = $"{rectTransform1Parent.name}_{rectTransform1Name}";
                rectTransform2Name = $"{rectTransform2Parent.name}_{rectTransform2Name}";

                rectTransform1Parent = rectTransform1Parent.parent;
                rectTransform2Parent = rectTransform2Parent.parent;
            }

            return (rectTransform1Name, rectTransform2Name);
        }

        /// <summary>
        /// 選択されたGameObjectのRectTransformを取得する
        /// </summary>
        /// <returns></returns>
        private static (RectTransform, RectTransform) GetSelectedGameObjectRectTransforms()
        {
            var (gameObject1, gameObject2) = GetSelectedGameObjects();
            var rectTransform1 = gameObject1.GetComponent<RectTransform>();
            var rectTransform2 = gameObject2.GetComponent<RectTransform>();
            return (rectTransform1, rectTransform2);
        }

        /// <summary>
        /// 選択されたGameObjectを取得する
        /// </summary>
        /// <returns></returns>
        private static (GameObject, GameObject) GetSelectedGameObjects()
        {
            // 処理実行時にSelection.countから取得できるGameObject数がUnity2021と2020で違うため
            // メニュー表示時(targetGameObjectsが空の時)に取得できたGameObject数と同じ場合のみ対象GameObjectとして登録する
            // ・Unity2021.3.6 :メニュー表示時のGameObject数(今回の場合は2)が常に返ってくる
            // ・Unity2020.3.20:メニュー表示時のGameObject数(今回の場合は2)と処理実行時のGameObject数(今回の場合は1)で違う値が返ってくる
            if (Selection.count >= TargetGameObjectsCount && targetGameObjects.Count == 0)
            {
                targetGameObjects.Add(Selection.gameObjects[0]);
                targetGameObjects.Add(Selection.gameObjects[1]);
            }

            return (targetGameObjects[0], targetGameObjects[1]);
        }

        /// <summary>
        /// HierarchyWindowItemOnGUIのデリゲート処理
        /// </summary>
        /// <param name="instanceID"></param>
        /// <param name="selectionRect"></param>
        private static void OnHierarchyWindowItemOnGUI(int instanceID, Rect selectionRect)
        {
            if (Validate() == false)
            {
                return;
            }

            var current = Event.current;

            // Ctrl + 右クリック
            // ※Shift押下により選択数が変わる場合があるためShift押下無しを条件に追加
            if (current.control && current.shift == false && current.type == EventType.MouseDown && current.button == 1)
            {
                targetGameObjects.Clear();

                // 表示するメニュー名の設定
                var (rectTransform1Name, rectTransform2Name) = GetSelectedGameObjectRectTransformNames();
                var options = new GUIContent[]
                {
                    // from Selection.gameObjects[0] to Selection.gameObjects[1]
                    new GUIContent(string.Format(CustomMenuNameFormat, rectTransform1Name, rectTransform2Name)),

                    // from Selection.gameObjects[1] to Selection.gameObjects[0]
                    new GUIContent(string.Format(CustomMenuNameFormat, rectTransform2Name, rectTransform1Name))
                };

                // 表示位置の設定
                // width,heightはoptionsでサイズ可変になるようなので0指定
                var mousePosition = current.mousePosition;
                var position = new Rect(mousePosition.x, mousePosition.y, 0, 0);

                // カスタムメニューの表示
                EditorUtility.DisplayCustomMenu(position, options, -1, Callback, Selection.activeGameObject);
            }
        }

        /// <summary>
        /// 対象GameObjectの指定コンポーネント型の置換
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="src"></param>
        /// <param name="dst"></param>
        private static void ReplaceComponentsIfDifferent<T>(GameObject src, GameObject dst) where T : Component
        {
            // Undo対象を登録
            var dstTargetComponent = dst.GetComponent<T>();
            Undo.RecordObject(dstTargetComponent, $"ReplaceComponentsIfDifferentIn{typeof(T).Name}");

            ComponentUtility.ReplaceComponentsIfDifferent(src, dst, (component) => component is T);
        }

        /// <summary>
        /// HierarchyWindowItemOnGUIのデリゲート設定
        /// </summary>
        [InitializeOnLoadMethod]
        private static void SetDelegate()
        {
            EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyWindowItemOnGUI;
        }

        /// <summary>
        /// カスタムメニュー表示条件の検証
        /// </summary>
        /// <returns></returns>
        private static bool Validate()
        {
            if (Selection.activeTransform == null)
            {
                return false;
            }

            // 選択しているGameObject数は固定値
            if (Selection.gameObjects.Length != TargetGameObjectsCount)
            {
                return false;
            }

            // RectTransformを持つGameObjectのみ対象
            foreach (var gameObject in Selection.gameObjects)
            {
                var rectTransform = gameObject.GetComponent<RectTransform>();
                if (rectTransform == null)
                {
                    return false;
                }
            }

            return true;
        }
    }
}

Editor拡張適用方法については特殊なフォルダー名 - Unityマニュアル参照

さいごに

このEditor拡張により RectTransform のコピペの手間とコピペミスが減りました。
今回は同階層の GameObject の RectTransform のコピペまでしか対応していないので、階層が違う場合でのコピペの要件が出てくるようなら改修したい。
ComponentUtility.ReplaceComponentsIfDifferent メソッドを使えば、他の Component のコピペも出来るので、改変して他の用途のEditor拡張にも使えそう。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1