266 lines
8.6 KiB
C#
266 lines
8.6 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEditor;
|
||
using System.Collections;
|
||
|
||
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;
|
||
|
||
public List<Transform> _points = new List<Transform>();
|
||
private List<Vector3> _cachedWorldPositions = new List<Vector3>();
|
||
|
||
// Свойства
|
||
public bool IsCyclic => _cyclic;
|
||
public int PointsCount => _points.Count;
|
||
public Transform FirstPoint => _points[0];
|
||
public bool Busy/* { get; private set; }*/;
|
||
private Coroutine businessCoroutine;
|
||
void Start()
|
||
{
|
||
CachePoints();
|
||
Busy = false;
|
||
}
|
||
|
||
public void SetBusiness(bool value)
|
||
{
|
||
Busy = value;
|
||
}
|
||
public void SetBusiness(bool value, float delay)
|
||
{
|
||
if(businessCoroutine != null)
|
||
StopCoroutine(businessCoroutine);
|
||
|
||
businessCoroutine = StartCoroutine(DelayedBusiness(value, delay));
|
||
}
|
||
|
||
private IEnumerator DelayedBusiness(bool value, float delay)
|
||
{
|
||
yield return new WaitForSeconds(delay);
|
||
Busy = value;
|
||
businessCoroutine = null;
|
||
}
|
||
|
||
/// <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);
|
||
}
|
||
|
||
}
|
||
|
||
/// <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;
|
||
|
||
// Коэффициенты с натяжением
|
||
|
||
|
||
Vector3 result = 0.5f * (
|
||
2.0f * point1 +
|
||
(-point0 + point2) * (segmentT) +
|
||
(2.0f * point0 - 5.0f * point1 + 4.0f * point2 - point3) * (t2) +
|
||
(-point0 + 3.0f * point1 - 3.0f * point2 + point3) * (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;
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
} |