一、两个需求
在拼UI的过程中,经常需要手动干两件事。
如果能够自动处理,可以节省大量时间,对性能也有好处(不必要的重叠可能导致不能合批)。
(更重要的,这是布局强迫症的福音)
需求1:
为选中的多个同级物体增加一个Box,
使其位置等于 所有选中物体的总中心
使其大小等于 所有选中物体的总大小。
执行后,选中物体的世界坐标均不变。

需求2:
调整选中物体,使其位置等于 所有子物体(不包含自身)的总中心。
使其大小等于 所有子物体(不包含自身)的总大小。
调整后,选中物体的锚点、中心点保持不变。
调整后,选中物体的所有子物体的世界坐标均不变。

二、两个实现
实现1:
using UnityEngine;
using UnityEditor;
using System.Linq;
namespace NRatel
{
/// <summary>
/// 意图:
/// 为选中的多个同级物体增加一个Box,
/// 使其位置等于 所有选中物体的总中心
/// 使其大小等于 所有选中物体的总大小。
/// 执行后,选中物体的世界坐标均不变。
/// 思路:
/// 第一步:计算选中物体的总中心、总大小。
/// 第二步:创建Box,设置位置和大小。
/// 第三步:将选中物体依次挂入Box(选中物体世界坐标不发生变化)。
/// </summary>
public class CreateUIBox : Editor
{
[MenuItem("GameObject/Tools/CreateUIBox", false, 0)]
public static void DoCreate()
{
if (Selection.gameObjects == null || Selection.gameObjects.Length <= 0) { return; }
if (Selection.gameObjects.Length == 1 && Selection.gameObjects[0].name == "#UIBox#") { return; } //防止多次操作
Transform parent = Selection.gameObjects[0].transform.parent;
for (int i = 0; i < Selection.gameObjects.Length; i++)
{
if (Selection.gameObjects[i].transform.parent != parent)
{
EditorUtility.DisplayDialog("Error", "暂只允许处理同级物体", "OK");
return;
}
}
//计算选择物体的总Bounds
Bounds bounds = RectTransformUtility.CalculateRelativeRectTransformBounds(parent, Selection.gameObjects[0].transform);
for (int i = 1; i < Selection.gameObjects.Length; i++)
{
Bounds b = RectTransformUtility.CalculateRelativeRectTransformBounds(parent, Selection.gameObjects[i].transform);
bounds.Encapsulate(b);
}
Undo.IncrementCurrentGroup();
int groupIndex = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("CreateUIBox");
{
//创建Box
GameObject box = new GameObject("#UIBox#", typeof(RectTransform));
Undo.RegisterCreatedObjectUndo(box, "CreateUIBox");
//设置box位置、大小
RectTransform boxRT = box.GetComponent<RectTransform>();
boxRT.SetParent(parent);
boxRT.localPosition = bounds.center;
boxRT.localScale = Vector3.one;
boxRT.sizeDelta = new Vector2(bounds.size.x, bounds.size.y);
//将选择的物体按原Sibling挂入
GameObject[] sortedObjs = Selection.gameObjects.OrderBy(x => x.transform.GetSiblingIndex()).ToArray();
for (int i = 0; i < sortedObjs.Length; i++)
{
Undo.SetTransformParent(sortedObjs[i].transform, boxRT, "MoveItemToBox");
}
Selection.activeGameObject = box;
}
Undo.CollapseUndoOperations(groupIndex);
}
}
}
要注意:
1、同时选中多个物体,由MenuItem执行时,会触发多次,应想办法避免。我这里的实现可能不太完美。
2、Bounds bounds = new Bounds(); 这样的创建会将 Vector3.zero 包进去。让我查了大半天。。
3、操作需要可撤销。
---------------------------------- NRatel割 ----------------------------------
实现2:
using UnityEngine;
using UnityEditor;
namespace NRatel
{
/// <summary>
/// 意图:
/// 调整选中物体
/// 使其位置等于 所有子物体(不包含自身)的总中心。
/// 使其大小等于 所有子物体(不包含自身)的总大小。
/// 调整后,选中物体的锚点、中心点保持不变。
/// 调整后,选中物体的所有子物体的世界坐标均不变。
/// 思路:
/// 第一步:计算所有子物体(不包含自身)的总中心 和 总大小,
/// 第二步:将Box大小设为所有子物体的总大小、将Box移动至所有子物体的总中心。
/// 第三步:将子物体移动至原位置。
/// </summary>
public class AdjustUIBox : Editor
{
[MenuItem("GameObject/Tools/AdjustUIBox", false, 0)]
public static void DoAdjust()
{
if (Selection.gameObjects == null || Selection.gameObjects.Length <= 0) { return; }
if (Selection.gameObjects.Length > 1)
{
EditorUtility.DisplayDialog("Error", "只允许同时处理一个物体", "OK");
return;
}
Transform box = Selection.gameObjects[0].transform;
Transform boxParent = box.parent;
Transform child0 = box.GetChild(0);
//计算所有子物体(不包含自身)的总中心 和 总大小
Bounds boundsInBox = RectTransformUtility.CalculateRelativeRectTransformBounds(box, child0); //box坐标系下
Bounds boundsInBoxParent = RectTransformUtility.CalculateRelativeRectTransformBounds(boxParent, child0); //boxParent坐标系下
for (int i = 1; i < box.childCount; i++)
{
Transform childi = box.GetChild(i);
boundsInBox.Encapsulate(RectTransformUtility.CalculateRelativeRectTransformBounds(box, childi));
boundsInBoxParent.Encapsulate(RectTransformUtility.CalculateRelativeRectTransformBounds(boxParent, childi));
}
//Debug.Log("box: " + boundsInBox.center + ", " + boundsInBox.size);
//Debug.Log("boxParent: " + boundsInBoxParent.center + ", " + boundsInBoxParent.size);
Undo.IncrementCurrentGroup();
int groupIndex = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("AdjustUIBox");
{
//设置box位置
RectTransform boxRT = box as RectTransform;
Undo.RecordObject(boxRT, "boxRT");
boxRT.localPosition = boundsInBoxParent.center;
boxRT.sizeDelta = new Vector2(boundsInBoxParent.size.x, boundsInBoxParent.size.y); //修改box大小,只会影响 anchoredPosition,不会影响localPosition。
//设置子物体位置
for (int i = 0; i < box.childCount; i++)
{
Transform child = box.GetChild(i);
Undo.RecordObject(child, "boxChild" + i);
child.localPosition = child.localPosition - boundsInBox.center;
}
}
Undo.CollapseUndoOperations(groupIndex);
}
}
}
要注意:
1、需求1中要注意的2、3条事项。
2、对 box 的操作应该在 boxParent 坐标系下进行;对 box子物体的操作应该在box坐标系下进行。
---------------------------------- NRatel割 ----------------------------------
三、其他
顺便分析一下 RectTransformUtility.CalculateRelativeRectTransformBounds 的源码。
1、取 child 及其所有子物体的角世界坐标。
2、变换到 root 本地坐标系下。
3、遍历融合所有角坐标,得到最小坐标和最大坐标,确定出Bounds。
public static Bounds CalculateRelativeRectTransformBounds(Transform root, Transform child)
{
RectTransform[] rects = child.GetComponentsInChildren<RectTransform>(false);
if (rects.Length > 0)
{
Vector3 vMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
Vector3 vMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
Matrix4x4 toLocal = root.worldToLocalMatrix;
for (int i = 0, imax = rects.Length; i < imax; i++)
{
rects[i].GetWorldCorners(s_Corners);
for (int j = 0; j < 4; j++)
{
Vector3 v = toLocal.MultiplyPoint3x4(s_Corners[j]);
vMin = Vector3.Min(v, vMin);
vMax = Vector3.Max(v, vMax);
}
}
Bounds b = new Bounds(vMin, Vector3.zero);
b.Encapsulate(vMax);
return b;
}
return new Bounds(Vector3.zero, Vector3.zero);
}