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;
if (!p1.connections.Contains(p2))
continue;
ResolveConnectionVisual(
p1,
p2,
out Vector3 from,
out Vector3 to,
out float proximity);
float alpha = Mathf.Lerp(0f, 0.7f, proximity) * fadeOutAlpha;
Color lineColor = nonTargetConnectionColor;
lineColor.a = alpha * nonTargetAlphaScale * nonTargetConnectionColor.a;
DrawClippedLine(from, to, 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++)
{
CandidateParticle first = redParticles[i];
CandidateParticle second = redParticles[j];
if (!first.connections.Contains(second))
continue;
ResolveConnectionVisual(
first,
second,
out Vector3 from,
out Vector3 to,
out float proximity);
float alpha = Mathf.Lerp(0.4f, 1f, proximity);
Color lineColor = targetConnectionColor;
lineColor.a = alpha * targetAlphaScale * targetConnectionColor.a;
DrawClippedLine(from, to, lineColor);
}
}
}
private void ResolveConnectionVisual(
CandidateParticle first,
CandidateParticle second,
out Vector3 from,
out Vector3 to,
out float proximity)
{
if (manager != null &&
manager.TryGetConnectionVisual(
first,
second,
out from,
out to,
out proximity))
{
return;
}
from = first.transform.position;
to = second.transform.position;
float distance = Vector3.Distance(from, to);
proximity = 1f - Mathf.Clamp01(distance / Mathf.Max(0.0001f, connectionDistance));
}
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;
}
}
}