293 lines
9.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
public class BezierWay : MonoBehaviour
{
[Header("Настройки кривой")]
[SerializeField] private bool _cyclic = false;
[SerializeField] private float _gizmoSize = 0.2f;
[SerializeField] private int _gizmoSegments = 50;
[SerializeField] private Color _gizmoColor = Color.green;
[SerializeField] private Color _pointColor = Color.white;
[SerializeField] private Color _controlPointColor = Color.yellow;
[Header("Настройки сглаживания")]
[Range(0, 1)]
[SerializeField] private float _tension = 0.5f; // Натяжение кривой (0 - прямая, 1 - максимальное сглаживание)
public List<Transform> _points = new List<Transform>();
private List<Vector3> _cachedWorldPositions = new List<Vector3>();
private bool _isDirty = true;
// Свойства
public bool IsCyclic => _cyclic;
public int PointsCount => _points.Count;
public Transform FirstPoint => _points[0];
public float Tension { get => _tension; set { _tension = Mathf.Clamp01(value); _isDirty = true; } }
public bool Busy { get; private set; }
void Start()
{
CachePoints();
Busy = false;
}
public void SetBusiness(bool value) { Busy = value; }
void Update()
{
// Автоматическое обновление при изменении позиций точек в редакторе
if (_isDirty)
{
CachePoints();
_isDirty = false;
}
}
/// <summary>
/// Кэширует все дочерние объекты как точки кривой
/// </summary>
[ContextMenu("Cache Points")]
public void CachePoints()
{
_points.Clear();
// Собираем все дочерние объекты
foreach (Transform child in transform)
{
_points.Add(child);
}
if (_points.Count < 2)
{
Debug.LogWarning($"BezierWay: Недостаточно точек для построения кривой (минимум 2). Найдено: {_points.Count}");
return;
}
// Кэшируем мировые позиции
_cachedWorldPositions.Clear();
foreach (var point in _points)
{
if (point != null)
_cachedWorldPositions.Add(point.position);
else
_cachedWorldPositions.Add(Vector3.zero);
}
Debug.Log($"BezierWay: Закэшировано {_points.Count} точек");
}
/// <summary>
/// Помечает кривую как "грязную" для обновления кэша
/// </summary>
public void MarkDirty()
{
_isDirty = true;
}
/// <summary>
/// Получает позицию на кривой Безье по параметру t (0-1)
/// </summary>
/// <param name="t">Параметр от 0 до 1</param>
/// <returns>Позиция на кривой</returns>
public Vector3 GetPoint(float t)
{
if (_points.Count < 2)
{
Debug.LogError("Недостаточно точек для интерполяции");
return Vector3.zero;
}
t = Mathf.Clamp01(t);
if (_points.Count == 2)
{
// Простая линейная интерполяция для двух точек
return Vector3.Lerp(_cachedWorldPositions[0], _cachedWorldPositions[1], t);
}
// Для кривых с 3+ точками используем Catmull-Rom сплайны (аппроксимация Безье)
return GetCatmullRomPosition(t);
}
/// <summary>
/// Получает позицию на кривой с использованием Catmull-Rom сплайнов
/// </summary>
private Vector3 GetCatmullRomPosition(float t)
{
int pointCount = _cachedWorldPositions.Count;
// Вычисляем сегмент и локальный t внутри сегмента
float segmentT;
int p0, p1, p2, p3;
if (_cyclic)
{
// Циклическая кривая
float segmentFloat = t * pointCount;
int segmentIndex = Mathf.FloorToInt(segmentFloat);
segmentT = segmentFloat - segmentIndex;
// Индексы для циклической кривой
p1 = segmentIndex % pointCount;
p2 = (p1 + 1) % pointCount;
p0 = (p1 - 1 + pointCount) % pointCount;
p3 = (p2 + 1) % pointCount;
}
else
{
// Нециклическая кривая
int segments = pointCount - 1;
float segmentFloat = t * segments;
int segmentIndex = Mathf.Clamp(Mathf.FloorToInt(segmentFloat), 0, segments - 1);
segmentT = segmentFloat - segmentIndex;
p1 = segmentIndex;
p2 = segmentIndex + 1;
p0 = Mathf.Max(0, p1 - 1);
p3 = Mathf.Min(pointCount - 1, p2 + 1);
}
// Получаем позиции точек
Vector3 point0 = _cachedWorldPositions[p0];
Vector3 point1 = _cachedWorldPositions[p1];
Vector3 point2 = _cachedWorldPositions[p2];
Vector3 point3 = _cachedWorldPositions[p3];
// Catmull-Rom интерполяция
float t2 = segmentT * segmentT;
float t3 = t2 * segmentT;
// Коэффициенты с натяжением
float tensionFactor = _tension;
Vector3 result = 0.5f * (
2.0f * point1 +
(-point0 + point2) * (tensionFactor * segmentT) +
(2.0f * point0 - 5.0f * point1 + 4.0f * point2 - point3) * (tensionFactor * t2) +
(-point0 + 3.0f * point1 - 3.0f * point2 + point3) * (tensionFactor * t3)
);
return result;
}
/// <summary>
/// Получает касательную на кривой в точке t
/// </summary>
public Vector3 GetTangent(float t)
{
float delta = 0.01f;
Vector3 p1 = GetPoint(Mathf.Clamp01(t - delta));
Vector3 p2 = GetPoint(Mathf.Clamp01(t + delta));
return (p2 - p1).normalized;
}
/// <summary>
/// Получает точку с нормализацией по длине кривой
/// </summary>
public Vector3 GetPointByLength(float normalizedLength)
{
return GetPoint(normalizedLength);
}
/// <summary>
/// Получает массив позиций всех точек кривой
/// </summary>
public Vector3[] GetPointsPositions()
{
return _cachedWorldPositions.ToArray();
}
void OnDrawGizmos()
{
// Обновляем кэш позиций перед использованием в редакторе
UpdateCacheForEditor();
if (_points.Count < 2)
{
return;
}
// Рисуем контрольные точки
Gizmos.color = _pointColor;
foreach (var point in _points)
{
if (point != null)
{
Gizmos.DrawSphere(point.position, _gizmoSize);
}
}
// Рисуем кривую
Gizmos.color = _gizmoColor;
Vector3 prevPoint = GetPoint(0);
for (int i = 1; i <= _gizmoSegments; i++)
{
float t = i / (float)_gizmoSegments;
Vector3 currentPoint = GetPoint(t);
Gizmos.DrawLine(prevPoint, currentPoint);
prevPoint = currentPoint;
}
// Если циклическая, рисуем замыкающую линию
if (_cyclic && _points.Count > 2)
{
Gizmos.color = Color.cyan;
Vector3 endPoint = GetPoint(1);
Vector3 startPoint = GetPoint(0);
Gizmos.DrawLine(endPoint, startPoint);
}
// Рисуем направление кривой (стрелки)
DrawDirectionArrows();
}
/// <summary>
/// Обновляет кэш для использования в редакторе
/// </summary>
private void UpdateCacheForEditor()
{
// Очищаем и обновляем список точек из дочерних объектов
_points.Clear();
foreach (Transform child in transform)
{
_points.Add(child);
}
// Обновляем кэш позиций
_cachedWorldPositions.Clear();
foreach (var point in _points)
{
if (point != null)
_cachedWorldPositions.Add(point.position);
else
_cachedWorldPositions.Add(Vector3.zero);
}
}
private void DrawDirectionArrows()
{
Gizmos.color = Color.red;
int arrowsCount = Mathf.Min(10, _gizmoSegments);
for (int i = 0; i <= arrowsCount; i++)
{
float t = i / (float)arrowsCount;
Vector3 point = GetPoint(t);
Vector3 tangent = GetTangent(t);
if (tangent != Vector3.zero)
{
Vector3 arrowTip = point + tangent * _gizmoSize * 1.5f;
Vector3 perpendicular = Vector3.Cross(tangent, Vector3.up).normalized;
Gizmos.DrawLine(point, arrowTip);
Gizmos.DrawLine(arrowTip, arrowTip - tangent * _gizmoSize * 0.3f + perpendicular * _gizmoSize * 0.3f);
Gizmos.DrawLine(arrowTip, arrowTip - tangent * _gizmoSize * 0.3f - perpendicular * _gizmoSize * 0.3f);
}
}
}
}