【Unity】在编辑器里仅移动或旋转父物体,而不影响子物体

仓库

仓库链接: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】在编辑器里仅移动或旋转父物体,而不影响子物体-水波萌
同时操作多个UI父物体

也支持操作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】在编辑器里仅移动或旋转父物体,而不影响子物体-水波萌

开启后,无论是移动还是旋转,子物体都不会跟着变化。

注意,这个开关的状态并不会被记录,关闭或切换面板就会重置。

本质是为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

无论哪一种实现,原理都很简单粗暴,就是移动前把原来的位置和角度记录下来,没什么好说的。

© 版权声明
THE END
喜欢就点个赞吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容