Unity曲线路径点快速创建工具

前言

在Unity项目开发中,我们经常需要创建平滑的路径供角色或物体移动。虽然可以使用简单的直线连接点,但这样的路径往往显得生硬。通过贝塞尔曲线,我们可以创建更加自然流畅的路径。本文将介绍一个基于二次贝塞尔曲线的路径创建工具的实现。


一、效果演示

在这里插入图片描述

二、实现思路

1、核心类设计

工具主要包含两个核心类:
CurveLineCreator: 负责整体路径的管理
CurveLineNode: 负责单个节点的行为和曲线计算

2、贝塞尔曲线

这里使用二次贝塞尔曲线公式:B(t) = (1-t)²P₀ + 2(1-t)tP₁ + t²P₂
P₀: 起始控制点
P₁: 当前节点位置
P₂: 结束控制点
t: 插值参数 [0,1]

private void CalculateBezierPoint()
{
    if (!previousNode || !nextNode) return;
    curvePoints.Clear();
    // 计算控制点
    Vector3 startPoint = transform.position - (transform.position - previousNode.transform.position).normalized * previousCurveLength;
    Vector3 endPoint = transform.position - (transform.position - nextNode.transform.position).normalized * nextCurveLength;
    
    // 计算曲线上的点
    for (int i = 0; i < curvePointNumber; i++)
    {
        float t = i / (curvePointNumber - 1.0f);
        Vector3 position = (1.0f - t) * (1.0f - t) * startPoint + 
                          2.0f * (1.0f - t) * t * transform.position + 
                          t * t * endPoint;
        curvePoints.Add(position);
    }
}
3、控制逻辑以及编辑器工具和可视化处理

见三,完整代码即可

三、完整代码

这里直接上完整代码了,大家有问题可以在评论区留言

一、CurveLineNode

using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;

public class CurveLineNode : MonoBehaviour
{
    [HideInInspector] public bool isCurve;
    [HideInInspector][Range(5, 20)] public int curvePointNumber = 10;
    [HideInInspector] public CurveLineNode previousNode;
    [HideInInspector] public CurveLineNode nextNode;
    [HideInInspector] public CurveLineCreator createCurveLine;
    [Range(0, 10)] public float previousCurveLength = 2f;
    [Range(0, 10)] public float nextCurveLength = 2f;
    [HideInInspector] public List<Vector3> curvePoints = new();

    public void OnValidate()
    {
        if (isCurve)
        {
            curvePoints.Clear();
            nextCurveLength = Mathf.Min(nextCurveLength, Vector3.Distance(transform.position, nextNode.isCurve ? nextNode.curvePoints.First() : nextNode.transform.position));

            previousCurveLength = Mathf.Min(previousCurveLength, Vector3.Distance(transform.position, previousNode.isCurve ? previousNode.curvePoints.Last() : previousNode.transform.position));
            CalculateBezierPoint();
        }
    }

    private void OnDrawGizmos()
    {
        if (!createCurveLine.drawGizmos) return;
        if (transform.hasChanged)
        {
            OnValidate();
            transform.hasChanged = false;
        }
        if (isCurve)
        {
            Gizmos.color = Color.green;
            for (int i = 0; i < curvePoints.Count - 1; i++)
            {
                Gizmos.DrawLine(curvePoints[i], curvePoints[i + 1]);
            }
        }
        if (nextNode != null)
        {
            Gizmos.color = Color.red;
            Gizmos.DrawLine(isCurve ? curvePoints.Last() : transform.position, nextNode.isCurve ? nextNode.curvePoints.First() : nextNode.transform.position);
        }
    }

    // 计算贝塞尔曲线上的点
    private void CalculateBezierPoint()
    {
        if (!previousNode || !nextNode) return;
        curvePoints.Clear();
        Vector3 startPoint = transform.position - (transform.position - previousNode.transform.position).normalized * previousCurveLength;
        Vector3 endPoint = transform.position - (transform.position - nextNode.transform.position).normalized * nextCurveLength;
        for (int i = 0; i < curvePointNumber; i++)
        {
            float t = i / (curvePointNumber - 1.0f);
            Vector3 position = (1.0f - t) * (1.0f - t) * startPoint + 2.0f * (1.0f - t) * t * transform.position + t * t * endPoint;
            curvePoints.Add(position);
        }
    }
}

