1693 lines
65 KiB
C#
1693 lines
65 KiB
C#
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
|
||
}
|
||
}
|
||
|