381 lines
14 KiB
C#
381 lines
14 KiB
C#
using UnityEngine;
|
||
using System.Collections.Generic;
|
||
using Shapes;
|
||
|
||
namespace AibisDream.MiniGame.Language
|
||
{
|
||
/// <summary>
|
||
/// 连接线渲染器:使用Shapes绘制粒子间的连接线
|
||
/// 注意:sortingOrder 设置为 0,确保低于文字(文字 sortingOrder = 10)
|
||
/// </summary>
|
||
[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<CandidateParticle> candidateParticles;
|
||
private List<EffectPropagation> 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<CandidateParticle> candidates,
|
||
List<EffectPropagation> 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<CandidateParticle>();
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Liang–Barsky line clipping against a world-space axis-aligned rectangle.
|
||
/// Kept public for component tests and editor diagnostics.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置连接线样式
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保留 API;不再对连线做 HDR 倍增,颜色以 SetConnectionStyles 为准。
|
||
/// </summary>
|
||
public void SetGlowSettings(bool enabled, float targetIntensity, float nonTargetIntensity, float propagationIntensity)
|
||
{
|
||
enableGlow = false;
|
||
targetGlowIntensity = 1f;
|
||
nonTargetGlowIntensity = 1f;
|
||
propagationGlowIntensity = 1f;
|
||
}
|
||
}
|
||
}
|