Files
aibis-dream/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs
T
2025-11-13 20:30:13 +08:00

1184 lines
45 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 CompletionPhase
{
None, // 未完成
Completed, // 已完成但尚未进入聚焦流程
Focusing, // 正在聚焦放大并收敛
Finished // 全流程完成
}
/// <summary>
/// 语言粒子系统主管理器
/// </summary>
public class LanguageParticleManager : MonoBehaviour
{
[Header("Canvas设置")]
[SerializeField] private Canvas worldCanvas;
[SerializeField] private float canvasSize = 10f;
[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 string targetSentence = "这是一个测试示例";
[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 bool enableAutoPropagation = true; // 是否启用自动传播
[SerializeField] private float autoPropagationInterval = 2f; // 自动传播间隔(秒)
[SerializeField] private int autoPropagationCount = 1; // 每次自动触发的传播数量
[Header("传播效果参数")]
[SerializeField] private float propagationDuration = 1f; // 传播效果持续时间
[SerializeField] private float propagationDelay = 0.08f; // 传播延迟(每层之间的间隔)
[SerializeField] private int maxPropagationNodes = 15; // 最大传播节点数量
[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 propagationColor = new Color(1f, 0.59f, 0.2f, 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 float targetParticleFontSize = 3.5f; // 目标粒子字体大小
[SerializeField] private float nonTargetParticleFontSize = 2.5f; // 非目标粒子字体大小
[SerializeField] private float floatingTextFontSize = 2f; // 背景浮动文字大小
[SerializeField] private bool targetParticleBold = true; // 目标粒子是否加粗
[SerializeField] private bool nonTargetParticleBold = false; // 非目标粒子是否加粗
[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);// 非目标粒子连线颜色
[SerializeField] private Color propagationLineColor = new Color(1f, 0.59f, 0.2f, 1f); // 传播线颜色
[SerializeField] private float targetConnectionThickness = 0.04f; // 目标粒子连线粗细
[SerializeField] private float nonTargetConnectionThickness = 0.02f; // 非目标粒子连线粗细
[SerializeField] private float propagationLineThickness = 0.03f; // 传播线粗细
[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 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 List<EffectPropagation> effectPropagations = 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 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 float autoPropagationTimer = 0f;
private bool focusTweensCompleted = false;
// 边界
private Bounds movementBounds;
// 连接渲染(使用Shapes
private ConnectionRenderer connectionRenderer;
private void Start()
{
InitializeCanvas();
InitializeParticles();
InitializeRenderers();
UpdateStatusUI();
if (focusSequenceButton != null)
{
focusSequenceButton.interactable = false;
focusSequenceButton.gameObject.SetActive(false);
focusSequenceButton.onClick.RemoveListener(HandleFocusButtonClicked);
focusSequenceButton.onClick.AddListener(HandleFocusButtonClicked);
}
}
private void InitializeCanvas()
{
// 如果没有指定canvas,创建一个
if (worldCanvas == null)
{
GameObject canvasObj = new GameObject("Language Canvas");
canvasObj.transform.SetParent(transform);
canvasObj.transform.localPosition = Vector3.zero;
worldCanvas = canvasObj.AddComponent<Canvas>();
worldCanvas.renderMode = RenderMode.WorldSpace;
RectTransform rectTransform = canvasObj.GetComponent<RectTransform>();
rectTransform.sizeDelta = new Vector2(canvasSize, canvasSize);
}
// 确保 Canvas 配置正确
if (worldCanvas != null)
{
// 设置 WorldSpace Canvas 的相机
if (worldCanvas.worldCamera == null)
{
worldCanvas.worldCamera = Camera.main;
}
}
// 设置运动边界
float halfSize = canvasSize / 2f - textMargin;
movementBounds = new Bounds(
worldCanvas.transform.position,
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);
particle.SetMovementBounds(movementBounds);
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, propagationColor, targetCalmColor);
particle.SetFontStyles(targetParticleFontSize, nonTargetParticleFontSize, targetParticleBold, nonTargetParticleBold);
particle.SetMovementBounds(movementBounds);
candidateParticles.Add(particle);
}
// 选择红色粒子
SelectRedParticles();
}
private GameObject CreateParticleObject(Vector3 position, string name)
{
GameObject obj = new GameObject(name);
// 添加 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()
{
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 InitializeRenderers()
{
// 连接线渲染器
GameObject connObj = new GameObject("ConnectionRenderer");
connObj.transform.SetParent(transform);
connObj.transform.localPosition = Vector3.zero;
connectionRenderer = connObj.AddComponent<ConnectionRenderer>();
connectionRenderer.manager = this;
connectionRenderer.connectionDistance = connectionDistance;
connectionRenderer.SetConnectionStyles(
targetConnectionColor,
nonTargetConnectionColor,
propagationLineColor,
targetConnectionThickness,
nonTargetConnectionThickness,
propagationLineThickness
);
connectionRenderer.NonTargetAlphaScale = lineAlphaMultiplier;
connectionRenderer.TargetAlphaScale = 1f;
}
private void SelectRedParticles()
{
targetParticles.Clear();
List<CandidateParticle> shuffled = candidateParticles.OrderBy(x => Random.value).ToList();
float minDistance = Mathf.Min(movementBounds.size.x, movementBounds.size.y) / (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);
}
}
// 如果不够,强制添加
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]);
}
}
}
private void Update()
{
// 初始化检查
if (!initializationComplete)
{
initializationFrames++;
if (initializationFrames >= 30)
{
initializationComplete = true;
}
else if (initializationFrames >= 20)
{
// 建立初始连接状态
BuildConnectionMap();
}
}
if (completionPhase == CompletionPhase.None)
{
UpdateConnections();
UpdateStatusUI();
if (initializationComplete)
{
DetectConnectionChanges();
UpdateAutoPropagation();
}
UpdateEffectPropagations();
if (!isCompleted && CheckRedParticlesConnected())
{
isCompleted = true;
OnTargetsCompleted();
}
if (allowInput)
{
UpdateInput();
}
ApplySeparationForces();
}
else
{
UpdateConnections();
UpdateStatusUI();
}
if (completionPhase == CompletionPhase.Focusing)
{
UpdateFocusSequence();
}
UpdateRenderers();
}
private void UpdateRenderers()
{
// 更新连接线渲染器
if (connectionRenderer != null)
{
connectionRenderer.SetData(
candidateParticles,
effectPropagations,
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;
// 检测新连接
if (!previousConnections.ContainsKey(key))
{
var p1 = candidateParticles[i];
var p2 = candidateParticles[j];
// 检查是否是红色粒子与蓝色粒子的新连接
bool isRedToBlue = (p1.isRed && !p2.isRed && !p2.isOrange && p1.originalIsRed) ||
(!p1.isRed && !p1.isOrange && p2.isRed && p2.originalIsRed);
if (isRedToBlue)
{
var redParticle = p1.isRed ? p1 : p2;
var blueParticle = p1.isRed ? p2 : p1;
if (!blueParticle.isStatic && !blueParticle.hasEffect)
{
TriggerEffectPropagation(redParticle, blueParticle);
}
}
}
}
}
}
previousConnections = currentConnections;
}
private void TriggerEffectPropagation(CandidateParticle redParticle, CandidateParticle blueParticle)
{
effectPropagations.Add(new EffectPropagation(
redParticle,
propagationDuration,
propagationDelay,
maxPropagationNodes
));
}
private void UpdateEffectPropagations()
{
for (int i = effectPropagations.Count - 1; i >= 0; i--)
{
effectPropagations[i].Update(Time.deltaTime);
if (!effectPropagations[i].IsActive)
{
effectPropagations[i].Cleanup();
effectPropagations.RemoveAt(i);
}
}
}
private void UpdateAutoPropagation()
{
if (!enableAutoPropagation) return;
if (targetParticles.Count == 0) return;
autoPropagationTimer += Time.deltaTime;
if (autoPropagationTimer >= autoPropagationInterval)
{
autoPropagationTimer = 0f;
// 随机选择目标粒子触发传播效果
int triggerCount = Mathf.Min(autoPropagationCount, targetParticles.Count);
var shuffledTargets = targetParticles.OrderBy(x => Random.value).Take(triggerCount).ToList();
foreach (var targetParticle in shuffledTargets)
{
// 触发传播效果(即使没有建立新连接)
TriggerEffectPropagation(targetParticle, null);
}
}
}
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;
effectPropagations.Clear();
autoPropagationTimer = 0f;
focusVisuals.Clear();
finalTargetCharacters.Clear();
finalTargetPositions.Clear();
focusTweensCompleted = false;
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;
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.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(estimatedRadius * 0.25f, 0.06f, 0.15f);
disc.Color = new Color(1f, 0.78f, 0.39f, 0.8f);
visual.ringDisc = disc;
GameObject labelObj = new GameObject("ProbabilityLabel");
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 targetPosition = Vector3.Lerp(visual.focusPosition, visual.finalPosition, Mathf.SmoothStep(0f, 1f, arrangementProgress));
visual.particle.transform.position = targetPosition;
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);
}
}
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;
}
private void ResetGame()
{
isCompleted = false;
completionPhase = CompletionPhase.None;
completionTimer = 0f;
focusTimer = 0f;
fadeOutAlpha = 1f;
allowInput = true;
orderedRedParticles.Clear();
focusVisuals.Clear();
finalTargetCharacters.Clear();
finalTargetPositions.Clear();
effectPropagations.Clear();
previousConnections.Clear();
initializationComplete = false;
initializationFrames = 0;
autoPropagationTimer = 0f;
focusTweensCompleted = false;
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();
}
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 (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;
}
}
}