using System; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace AibisDream { public class CutLine : MonoBehaviour { #region Shader参数 private static readonly int RangeCount = Shader.PropertyToID("_RangeCount"); #endregion private LineRenderer _lineRenderer; private Material _lineMaterial; private Vector2[] _linePoints; private float[] _normalizedLinePoints; private float _totalLength; private Bounds _bounds; private List _recordedRanges = new(); private static readonly int Ranges = Shader.PropertyToID("_Ranges"); private static readonly int TotalLength = Shader.PropertyToID("_TotalLength"); public event Action OnCuttingDown; #region 初始化 public void Init(List shapePoints, float clickRadius) { if (!_lineRenderer) { _lineRenderer = GetComponent(); _lineMaterial = _lineRenderer.materials[0]; } _lineRenderer.gameObject.SetActive(true); // 清除旧数据 ClearOldData(); // 生成归一化点 NormalizePoints(shapePoints); // 按照新点位重绘线条 RedrawLine(shapePoints); // 生成基本的包围盒 GeneBoundingBox(clickRadius); ClearRecordedRanges(); } private void ClearOldData() { _recordedRanges.Clear(); _totalLength = 0; } private void RedrawLine(List shapePoints) { // 画线 var linePoints = shapePoints.ToArray(); _lineRenderer.positionCount = linePoints.Length; _lineRenderer.SetPositions(linePoints); // 设置Shader _lineMaterial.SetFloat(TotalLength, _totalLength); _lineMaterial.SetInt(RangeCount, 1); _lineMaterial.SetVectorArray(Ranges, new List { Vector4.zero }); } private void UpdateMaterial() { if (_recordedRanges.Count <= 0) { return; } // 将Range组装成Shader可以读取的格式 var ranges = new List(); for (int i = 0; i < _recordedRanges.Count; i += 2) { var v1 = _recordedRanges[i]; var v2 = i + 1 < _recordedRanges.Count ? _recordedRanges[i + 1] : Vector2.zero; var combined = new Vector4(v1.x, v1.y, v2.x, v2.y); ranges.Add(combined); } // 更新线条 _lineMaterial.SetInt(RangeCount, ranges.Count); _lineMaterial.SetVectorArray(Ranges, ranges); } private void NormalizePoints(List shapePoints) { // 先生成首尾相连的点,默认Loop shapePoints.Add(shapePoints[0]); _linePoints = shapePoints .Select(item => new Vector2(item.x, item.y)) .ToArray(); var segmentLengths = new List(); // 计算总长度和各段长度 for (var i = 1; i < _linePoints.Length; i++) { var segmentLength = Vector2.Distance(_linePoints[i - 1], _linePoints[i]); segmentLengths.Add(segmentLength); _totalLength += segmentLength; } // 计算每个点的归一化位置 var normalizedLinePoints = new float[segmentLengths.Count + 1]; normalizedLinePoints[0] = 0f; var accumulatedLength = 0f; for (var i = 0; i < segmentLengths.Count; i++) { accumulatedLength += segmentLengths[i]; normalizedLinePoints[i + 1] = accumulatedLength / _totalLength; } _normalizedLinePoints = normalizedLinePoints; } private void GeneBoundingBox(float clickRadius) { _bounds = new Bounds(); foreach (var point in _linePoints) { _bounds.Encapsulate(point); } _bounds.Expand(clickRadius * 2); } #endregion #region 切割检测 public void DetectLineClick(Vector2 clickPosition, float clickRadius) { // 先进行包围盒检测,不在包围盒直接返回 if (!_bounds.Contains(clickPosition)) { return; } List newRanges = new List(); // 检查每条线段 for (int i = 1; i < _linePoints.Length; i++) { Vector2 start = _linePoints[i - 1]; Vector2 end = _linePoints[i]; // 计算点覆盖线段的范围 if (!TryGetCoverRange(start, end, clickPosition, clickRadius, out var rawRange)) { // 如果未覆盖直接计算下一段 continue; } // 将线段范围转成整体范围 var normalizedStart = _normalizedLinePoints[i - 1]; var normalizedEnd = _normalizedLinePoints[i]; var normalizedDistance = normalizedEnd - normalizedStart; var range = rawRange * normalizedDistance + new Vector2(normalizedStart, normalizedStart); newRanges.Add(range); } // 合并新检测到的区间 foreach (Vector2 range in newRanges) { _recordedRanges.Add(range); } // 合并所有重叠区间 MergeRanges(); // 判断当前切割有没有割到95% if (CalcTotalRange() >= 0.95) { Debug.Log("cutting All"); OnCuttingDown?.Invoke(); } // 打印当前所有区间(调试用) Debug.Log($"rangesCount:{_recordedRanges.Count}"); string log = "Ranges: "; foreach (Vector2 range in _recordedRanges) { log += $"{range.x:F2}-{range.y:F2}; "; } Debug.Log(log); UpdateMaterial(); } private bool TryGetCoverRange(Vector2 lineStart, Vector2 lineEnd, Vector2 point, float radius, out Vector2 rawRange) { // 计算线段方向向量和长度 Vector2 lineDir = lineEnd - lineStart; float lineLength = lineDir.magnitude; // 如果线段长度为零(两个端点重合) if (Mathf.Approximately(lineLength, 0f)) { // 检查点是否覆盖了这个点 var distance = Vector2.Distance(lineStart, point); rawRange = distance <= radius ? new Vector2(0f, 1f) : default; return distance <= radius; } // 计算点到线段所在直线距离 Vector2 lineDirNormalized = lineDir / lineLength; // 归一化方向向量 Vector2 pointToStart = point - lineStart; // 计算点到线段起点的向量 float projection = Vector2.Dot(pointToStart, lineDirNormalized); // 计算投影长度(点在线段上的投影位置) float closestT = Mathf.Clamp01(projection / lineLength); // 计算实际最近点在线段上的比例位置 Vector2 closestPoint = lineStart + closestT * lineDir; // 计算最近点坐标 float distanceToLine = Vector2.Distance(point, closestPoint); // 计算点到最近点的距离 // 如果距离大于半径,完全没有覆盖 if (distanceToLine > radius) { rawRange = default; return false; } // 计算覆盖范围 float halfCoveredLength = Mathf.Sqrt(radius * radius - distanceToLine * distanceToLine); // 计算实际覆盖的线段长度(勾股定理) float startCoverage = (projection - halfCoveredLength) / lineLength; float endCoverage = (projection + halfCoveredLength) / lineLength; // 计算覆盖的起点和终点比例 startCoverage = Mathf.Clamp01(startCoverage); endCoverage = Mathf.Clamp01(endCoverage); // 限制在0-1范围内 // 如果起点和终点相同,表示只覆盖了一个点 if (Mathf.Approximately(startCoverage, endCoverage)) { rawRange = default; return false; } rawRange = new Vector2(startCoverage, endCoverage); return true; } private void MergeRanges() { if (_recordedRanges.Count <= 1) return; // 先按起始位置排序 _recordedRanges.Sort((a, b) => a.x.CompareTo(b.x)); List mergedRanges = new List(); Vector2 currentRange = _recordedRanges[0]; for (int i = 1; i < _recordedRanges.Count; i++) { Vector2 nextRange = _recordedRanges[i]; // 检查是否有重叠或相邻 if (nextRange.x - currentRange.y <= 0.05f) { // 合并区间 currentRange.y = Mathf.Max(currentRange.y, nextRange.y); } else { mergedRanges.Add(currentRange); currentRange = nextRange; } } mergedRanges.Add(currentRange); _recordedRanges = mergedRanges; } private float CalcTotalRange() { return _recordedRanges.Select(item => item.y - item.x).Sum(); } #endregion public void HideLine() { gameObject.SetActive(false); } // 获取当前所有记录的范围(0-1之间的值) public List GetRecordedRanges() { return new List(_recordedRanges); } // 清除所有记录的范围 public void ClearRecordedRanges() { _recordedRanges.Clear(); } } }