#if UNITY_EDITOR
[CustomEditor(typeof(CurveLineNode))]
public class CurveLineNodeUI : Editor
{
    public CurveLineNode pathNode;
    public SerializedProperty isCurve;
    public SerializedProperty curvePointNumber;
    public CurveLineNode nextNode;
    public CurveLineNode previousNode;
    private void OnEnable()
    {
        pathNode = target as CurveLineNode;
        isCurve = serializedObject.FindProperty("isCurve");
        curvePointNumber = serializedObject.FindProperty("curvePointNumber");
        nextNode = serializedObject.FindProperty("nextNode").objectReferenceValue as CurveLineNode;
        previousNode = serializedObject.FindProperty("previousNode").objectReferenceValue as CurveLineNode;
    }
    public override void OnInspectorGUI()
    {
        serializedObject.Update();

        if (pathNode.previousNode == null)
        {
            EditorGUILayout.HelpBox("这是第一个节点", MessageType.Info);
        }
        else if (pathNode.nextNode == null)
        {
            EditorGUILayout.HelpBox("这是最后一个节点", MessageType.Info);
        }

        if (nextNode && previousNode)
        {
            EditorGUILayout.PropertyField(serializedObject.FindProperty("isCurve"));
            if (isCurve.boolValue)
            {
                EditorGUILayout.PropertyField(curvePointNumber);
                EditorGUILayout.PropertyField(serializedObject.FindProperty("previousCurveLength"));
                EditorGUILayout.PropertyField(serializedObject.FindProperty("nextCurveLength"));
            }
        }

        using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
        {
            GUI.enabled = false;
            if (previousNode == null || nextNode == null)
            {
                EditorGUILayout.PropertyField(serializedObject.FindProperty("isCurve"));
            }
            if (previousNode != null)
            {
                EditorGUILayout.PropertyField(serializedObject.FindProperty("previousNode"));
            }
            if (nextNode != null)
            {
                EditorGUILayout.PropertyField(serializedObject.FindProperty("nextNode"));
            }
            GUI.enabled = true;
        }
        serializedObject.ApplyModifiedProperties();

        EditorGUILayout.Space();
        using (new GUILayout.VerticalScope(EditorStyles.selectionRect))
        {
            if (GUILayout.Button("创建新节点", GUILayout.Height(40)))
            {
                pathNode.createCurveLine.CreateNode();
            }
        }
        if (GUILayout.Button("移除此节点", GUILayout.Height(25)))
        {
            pathNode.createCurveLine.RemoveNode(pathNode);
        }
    }
}
#endif

二、CurveLineCreator

using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;

public class CurveLineCreator : MonoBehaviour
{
    public List<CurveLineNode> pathNodes = new();
    public float defaultCurveLength = 2f;
    public List<Vector3> AllPoints => pathNodes.SelectMany<CurveLineNode, Vector3>(node => node.isCurve ? node.curvePoints : new[] { node.transform.position }).ToList();
    [Tooltip("是否在场景中绘制可视化路径")] public bool drawGizmos = true;
    [Tooltip("当删除或者异常时自动重命名节点,创建时不受影响")] public bool autoRename = true;

    /// <summary>
    /// 创建新节点
    /// </summary>
    public void CreateNode()
    {
        GameObject go = new GameObject("Node" + pathNodes.Count.ToString("D2"));
        go.transform.SetParent(transform);
        go.transform.position = pathNodes.Count > 0 ? pathNodes.Last().transform.position : transform.position;
        CurveLineNode _node = go.AddComponent<CurveLineNode>();
        _node.previousCurveLength = defaultCurveLength;
        _node.nextCurveLength = defaultCurveLength;
        _node.createCurveLine = this;
        pathNodes.Add(_node);
#if UNITY_EDITOR
        Selection.activeObject = go;
#endif
        RecalculateNodes();
    }

    private void OnDrawGizmos()
    {
        if (transform.hasChanged)
        {
            pathNodes.ForEach(x => x.OnValidate());
            transform.hasChanged = false;
        }
        if (pathNodes.Any(x => x == null))
        {
            RecalculateNodes();
        }
    }

