如何通过自定义编辑器实现两条贝塞尔曲线的联动更新
嘿,我刚好做过类似的自定义编辑器需求,帮你梳理下怎么解决这个同步延迟的问题,顺便把三个功能的实现细节都理清楚~
解决BezierPair编辑器同步问题的完整方案
首先,你的核心问题是编辑器端属性同步没有实时触发——默认情况下,Unity编辑器只有在选中对象或者属性面板刷新时才会更新值,所以需要主动监听属性变更并强制刷新视图。下面分三个功能逐一实现,重点解决P0同步的实时性问题:
一、实时同步P0点(支持开关)
问题根源
之前的实现可能只在BezierPair被选中时才检查同步,没有在属性变更瞬间触发同步操作。我们需要在修改任意一条曲线的P0时,立刻同步另一条,并且通知编辑器刷新视图。
实现步骤
- 在自定义编辑器中,使用
SerializedProperty处理所有属性(确保Undo/Redo正常工作)。 - 当修改P0时,先记录Undo操作,再同步另一条曲线的P0,最后标记对象为脏并强制刷新。
- 重写
OnSceneGUI(),处理场景视图中拖拽控制点的同步(因为直接拖场景中的点不会触发面板的OnInspectorGUI)。
二、P1点保持同向同距离
这里的核心是基于自身P0的偏移量同步,而不是直接复制P1坐标。比如曲线A的P1相对于A的P0偏移了(2,0,0),那么曲线B的P1也要相对于B的P0偏移(2,0,0),这样不管两条曲线的P0在哪,P1的相对位置都一致。
三、P2点沿对方P0-P3连线镜像
需要实现一个点关于直线的镜像计算函数,当修改某条曲线的P2时,以该曲线的P0-P3为镜像轴,计算另一条曲线P2的镜像位置并同步。
完整代码示例
首先是BezierPair的基础脚本:
using UnityEngine; public class BezierPair : MonoBehaviour { public BezierCurve curveA; public BezierCurve curveB; public bool syncP0 = true; // P0同步开关 } // 假设你的BezierCurve脚本有这些属性(来自Catlike Coding的实现) public class BezierCurve : MonoBehaviour { public Vector3 p0, p1, p2, p3; }
然后是自定义编辑器脚本:
using UnityEditor; using UnityEngine; [CustomEditor(typeof(BezierPair))] public class BezierPairEditor : Editor { private SerializedProperty _curveA; private SerializedProperty _curveB; private SerializedProperty _syncP0; private void OnEnable() { // 绑定序列化属性 _curveA = serializedObject.FindProperty("curveA"); _curveB = serializedObject.FindProperty("curveB"); _syncP0 = serializedObject.FindProperty("syncP0"); } public override void OnInspectorGUI() { serializedObject.Update(); // 绘制P0同步开关 EditorGUILayout.PropertyField(_syncP0); EditorGUILayout.Space(); // 绘制两条曲线的属性 DrawCurveInspector(_curveA, "Curve A"); EditorGUILayout.Space(); DrawCurveInspector(_curveB, "Curve B"); serializedObject.ApplyModifiedProperties(); } private void DrawCurveInspector(SerializedProperty curveProp, string label) { EditorGUILayout.LabelField(label, EditorStyles.boldLabel); if (curveProp.objectReferenceValue == null) { EditorGUILayout.PropertyField(curveProp); return; } SerializedObject curveObj = new SerializedObject(curveProp.objectReferenceValue); curveObj.Update(); var p0 = curveObj.FindProperty("p0"); var p1 = curveObj.FindProperty("p1"); var p2 = curveObj.FindProperty("p2"); var p3 = curveObj.FindProperty("p3"); // 1. 处理P0同步 if (_syncP0.boolValue) { Vector3 newP0 = EditorGUILayout.Vector3Field("P0", p0.vector3Value); if (newP0 != p0.vector3Value) { // 记录Undo操作,支持撤销 Undo.RecordObject(curveProp.objectReferenceValue, "Modify P0"); Undo.RecordObject(GetOppositeCurve(curveProp).objectReferenceValue, "Sync P0"); // 同步两条曲线的P0 p0.vector3Value = newP0; GetOppositeCurve(curveProp).FindPropertyRelative("p0").vector3Value = newP0; // 标记脏数据+强制刷新 MarkDirtyAndRefresh(curveProp.objectReferenceValue, GetOppositeCurve(curveProp).objectReferenceValue); } } else { EditorGUILayout.PropertyField(p0); } // 2. 处理P1同向同距离同步 Vector3 p0Pos = p0.vector3Value; Vector3 p1Offset = p1.vector3Value - p0Pos; Vector3 newOffset = EditorGUILayout.Vector3Field("P1 Offset from P0", p1Offset); if (newOffset != p1Offset) { Undo.RecordObject(curveProp.objectReferenceValue, "Modify P1 Offset"); Undo.RecordObject(GetOppositeCurve(curveProp).objectReferenceValue, "Sync P1 Offset"); p1.vector3Value = p0Pos + newOffset; var oppositeCurve = GetOppositeCurve(curveProp); oppositeCurve.FindPropertyRelative("p1").vector3Value = oppositeCurve.FindPropertyRelative("p0").vector3Value + newOffset; MarkDirtyAndRefresh(curveProp.objectReferenceValue, oppositeCurve.objectReferenceValue); } // 3. 处理P2镜像同步 Vector3 newP2 = EditorGUILayout.Vector3Field("P2", p2.vector3Value); if (newP2 != p2.vector3Value) { Undo.RecordObject(curveProp.objectReferenceValue, "Modify P2"); Undo.RecordObject(GetOppositeCurve(curveProp).objectReferenceValue, "Mirror P2"); p2.vector3Value = newP2; var oppositeCurve = GetOppositeCurve(curveProp); // 以当前曲线的P0-P3为轴,镜像对方的P2 Vector3 mirroredP2 = MirrorPoint(oppositeCurve.FindPropertyRelative("p2").vector3Value, p0Pos, p3.vector3Value); oppositeCurve.FindPropertyRelative("p2").vector3Value = mirroredP2; MarkDirtyAndRefresh(curveProp.objectReferenceValue, oppositeCurve.objectReferenceValue); } // 绘制P3(无需同步) EditorGUILayout.PropertyField(p3); curveObj.ApplyModifiedProperties(); } // 获取另一条曲线的序列化属性 private SerializedProperty GetOppositeCurve(SerializedProperty current) { return current == _curveA ? _curveB : _curveA; } // 计算点关于直线的镜像点 private Vector3 MirrorPoint(Vector3 point, Vector3 lineStart, Vector3 lineEnd) { Vector3 lineDir = lineEnd - lineStart; float projection = Vector3.Dot(point - lineStart, lineDir) / lineDir.sqrMagnitude; Vector3 closestPoint = lineStart + projection * lineDir; return closestPoint + (closestPoint - point); } // 标记对象脏并刷新视图 private void MarkDirtyAndRefresh(params Object[] objects) { foreach (var obj in objects) { EditorUtility.SetDirty(obj); } EditorApplication.QueuePlayerLoopUpdate(); Repaint(); SceneView.RepaintAll(); } // 处理场景视图中拖拽控制点的同步 public override void OnSceneGUI() { base.OnSceneGUI(); var pair = (BezierPair)target; if (pair.curveA == null || pair.curveB == null) return; // 实时检查P0同步 if (_syncP0.boolValue && pair.curveA.p0 != pair.curveB.p0) { Undo.RecordObject(pair.curveA, "Sync P0"); Undo.RecordObject(pair.curveB, "Sync P0"); // 以当前选中的曲线的P0为准 Vector3 syncValue = Selection.activeTransform == pair.curveA.transform ? pair.curveA.p0 : pair.curveB.p0; pair.curveA.p0 = syncValue; pair.curveB.p0 = syncValue; MarkDirtyAndRefresh(pair.curveA, pair.curveB); } // 这里可以补充P1和P2的场景拖拽同步逻辑,和面板逻辑一致 } }
关键注意点
- Undo/Redo支持:必须用
Undo.RecordObject记录每一次修改,否则用户无法撤销操作,这是编辑器工具的基本要求。 - 实时刷新:
MarkDirtyAndRefresh方法里的EditorApplication.QueuePlayerLoopUpdate()和SceneView.RepaintAll()确保修改立刻在视图中生效,不需要选中BezierPair才更新。 - 场景视图兼容:重写
OnSceneGUI()处理直接拖拽场景中控制点的情况,避免面板和场景视图不同步。
这样修改后,你的三个功能都能正常工作,而且P0同步会实时触发,再也不需要选中BezierPair才更新啦!
内容的提问来源于stack exchange,提问作者Bane
相关产品推荐
相关产品推荐

