You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过自定义编辑器实现两条贝塞尔曲线的联动更新

嘿,我刚好做过类似的自定义编辑器需求,帮你梳理下怎么解决这个同步延迟的问题,顺便把三个功能的实现细节都理清楚~

解决BezierPair编辑器同步问题的完整方案

首先,你的核心问题是编辑器端属性同步没有实时触发——默认情况下,Unity编辑器只有在选中对象或者属性面板刷新时才会更新值,所以需要主动监听属性变更并强制刷新视图。下面分三个功能逐一实现,重点解决P0同步的实时性问题:

一、实时同步P0点(支持开关)

问题根源

之前的实现可能只在BezierPair被选中时才检查同步,没有在属性变更瞬间触发同步操作。我们需要在修改任意一条曲线的P0时,立刻同步另一条,并且通知编辑器刷新视图。

实现步骤

  1. 在自定义编辑器中,使用SerializedProperty处理所有属性(确保Undo/Redo正常工作)。
  2. 当修改P0时,先记录Undo操作,再同步另一条曲线的P0,最后标记对象为脏并强制刷新。
  3. 重写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的场景拖拽同步逻辑,和面板逻辑一致
    }
}

关键注意点

  1. Undo/Redo支持:必须用Undo.RecordObject记录每一次修改,否则用户无法撤销操作,这是编辑器工具的基本要求。
  2. 实时刷新:MarkDirtyAndRefresh方法里的EditorApplication.QueuePlayerLoopUpdate()和SceneView.RepaintAll()确保修改立刻在视图中生效,不需要选中BezierPair才更新。
  3. 场景视图兼容:重写OnSceneGUI()处理直接拖拽场景中控制点的情况,避免面板和场景视图不同步。

这样修改后,你的三个功能都能正常工作,而且P0同步会实时触发,再也不需要选中BezierPair才更新啦!

内容的提问来源于stack exchange,提问作者Bane

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 09:35:58