    /// <summary>
    /// 当有空节点时重新计算所有节点
    /// </summary>
    public void RecalculateNodes()
    {
        pathNodes.RemoveAll(x => x == null);
        if (pathNodes.Count > 1)
        {
            pathNodes[0].isCurve = false;
            pathNodes[0].previousNode = null;
            pathNodes[0].nextNode = pathNodes[1];

            for (int i = 1; i < pathNodes.Count - 1; i++)
            {
                pathNodes[i].previousNode = pathNodes[i - 1];
                pathNodes[i].nextNode = pathNodes[i + 1];
            }

            pathNodes[pathNodes.Count - 1].nextNode = null;
            pathNodes[pathNodes.Count - 1].isCurve = false;
            pathNodes[pathNodes.Count - 1].previousNode = pathNodes[pathNodes.Count - 2];

            pathNodes.ForEach(x => x.OnValidate());
        }
        if (autoRename)
        {
            pathNodes.ForEach(x => x.name = "Node" + pathNodes.IndexOf(x).ToString("D2"));
        }
    }

    /// <summary>
    /// 删除最后一个节点
    /// </summary>
    public void RemoveLastNode()
    {
        if (pathNodes.Count > 1)
        {
            if (Application.isPlaying)
            {
                Destroy(pathNodes.Last().gameObject);
            }
            else
            {
                DestroyImmediate(pathNodes.Last().gameObject);
            }
            RecalculateNodes();
        }
    }

    /// <summary>
    /// 删除第一个节点
    /// </summary>
    public void RemoveFirstNode()
    {
        if (pathNodes.Count > 1)
        {
            if (Application.isPlaying)
            {
                Destroy(pathNodes.First().gameObject);
            }
            else
            {
                DestroyImmediate(pathNodes.First().gameObject);
            }
            RecalculateNodes();
        }
    }

    /// <summary>
    /// 删除指定节点
    /// </summary>
    /// <param name="node">指定节点</param>
    public void RemoveNode(CurveLineNode node)
    {
        if (Application.isPlaying)
        {
            Destroy(node.gameObject);
        }
        else
        {
            DestroyImmediate(node.gameObject);
        }
        RecalculateNodes();
    }

    /// <summary>
    /// 清除所有节点
    /// </summary>
    public void ClearNodes()
    {
        foreach (var node in pathNodes)
        {
            if (node == null) continue;
            if (Application.isPlaying)
            {
                Destroy(node.gameObject);
            }
            else
            {
                DestroyImmediate(node.gameObject);
            }
        }
        pathNodes.Clear();
    }
}

#if UNITY_EDITOR
[CustomEditor(typeof(CurveLineCreator))]
public class CreateCurveLineUI : Editor
{
    public CurveLineCreator createCurveLine;

    private void OnEnable()
    {
        createCurveLine = target as CurveLineCreator;
    }

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();
        using (new GUILayout.VerticalScope(EditorStyles.selectionRect))
        {
            if (GUILayout.Button("创建新节点", GUILayout.Height(40)))
            {
                createCurveLine.CreateNode();
            }
        }
        if (GUILayout.Button("删除最后一个节点", GUILayout.Height(25)))
        {
            createCurveLine.RemoveLastNode();
        }
        else if (GUILayout.Button("清除所有节点", GUILayout.Height(25)))
        {
            createCurveLine.ClearNodes();
        }
        else if (GUILayout.Button("重新计算所有节点", GUILayout.Height(25)))
        {
            createCurveLine.RecalculateNodes();
        }
    }
}
#endif

使用方法:

1、创建空物体,添加CurveLineCreator组件
2、点击"创建新节点"按钮添加路径点选择节点,
可以:
1、移动节点位置
2、切换直线/曲线模式
3、调整曲线控制点距离
4、调整曲线精度


部分主要参数说明:

参数名称解释
defaultCurveLength默认曲线控制点距离
drawGizmos是否在Scene视图中显示路径
autoRename是否自动重命名节点
curvePointNumber曲线采样点数量
previousCurveLength/nextCurveLength前后控制点距离

四、总结

这个工具提供了一个简单但实用的路径创建解决方案。通过贝塞尔曲线,我们可以轻松创建平滑的路径,适用于:巡逻路径设计、相机轨道制作、动画路径规划、等等…


希望这个工具能帮助到遇到类似问题的开发者,同时,我也会不定时更新更多的Unity实用工具、技巧以及高级功能。


如果觉得这篇文章对你有帮助,欢迎点赞收藏!欢迎在评论区留言交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sparkle Star

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值