Files
aibis-dream/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs
T

413 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
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<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++)
{
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;
}
/// <summary>
/// LiangBarsky 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;
}
}
}