前言
在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实用工具、技巧以及高级功能。