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

1693 lines
65 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;
using System.Collections.Generic;
using System.Linq;
using DG.Tweening;
using Shapes;
using TMPro;
using UnityEngine.UI;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 粒子排布区域形状
/// </summary>
public enum LayoutShape
{
Rectangle, // 矩形
Circle // 圆形
}
/// <summary>
/// 游戏完成阶段
/// </summary>
public enum CompletionPhase
{
None, // 未完成
Completed, // 已完成但尚未进入聚焦流程
Focusing, // 正在聚焦放大并收敛
Finished // 全流程完成
}
/// <summary>
/// 语言粒子系统主管理器
/// </summary>
public class LanguageParticleManager : MonoBehaviour
{
[Header("Canvas设置")]
[SerializeField] private Canvas worldCanvas;
[SerializeField] private float canvasSize = 10f;
[SerializeField] private LayoutShape layoutShape = LayoutShape.Rectangle;
[Tooltip("圆形布局时的半径(留空则自动使用 canvasSize/2 - textMargin")]
[SerializeField] private float circleRadiusOverride = -1f;
[Header("字体设置")]
[SerializeField] private TMP_FontAsset chineseFontAsset;
[Header("粒子预制体")]
[SerializeField] private GameObject floatingParticlePrefab;
[SerializeField] private GameObject candidateParticlePrefab;
[Header("粒子数量")]
[SerializeField] private int floatingCount = 100;
[SerializeField] private int candidateCount = 50;
[SerializeField] private int redParticleCount = 8;
[Header("游戏参数")]
[SerializeField] private float connectionDistance = 2f; // Unity单位
[SerializeField] private float textMargin = 1f;
[Header("游戏配置")]
[SerializeField] private List<string> anxietyPhrases = new List<string>(); // 焦虑短句配置
[SerializeField] private string targetSentence = "这是一个测试示例"; // 目标句子
[SerializeField] private string completionDialogNode = ""; // 完成时触发的对话节点
[Header("波形参数")]
[SerializeField] private float waveformAmplitude = 0.3f;
[SerializeField] private float interactionRadius = 0.5f;
[Header("鼠标交互参数")]
[SerializeField] private float mouseInteractionRadius = 1.5f;
[SerializeField] private float repulsionForce = 2f;
[SerializeField] private float attractionForce = 1.5f;
[Header("颜色配置(用于粒子/字的颜色)")]
[SerializeField] private Color targetParticleColor = new Color(1f, 0.39f, 0.39f, 1f); // 目标粒子颜色(红)
[SerializeField] private Color nonTargetParticleColor = new Color(0.2f, 0.78f, 1f, 1f); // 非目标粒子颜色(蓝)
[SerializeField] private Color floatingTextColor = new Color(0.7f, 0.78f, 1f, 1f); // 背景浮动文字颜色
[SerializeField] private Color targetCalmColor = new Color(0.78f, 0.59f, 0.59f, 1f); // 目标粒子平静后颜色
[Header("连接线颜色(用于连线的颜色,与上面可分开调节)")]
[SerializeField] private Color targetConnectionColor = new Color(1f, 0.39f, 0.39f, 1f); // 目标连线颜色
[SerializeField] private Color nonTargetConnectionColor = new Color(0.39f, 0.78f, 1f, 0.7f);// 非目标连线颜色
[Header("文字样式配置")]
[SerializeField] private float targetParticleFontSize = 4.5f; // 目标粒子字体大小
[SerializeField] private float nonTargetParticleFontSize = 3.7f; // 非目标粒子字体大小
[SerializeField] private float floatingTextFontSize = 3f; // 背景浮动文字大小
[SerializeField] private bool targetParticleBold = true; // 目标粒子是否加粗
[SerializeField] private bool nonTargetParticleBold = false; // 非目标粒子是否加粗
[Header("连接线粗细")]
[SerializeField] private float targetConnectionThickness = 0.04f; // 目标连线粗细
[SerializeField] private float nonTargetConnectionThickness = 0.02f; // 非目标连线粗细
[Header("Bloom 发光效果")]
[SerializeField] private bool enableBloomGlow = true; // 是否启用 Bloom 发光
[SerializeField, Range(1f, 5f)] private float textGlowIntensity = 1.8f; // 文字发光强度
[SerializeField, Range(1f, 5f)] private float targetTextGlowIntensity = 2.5f; // 目标粒子文字发光强度
[SerializeField, Range(1f, 5f)] private float lineGlowIntensity = 2f; // 连接线发光强度
[SerializeField, Range(1f, 5f)] private float targetLineGlowIntensity = 2.5f; // 目标连接线发光强度
[SerializeField, Range(0f, 1f)] private float glowDilate = 0.3f; // 发光扩散
[SerializeField, Range(0f, 1f)] private float glowSoftness = 0.5f; // 发光柔和度
[Header("目标 UI")]
[SerializeField] private TMP_Text integrationProgressText;
[SerializeField] private TMP_Text interferenceCountText;
[Header("完成阶段 UI")]
[SerializeField] private Button focusSequenceButton;
[SerializeField] private float focusZoomScale = 1.6f;
[SerializeField] private float focusMoveDuration = 1.2f;
[SerializeField] private float focusFadeDuration = 1f;
[SerializeField] private float probabilityDuration = 3.5f;
[SerializeField] private float predictionSequenceGap = 0.4f;
[SerializeField] private float spreadDistance = 0.35f;
[Header("聚焦表现")]
[SerializeField] private Color focusRingColor = Color.white;
[SerializeField, Range(0.01f, 0.3f)] private float focusRingThickness = 0.08f;
[SerializeField, Range(0.05f, 1f)] private float focusRingAlpha = 0.85f;
[SerializeField] private float statusFadeOutDuration = 0.45f;
[SerializeField] private float shuffleDistance = 0.35f;
[SerializeField] private float shuffleFrequency = 3.5f;
[SerializeField] private float shuffleVerticalScale = 0.6f;
[Header("开场表现")]
[SerializeField] private float entranceDelay = 0.45f;
[SerializeField] private int entranceCoreCount = 8;
[SerializeField] private float entranceCoreRadius = 0.55f;
[SerializeField] private float entranceInitialScale = 0.25f;
[SerializeField] private float entranceCoreBurstDuration = 1f;
[SerializeField] private float entranceCoreStagger = 0.08f;
[SerializeField] private float entranceCascadeDuration = 0.8f;
[SerializeField] private float entranceCascadeStagger = 0.03f;
[SerializeField] private float entranceSearchRadius = 2f;
[Header("渲染层设置")]
[SerializeField] private string renderLayerName = "HuoshanScreen";
private int renderLayer = 0;
[Header("调试/测试")]
[SerializeField] private bool enableDebugShortcuts = true;
[SerializeField] private KeyCode debugCompleteKey = KeyCode.F5;
[SerializeField] private KeyCode debugResetKey = KeyCode.F9;
[SerializeField] private KeyCode debugFullTestKey = KeyCode.F10;
[SerializeField] private bool autoRunDebugTestOnStart = false;
[Header("布局优化")]
[SerializeField] private bool enableSeparation = true; // 是否启用粒子分离力
[SerializeField] private float minParticleSpacing = 0.6f; // 粒子最小间距
[SerializeField] private float separationForce = 2.5f; // 分离力度
[SerializeField] private float lineAlphaMultiplier = 0.65f; // 连接线透明度缩放
// 粒子列表
private List<FloatingTextParticle> floatingParticles = new List<FloatingTextParticle>();
private List<CandidateParticle> candidateParticles = new List<CandidateParticle>();
private List<CandidateParticle> targetParticles = new List<CandidateParticle>(); // 目标粒子列表(原 redParticles
// 效果系统(传播效果已移除,保留空列表供渲染器使用)
private static readonly List<EffectPropagation> s_emptyPropagations = new List<EffectPropagation>();
private Dictionary<string, bool> previousConnections = new Dictionary<string, bool>();
private bool initializationComplete = false;
private int initializationFrames = 0;
// 游戏状态
private bool isCompleted = false;
private CompletionPhase completionPhase = CompletionPhase.None;
private float completionTimer = 0f;
private float fadeOutAlpha = 1f;
private List<CandidateParticle> orderedRedParticles = new List<CandidateParticle>();
private bool allowInput = true;
private float focusTimer = 0f;
private Dictionary<CandidateParticle, TargetFocusVisual> focusVisuals = new Dictionary<CandidateParticle, TargetFocusVisual>();
private Vector3 focusCentroid;
private List<string> finalTargetCharacters = new List<string>();
private List<Vector3> finalTargetPositions = new List<Vector3>();
private bool interferenceLogicSuspended = false;
private bool statusUIHidden = false;
private float integrationTextBaseAlpha = 1f;
private float interferenceTextBaseAlpha = 1f;
private Coroutine entranceRoutine;
private Coroutine debugTestRoutine;
private readonly Dictionary<CandidateParticle, Vector3> entranceTargets = new Dictionary<CandidateParticle, Vector3>();
private class TargetFocusVisual
{
public CandidateParticle particle;
public GameObject ringObject;
public Disc ringDisc;
public TMP_Text probabilityLabel;
public float probability = 90f;
public float targetProbability = 100f;
public float startShakeIntensity;
public string finalChar;
public bool stabilized;
public Vector3 focusPosition;
public Vector3 finalPosition;
public int orderIndex;
}
private bool focusTweensCompleted = false;
// 边界
private Bounds movementBounds;
private Vector3 boundsCenter;
private float boundsRadius; // 圆形时有效
// 连接渲染(使用Shapes
private ConnectionRenderer connectionRenderer;
// 噪点叠加渲染器(复古显示器效果)
private NoiseOverlayRenderer noiseOverlayRenderer;
[Header("复古显示器效果")]
[SerializeField] private bool enableRetroEffect = true;
[SerializeField, Range(0f, 1f)] private float retroEffectIntensity = 0.5f;
// 是否已经初始化
private bool isInitialized = false;
private void Start()
{
// Start 中不再自动初始化,等待外部调用 InitializeSystem
}
/// <summary>
/// 初始化系统(可以由外部调用,用于配置游戏参数)
/// </summary>
/// <param name="phrases">焦虑短句列表</param>
/// <param name="sentence">目标句子</param>
/// <param name="dialogNode">完成时触发的对话节点</param>
public void InitializeSystem(List<string> phrases, string sentence, string dialogNode = null)
{
// 如果是第一次初始化,先执行基础初始化
if (!isInitialized)
{
InitializeCanvas();
InitializeParticles();
InitializeRenderers();
if (integrationProgressText != null)
{
integrationTextBaseAlpha = integrationProgressText.color.a;
}
if (interferenceCountText != null)
{
interferenceTextBaseAlpha = interferenceCountText.color.a;
}
if (focusSequenceButton != null)
{
focusSequenceButton.interactable = false;
focusSequenceButton.gameObject.SetActive(false);
focusSequenceButton.onClick.RemoveListener(HandleFocusButtonClicked);
focusSequenceButton.onClick.AddListener(HandleFocusButtonClicked);
}
isInitialized = true;
}
// 设置游戏配置
anxietyPhrases = phrases ?? new List<string>();
targetSentence = sentence ?? "这是一个测试示例";
completionDialogNode = dialogNode ?? "";
// 设置全局焦虑短句配置
TextParticle.SetAnxietyPhrases(anxietyPhrases);
// 根据目标句子更新目标粒子数量
redParticleCount = Mathf.Min(sentence.Length, candidateCount);
// 重新初始化游戏状态并播放入场动画
ResetGame(true);
}
/// <summary>
/// 停止系统(关闭时调用,但保持初始化状态,可以重新开启)
/// </summary>
public void StopSystem()
{
// 如果未初始化,直接返回
if (!isInitialized)
return;
if (entranceRoutine != null)
{
StopCoroutine(entranceRoutine);
entranceRoutine = null;
}
if (debugTestRoutine != null)
{
StopCoroutine(debugTestRoutine);
debugTestRoutine = null;
}
// 停止所有 DOTween 动画
DOTween.Kill(this);
foreach (var particle in candidateParticles)
{
if (particle != null)
{
particle.transform.DOKill();
}
}
foreach (var particle in floatingParticles)
{
if (particle != null)
{
particle.transform.DOKill();
}
}
// 重置游戏状态(但不重置 isInitialized,这样可以重新开启)
isCompleted = false;
completionPhase = CompletionPhase.None;
allowInput = false;
}
private void InitializeCanvas()
{
// 获取渲染层索引
renderLayer = LayerMask.NameToLayer(renderLayerName);
if (renderLayer == -1)
{
Debug.LogWarning($"Layer '{renderLayerName}' not found, using default layer.");
renderLayer = 0;
}
// 如果没有指定canvas,创建一个
if (worldCanvas == null)
{
GameObject canvasObj = new GameObject("Language Canvas");
canvasObj.transform.SetParent(transform);
canvasObj.transform.localPosition = Vector3.zero;
canvasObj.layer = renderLayer;
worldCanvas = canvasObj.AddComponent<Canvas>();
worldCanvas.renderMode = RenderMode.WorldSpace;
RectTransform rectTransform = canvasObj.GetComponent<RectTransform>();
rectTransform.sizeDelta = new Vector2(canvasSize, canvasSize);
}
else
{
// 设置已有 Canvas 的 layer
worldCanvas.gameObject.layer = renderLayer;
}
// 确保 Canvas 配置正确
if (worldCanvas != null)
{
// 设置 WorldSpace Canvas 的相机
if (worldCanvas.worldCamera == null)
{
worldCanvas.worldCamera = Camera.main;
}
}
// 设置运动边界
boundsCenter = worldCanvas.transform.position;
float halfSize = canvasSize / 2f - textMargin;
if (layoutShape == LayoutShape.Circle)
{
boundsRadius = circleRadiusOverride > 0 ? circleRadiusOverride : halfSize;
movementBounds = new Bounds(boundsCenter, new Vector3(boundsRadius * 2f, boundsRadius * 2f, 0f));
}
else
{
boundsRadius = 0f;
movementBounds = new Bounds(
boundsCenter,
new Vector3(halfSize * 2f, halfSize * 2f, 0f)
);
}
}
private void InitializeParticles()
{
// 创建背景浮动文字
for (int i = 0; i < floatingCount; i++)
{
Vector3 pos = GetRandomPositionInBounds();
GameObject obj = CreateParticleObject(pos, $"FloatingParticle_{i}");
FloatingTextParticle particle = obj.AddComponent<FloatingTextParticle>();
particle.SetFont(chineseFontAsset);
particle.SetColor(floatingTextColor);
particle.SetFontSize(floatingTextFontSize);
SetParticleBounds(particle);
// 设置发光效果
if (enableBloomGlow)
{
particle.SetGlowSettings(true, textGlowIntensity, floatingTextColor);
particle.SetUnderlaySettings(glowDilate, glowSoftness);
}
floatingParticles.Add(particle);
}
// 创建候选粒子
for (int i = 0; i < candidateCount; i++)
{
Vector3 pos = GetRandomPositionInBounds();
GameObject obj = CreateParticleObject(pos, $"CandidateParticle_{i}");
CandidateParticle particle = obj.AddComponent<CandidateParticle>();
particle.SetFont(chineseFontAsset);
particle.SetColors(targetParticleColor, nonTargetParticleColor, nonTargetParticleColor, targetCalmColor);
particle.SetFontStyles(targetParticleFontSize, nonTargetParticleFontSize, targetParticleBold, nonTargetParticleBold);
SetParticleBounds(particle);
// 设置发光效果(目标粒子和非目标粒子使用不同强度)
if (enableBloomGlow)
{
particle.SetGlowSettings(true, textGlowIntensity, nonTargetParticleColor);
particle.SetUnderlaySettings(glowDilate, glowSoftness);
}
candidateParticles.Add(particle);
}
// 选择红色粒子
SelectRedParticles();
}
private GameObject CreateParticleObject(Vector3 position, string name)
{
GameObject obj = new GameObject(name);
obj.layer = renderLayer;
// 添加 RectTransform(在 Canvas 下必需)
RectTransform rectTransform = obj.AddComponent<RectTransform>();
rectTransform.SetParent(worldCanvas.transform, false);
rectTransform.position = position;
rectTransform.localScale = Vector3.one;
rectTransform.sizeDelta = Vector2.zero; // 父对象不需要大小
return obj;
}
private Vector3 GetRandomPositionInBounds()
{
if (layoutShape == LayoutShape.Circle)
{
Vector2 offset = Random.insideUnitCircle * boundsRadius;
return boundsCenter + new Vector3(offset.x, offset.y, 0f);
}
float x = Random.Range(movementBounds.min.x, movementBounds.max.x);
float y = Random.Range(movementBounds.min.y, movementBounds.max.y);
return new Vector3(x, y, 0f);
}
private void SetParticleBounds(TextParticle particle)
{
particle.SetMovementBounds(movementBounds);
if (layoutShape == LayoutShape.Circle)
particle.SetCircularBounds(boundsCenter, boundsRadius);
}
private void InitializeRenderers()
{
// 连接线渲染器
GameObject connObj = new GameObject("ConnectionRenderer");
connObj.transform.SetParent(transform);
connObj.transform.localPosition = Vector3.zero;
connObj.layer = renderLayer;
connectionRenderer = connObj.AddComponent<ConnectionRenderer>();
connectionRenderer.manager = this;
connectionRenderer.connectionDistance = connectionDistance;
connectionRenderer.SetConnectionStyles(
targetConnectionColor,
nonTargetConnectionColor,
nonTargetConnectionColor,
targetConnectionThickness,
nonTargetConnectionThickness,
nonTargetConnectionThickness
);
connectionRenderer.NonTargetAlphaScale = lineAlphaMultiplier;
connectionRenderer.TargetAlphaScale = 1f;
// 设置连接线发光效果
if (enableBloomGlow)
{
connectionRenderer.SetGlowSettings(
true,
targetLineGlowIntensity,
lineGlowIntensity,
lineGlowIntensity
);
}
// 噪点叠加渲染器(复古显示器效果)
if (enableRetroEffect)
{
GameObject noiseObj = new GameObject("NoiseOverlayRenderer");
noiseObj.transform.SetParent(transform);
noiseObj.transform.localPosition = Vector3.zero;
noiseObj.layer = renderLayer;
noiseOverlayRenderer = noiseObj.AddComponent<NoiseOverlayRenderer>();
// 设置效果区域为 Canvas 区域
Vector3 center = worldCanvas != null ? worldCanvas.transform.position : Vector3.zero;
noiseOverlayRenderer.SetArea(center, new Vector2(canvasSize, canvasSize));
noiseOverlayRenderer.SetIntensity(retroEffectIntensity);
}
}
private void SelectRedParticles()
{
targetParticles.Clear();
List<CandidateParticle> shuffled = candidateParticles.OrderBy(x => Random.value).ToList();
float effectiveSize = layoutShape == LayoutShape.Circle
? boundsRadius * 2f
: Mathf.Min(movementBounds.size.x, movementBounds.size.y);
float minDistance = effectiveSize / (redParticleCount * 0.8f);
foreach (var particle in shuffled)
{
if (targetParticles.Count >= redParticleCount) break;
bool tooClose = false;
foreach (var selected in targetParticles)
{
if (Vector3.Distance(particle.transform.position, selected.transform.position) < minDistance)
{
tooClose = true;
break;
}
}
if (!tooClose || targetParticles.Count == 0)
{
particle.isRed = true;
particle.originalIsRed = true;
targetParticles.Add(particle);
// 为目标粒子设置更强的发光效果
if (enableBloomGlow)
{
particle.SetGlowSettings(true, targetTextGlowIntensity, targetParticleColor);
}
}
}
// 如果不够,强制添加
int remaining = redParticleCount - targetParticles.Count;
for (int i = 0; i < remaining && i < shuffled.Count; i++)
{
if (!shuffled[i].isRed)
{
shuffled[i].isRed = true;
shuffled[i].originalIsRed = true;
targetParticles.Add(shuffled[i]);
// 为目标粒子设置更强的发光效果
if (enableBloomGlow)
{
shuffled[i].SetGlowSettings(true, targetTextGlowIntensity, targetParticleColor);
}
}
}
}
private void Update()
{
// 如果系统未初始化,不执行任何逻辑
if (!isInitialized)
return;
// 初始化检查
if (!initializationComplete)
{
initializationFrames++;
if (initializationFrames >= 30)
{
initializationComplete = true;
}
else if (initializationFrames >= 20)
{
// 建立初始连接状态
BuildConnectionMap();
}
}
HandleDebugShortcuts();
UpdateConnections();
if (!statusUIHidden)
{
UpdateStatusUI();
}
if (completionPhase == CompletionPhase.None)
{
if (initializationComplete && !interferenceLogicSuspended)
{
DetectConnectionChanges();
}
if (!isCompleted && CheckRedParticlesConnected())
{
isCompleted = true;
OnTargetsCompleted();
}
if (allowInput && !interferenceLogicSuspended)
{
UpdateInput();
}
if (!interferenceLogicSuspended)
{
ApplySeparationForces();
}
}
if (completionPhase == CompletionPhase.Focusing)
{
UpdateFocusSequence();
}
UpdateRenderers();
}
private void UpdateRenderers()
{
// 更新连接线渲染器
if (connectionRenderer != null)
{
connectionRenderer.SetData(
candidateParticles,
s_emptyPropagations,
completionPhase,
fadeOutAlpha
);
}
}
private void UpdateConnections()
{
// 重置所有连接
foreach (var particle in candidateParticles)
{
particle.connections.Clear();
}
// 检测连接
for (int i = 0; i < candidateParticles.Count; i++)
{
for (int j = i + 1; j < candidateParticles.Count; j++)
{
float distance = Vector3.Distance(
candidateParticles[i].transform.position,
candidateParticles[j].transform.position
);
if (distance < connectionDistance)
{
candidateParticles[i].connections.Add(candidateParticles[j]);
candidateParticles[j].connections.Add(candidateParticles[i]);
}
}
}
}
private void BuildConnectionMap()
{
previousConnections.Clear();
for (int i = 0; i < candidateParticles.Count; i++)
{
for (int j = i + 1; j < candidateParticles.Count; j++)
{
float distance = Vector3.Distance(
candidateParticles[i].transform.position,
candidateParticles[j].transform.position
);
if (distance < connectionDistance)
{
string key = $"{Mathf.Min(i, j)}-{Mathf.Max(i, j)}";
previousConnections[key] = true;
}
}
}
}
private void DetectConnectionChanges()
{
Dictionary<string, bool> currentConnections = new Dictionary<string, bool>();
for (int i = 0; i < candidateParticles.Count; i++)
{
for (int j = i + 1; j < candidateParticles.Count; j++)
{
float distance = Vector3.Distance(
candidateParticles[i].transform.position,
candidateParticles[j].transform.position
);
if (distance < connectionDistance)
{
string key = $"{Mathf.Min(i, j)}-{Mathf.Max(i, j)}";
currentConnections[key] = true;
}
}
}
previousConnections = currentConnections;
}
private bool CheckRedParticlesConnected()
{
if (targetParticles.Count == 0) return false;
if (targetParticles.Count == 1) return true;
// 检查所有目标粒子是否连通
HashSet<CandidateParticle> visited = new HashSet<CandidateParticle>();
Queue<CandidateParticle> queue = new Queue<CandidateParticle>();
queue.Enqueue(targetParticles[0]);
visited.Add(targetParticles[0]);
while (queue.Count > 0)
{
var current = queue.Dequeue();
foreach (var connected in current.connections)
{
if (connected.isRed && !visited.Contains(connected))
{
visited.Add(connected);
queue.Enqueue(connected);
}
}
}
if (visited.Count != targetParticles.Count)
return false;
// 检查目标粒子是否与非目标粒子分离
foreach (var red in targetParticles)
{
foreach (var other in candidateParticles)
{
if (other.isRed) continue;
float dist = Vector3.Distance(red.transform.position, other.transform.position);
if (dist < connectionDistance)
return false;
}
}
return true;
}
private void OnTargetsCompleted()
{
completionPhase = CompletionPhase.Completed;
completionTimer = 0f;
focusTimer = 0f;
fadeOutAlpha = 1f;
allowInput = false;
focusVisuals.Clear();
finalTargetCharacters.Clear();
finalTargetPositions.Clear();
focusTweensCompleted = false;
interferenceLogicSuspended = false;
SetStatusUIVisibility(true, 0f);
orderedRedParticles = GetOrderedRedParticles();
int displayLength = Mathf.Min(orderedRedParticles.Count, targetSentence.Length);
if (displayLength < orderedRedParticles.Count)
{
orderedRedParticles = orderedRedParticles.Take(displayLength).ToList();
}
for (int i = 0; i < displayLength; i++)
{
finalTargetCharacters.Add(targetSentence[i].ToString());
}
Vector3 displayCenter = worldCanvas != null ? worldCanvas.transform.position : Vector3.zero;
if (finalTargetCharacters.Count > 0)
{
float spacing = 0.9f;
float totalWidth = spacing * Mathf.Max(0, finalTargetCharacters.Count - 1);
float startX = -totalWidth / 2f;
finalTargetPositions.Clear();
for (int i = 0; i < finalTargetCharacters.Count; i++)
{
finalTargetPositions.Add(displayCenter + new Vector3(startX + i * spacing, 0f, 0f));
}
}
if (focusSequenceButton != null)
{
focusSequenceButton.gameObject.SetActive(true);
focusSequenceButton.interactable = true;
}
foreach (var particle in candidateParticles)
{
if (!particle.isRed)
{
particle.isCalmed = true;
particle.hasEffect = false;
particle.isOrange = false;
particle.changeSpeedMultiplier = 1f;
}
}
foreach (var particle in floatingParticles)
{
particle.isCalmed = true;
}
}
private List<CandidateParticle> GetOrderedRedParticles()
{
if (targetParticles.Count == 0) return new List<CandidateParticle>();
if (targetParticles.Count == 1) return new List<CandidateParticle>(targetParticles);
// 从最左边的粒子开始
var ordered = new List<CandidateParticle>();
var startParticle = targetParticles.OrderBy(p => p.transform.position.x).First();
HashSet<CandidateParticle> visited = new HashSet<CandidateParticle>();
Stack<CandidateParticle> stack = new Stack<CandidateParticle>();
stack.Push(startParticle);
visited.Add(startParticle);
while (stack.Count > 0)
{
var current = stack.Pop();
ordered.Add(current);
foreach (var connected in current.connections)
{
if (connected.isRed && !visited.Contains(connected))
{
visited.Add(connected);
stack.Push(connected);
}
}
}
return ordered;
}
private void HandleFocusButtonClicked()
{
if (completionPhase != CompletionPhase.Completed)
return;
BeginFocusSequence();
}
private void BeginFocusSequence()
{
if (focusSequenceButton != null)
{
focusSequenceButton.interactable = false;
}
completionPhase = CompletionPhase.Focusing;
focusTimer = 0f;
focusTweensCompleted = false;
interferenceLogicSuspended = true;
SetStatusUIVisibility(false, statusFadeOutDuration);
if (connectionRenderer != null)
{
connectionRenderer.NonTargetAlphaScale = 0f;
connectionRenderer.TargetAlphaScale = 1.2f;
}
foreach (var particle in floatingParticles)
{
particle.isCalmed = true;
particle.velocity = Vector2.zero;
FadeOutTextParticle(particle, focusFadeDuration);
}
foreach (var particle in candidateParticles)
{
if (!particle.isRed)
{
particle.isCalmed = true;
particle.velocity = Vector2.zero;
FadeOutTextParticle(particle, focusFadeDuration);
}
else
{
particle.isStatic = false;
particle.isCalmed = false;
particle.InitializeAnxietyEffect();
particle.changeSpeedMultiplier = 6f;
particle.anxietyShakeIntensity = Mathf.Max(particle.anxietyShakeIntensity, 4f);
particle.SetChangeInterval(0.05f);
particle.ResetChangeTimer(0.05f);
}
}
focusCentroid = ComputeCentroid(orderedRedParticles);
Vector3 focusTargetCenter = worldCanvas != null ? worldCanvas.transform.position : Vector3.zero;
focusVisuals.Clear();
int tweenTargetCount = orderedRedParticles.Count;
int completedTweens = 0;
for (int i = 0; i < orderedRedParticles.Count; i++)
{
var particle = orderedRedParticles[i];
Vector3 offset = particle.transform.position - focusCentroid;
float indexCenter = (orderedRedParticles.Count - 1) * 0.5f;
Vector3 spreadOffset = new Vector3((i - indexCenter) * spreadDistance, Mathf.Sin(i * 1.4f) * spreadDistance * 0.6f, 0f);
Vector3 focusPosition = focusTargetCenter + offset * focusZoomScale + spreadOffset;
Vector3 finalPosition = finalTargetPositions.Count > i ? finalTargetPositions[i] : focusTargetCenter;
particle.transform.DOKill();
particle.transform.DOMove(focusPosition, focusMoveDuration)
.SetEase(Ease.InOutQuad)
.OnComplete(() =>
{
completedTweens++;
if (completedTweens >= tweenTargetCount)
{
focusTweensCompleted = true;
focusTimer = 0f;
}
});
particle.transform.DOScale(Vector3.one * focusZoomScale, focusMoveDuration).SetEase(Ease.InOutQuad);
string finalChar = i < finalTargetCharacters.Count ? finalTargetCharacters[i] : particle.currentChar;
TargetFocusVisual visual = CreateFocusVisual(particle, finalChar);
visual.focusPosition = focusPosition;
visual.finalPosition = finalPosition;
visual.orderIndex = i;
focusVisuals[particle] = visual;
}
if (tweenTargetCount == 0)
{
focusTweensCompleted = true;
focusTimer = 0f;
}
}
private TargetFocusVisual CreateFocusVisual(CandidateParticle particle, string finalChar)
{
TargetFocusVisual visual = new TargetFocusVisual
{
particle = particle,
finalChar = finalChar,
startShakeIntensity = Mathf.Max(particle.anxietyShakeIntensity, 4f)
};
GameObject ringObj = new GameObject("FocusRing");
ringObj.layer = renderLayer;
ringObj.transform.SetParent(particle.transform, false);
visual.ringObject = ringObj;
float estimatedRadius = 0.55f;
if (particle != null && particle.textMesh != null)
{
estimatedRadius = Mathf.Clamp(particle.textMesh.fontSize * 0.05f, 0.35f, 0.75f);
}
var disc = ringObj.AddComponent<Disc>();
disc.Type = DiscType.Ring;
disc.Radius = estimatedRadius;
disc.Thickness = Mathf.Clamp(focusRingThickness, 0.01f, 0.3f);
Color ringColor = focusRingColor;
ringColor.a = focusRingAlpha;
disc.Color = ringColor;
visual.ringDisc = disc;
GameObject labelObj = new GameObject("ProbabilityLabel");
labelObj.layer = renderLayer;
labelObj.transform.SetParent(particle.transform, false);
RectTransform rectTransform = labelObj.AddComponent<RectTransform>();
rectTransform.localPosition = new Vector3(0f, estimatedRadius + 0.55f, 0f);
rectTransform.localScale = Vector3.one;
rectTransform.sizeDelta = new Vector2(2f, 0.6f);
var label = labelObj.AddComponent<TextMeshPro>();
if (chineseFontAsset != null)
{
label.font = chineseFontAsset;
}
label.fontSize = 2f;
label.alignment = TextAlignmentOptions.Center;
label.text = $"{Mathf.RoundToInt(visual.probability)}%";
visual.probabilityLabel = label;
return visual;
}
private void UpdateFocusSequence()
{
if (!focusTweensCompleted)
return;
focusTimer += Time.deltaTime;
float baseInterval = Mathf.Max(0.2f, probabilityDuration);
float arrangementDuration = Mathf.Max(0.2f, probabilityDuration * 0.6f);
foreach (var visual in focusVisuals.Values)
{
if (visual == null || visual.particle == null)
continue;
float localTime = focusTimer - visual.orderIndex * predictionSequenceGap;
float probabilityProgress = Mathf.Clamp01(localTime / baseInterval);
float arrangementProgress = Mathf.Clamp01(localTime / arrangementDuration);
float probability = Mathf.Lerp(visual.probability, visual.targetProbability, probabilityProgress);
if (visual.probabilityLabel != null)
{
visual.probabilityLabel.text = $"{Mathf.RoundToInt(probability)}%";
}
float interval = Mathf.Lerp(0.05f, 0.8f, probabilityProgress);
visual.particle.SetChangeInterval(interval);
visual.particle.changeSpeedMultiplier = Mathf.Lerp(6f, 1f, probabilityProgress);
visual.particle.anxietyShakeIntensity = Mathf.Lerp(visual.startShakeIntensity, 0f, probabilityProgress);
Vector3 dynamicTarget = GetDynamicArrangementTarget(visual, arrangementProgress, localTime);
Vector3 targetPosition = Vector3.Lerp(visual.focusPosition, dynamicTarget, Mathf.SmoothStep(0f, 1f, arrangementProgress));
Vector3 shuffleOffset = CalculateShuffleOffset(visual, arrangementProgress, localTime);
visual.particle.transform.position = targetPosition + shuffleOffset;
if (!visual.stabilized && probabilityProgress >= 1f)
{
visual.particle.ForceSetCharacter(visual.finalChar);
visual.particle.SetChangeInterval(1.5f);
visual.particle.ResetChangeTimer(visual.particle.changeSpeedMultiplier);
visual.particle.isStatic = true;
visual.stabilized = true;
if (visual.probabilityLabel != null)
{
visual.probabilityLabel.text = "100%";
}
}
}
bool allStable = focusVisuals.Values.All(v => v.stabilized);
if (allStable)
{
CompleteFocusSequence();
}
}
private void CompleteFocusSequence()
{
foreach (var visual in focusVisuals.Values)
{
if (visual.ringObject != null)
{
Destroy(visual.ringObject);
}
if (visual.probabilityLabel != null)
{
Destroy(visual.probabilityLabel.gameObject);
}
if (visual.particle != null)
{
visual.particle.transform.DOScale(Vector3.one * focusZoomScale, 0.3f).SetEase(Ease.OutQuad);
visual.particle.isStatic = true;
visual.particle.changeSpeedMultiplier = 1f;
}
}
focusVisuals.Clear();
if (connectionRenderer != null)
{
connectionRenderer.TargetAlphaScale = 0f;
}
completionPhase = CompletionPhase.Finished;
if (focusSequenceButton != null)
{
focusSequenceButton.gameObject.SetActive(false);
}
// 触发完成对话节点
if (!string.IsNullOrEmpty(completionDialogNode))
{
DialogController.Instance?.StartDialogNode(completionDialogNode);
}
}
private Vector3 GetDynamicArrangementTarget(TargetFocusVisual visual, float arrangementProgress, float localTime)
{
if (visual == null)
{
return Vector3.zero;
}
Vector3 target = visual.finalPosition;
if (!visual.stabilized && finalTargetPositions.Count > 1)
{
int swapIndex = (visual.orderIndex + 1) % finalTargetPositions.Count;
swapIndex = Mathf.Clamp(swapIndex, 0, finalTargetPositions.Count - 1);
Vector3 neighbor = finalTargetPositions[swapIndex];
float swapStrength = Mathf.Clamp01(Mathf.Sin(Mathf.Max(0f, localTime) * shuffleFrequency * 0.25f));
float blend = (1f - arrangementProgress) * swapStrength * 0.5f;
target = Vector3.Lerp(target, neighbor, blend);
}
return target;
}
private Vector3 CalculateShuffleOffset(TargetFocusVisual visual, float arrangementProgress, float localTime)
{
if (visual == null || shuffleDistance <= 0f || visual.stabilized)
{
return Vector3.zero;
}
float shuffleAmount = Mathf.Clamp01(1f - arrangementProgress);
if (shuffleAmount <= 0f)
{
return Vector3.zero;
}
float timeSeed = (focusTimer + localTime * 0.35f) * shuffleFrequency + visual.orderIndex * 0.75f;
float x = Mathf.Sin(timeSeed) * shuffleDistance * shuffleAmount;
float y = Mathf.Cos(timeSeed * 0.8f) * shuffleDistance * shuffleVerticalScale * shuffleAmount;
return new Vector3(x, y, 0f);
}
private void FadeOutTextParticle(TextParticle particle, float duration)
{
if (particle == null) return;
DOTween.To(() => particle.alpha, value => particle.alpha = value, 0f, duration)
.SetEase(Ease.OutQuad);
if (particle.glowSprite != null)
{
particle.glowSprite.DOFade(0f, duration);
}
}
private Vector3 ComputeCentroid(List<CandidateParticle> particles)
{
if (particles == null || particles.Count == 0)
{
return worldCanvas != null ? worldCanvas.transform.position : Vector3.zero;
}
Vector3 sum = Vector3.zero;
foreach (var particle in particles)
{
sum += particle.transform.position;
}
return sum / particles.Count;
}
private void UpdateInput()
{
// 只在允许输入时处理鼠标逻辑
if (!allowInput || completionPhase != CompletionPhase.None) return;
Vector2 mouseWorldPos = GetMouseWorldPosition();
// 左键点击:排斥附近粒子
if (Input.GetMouseButton(0))
{
ApplyForceToNearbyParticles(mouseWorldPos, mouseInteractionRadius, repulsionForce);
}
// 右键点击:吸引附近粒子
if (Input.GetMouseButton(1))
{
ApplyForceToNearbyParticles(mouseWorldPos, mouseInteractionRadius, -attractionForce);
}
}
private void ApplyForceToNearbyParticles(Vector2 center, float repulsionRadius, float repulsionForce)
{
foreach (var particle in candidateParticles)
{
Vector2 particlePos = particle.transform.position;
float distance = Vector2.Distance(center, particlePos);
if (distance < repulsionRadius && distance > 0.01f)
{
// 计算方向和力度
Vector2 direction = (particlePos - center).normalized;
float forceMagnitude = repulsionForce * (1f - distance / repulsionRadius);
// 应用力
particle.ApplyForce(direction * forceMagnitude);
}
}
}
private Vector2 GetMouseWorldPosition()
{
Vector3 mousePos = Input.mousePosition;
Camera cam = Camera.main;
if (cam != null)
{
// 对于正交相机,需要使用正确的Z距离
float zDistance = cam.transform.position.z - worldCanvas.transform.position.z;
Vector3 worldPos = cam.ScreenToWorldPoint(new Vector3(mousePos.x, mousePos.y, Mathf.Abs(zDistance)));
return new Vector2(worldPos.x, worldPos.y);
}
return Vector2.zero;
}
public void ResetGame(bool playEntranceEffect = true)
{
isCompleted = false;
completionPhase = CompletionPhase.None;
completionTimer = 0f;
focusTimer = 0f;
fadeOutAlpha = 1f;
allowInput = !playEntranceEffect;
orderedRedParticles.Clear();
focusVisuals.Clear();
finalTargetCharacters.Clear();
finalTargetPositions.Clear();
previousConnections.Clear();
initializationComplete = false;
initializationFrames = 0;
focusTweensCompleted = false;
interferenceLogicSuspended = false;
SetStatusUIVisibility(true, 0f);
if (entranceRoutine != null)
{
StopCoroutine(entranceRoutine);
entranceRoutine = null;
}
if (focusSequenceButton != null)
{
focusSequenceButton.onClick.RemoveListener(HandleFocusButtonClicked);
focusSequenceButton.onClick.AddListener(HandleFocusButtonClicked);
focusSequenceButton.interactable = false;
focusSequenceButton.gameObject.SetActive(false);
}
if (connectionRenderer != null)
{
connectionRenderer.NonTargetAlphaScale = lineAlphaMultiplier;
connectionRenderer.TargetAlphaScale = 1f;
}
// 重置所有粒子
foreach (var p in candidateParticles)
{
p.isRed = false;
p.isOrange = false;
p.hasEffect = false;
p.isStatic = false;
p.originalIsRed = false;
p.changeSpeedMultiplier = 1f;
p.vibrationOffset = Vector2.zero;
p.velocity = new Vector2(Random.Range(-0.5f, 0.5f), Random.Range(-0.5f, 0.5f));
p.transform.position = GetRandomPositionInBounds();
p.isCalmed = false;
p.alpha = p.baseAlpha;
p.transform.localScale = Vector3.one;
p.transform.DOKill();
if (p.glowSprite != null)
{
Color glowColor = p.glowSprite.color;
glowColor.a = 0.2f;
p.glowSprite.color = glowColor;
}
}
foreach (var floating in floatingParticles)
{
floating.isCalmed = false;
floating.alpha = floating.baseAlpha;
floating.transform.DOKill();
}
SelectRedParticles();
UpdateStatusUI();
if (playEntranceEffect)
{
PlayEntranceSequence();
}
}
private void HandleDebugShortcuts()
{
if (!enableDebugShortcuts || !Application.isPlaying)
return;
// 如果未初始化,先初始化
if (!isInitialized)
return;
if (Input.GetKeyDown(debugCompleteKey))
{
DebugCompleteTargets();
}
if (Input.GetKeyDown(debugResetKey))
{
ResetGame();
}
if (Input.GetKeyDown(debugFullTestKey))
{
QueueDebugFullFlow();
}
}
[ContextMenu("Debug/一键完成目标")]
public void DebugCompleteTargets()
{
if (!isCompleted)
{
isCompleted = true;
OnTargetsCompleted();
}
if (completionPhase == CompletionPhase.Completed)
{
BeginFocusSequence();
}
}
[ContextMenu("Debug/重置场景")]
public void DebugResetScene()
{
ResetGame();
}
[ContextMenu("Debug/完整流程测试")]
public void DebugPlayFullFlow()
{
QueueDebugFullFlow();
}
private void QueueDebugFullFlow()
{
if (!gameObject.activeInHierarchy)
return;
if (debugTestRoutine != null)
{
StopCoroutine(debugTestRoutine);
}
debugTestRoutine = StartCoroutine(DebugFullFlowRoutine());
}
private IEnumerator DebugFullFlowRoutine()
{
DebugCompleteTargets();
if (completionPhase == CompletionPhase.Completed)
{
BeginFocusSequence();
}
while (completionPhase != CompletionPhase.Finished)
{
yield return null;
}
yield return new WaitForSeconds(0.5f);
ResetGame();
debugTestRoutine = null;
}
private void PlayEntranceSequence()
{
if (!gameObject.activeInHierarchy || candidateParticles.Count == 0)
return;
if (entranceRoutine != null)
{
StopCoroutine(entranceRoutine);
}
entranceRoutine = StartCoroutine(EntranceSequenceRoutine());
}
private IEnumerator EntranceSequenceRoutine()
{
allowInput = false;
entranceTargets.Clear();
Vector3 burstOrigin = worldCanvas != null ? worldCanvas.transform.position : transform.position;
List<CandidateParticle> particles = candidateParticles.Where(p => p != null).ToList();
if (particles.Count == 0)
{
allowInput = true;
entranceRoutine = null;
yield break;
}
float initialScale = Mathf.Max(0.05f, entranceInitialScale);
foreach (var candidate in particles)
{
entranceTargets[candidate] = candidate.transform.position;
candidate.transform.DOKill();
candidate.velocity = Vector2.zero;
candidate.transform.position = burstOrigin;
candidate.transform.localScale = Vector3.one * initialScale;
candidate.alpha = 0f;
if (candidate.glowSprite != null)
{
Color glowColor = candidate.glowSprite.color;
glowColor.a = 0f;
candidate.glowSprite.color = glowColor;
}
}
yield return new WaitForSeconds(Mathf.Max(0f, entranceDelay));
int coreCount = Mathf.Clamp(entranceCoreCount, 1, particles.Count);
var shuffled = particles.OrderBy(_ => Random.value).ToList();
var coreCandidates = shuffled.Take(coreCount).ToList();
var remainingCandidates = shuffled.Skip(coreCount).ToList();
float radius = Mathf.Max(0.1f, entranceCoreRadius);
float maxCoreTime = 0f;
for (int i = 0; i < coreCandidates.Count; i++)
{
var candidate = coreCandidates[i];
float angle = (Mathf.PI * 2f / Mathf.Max(1, coreCandidates.Count)) * i;
Vector3 startPos = burstOrigin + new Vector3(Mathf.Cos(angle), Mathf.Sin(angle), 0f) * radius;
candidate.transform.position = startPos;
float delay = i * Mathf.Max(0f, entranceCoreStagger);
Sequence seq = DOTween.Sequence();
seq.SetDelay(delay);
seq.Append(candidate.transform.DOMove(entranceTargets[candidate], entranceCoreBurstDuration).SetEase(Ease.OutCubic));
seq.Join(candidate.transform.DOScale(Vector3.one, entranceCoreBurstDuration).SetEase(Ease.OutBack));
seq.Join(DOTween.To(() => candidate.alpha, value => candidate.alpha = value, candidate.baseAlpha, entranceCoreBurstDuration).SetEase(Ease.OutQuad));
if (candidate.glowSprite != null)
{
seq.Join(candidate.glowSprite.DOFade(0.2f, entranceCoreBurstDuration));
}
seq.OnComplete(() =>
{
candidate.velocity = new Vector2(Random.Range(-0.5f, 0.5f), Random.Range(-0.5f, 0.5f));
});
maxCoreTime = Mathf.Max(maxCoreTime, delay + entranceCoreBurstDuration);
}
if (coreCandidates.Count > 0)
{
yield return new WaitForSeconds(maxCoreTime * 0.6f);
}
float cascadeDuration = 0f;
for (int i = 0; i < remainingCandidates.Count; i++)
{
var candidate = remainingCandidates[i];
Vector2 offset = Random.insideUnitCircle * entranceSearchRadius;
candidate.transform.position = burstOrigin + new Vector3(offset.x, offset.y, 0f);
float delay = i * Mathf.Max(0f, entranceCascadeStagger);
Sequence seq = DOTween.Sequence();
seq.SetDelay(delay);
seq.Append(candidate.transform.DOMove(entranceTargets[candidate], entranceCascadeDuration).SetEase(Ease.OutQuad));
seq.Join(candidate.transform.DOScale(Vector3.one, entranceCascadeDuration * 0.8f).SetEase(Ease.OutBack));
seq.Join(DOTween.To(() => candidate.alpha, value => candidate.alpha = value, candidate.baseAlpha, entranceCascadeDuration).SetEase(Ease.OutSine));
if (candidate.glowSprite != null)
{
seq.Join(candidate.glowSprite.DOFade(0.2f, entranceCascadeDuration));
}
seq.OnComplete(() =>
{
candidate.velocity = new Vector2(Random.Range(-0.5f, 0.5f), Random.Range(-0.5f, 0.5f));
});
cascadeDuration = Mathf.Max(cascadeDuration, delay + entranceCascadeDuration);
}
float waitTime = Mathf.Max(maxCoreTime, cascadeDuration);
if (waitTime > 0f)
{
yield return new WaitForSeconds(waitTime + 0.2f);
}
foreach (var candidate in particles)
{
if (candidate == null) continue;
candidate.alpha = candidate.baseAlpha;
candidate.transform.localScale = Vector3.one;
}
entranceTargets.Clear();
entranceRoutine = null;
allowInput = true;
}
private void ApplySeparationForces()
{
if (!enableSeparation || candidateParticles.Count <= 1)
return;
float minSpacingSqr = minParticleSpacing * minParticleSpacing;
for (int i = 0; i < candidateParticles.Count - 1; i++)
{
var particleA = candidateParticles[i];
if (particleA.isStatic)
continue;
Vector2 posA = particleA.transform.position;
for (int j = i + 1; j < candidateParticles.Count; j++)
{
var particleB = candidateParticles[j];
if (particleB.isStatic)
continue;
Vector2 posB = particleB.transform.position;
Vector2 diff = posA - posB;
float distSqr = diff.sqrMagnitude;
if (distSqr < minSpacingSqr && distSqr > 0.0001f)
{
float distance = Mathf.Sqrt(distSqr);
float pushStrength = 1f - (distance / minParticleSpacing);
Vector2 direction = diff / distance;
Vector2 push = direction * (pushStrength * separationForce);
particleA.ApplyForce(push * Time.deltaTime);
particleB.ApplyForce(-push * Time.deltaTime);
}
}
}
}
private void UpdateStatusUI()
{
if (statusUIHidden)
return;
if (integrationProgressText != null)
{
float ratio = CalculateIntegrationRatio();
int percent = Mathf.RoundToInt(ratio * 100f);
integrationProgressText.text = $"{percent}% 语言整合完成度";
}
if (interferenceCountText != null)
{
int interferenceCount = CountInterferenceNodes();
interferenceCountText.text = $"{interferenceCount} 处异常思绪仍在干扰表达";
}
}
private float CalculateIntegrationRatio()
{
if (targetParticles.Count == 0)
return 0f;
HashSet<CandidateParticle> visited = new HashSet<CandidateParticle>();
int largestCluster = 0;
foreach (var red in targetParticles)
{
if (visited.Contains(red))
continue;
int clusterSize = 0;
Queue<CandidateParticle> queue = new Queue<CandidateParticle>();
queue.Enqueue(red);
visited.Add(red);
while (queue.Count > 0)
{
var current = queue.Dequeue();
clusterSize++;
foreach (var connected in current.connections)
{
if (connected == null || !connected.isRed)
continue;
if (!visited.Contains(connected))
{
visited.Add(connected);
queue.Enqueue(connected);
}
}
}
if (clusterSize > largestCluster)
{
largestCluster = clusterSize;
}
}
largestCluster = Mathf.Clamp(largestCluster, 0, targetParticles.Count);
return largestCluster / Mathf.Max(1f, targetParticles.Count);
}
private int CountInterferenceNodes()
{
if (targetParticles.Count == 0)
return 0;
HashSet<CandidateParticle> interferingParticles = new HashSet<CandidateParticle>();
foreach (var red in targetParticles)
{
foreach (var connected in red.connections)
{
if (connected == null)
continue;
if (!connected.isRed)
{
interferingParticles.Add(connected);
}
}
}
return interferingParticles.Count;
}
private void SetStatusUIVisibility(bool visible, float duration = 0f)
{
statusUIHidden = !visible;
float integrationAlpha = visible ? integrationTextBaseAlpha : 0f;
float interferenceAlpha = visible ? interferenceTextBaseAlpha : 0f;
AnimateStatusText(integrationProgressText, integrationAlpha, duration);
AnimateStatusText(interferenceCountText, interferenceAlpha, duration);
}
private void AnimateStatusText(TMP_Text text, float targetAlpha, float duration)
{
if (text == null)
return;
DOTween.Kill(text);
if (duration > 0f)
{
text.DOFade(targetAlpha, duration);
}
else
{
Color color = text.color;
color.a = targetAlpha;
text.color = color;
}
}
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
Vector3 center = worldCanvas != null ? worldCanvas.transform.position : transform.position;
float halfSize = canvasSize / 2f - textMargin;
float radius = layoutShape == LayoutShape.Circle && circleRadiusOverride > 0
? circleRadiusOverride
: halfSize;
Gizmos.color = new Color(0.2f, 0.8f, 1f, 0.5f);
if (layoutShape == LayoutShape.Circle)
{
// 圆形:用多段线近似圆
const int segments = 64;
Vector3 prev = center + new Vector3(radius, 0f, 0f);
for (int i = 1; i <= segments; i++)
{
float angle = (float)i / segments * Mathf.PI * 2f;
Vector3 curr = center + new Vector3(Mathf.Cos(angle) * radius, Mathf.Sin(angle) * radius, 0f);
Gizmos.DrawLine(prev, curr);
prev = curr;
}
}
else
{
// 矩形
Vector3 ext = new Vector3(halfSize, halfSize, 0f);
Gizmos.DrawLine(center + new Vector3(-ext.x, -ext.y, 0f), center + new Vector3(ext.x, -ext.y, 0f));
Gizmos.DrawLine(center + new Vector3(ext.x, -ext.y, 0f), center + new Vector3(ext.x, ext.y, 0f));
Gizmos.DrawLine(center + new Vector3(ext.x, ext.y, 0f), center + new Vector3(-ext.x, ext.y, 0f));
Gizmos.DrawLine(center + new Vector3(-ext.x, ext.y, 0f), center + new Vector3(-ext.x, -ext.y, 0f));
}
}
#endif
}
}