using UnityEngine; using System.Collections.Generic; using Shapes; namespace AibisDream.MiniGame.Language { /// /// 连接线渲染器:使用Shapes绘制粒子间的连接线 /// 注意:sortingOrder 设置为 0,确保低于文字(文字 sortingOrder = 10) /// [ExecuteAlways] public class ConnectionRenderer : ImmediateModeShapeDrawer { [Header("引用")] public LanguageParticleManager manager; [Header("连接线设置")] public float connectionDistance = 2f; private Color nonTargetConnectionColor = new Color(1f, 0.3137255f, 0.4392157f, 0.7f); // 干扰连线 #FF5070 private Color targetConnectionColor = new Color(0.627451f, 1f, 0.8470588f, 1f); // 目标连线 #A0FFD8 private float nonTargetLineThickness = 0.02f; private float targetLineThickness = 0.04f; [Header("效果传播设置")] private Color propagationColor = new Color(1f, 0.59f, 0.2f, 1f); private float propagationLineThickness = 0.03f; [Header("发光设置(Bloom)")] [SerializeField] private bool enableGlow = true; [SerializeField, Range(1f, 5f)] private float targetGlowIntensity = 2f; // 目标线发光强度 [SerializeField, Range(1f, 3f)] private float nonTargetGlowIntensity = 1.2f; // 非目标线发光强度 [SerializeField, Range(1f, 5f)] private float propagationGlowIntensity = 3f; // 传播效果发光强度 private List candidateParticles; private List effectPropagations; private CompletionPhase completionPhase; private float fadeOutAlpha; private float nonTargetAlphaScale = 1f; private float targetAlphaScale = 1f; private Bounds screenBounds; private bool hasScreenBounds; private bool screenClippingEnabled; private bool warnedMissingBounds; public float NonTargetAlphaScale { get => nonTargetAlphaScale; set => nonTargetAlphaScale = Mathf.Clamp01(value); } public float TargetAlphaScale { get => targetAlphaScale; set => targetAlphaScale = Mathf.Clamp(value, 0f, 1.5f); } public void SetData( List candidates, List propagations, CompletionPhase phase, float fadeAlpha) { candidateParticles = candidates; effectPropagations = propagations; completionPhase = phase; fadeOutAlpha = fadeAlpha; } public void SetScreenBounds(Bounds bounds, bool valid = true) { screenBounds = bounds; hasScreenBounds = valid && bounds.size.x > 0.001f && bounds.size.y > 0.001f; if (hasScreenBounds) warnedMissingBounds = false; } public void SetScreenClippingEnabled(bool enabled) { screenClippingEnabled = enabled; } public override void DrawShapes(Camera cam) { if (candidateParticles == null || candidateParticles.Count == 0) return; if (screenClippingEnabled && !hasScreenBounds && !warnedMissingBounds) { warnedMissingBounds = true; Debug.LogWarning("[ConnectionRenderer] 未提供有效的 ExpressionScreenMaskRect,连线将沿用未裁切行为。"); } using (Draw.Command(cam)) { Draw.BlendMode = ShapesBlendMode.Transparent; Draw.ZTest = UnityEngine.Rendering.CompareFunction.Always; // 注意:连接线默认在底层,文字的 sortingOrder = 10 会显示在上层 // 绘制蓝色粒子间的连线 if (completionPhase == CompletionPhase.None || completionPhase == CompletionPhase.Completed) { DrawBlueConnections(); } // 绘制红色粒子间的连线 if (completionPhase == CompletionPhase.None || completionPhase == CompletionPhase.Completed || completionPhase == CompletionPhase.Focusing) { DrawRedConnections(); } // 绘制效果传播线 if (effectPropagations != null && effectPropagations.Count > 0) { DrawPropagationEffects(); } } } private void DrawBlueConnections() { Draw.LineGeometry = LineGeometry.Flat2D; Draw.Thickness = nonTargetLineThickness; for (int i = 0; i < candidateParticles.Count; i++) { for (int j = i + 1; j < candidateParticles.Count; j++) { var p1 = candidateParticles[i]; var p2 = candidateParticles[j]; // 跳过红色粒子 if (p1.isRed && p2.isRed) continue; float distance = Vector3.Distance(p1.transform.position, p2.transform.position); if (distance < connectionDistance) { float alpha = Mathf.Lerp(0.7f, 0f, distance / connectionDistance) * fadeOutAlpha; Color lineColor = nonTargetConnectionColor; lineColor.a = alpha * nonTargetAlphaScale * nonTargetConnectionColor.a; DrawClippedLine(p1.transform.position, p2.transform.position, lineColor); } } } } private void DrawRedConnections() { Draw.LineGeometry = LineGeometry.Flat2D; Draw.Thickness = targetLineThickness; var redParticles = new List(); foreach (var p in candidateParticles) { if (p.isRed) redParticles.Add(p); } for (int i = 0; i < redParticles.Count; i++) { for (int j = i + 1; j < redParticles.Count; j++) { float distance = Vector3.Distance( redParticles[i].transform.position, redParticles[j].transform.position ); if (distance < connectionDistance) { float alpha = Mathf.Lerp(1f, 0.4f, distance / connectionDistance); Color lineColor = targetConnectionColor; lineColor.a = alpha * targetAlphaScale * targetConnectionColor.a; DrawClippedLine(redParticles[i].transform.position, redParticles[j].transform.position, lineColor); } } } } private void DrawPropagationEffects() { Draw.LineGeometry = LineGeometry.Flat2D; Draw.Thickness = propagationLineThickness; foreach (var propagation in effectPropagations) { if (propagation.PropagatedParticles == null) continue; foreach (var info in propagation.PropagatedParticles) { if (info.source == null || info.particle == null) continue; // 绘制连接线(面板色直出) Color lineColor = propagationColor; lineColor.a = 0.3f * propagationColor.a; DrawClippedLine(info.source.transform.position, info.particle.transform.position, lineColor); // 绘制脉冲光点 if (info.pulseProgress < 1f) { Vector3 pulsePos = Vector3.Lerp( info.source.transform.position, info.particle.transform.position, info.pulseProgress ); float pulseSize = Mathf.Lerp(0.03f, 0.08f, info.pulseProgress); float pulseAlpha = Mathf.Lerp(1f, 0f, info.pulseProgress); Color pulseColor = new Color(1f, 0.78f, 0.39f, pulseAlpha); float visualRadius = pulseSize * 1.5f; if (TryConstrainPoint(pulsePos, visualRadius, out Vector3 containedPulse)) { Draw.Ring(containedPulse, pulseSize, pulseSize * 0.5f, pulseColor); // 光晕 pulseColor.a = pulseAlpha * 0.5f; Draw.Disc(containedPulse, visualRadius, pulseColor); } } // 绘制箭头 if (info.pulseProgress > 0.5f && info.pulseProgress < 1f) { Vector3 arrowPos = Vector3.Lerp( info.source.transform.position, info.particle.transform.position, 0.5f ); Vector3 direction = (info.particle.transform.position - info.source.transform.position).normalized; float angle = Mathf.Atan2(direction.y, direction.x); Color arrowColor = new Color(1f, 0.78f, 0.39f, 0.8f); const float arrowSize = 0.1f; if (TryConstrainPoint(arrowPos, arrowSize, out Vector3 containedArrow)) DrawArrow(containedArrow, angle, arrowSize, arrowColor); } } } } private void DrawArrow(Vector3 position, float angle, float size, Color color) { Vector3 tip = position + new Vector3(Mathf.Cos(angle), Mathf.Sin(angle), 0) * size; Vector3 left = position + new Vector3( Mathf.Cos(angle + 2.5f), Mathf.Sin(angle + 2.5f), 0 ) * size * 0.5f; Vector3 right = position + new Vector3( Mathf.Cos(angle - 2.5f), Mathf.Sin(angle - 2.5f), 0 ) * size * 0.5f; Draw.Triangle(tip, left, right, color); } private void DrawClippedLine(Vector3 from, Vector3 to, Color color) { if (screenClippingEnabled && hasScreenBounds) { if (!TryClipLineToBounds(from, to, screenBounds, out from, out to)) return; } Draw.Line(from, to, color); } private bool TryConstrainPoint(Vector3 point, float radius, out Vector3 constrained) { if (!screenClippingEnabled || !hasScreenBounds) { constrained = point; return true; } float minX = screenBounds.min.x + radius; float maxX = screenBounds.max.x - radius; float minY = screenBounds.min.y + radius; float maxY = screenBounds.max.y - radius; if (minX > maxX || minY > maxY) { constrained = point; return false; } constrained = new Vector3( Mathf.Clamp(point.x, minX, maxX), Mathf.Clamp(point.y, minY, maxY), point.z); return true; } /// /// Liang–Barsky line clipping against a world-space axis-aligned rectangle. /// Kept public for component tests and editor diagnostics. /// public static bool TryClipLineToBounds( Vector3 from, Vector3 to, Bounds bounds, out Vector3 clippedFrom, out Vector3 clippedTo) { clippedFrom = from; clippedTo = to; float dx = to.x - from.x; float dy = to.y - from.y; float t0 = 0f; float t1 = 1f; if (!ClipTest(-dx, from.x - bounds.min.x, ref t0, ref t1) || !ClipTest(dx, bounds.max.x - from.x, ref t0, ref t1) || !ClipTest(-dy, from.y - bounds.min.y, ref t0, ref t1) || !ClipTest(dy, bounds.max.y - from.y, ref t0, ref t1)) { return false; } clippedFrom = Vector3.LerpUnclamped(from, to, t0); clippedTo = Vector3.LerpUnclamped(from, to, t1); return true; } private static bool ClipTest(float p, float q, ref float t0, ref float t1) { if (Mathf.Approximately(p, 0f)) return q >= 0f; float r = q / p; if (p < 0f) { if (r > t1) return false; if (r > t0) t0 = r; } else { if (r < t0) return false; if (r < t1) t1 = r; } return true; } /// /// 设置连接线样式 /// public void SetConnectionStyles( Color targetColor, Color nonTargetColor, Color propColor, float targetThickness, float nonTargetThickness, float propThickness) { targetConnectionColor = targetColor; nonTargetConnectionColor = nonTargetColor; propagationColor = propColor; targetLineThickness = targetThickness; nonTargetLineThickness = nonTargetThickness; propagationLineThickness = propThickness; } /// /// 保留 API;不再对连线做 HDR 倍增,颜色以 SetConnectionStyles 为准。 /// public void SetGlowSettings(bool enabled, float targetIntensity, float nonTargetIntensity, float propagationIntensity) { enableGlow = false; targetGlowIntensity = 1f; nonTargetGlowIntensity = 1f; propagationGlowIntensity = 1f; } } }