はじめに
Unity2021から、下図のように CopyComponent や PasteComponentValue が階層化され、実行までの手間が増えている状況。
開発作業時に RectTransform のコピーを頻繁に実行するため、楽にコピペ出来るようにEditor拡張を作ってみた。
詳細
作業環境
- Unity 2021.3.6f1
- Unity 2020.3.20f1
操作手順
- コピー元(From)とコピー先(To)の RectTransform がアタッチされている GameObject を2つ選択
- キーボードの Ctrl + 右クリック にて、動作項目が表示されたウインドウを表示
※選択数が2つという条件がウインドウ表示の条件となっている - 選択した GameObject の From と To を確認して、実行したい動作項目をクリックすることで RectTransform のコピペ実行
実行完了後に Debug ログを出力 - 元に戻したい場合は、キーボードの 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拡張にも使えそう。