using System.Collections.Generic; using Shapes; using UnityEngine; using UnityEngine.Rendering.Universal; using UnityEngine.VFX; 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; // 发射速率变化速度 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(); 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; } } 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 (cutLight != null) { cutLight.enabled = true; float intensity = shakeOffset.magnitude / shakeAmount; // 得到 [0,1] 比例 cutLight.intensity = intensity * lightMaxIntensity; } // if (sparkVFX != null) // { // sparkVFX.SendEvent("OnPlay"); // 这里的 "OnBurst" 是你在VFX Graph里定义的事件名,需要一致 // } } else { shakeOffset = Vector3.zero; if (isInTractionMode) { transform.position = targetPosition; } if (cutLight != null) { cutLight.intensity = 0f; cutLight.enabled = false; } // if (sparkVFX != null) // { // sparkVFX.SendEvent("OnStop"); // 这里的 "OnBurst" 是你在VFX Graph里定义的事件名,需要一致 // } } } 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; 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; } }