仓库
仓库链接:ModiParentOnly: 一种Unity编辑器实用工具,按住Shift键就可以在移动或旋转物体时,使子物体们保持原样。
以后我写文章基本都会放到仓库里,方便获取最新代码。
前提
有时候想调整父物体的位置或角度,但又希望子物体能保持原状。Unity似乎没有直接提供这种功能,所以做了个扩展。
使用方式
选中你想调整的父物体,移动或旋转时按住“Shift”键就会生效,松开则失效。你可以在一次移动过程中多次开关此功能,无论如何,此次操作都能够正常地撤销和重做。
自带一个按钮,你可以使用Unity编辑器顶部菜单栏的”Tools/移动到子物体位置中心”,来把选中的所有父物体,各自移动到其子物体们的中心位置。
如果你不喜欢Shift键,可以改成其他快捷键。如果用来调整UI边框,推荐使用大写锁定键(CapsLock),这样就不会和锁定比例(Shift)、固定单位(Ctrl)和移动视图(Alt)冲突了。
private static EventModifiers Key = EventModifiers.Shift;
//修改为你喜欢的快捷键,例如
private static EventModifiers Key = EventModifiers.CapsLock; //大写锁定键
//也可以让两个快捷键都能生效
private static EventModifiers Key = EventModifiers.Shift| EventModifiers.CapsLock;
支持同时操作多个父物体,在同时移动时会为其他非活跃父物体绘制坐标轴。需要同时旋转的情况似乎不多见,就没画了。
![图片[1]-【Unity】在编辑器里仅移动或旋转父物体,而不影响子物体-水波萌](https://www.shuibo.moe/wp-content/uploads/2025/08/QQ20250821-041657.png)
也支持操作RectTransform。理论上Transform的子类都可以,原理就是不断修改子物体们的位置和角度使它们保持原样,没有什么破坏性操作。
代码
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
[InitializeOnLoad]
public class ModiParentOnly
{
public static ModiParentOnly _ins;
//******修改这一行代码改变快捷键******
private static EventModifiers Key = EventModifiers.Shift;
private bool _prevHeld;
private bool _currentHeld;
static ModiParentOnly()
{
_ins = new ModiParentOnly();
}
private ModiParentOnly()
{
SceneView.duringSceneGui += KeyHeld;
}
~ModiParentOnly()
{
StopTracking();
SceneView.duringSceneGui -= KeyHeld;
}
private void KeyHeld(SceneView view)
{
if (Event.current != null)
{
_currentHeld = (Event.current.modifiers & Key) != 0;
if (_currentHeld != _prevHeld)
{
if (_currentHeld)
{
StartTracking();
}
else
{
StopTracking();
}
}
_prevHeld = _currentHeld;
}
}
private struct TransformValueInfo
{
public Vector3 position;
public Quaternion rotation;
public TransformValueInfo(Transform transform)
{
position = transform.position;
rotation = transform.rotation;
}
public void Get(Transform transform)
{
position = transform.position;
rotation = transform.rotation;
}
public bool IsSameTo(Transform transform)
{
return position == transform.position &&
rotation == transform.rotation;
}
public void ApplyTo(Transform transform)
{
transform.SetPositionAndRotation(position, rotation);
}
}
private class ParentInfo
{
public Transform target;
public TransformValueInfo last;
public TransformValueInfo[] children;
public ParentInfo(Transform parent)
{
target = parent;
last = new TransformValueInfo(parent);
children = new TransformValueInfo[parent.childCount];
for (int i = 0; i < parent.childCount; i++)
{
var trans = parent.GetChild(i).transform;
children[i] = new TransformValueInfo(trans);
}
}
}
private ParentInfo[] _infos;
public void ModiOnly(Transform parent,System.Action<Transform> modi)
{
var info = new ParentInfo(parent);
modi?.Invoke(parent);
RestoreChildren(info);
}
private void StartTracking()
{
var selections = Selection.gameObjects;
_infos = new ParentInfo[selections.Length];
for (int i = 0; i < _infos.Length; i++)
{
_infos[i] = new ParentInfo(selections[i].transform);
}
SceneView.duringSceneGui += Tracking;
}
private void StopTracking()
{
_infos = null;
SceneView.duringSceneGui -= Tracking;
}
private void Tracking(SceneView view)
{
if (_infos == null) return;
// 检测父物体变化
for (int i = 0; i < _infos.Length; i++)
{
var info = _infos[i];
DrawAxes(info.target,view.camera);
if (!info.last.IsSameTo(info.target))
{
RestoreChildren(info);
info.last.Get(info.target);
}
}
}
private void DrawAxes(Transform target,Camera camera)
{
if (!target) return;
if (Selection.activeTransform == target) return;
// 绘制模式判断
if (Tools.current != Tool.Move) return;
Camera cam = camera;
Vector3 origin = target.position;
// 计算基于摄像机距离的尺寸
float distance = Vector3.Distance(cam.transform.position, origin);
float baseScale = distance / 5f;
float axisLength = 0.6f * baseScale;
float arrowSize = 0.1f * baseScale;
float lineThickness = 1.7f;
// 获取方向向量
bool isGlobal = Tools.pivotRotation == PivotRotation.Global;
Vector3 right = isGlobal ? Vector3.right : target.right;
Vector3 up = isGlobal ? Vector3.up : target.up;
Vector3 forward = isGlobal ? Vector3.forward : target.forward;
// 带抗锯齿的线段绘制
Handles.zTest = UnityEngine.Rendering.CompareFunction.LessEqual;
// X轴
using (new Handles.DrawingScope(Color.red))
{
Handles.DrawLine(origin, origin + right * axisLength, lineThickness);
DrawArrow(origin + right * axisLength, right, arrowSize);
}
// Y轴
using (new Handles.DrawingScope(Color.green))
{
Handles.DrawLine(origin, origin + up * axisLength, lineThickness);
DrawArrow(origin + up * axisLength, up, arrowSize);
}
// Z轴
using (new Handles.DrawingScope(Color.blue))
{
Handles.DrawLine(origin, origin + forward * axisLength, lineThickness);
DrawArrow(origin + forward * axisLength, forward, arrowSize);
}
}
private static void DrawArrow(Vector3 tipPos, Vector3 direction, float size)
{
float visibleSize = size * 1f;
Handles.ConeHandleCap(0,tipPos - 0.3f * visibleSize * direction,Quaternion.LookRotation(direction),visibleSize,EventType.Repaint);
}
private void RestoreChildren(ParentInfo info)
{
if (info.children.Length == 0) return;
for (int i = 0; i < info.children.Length; i++)
{
var child =info.target.GetChild(i);
Undo.RecordObject(child,"RestoreChild");
info.children[i].ApplyTo(child);
}
}
[MenuItem("Tools/移动到子物体位置中心")]
public static void ToCenter()
{
var parents = Selection.gameObjects;
foreach(var p in parents)
{
var trans = p.transform;
Vector3 sum = Vector3.zero;
for (int i = 0; i < trans.childCount; i++)
{
sum += trans.GetChild(i).position;
}
_ins.ModiOnly(trans, t => t.position = sum / trans.childCount);
}
}
}
#endif
旧版实现
旧版编辑器的实现功能更加受限,并且有一定的冲突风险,不推荐使用。
可以按住shift键操作,也可以在Inspector里暂时开启。
![图片[2]-【Unity】在编辑器里仅移动或旋转父物体,而不影响子物体-水波萌](https://www.shuibo.moe/wp-content/uploads/2025/08/QQ20250820-211541.png)
开启后,无论是移动还是旋转,子物体都不会跟着变化。
注意,这个开关的状态并不会被记录,关闭或切换面板就会重置。
本质是为Transform新写了一个Editor。如果你已经为Transform写了其他Editor,可以将继承对象改为你的Editor。
[CustomEditor(typeof(Transform), false,isFallback =false)]
public class ExtensionTransformEditor : Editor //改成你的Editor
如果想把功能的开关改成全局的,可以修改相应的字段为静态字段。不过这样仍然只是缓存而已,在重启编辑器或者重新编译代码后仍然会丢失。
private bool _isModiParentOnly;
//修改为
private static bool _isModiParentOnly;
如果你不喜欢Shift键,可以改成其他快捷键。
private static EventModifiers Key = EventModifiers.Shift;
//修改为你喜欢的快捷键,例如
private static EventModifiers Key = EventModifiers.Control; //Ctrl键
扩展对RectTransform不生效,因为现有的实现会破坏原有的布局。如果要更加“原生”一点,可以改成包装类,用反射获取原实例的,不过那就是另外一种实现方式了,暂时懒得写。
也不支持多选,因为Editor的多选操作比较复杂,懒得写。
代码如下
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(Transform), false, isFallback = false)]
public class ExtensionTransformEditor : Editor
{
private bool _isModiParentOnly;
//按住哪个键生效
private static EventModifiers Key = EventModifiers.Shift;
private bool _prevHeld;
private bool _currentHeld;
private struct TransformValueInfo
{
public Vector3 position;
public Quaternion rotation;
public TransformValueInfo(Transform transform)
{
position = transform.position;
rotation = transform.rotation;
}
public void Get(Transform transform)
{
position = transform.position;
rotation = transform.rotation;
}
public bool IsSameTo(Transform transform)
{
return position == transform.position &&
rotation == transform.rotation;
}
public void ApplyTo(Transform transform)
{
transform.SetPositionAndRotation(position, rotation);
}
}
private TransformValueInfo _last;
private TransformValueInfo[] _children;
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
EditorGUI.BeginChangeCheck();
var label = new GUIContent("操作不影响子物体")
{
tooltip = "开启后,移动或旋转此物体,不会改变子物体的绝对位置和绝对角度。"
};
var tmp = EditorGUILayout.Toggle(label, _isModiParentOnly);
if (EditorGUI.EndChangeCheck())
{
_isModiParentOnly = tmp;
OnTrackingChange();
}
}
private void OnTrackingChange()
{
if (_isModiParentOnly)
{
StartTracking();
}
else
{
StopTracking();
}
}
private void StartTracking()
{
if (target is not Transform parent) return;
_last = new TransformValueInfo(parent);
_children = new TransformValueInfo[parent.childCount];
for (int i = 0; i < parent.childCount; i++)
{
var trans = parent.GetChild(i).transform;
_children[i] = new TransformValueInfo(trans);
}
SceneView.duringSceneGui += Tracking;
}
private void StopTracking()
{
_last = default;
_children = null;
SceneView.duringSceneGui -= Tracking;
}
private void KeyHeld(SceneView view)
{
if (Event.current != null)
{
_currentHeld = (Event.current.modifiers & Key) != 0;
if (_currentHeld != _prevHeld)
{
_isModiParentOnly = _currentHeld;
OnTrackingChange();
}
_prevHeld = _currentHeld;
}
}
private void OnEnable()
{
SceneView.duringSceneGui += KeyHeld;
//这是为静态开关预留的。
if(_isModiParentOnly)
{
StartTracking();
}
}
private void OnDisable()
{
StopTracking();
SceneView.duringSceneGui -= KeyHeld;
}
private void Tracking(SceneView view)
{
if (!_isModiParentOnly) return;
if (target is not Transform parent) return;
// 检测父物体变化
if (!_last.IsSameTo(parent))
{
RestoreChildren();
_last.Get(parent);
}
}
private void RestoreChildren()
{
if (target is not Transform parent || _children.Length == 0) return;
//记录子物体状态,以便能正常撤销
Undo.RecordObjects(parent.GetComponentsInChildren<Transform>(), "RestoreChildren");
for (int i = 0; i < _children.Length; i++)
{
var child = parent.GetChild(i);
_children[i].ApplyTo(child);
}
}
}
#endif
无论哪一种实现,原理都很简单粗暴,就是移动前把原来的位置和角度记录下来,没什么好说的。
文章版权归作者所有






暂无评论内容