using System.Collections.Generic; using Shapes; using UnityEngine; using UnityEngine.Rendering.Universal; using UnityEngine.VFX; using DG.Tweening; public class GearFollower : MonoBehaviour { [Header("参数")] public SpriteRenderer targetSpriteRenderer; public float snapDistance = 0.3f; public float detachDistance = 1.0f; public float tractionSpeed = 2f; public float edgePointsScale = 1.2f; // 边缘点缩放倍率 [Header("切割轨迹")] public LineRenderer cutLineRenderer; public float cutLineWidth = 0.1f; public float noiseAmount = 0.05f; // 毛刺偏移幅度 public float segmentDuration = 4f; // 线段持续时间 [Header("震动与光照反馈")] public Light2D cutLight; // 需要使用 Unity 的 2D 点光源(URP) public float lightPulseSpeed = 5f; public float lightMaxIntensity = 1.5f; public float shakeAmount = 0.05f; [Header("VFX火花")] public VisualEffect sparkVFX; // 关联Spark VFX Graph组件 public float maxEmissionRate = 50f; // 最大发射速率 public float emissionChangeSpeed = 100f; // 发射速率变化速度 [Header("切割完成效果")] public float cutCompletionThreshold = 0.95f; // 切割完成阈值(切割路径占边缘的比例) public float minCutLength = 0.5f; // 最小切割长度(相对于总长度的比例) public float targetShakeAmount = 0.02f; // 目标抖动幅度 public float cutCompleteTiltAngle = 15f; // 切割完成后的倾斜角度 public float cutCompleteOffset = 0.2f; // 切割完成后的位移 public float cutCompleteScale = 1.2f; // 切割完成后的放大倍数 public float fadeOutDuration = 1f; // 淡出持续时间 public float cutCompleteDelay = 1f; // 切割完成后的延迟时间 public float scaleUpDuration = 1f; // 放大动画持续时间 public float fadeOutDelay = 1f; // 淡出前的延迟时间 public AudioClip cutCompleteSound; // 切割完成音效 private float totalEdgeLength = 0f; // 总边缘长度 private float cutLength = 0f; // 已切割长度 private bool isCutComplete = false; private bool isFadingOut = false; private float fadeOutTimer = 0f; private Vector3 originalTargetPosition; private Quaternion originalTargetRotation; private Vector3 originalTargetScale; private AudioSource audioSource; private Sequence cutCompleteSequence; // DOTween序列 private HashSet cutEdgeIndices = new HashSet(); // 记录已切割的边缘点索引 private float currentEmissionRate = 0f; private Vector3 originalLocalPosition; private List edgePoints; private List originalEdgePoints; // 存储原始边缘点 private int currentEdgeIndex = 0; private int tractionDirection = 1; private enum State { Follow, Traction } private State currentState = State.Follow; private Camera mainCamera; private bool isCutting = false; private float directionSwitchCooldown = 0.2f; private float directionSwitchTimer = 0f; private class TimedPoint { public Vector3 position; public float timeAdded; public TimedPoint(Vector3 pos, float time) { position = pos; timeAdded = time; } } private List timedCutPoints = new List(); public float cutLineDelay = 3f; private Vector3 shakeOffset = Vector3.zero; private Vector3 targetPosition = Vector3.zero; private bool isInTractionMode = false; void Start() { mainCamera = Camera.main; LoadEdgePointsFromSprite(); CalculateTotalEdgeLength(); if (cutLineRenderer != null) { cutLineRenderer.positionCount = 0; cutLineRenderer.widthCurve = AnimationCurve.Constant(0, 1, cutLineWidth); cutLineRenderer.material = new Material(Shader.Find("Sprites/Default")); cutLineRenderer.numCapVertices = 0; cutLineRenderer.sortingOrder = 1; } if (cutLight != null) { cutLight.intensity = 0f; cutLight.enabled = false; } // 初始化目标相关变量 if (targetSpriteRenderer != null) { originalTargetPosition = targetSpriteRenderer.transform.position; originalTargetRotation = targetSpriteRenderer.transform.rotation; originalTargetScale = targetSpriteRenderer.transform.localScale; } // 添加音频源组件 audioSource = gameObject.AddComponent(); audioSource.playOnAwake = false; audioSource.spatialBlend = 0f; // 2D音效 } void Update() { Vector2 mouseWorldPos = mainCamera.ScreenToWorldPoint(Input.mousePosition); if (Input.GetMouseButton(0)) { transform.Rotate(Vector3.forward, -360f * Time.deltaTime); } switch (currentState) { case State.Follow: UpdateFollowState(mouseWorldPos); break; case State.Traction: UpdateTractionState(mouseWorldPos); break; } UpdateCutLineRenderer(); UpdateVisualFeedback(); } private void UpdateVisualFeedback() { if (isInTractionMode && isCutting) { // 原始小幅震动 Vector2 baseShake = Random.insideUnitCircle * shakeAmount; shakeOffset = (Vector3)baseShake; transform.position = targetPosition + shakeOffset; // 目标抖动 if (targetSpriteRenderer != null && !isCutComplete) { Vector2 targetShake = Random.insideUnitCircle * targetShakeAmount; targetSpriteRenderer.transform.position = originalTargetPosition + (Vector3)targetShake; } // 根据震动幅度控制灯光强度 if (cutLight != null) { cutLight.enabled = true; float intensity = shakeOffset.magnitude / shakeAmount; // 得到 [0,1] 比例 cutLight.intensity = intensity * lightMaxIntensity; } } else { shakeOffset = Vector3.zero; if (isInTractionMode) { transform.position = targetPosition; } if (cutLight != null) { cutLight.intensity = 0f; cutLight.enabled = false; } } // 处理淡出效果 if (isFadingOut) { fadeOutTimer += Time.deltaTime; float alpha = 1f - (fadeOutTimer / fadeOutDuration); if (targetSpriteRenderer != null) { Color color = targetSpriteRenderer.color; color.a = alpha; targetSpriteRenderer.color = color; } // 齿轮也淡出 SpriteRenderer gearRenderer = GetComponent(); if (gearRenderer != null) { Color gearColor = gearRenderer.color; gearColor.a = alpha; gearRenderer.color = gearColor; } if (fadeOutTimer >= fadeOutDuration) { // 淡出完成后销毁物体 Destroy(gameObject); if (targetSpriteRenderer != null) { Destroy(targetSpriteRenderer.gameObject); } } } } private void LoadEdgePointsFromSprite() { edgePoints = new List(); originalEdgePoints = new List(); if (targetSpriteRenderer == null) { Debug.LogError("targetSpriteRenderer 为空"); return; } List shapePoints = new List(); targetSpriteRenderer.sprite.GetPhysicsShape(0, shapePoints); if (shapePoints.Count == 0) { Debug.LogError("目标Sprite没有物理形状点"); return; } float desiredSegmentLength = 0.1f; edgePoints.Clear(); originalEdgePoints.Clear(); Vector2 center = Vector2.zero; foreach (Vector2 point in shapePoints) { center += point; } center /= shapePoints.Count; for (int i = 0; i < shapePoints.Count; i++) { Vector2 p1 = targetSpriteRenderer.transform.TransformPoint(shapePoints[i]); Vector2 p2 = targetSpriteRenderer.transform.TransformPoint(shapePoints[(i + 1) % shapePoints.Count]); Vector2 scaledP1 = center + (p1 - center) * edgePointsScale; Vector2 scaledP2 = center + (p2 - center) * edgePointsScale; float segmentLength = Vector2.Distance(scaledP1, scaledP2); int steps = Mathf.Max(1, Mathf.CeilToInt(segmentLength / desiredSegmentLength)); for (int s = 0; s < steps; s++) { float t = (float)s / steps; Vector2 interpolated = Vector2.Lerp(scaledP1, scaledP2, t); Vector2 originalInterpolated = Vector2.Lerp(p1, p2, t); edgePoints.Add(interpolated); originalEdgePoints.Add(originalInterpolated); } } if ((edgePoints[0] - edgePoints[edgePoints.Count - 1]).sqrMagnitude > 0.0001f) { edgePoints.Add(edgePoints[0]); originalEdgePoints.Add(originalEdgePoints[0]); } } private void UpdateFollowState(Vector2 mousePos) { transform.position = mousePos; isInTractionMode = false; int nearestIndex = FindNearestEdgePointIndex(mousePos, out float dist); if (dist < snapDistance) { currentEdgeIndex = nearestIndex; Vector2 prev = edgePoints[WrapIndex(currentEdgeIndex - 1)]; Vector2 next = edgePoints[WrapIndex(currentEdgeIndex + 1)]; float distToPrev = Vector2.Distance(mousePos, prev); float distToNext = Vector2.Distance(mousePos, next); tractionDirection = (distToNext < distToPrev) ? 1 : -1; targetPosition = edgePoints[currentEdgeIndex]; transform.position = targetPosition; currentState = State.Traction; isInTractionMode = true; } } private void UpdateTractionState(Vector2 mousePos) { Vector2 gearPos = transform.position - shakeOffset; if (Vector2.Distance(mousePos, gearPos) > detachDistance) { currentState = State.Follow; isInTractionMode = false; EndCutting(); return; } if (!Input.GetMouseButton(0)) { EndCutting(); return; } if (!isCutting) { StartCutting(); } int nextIndex = WrapIndex(currentEdgeIndex + tractionDirection); Vector2 from = edgePoints[currentEdgeIndex]; Vector2 to = edgePoints[nextIndex]; Vector2 forward = (to - from).normalized; Vector2 toMouse = (mousePos - from).normalized; float angle = Vector2.SignedAngle(forward, toMouse); if (Mathf.Abs(angle) > 120f) { directionSwitchTimer += Time.deltaTime; if (directionSwitchTimer > directionSwitchCooldown) { tractionDirection *= -1; directionSwitchTimer = 0f; nextIndex = WrapIndex(currentEdgeIndex + tractionDirection); from = edgePoints[currentEdgeIndex]; to = edgePoints[nextIndex]; forward = (to - from).normalized; } } else { directionSwitchTimer = 0f; } Vector2 targetPoint = GetClosestPointOnSegment(from, to, mousePos); Vector2 moveDir = (targetPoint - gearPos); float distance = moveDir.magnitude; float maxStep = tractionSpeed * Time.deltaTime; if (distance <= maxStep) { targetPosition = targetPoint; UpdateCutProgress(from, targetPoint); // 更新切割进度 if (Vector2.Distance(targetPoint, to) < 0.01f) { currentEdgeIndex = nextIndex; } } else { targetPosition = (Vector3)gearPos + (Vector3)(moveDir.normalized * maxStep); } transform.position = targetPosition + shakeOffset; // 使用原始边缘点来记录切割线 Vector2 originalFrom = originalEdgePoints[currentEdgeIndex]; Vector2 originalTo = originalEdgePoints[nextIndex]; Vector2 originalTargetPoint = GetClosestPointOnSegment(originalFrom, originalTo, transform.position - shakeOffset); // 记录切割点(带噪声模拟毛糙) Vector3 noisyPoint = originalTargetPoint; noisyPoint.x += Random.Range(-noiseAmount, noiseAmount); noisyPoint.y += Random.Range(-noiseAmount, noiseAmount); AddCutPoint(noisyPoint); if (sparkVFX != null) { // 计算从切割点到目标精灵中心的向量 Vector2 center = targetSpriteRenderer.transform.position; Vector2 toCenter = center - (Vector2)originalTargetPoint; // 计算法线方向(垂直于切割方向) Vector2 normal = Vector2.Perpendicular(moveDir.normalized); // 根据切割方向决定法线方向 if (tractionDirection > 0) { normal = -normal; } Vector3 velocity = (Vector3)normal * 10f; sparkVFX.SetVector3("Initial Velocity1", velocity); } } private void StartCutting() { isCutting = true; //cutLinePoints.Clear(); if (cutLineRenderer != null) { cutLineRenderer.positionCount = 0; } if (sparkVFX != null) { sparkVFX.Play(); } } private void AddCutPoint(Vector3 point) { if (timedCutPoints.Count == 0 || Vector3.Distance(timedCutPoints[timedCutPoints.Count - 1].position, point) > 0.05f) { timedCutPoints.Add(new TimedPoint(point, Time.time)); // 更新光照位置到最新切点 if (cutLight != null) { cutLight.transform.position = point; } // 把火花特效位置设置到当前切割点 if (sparkVFX != null) { sparkVFX.transform.position = point; } if (timedCutPoints.Count >= 2) { Vector3 prev = timedCutPoints[timedCutPoints.Count - 2].position; Vector3 curr = timedCutPoints[timedCutPoints.Count - 1].position; CreateSegment(prev, curr); } } } private void UpdateCutLineRenderer() { if (cutLineRenderer == null) return; float currentTime = Time.time; List visiblePoints = new List(); foreach (var timedPoint in timedCutPoints) { if (currentTime - timedPoint.timeAdded >= cutLineDelay) { visiblePoints.Add(timedPoint.position); } else { } } cutLineRenderer.positionCount = visiblePoints.Count; if (visiblePoints.Count > 0) cutLineRenderer.SetPositions(visiblePoints.ToArray()); } private void EndCutting() { isCutting = false; if (sparkVFX != null) { sparkVFX.Stop(); } } private int FindNearestEdgePointIndex(Vector2 pos, out float minDist) { int nearestIndex = 0; minDist = float.MaxValue; for (int i = 0; i < edgePoints.Count; i++) { float dist = Vector2.Distance(pos, edgePoints[i]); if (dist < minDist) { minDist = dist; nearestIndex = i; } } return nearestIndex; } private Vector2 GetClosestPointOnSegment(Vector2 a, Vector2 b, Vector2 p) { Vector2 ab = b - a; float t = Vector2.Dot(p - a, ab) / ab.sqrMagnitude; t = Mathf.Clamp01(t); return a + ab * t; } private int WrapIndex(int index) { if (index < 0) return edgePoints.Count - 2; if (index >= edgePoints.Count - 1) return 0; return index; } private void CreateSegment(Vector3 from, Vector3 to) { GameObject segObj = new GameObject("HeatLineSegment"); segObj.transform.parent = this.transform; LineRenderer segLine = segObj.AddComponent(); segLine.positionCount = 2; segLine.SetPosition(0, from); segLine.SetPosition(1, to); segLine.sortingOrder = 4; segLine.material = new Material(Shader.Find("Sprites/Default")); segLine.widthCurve = AnimationCurve.Constant(0, 1, cutLineWidth); segLine.numCapVertices = 0; var segmentController = segObj.AddComponent(); segmentController.duration = segmentDuration; } private void CalculateTotalEdgeLength() { totalEdgeLength = 0f; for (int i = 0; i < edgePoints.Count - 1; i++) { totalEdgeLength += Vector2.Distance(edgePoints[i], edgePoints[i + 1]); } } private void UpdateCutProgress(Vector2 from, Vector2 to) { cutLength += Vector2.Distance(from, to); float progress = cutLength / totalEdgeLength; // 记录已切割的边缘点 cutEdgeIndices.Add(currentEdgeIndex); // 检查是否满足切割完成条件 if (!isCutComplete && progress >= cutCompletionThreshold && progress >= minCutLength) { // 检查是否形成了连续的切割路径 bool hasContinuousCut = CheckContinuousCut(); if (hasContinuousCut) { isCutComplete = true; OnCutComplete(); } } } private bool CheckContinuousCut() { if (cutEdgeIndices.Count < 2) return false; // 将索引转换为有序列表 List sortedIndices = new List(cutEdgeIndices); sortedIndices.Sort(); // 检查是否有连续的切割点 int continuousCount = 1; int maxContinuousCount = 1; for (int i = 1; i < sortedIndices.Count; i++) { if (sortedIndices[i] == sortedIndices[i - 1] + 1) { continuousCount++; maxContinuousCount = Mathf.Max(maxContinuousCount, continuousCount); } else { continuousCount = 1; } } // 如果连续切割点的数量超过总点数的50%,认为形成了有效的切割路径 return maxContinuousCount >= edgePoints.Count * 0.5f; } private void OnCutComplete() { // 停止切割状态 isCutting = false; isInTractionMode = false; enabled = false; // 清理特效 if (cutLight != null) { cutLight.enabled = false; } if (sparkVFX != null) { sparkVFX.Stop(); } if (cutLineRenderer != null) { cutLineRenderer.positionCount = 0; } timedCutPoints.Clear(); // 目标倾斜和位移 if (targetSpriteRenderer != null) { // 计算随机方向 float randomAngle = Random.Range(-cutCompleteTiltAngle, cutCompleteTiltAngle); Vector2 randomOffset = Random.insideUnitCircle * cutCompleteOffset; // 立即应用倾斜和位移 targetSpriteRenderer.transform.rotation = Quaternion.Euler(0, 0, randomAngle); targetSpriteRenderer.transform.position = originalTargetPosition + (Vector3)randomOffset; // 创建动画序列 cutCompleteSequence = DOTween.Sequence(); // 等待一段时间后开始放大 cutCompleteSequence.AppendInterval(cutCompleteDelay); cutCompleteSequence.Append(targetSpriteRenderer.transform.DOScale(originalTargetScale * cutCompleteScale, scaleUpDuration) .SetEase(Ease.OutQuad)); // 等待一段时间后开始淡出 cutCompleteSequence.AppendInterval(fadeOutDelay); cutCompleteSequence.Append(targetSpriteRenderer.DOFade(0f, fadeOutDuration)); // 齿轮也同时淡出 SpriteRenderer gearRenderer = GetComponent(); if (gearRenderer != null) { cutCompleteSequence.Join(gearRenderer.DOFade(0f, fadeOutDuration)); } // 动画完成后销毁物体 cutCompleteSequence.OnComplete(() => { Destroy(gameObject); if (targetSpriteRenderer != null) { Destroy(targetSpriteRenderer.gameObject); } }); } } private void OnDestroy() { // 清理DOTween序列 if (cutCompleteSequence != null) { cutCompleteSequence.Kill(); } } }