1746 lines
64 KiB
C#
1746 lines
64 KiB
C#
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using TMPro;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using DG.Tweening;
|
||
using Shapes;
|
||
using AibisDream.FixSystem;
|
||
|
||
namespace AibisDream.MiniGame.Language
|
||
{
|
||
|
||
/// <summary>
|
||
/// 情绪类型
|
||
/// </summary>
|
||
public enum EmotionType
|
||
{
|
||
Joy, // 快乐 - 15Hz
|
||
Sadness, // 悲伤 - 40Hz
|
||
Anger, // 愤怒 - 65Hz
|
||
Fear, // 恐惧 - 90Hz
|
||
Neutral // 中性/噪音
|
||
}
|
||
|
||
/// <summary>
|
||
/// 情绪配置
|
||
/// </summary>
|
||
[System.Serializable]
|
||
public class EmotionConfig
|
||
{
|
||
public EmotionType type;
|
||
public float frequency; // 0-100 的频率值
|
||
public Color color;
|
||
public string displayName;
|
||
|
||
public static EmotionConfig Joy => new EmotionConfig
|
||
{
|
||
type = EmotionType.Joy,
|
||
frequency = 15f,
|
||
color = new Color(1f, 1f, 0f, 1f),
|
||
displayName = "JOY"
|
||
};
|
||
|
||
public static EmotionConfig Sadness => new EmotionConfig
|
||
{
|
||
type = EmotionType.Sadness,
|
||
frequency = 40f,
|
||
color = new Color(0f, 0.67f, 1f, 1f),
|
||
displayName = "SADNESS"
|
||
};
|
||
|
||
public static EmotionConfig Anger => new EmotionConfig
|
||
{
|
||
type = EmotionType.Anger,
|
||
frequency = 65f,
|
||
color = new Color(1f, 0.2f, 0.2f, 1f),
|
||
displayName = "ANGER"
|
||
};
|
||
|
||
public static EmotionConfig Fear => new EmotionConfig
|
||
{
|
||
type = EmotionType.Fear,
|
||
frequency = 90f,
|
||
color = new Color(0.67f, 0f, 1f, 1f),
|
||
displayName = "FEAR"
|
||
};
|
||
|
||
public static EmotionConfig Neutral => new EmotionConfig
|
||
{
|
||
type = EmotionType.Neutral,
|
||
frequency = -1f,
|
||
color = new Color(0.5f, 0.5f, 0.5f, 1f),
|
||
displayName = "NOISE"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 网格单元格数据
|
||
/// </summary>
|
||
[System.Serializable]
|
||
public class GridCell
|
||
{
|
||
public string word; // 词语内容
|
||
public EmotionType emotion;
|
||
public bool isTarget; // 是否是目标词
|
||
public int row;
|
||
public int col;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析目标配置
|
||
/// </summary>
|
||
[System.Serializable]
|
||
public class AnalysisTarget
|
||
{
|
||
public string name;
|
||
public EmotionType emotion;
|
||
public List<TargetWord> words = new List<TargetWord>();
|
||
public Vector2Int stripPosition; // 解码器条纹正确位置(行,列)
|
||
public bool isCorrectAnswer; // 是否是正确答案
|
||
}
|
||
|
||
/// <summary>
|
||
/// 目标词配置
|
||
/// </summary>
|
||
[System.Serializable]
|
||
public class TargetWord
|
||
{
|
||
public Vector2Int position; // 在网格中的位置(行,列)
|
||
public string word; // 词语内容
|
||
}
|
||
|
||
/// <summary>
|
||
/// 粒子视觉状态(用于平滑过渡)
|
||
/// </summary>
|
||
public class ParticleVisualState
|
||
{
|
||
public float targetAlpha = 1f;
|
||
public float currentAlpha = 1f;
|
||
public Color targetColor = Color.white;
|
||
public Color currentColor = Color.white;
|
||
public float targetScale = 1f;
|
||
public float currentScale = 1f;
|
||
public Vector3 basePosition;
|
||
public Vector3 jitterOffset = Vector3.zero;
|
||
public bool isShowingWord = false; // true = word, false = cover
|
||
public Tweener alphaTween;
|
||
public Tweener colorTween;
|
||
public Tweener scaleTween;
|
||
public Tweener jitterTween;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分析模式管理器
|
||
/// </summary>
|
||
public class AnalysisModeManager : MonoBehaviour
|
||
{
|
||
[Header("Canvas 引用(共享)")]
|
||
[SerializeField] private Canvas sharedCanvas; // 共享 LanguageParticleManager 的 canvas
|
||
|
||
[Header("UI 引用")]
|
||
[SerializeField] private RectTransform analysisPanel;
|
||
[SerializeField] private Button initializeButton;
|
||
[SerializeField] private Slider emotionSlider;
|
||
[SerializeField] private TMP_Text frequencyText;
|
||
[SerializeField] private TMP_Text signalStrengthText;
|
||
[SerializeField] private Button startFilterButton;
|
||
[SerializeField] private Button verifyButton;
|
||
|
||
[Header("网格配置(舞台布局)")]
|
||
[Tooltip("可选:用于定位网格中心的容器")]
|
||
[SerializeField] private RectTransform gridContainer;
|
||
[Tooltip("网格宽度(列数)")]
|
||
[SerializeField] private int gridWidth = 20;
|
||
[Tooltip("网格高度(行数)")]
|
||
[SerializeField] private int gridHeight = 20;
|
||
[Tooltip("单元格大小(Unity单位)")]
|
||
[SerializeField] private float cellSize = 0.4f;
|
||
[Tooltip("单元格间距(Unity单位)")]
|
||
[SerializeField] private float cellGap = 0.05f;
|
||
[Tooltip("文字大小")]
|
||
[SerializeField] private float cellFontSize = 2.5f;
|
||
[Tooltip("网格中心偏移量")]
|
||
[SerializeField] private Vector3 gridCenterOffset = Vector3.zero;
|
||
|
||
|
||
[Header("字体配置")]
|
||
[Tooltip("中文字体资源(用于网格文字)")]
|
||
[SerializeField] private TMP_FontAsset chineseFontAsset;
|
||
|
||
[Header("动画配置")]
|
||
|
||
[Tooltip("视觉效果过渡时长(颜色、透明度等)")]
|
||
[SerializeField] private float visualTransitionDuration = 0.15f;
|
||
[Tooltip("闪烁效果间隔")]
|
||
[SerializeField] private float glitchInterval = 0.1f;
|
||
[Tooltip("抖动强度")]
|
||
[SerializeField] private float jitterIntensity = 0.002f;
|
||
|
||
[Header("网格样式")]
|
||
[SerializeField] private Color gridLineColor = new Color(1f, 1f, 1f, 0.1f);
|
||
[SerializeField] private float gridThickness = 0.02f;
|
||
|
||
[Header("关卡配置(数据驱动)")]
|
||
[Tooltip("默认关卡配置数据(ScriptableObject)\n也可以通过代码动态加载其他关卡")]
|
||
[SerializeField] private AnalysisModeData currentLevelData;
|
||
|
||
// 运行时数据
|
||
private List<AnalysisTarget> analysisTargets = new List<AnalysisTarget>();
|
||
private AnalysisModeGenerator generator;
|
||
private int stripWidth; // 从关卡数据动态获取
|
||
private int stripHeight; // 从关卡数据动态获取
|
||
|
||
// 状态
|
||
private bool isAnalysisModeActive = false;
|
||
private bool isFilteringActive = false;
|
||
private float currentFrequency = 50f;
|
||
private GridCell[,] gridData;
|
||
private Dictionary<Vector2Int, TextParticle> gridParticles = new Dictionary<Vector2Int, TextParticle>();
|
||
private bool isInitialized = false;
|
||
private string completionNodeName = "";
|
||
|
||
// 平滑过渡状态存储
|
||
private Dictionary<Vector2Int, ParticleVisualState> particleStates = new Dictionary<Vector2Int, ParticleVisualState>();
|
||
|
||
// 闪烁计时器(用于平滑闪烁)
|
||
private Dictionary<Vector2Int, float> glitchTimers = new Dictionary<Vector2Int, float>();
|
||
|
||
// Shapes 格子绘制器
|
||
private GameObject mainGridDrawerObj;
|
||
private GameObject decoderGridDrawerObj;
|
||
|
||
private Vector3 gridStartPos; // 网格起始位置(左上角)
|
||
|
||
// 解码器相关
|
||
private Dictionary<Vector2Int, TextParticle> decoderParticles = new Dictionary<Vector2Int, TextParticle>();
|
||
private Vector2Int currentDecoderGridPos; // 解码器在主网格中的位置(行,列)
|
||
private bool isDraggingDecoder = false;
|
||
private Vector3 dragStartMouseWorldPos; // 拖拽开始时的鼠标世界坐标
|
||
private Vector3 dragStartDecoderWorldPos; // 拖拽开始时的解码器世界坐标
|
||
private Vector2Int dragStartGridPos; // 拖拽开始时的解码器网格坐标
|
||
|
||
private void Start()
|
||
{
|
||
FixSystemCenter.SystemDic.Register(this);
|
||
SetupUI();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 进入分析模式(由状态机调用)
|
||
/// </summary>
|
||
public void OnEnter()
|
||
{
|
||
// 开启分析模式视图
|
||
// if (!isAnalysisModeActive)
|
||
// {
|
||
// OpenView();
|
||
// }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 退出分析模式(由状态机调用)
|
||
/// </summary>
|
||
public void OnExit()
|
||
{
|
||
// 关闭分析模式视图
|
||
if (isAnalysisModeActive)
|
||
{
|
||
CloseView();
|
||
}
|
||
}
|
||
|
||
private void SetupUI()
|
||
{
|
||
if (analysisPanel != null)
|
||
{
|
||
analysisPanel.gameObject.SetActive(false);
|
||
}
|
||
|
||
if (initializeButton != null)
|
||
{
|
||
initializeButton.onClick.AddListener(OnInitializeClicked);
|
||
// 初始化按钮默认可用
|
||
initializeButton.interactable = true;
|
||
}
|
||
|
||
if (startFilterButton != null)
|
||
{
|
||
startFilterButton.onClick.AddListener(OnStartFilterClicked);
|
||
// 过滤按钮在初始化前不可用
|
||
startFilterButton.interactable = false;
|
||
}
|
||
|
||
if (verifyButton != null)
|
||
{
|
||
verifyButton.onClick.AddListener(OnVerifyClicked);
|
||
verifyButton.interactable = false;
|
||
}
|
||
|
||
if (emotionSlider != null)
|
||
{
|
||
emotionSlider.minValue = 0f;
|
||
emotionSlider.maxValue = 100f;
|
||
emotionSlider.value = 50f;
|
||
emotionSlider.onValueChanged.AddListener(OnEmotionSliderChanged);
|
||
// 滑动条在过滤模式开启前不可用
|
||
emotionSlider.interactable = false;
|
||
}
|
||
|
||
}
|
||
|
||
/// <summary>
|
||
/// 加载关卡数据(数据驱动)
|
||
/// </summary>
|
||
private void LoadLevelData(AnalysisModeData levelData)
|
||
{
|
||
if (levelData == null)
|
||
{
|
||
Debug.LogError("[AnalysisModeManager] 关卡数据为空!");
|
||
return;
|
||
}
|
||
|
||
// 验证配置
|
||
if (!levelData.Validate(out string errorMessage))
|
||
{
|
||
Debug.LogError($"[AnalysisModeManager] 关卡配置无效: {errorMessage}");
|
||
return;
|
||
}
|
||
|
||
currentLevelData = levelData;
|
||
|
||
// 创建生成器
|
||
generator = new AnalysisModeGenerator(levelData);
|
||
|
||
// 清空目标列表
|
||
analysisTargets.Clear();
|
||
|
||
// 使用生成器创建目标
|
||
AnalysisTarget target = generator.GenerateAnalysisTarget();
|
||
analysisTargets.Add(target);
|
||
|
||
// 更新解码器尺寸
|
||
stripWidth = levelData.decoderWidth;
|
||
stripHeight = levelData.decoderHeight;
|
||
|
||
Debug.Log($"[AnalysisModeManager] 已加载关卡: {levelData.levelName}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动分析模式(独立系统入口)
|
||
/// </summary>
|
||
/// <param name="levelData">关卡配置数据</param>
|
||
/// <param name="completionNode">完成回调节点(可选,会覆盖配置中的节点)</param>
|
||
public void OpenView(AnalysisModeData levelData = null, string completionNode = null)
|
||
{
|
||
if (isAnalysisModeActive) return;
|
||
|
||
// 加载关卡数据
|
||
if (levelData != null)
|
||
{
|
||
LoadLevelData(levelData);
|
||
}
|
||
else if (currentLevelData != null)
|
||
{
|
||
// 使用默认配置
|
||
LoadLevelData(currentLevelData);
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError("[AnalysisModeManager] 没有可用的关卡数据!");
|
||
return;
|
||
}
|
||
|
||
// 设置完成回调节点(优先使用参数,其次使用配置)
|
||
completionNodeName = !string.IsNullOrEmpty(completionNode)
|
||
? completionNode
|
||
: currentLevelData.successNodeName;
|
||
|
||
// 显示分析控制面板
|
||
isAnalysisModeActive = true;
|
||
if (analysisPanel != null)
|
||
{
|
||
analysisPanel.gameObject.SetActive(true);
|
||
}
|
||
|
||
// 自动初始化网格
|
||
if (!isInitialized)
|
||
{
|
||
InitializeGrid();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 关闭分析模式
|
||
/// </summary>
|
||
public void CloseView()
|
||
{
|
||
if (!isAnalysisModeActive) return;
|
||
|
||
isAnalysisModeActive = false;
|
||
|
||
// 重置状态
|
||
isFilteringActive = false;
|
||
isInitialized = false;
|
||
currentFrequency = 50f;
|
||
ClearGrid();
|
||
|
||
}
|
||
|
||
private void OnInitializeClicked()
|
||
{
|
||
// 只能初始化一次
|
||
if (isInitialized)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 禁用初始化按钮
|
||
if (initializeButton != null)
|
||
{
|
||
initializeButton.interactable = false;
|
||
}
|
||
|
||
InitializeGrid();
|
||
}
|
||
|
||
private void InitializeGrid()
|
||
{
|
||
if (currentLevelData == null || generator == null)
|
||
{
|
||
Debug.LogError("[AnalysisModeManager] 未加载关卡数据或生成器未初始化!");
|
||
return;
|
||
}
|
||
|
||
// 使用生成器创建网格数据(数据驱动,智能生成)
|
||
gridData = generator.GenerateGrid(gridWidth, gridHeight);
|
||
|
||
// 创建网格 UI(带动画)
|
||
StartCoroutine(AnimateGridCreation());
|
||
}
|
||
|
||
private System.Collections.IEnumerator AnimateGridCreation()
|
||
{
|
||
ClearGrid();
|
||
|
||
// 计算网格中心位置
|
||
Vector3 gridCenter = Vector3.zero;
|
||
if (sharedCanvas != null)
|
||
{
|
||
gridCenter = sharedCanvas.transform.position + gridCenterOffset;
|
||
}
|
||
|
||
// 计算网格总大小
|
||
float totalWidth = gridWidth * cellSize + (gridWidth - 1) * cellGap;
|
||
float totalHeight = gridHeight * cellSize + (gridHeight - 1) * cellGap;
|
||
|
||
// 计算起始位置(左上角)
|
||
gridStartPos = gridCenter - new Vector3(totalWidth / 2f, totalHeight / 2f, 0f);
|
||
|
||
// 创建主矩阵 Shapes 格子绘制器
|
||
CreateMainGridDrawer();
|
||
|
||
// 随机顺序创建单元格(Matrix 风格)
|
||
List<Vector2Int> cellPositions = new List<Vector2Int>();
|
||
for (int row = 0; row < gridHeight; row++)
|
||
{
|
||
for (int col = 0; col < gridWidth; col++)
|
||
{
|
||
cellPositions.Add(new Vector2Int(row, col));
|
||
}
|
||
}
|
||
cellPositions = cellPositions.OrderBy(x => Random.value).ToList();
|
||
|
||
int batchSize = 10;
|
||
for (int i = 0; i < cellPositions.Count; i++)
|
||
{
|
||
var pos = cellPositions[i];
|
||
CreateGridParticle(pos.x, pos.y, gridStartPos);
|
||
|
||
if (i % batchSize == 0)
|
||
{
|
||
yield return new WaitForSeconds(0.01f);
|
||
}
|
||
}
|
||
|
||
yield return new WaitForSeconds(0.5f);
|
||
|
||
isInitialized = true;
|
||
|
||
if (startFilterButton != null)
|
||
{
|
||
startFilterButton.interactable = true;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建主矩阵 Shapes 格子绘制器
|
||
/// </summary>
|
||
private void CreateMainGridDrawer()
|
||
{
|
||
if (sharedCanvas == null) return;
|
||
|
||
// 清理旧的绘制器
|
||
if (mainGridDrawerObj != null)
|
||
{
|
||
Destroy(mainGridDrawerObj);
|
||
}
|
||
|
||
// 创建绘制器对象
|
||
mainGridDrawerObj = new GameObject("MainGridDrawer");
|
||
// 保持在 Canvas 下
|
||
mainGridDrawerObj.transform.SetParent(sharedCanvas.transform, false);
|
||
|
||
// 设置位置到网格起始点(左上角),并保持旋转一致
|
||
mainGridDrawerObj.transform.position = gridStartPos;
|
||
mainGridDrawerObj.transform.rotation = sharedCanvas.transform.rotation;
|
||
mainGridDrawerObj.transform.localScale = Vector3.one;
|
||
|
||
// 添加 RectTransform(Canvas 下通常需要,虽然 Shapes 主要用 Transform)
|
||
RectTransform rect = mainGridDrawerObj.AddComponent<RectTransform>();
|
||
rect.anchorMin = new Vector2(0.5f, 0.5f);
|
||
rect.anchorMax = new Vector2(0.5f, 0.5f);
|
||
rect.sizeDelta = Vector2.zero;
|
||
|
||
// 计算步长
|
||
float stride = cellSize + cellGap;
|
||
float halfStride = stride / 2f;
|
||
|
||
// 创建网格容器(避免 Hierarchy 太乱)
|
||
GameObject linesContainer = new GameObject("Lines");
|
||
linesContainer.transform.SetParent(mainGridDrawerObj.transform, false);
|
||
linesContainer.transform.localPosition = Vector3.zero;
|
||
|
||
// 绘制水平线 (Rows + 1)
|
||
// Y轴向下为负,Row 0 中心在 0,上方边界在 +halfStride
|
||
float startX = -halfStride;
|
||
float endX = (gridWidth * stride) - halfStride;
|
||
|
||
for (int row = 0; row <= gridHeight; row++)
|
||
{
|
||
float yPos = -(row * stride) + halfStride;
|
||
CreateGridLine(linesContainer.transform, new Vector3(startX, yPos, 0), new Vector3(endX, yPos, 0));
|
||
}
|
||
|
||
// 绘制垂直线 (Cols + 1)
|
||
// X轴向右为正,Col 0 中心在 0,左方边界在 -halfStride
|
||
float startY = halfStride;
|
||
float endY = -(gridHeight * stride) + halfStride;
|
||
|
||
for (int col = 0; col <= gridWidth; col++)
|
||
{
|
||
float xPos = (col * stride) - halfStride;
|
||
CreateGridLine(linesContainer.transform, new Vector3(xPos, startY, 0), new Vector3(xPos, endY, 0));
|
||
}
|
||
}
|
||
|
||
private void CreateGridLine(Transform parent, Vector3 start, Vector3 end)
|
||
{
|
||
GameObject lineObj = new GameObject("GridLine");
|
||
lineObj.transform.SetParent(parent, false);
|
||
|
||
Line line = lineObj.AddComponent<Line>();
|
||
line.Start = start;
|
||
line.End = end;
|
||
line.Color = gridLineColor;
|
||
line.Thickness = gridThickness;
|
||
line.ThicknessSpace = ThicknessSpace.Meters; // 或者 Pixels,取决于需求,通常 World Space 用 Meters
|
||
line.SortingOrder = 5;
|
||
|
||
// 确保渲染顺序正确(如果是在 Overlay Canvas 下,Z轴可能无效,需要 SortingOrder)
|
||
// Shapes 通常会自动处理,但在 UI 中可能需要调整 Z
|
||
line.transform.localPosition = Vector3.zero;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建解码器
|
||
/// </summary>
|
||
private void CreateDecoder()
|
||
{
|
||
if (currentLevelData == null || sharedCanvas == null) return;
|
||
|
||
// 清理旧的解码器
|
||
ClearDecoder();
|
||
|
||
// 初始化解码器位置(网格坐标,默认在中心附近)
|
||
currentDecoderGridPos = new Vector2Int(
|
||
gridHeight / 2 - stripHeight / 2,
|
||
gridWidth / 2 - stripWidth / 2
|
||
);
|
||
|
||
// 创建解码器网格绘制器
|
||
CreateDecoderGridDrawer();
|
||
|
||
// 创建解码器文字粒子
|
||
CreateDecoderParticles();
|
||
|
||
// 更新解码器位置
|
||
UpdateDecoderPosition();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建解码器网格绘制器(类似MainGridDrawer)
|
||
/// </summary>
|
||
private void CreateDecoderGridDrawer()
|
||
{
|
||
if (sharedCanvas == null) return;
|
||
|
||
// 创建绘制器对象
|
||
decoderGridDrawerObj = new GameObject("DecoderGridDrawer");
|
||
decoderGridDrawerObj.transform.SetParent(sharedCanvas.transform, false);
|
||
|
||
// 设置初始位置
|
||
decoderGridDrawerObj.transform.rotation = sharedCanvas.transform.rotation;
|
||
decoderGridDrawerObj.transform.localScale = Vector3.one;
|
||
|
||
// 添加 RectTransform
|
||
RectTransform rect = decoderGridDrawerObj.AddComponent<RectTransform>();
|
||
rect.anchorMin = new Vector2(0.5f, 0.5f);
|
||
rect.anchorMax = new Vector2(0.5f, 0.5f);
|
||
rect.sizeDelta = Vector2.zero;
|
||
|
||
// 计算步长(与主网格一致)
|
||
float stride = cellSize + cellGap;
|
||
float halfStride = stride / 2f;
|
||
|
||
// 创建网格容器
|
||
GameObject linesContainer = new GameObject("Lines");
|
||
linesContainer.transform.SetParent(decoderGridDrawerObj.transform, false);
|
||
linesContainer.transform.localPosition = Vector3.zero;
|
||
|
||
// 绘制水平线 (stripHeight + 1 条)
|
||
float startX = -halfStride;
|
||
float endX = (stripWidth * stride) - halfStride;
|
||
|
||
for (int row = 0; row <= stripHeight; row++)
|
||
{
|
||
float yPos = -(row * stride) + halfStride;
|
||
CreateDecoderGridLine(linesContainer.transform, new Vector3(startX, yPos, 0), new Vector3(endX, yPos, 0));
|
||
}
|
||
|
||
// 绘制垂直线 (stripWidth + 1 条)
|
||
float startY = halfStride;
|
||
float endY = -(stripHeight * stride) + halfStride;
|
||
|
||
for (int col = 0; col <= stripWidth; col++)
|
||
{
|
||
float xPos = (col * stride) - halfStride;
|
||
CreateDecoderGridLine(linesContainer.transform, new Vector3(xPos, startY, 0), new Vector3(xPos, endY, 0));
|
||
}
|
||
|
||
// 添加拖拽功能
|
||
AddDecoderDragHandler();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建解码器网格线(与主网格不同的颜色和渲染顺序)
|
||
/// </summary>
|
||
private void CreateDecoderGridLine(Transform parent, Vector3 start, Vector3 end)
|
||
{
|
||
GameObject lineObj = new GameObject("DecoderGridLine");
|
||
lineObj.transform.SetParent(parent, false);
|
||
|
||
Line line = lineObj.AddComponent<Line>();
|
||
line.Start = start;
|
||
line.End = end;
|
||
// 使用 cyan 颜色(参考 web 版本的 border: 1px solid cyan)
|
||
line.Color = new Color(0f, 1f, 1f, 0.8f); // Cyan 青色,较高不透明度
|
||
line.Thickness = gridThickness * 1.5f; // 稍微粗一些
|
||
line.ThicknessSpace = ThicknessSpace.Meters;
|
||
line.SortingOrder = 10; // 更高的渲染顺序,显示在主网格之上
|
||
|
||
line.transform.localPosition = Vector3.zero;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加解码器拖拽处理器
|
||
/// </summary>
|
||
private void AddDecoderDragHandler()
|
||
{
|
||
if (decoderGridDrawerObj == null) return;
|
||
|
||
// 创建一个透明的Image作为拖拽区域
|
||
GameObject dragAreaObj = new GameObject("DragArea");
|
||
dragAreaObj.transform.SetParent(decoderGridDrawerObj.transform, false);
|
||
|
||
RectTransform dragRect = dragAreaObj.AddComponent<RectTransform>();
|
||
dragRect.anchorMin = Vector2.zero;
|
||
dragRect.anchorMax = Vector2.one;
|
||
dragRect.offsetMin = Vector2.zero;
|
||
dragRect.offsetMax = Vector2.zero;
|
||
|
||
float stride = cellSize + cellGap;
|
||
dragRect.sizeDelta = new Vector2(stripWidth * stride, stripHeight * stride);
|
||
dragRect.anchoredPosition = new Vector2(
|
||
(stripWidth * stride) / 2f,
|
||
-(stripHeight * stride) / 2f
|
||
);
|
||
|
||
// 添加半透明背景,让解码器区域更明显,同时也是拖拽响应区域
|
||
Image dragImage = dragAreaObj.AddComponent<Image>();
|
||
dragImage.color = new Color(0f, 0f, 0f, 0.1f); // 极淡的半透明背景,主要用于拖拽响应
|
||
dragImage.raycastTarget = true;
|
||
|
||
// 添加 EventTrigger 组件用于处理鼠标事件
|
||
UnityEngine.EventSystems.EventTrigger eventTrigger = dragAreaObj.AddComponent<UnityEngine.EventSystems.EventTrigger>();
|
||
|
||
// 开始拖拽
|
||
UnityEngine.EventSystems.EventTrigger.Entry beginDragEntry = new UnityEngine.EventSystems.EventTrigger.Entry();
|
||
beginDragEntry.eventID = UnityEngine.EventSystems.EventTriggerType.BeginDrag;
|
||
beginDragEntry.callback.AddListener((data) => { OnDecoderBeginDrag((UnityEngine.EventSystems.PointerEventData)data); });
|
||
eventTrigger.triggers.Add(beginDragEntry);
|
||
|
||
// 拖拽中
|
||
UnityEngine.EventSystems.EventTrigger.Entry dragEntry = new UnityEngine.EventSystems.EventTrigger.Entry();
|
||
dragEntry.eventID = UnityEngine.EventSystems.EventTriggerType.Drag;
|
||
dragEntry.callback.AddListener((data) => { OnDecoderDrag((UnityEngine.EventSystems.PointerEventData)data); });
|
||
eventTrigger.triggers.Add(dragEntry);
|
||
|
||
// 结束拖拽
|
||
UnityEngine.EventSystems.EventTrigger.Entry endDragEntry = new UnityEngine.EventSystems.EventTrigger.Entry();
|
||
endDragEntry.eventID = UnityEngine.EventSystems.EventTriggerType.EndDrag;
|
||
endDragEntry.callback.AddListener((data) => { OnDecoderEndDrag((UnityEngine.EventSystems.PointerEventData)data); });
|
||
eventTrigger.triggers.Add(endDragEntry);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建解码器文字粒子
|
||
/// </summary>
|
||
private void CreateDecoderParticles()
|
||
{
|
||
if (currentLevelData == null || sharedCanvas == null) return;
|
||
|
||
// 解析解码器文字
|
||
string decoderText = currentLevelData.decoderText;
|
||
List<string> words = SplitIntoTwoCharChunks(decoderText);
|
||
int wordIndex = 0;
|
||
|
||
// 创建HashSet用于快速查找孔洞
|
||
HashSet<Vector2Int> holes = new HashSet<Vector2Int>(currentLevelData.holePositions);
|
||
|
||
for (int row = 0; row < stripHeight; row++)
|
||
{
|
||
for (int col = 0; col < stripWidth; col++)
|
||
{
|
||
Vector2Int localPos = new Vector2Int(row, col);
|
||
|
||
// 如果是孔洞,跳过
|
||
if (holes.Contains(localPos))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 如果还有词可以显示
|
||
if (wordIndex < words.Count)
|
||
{
|
||
CreateDecoderParticle(row, col, words[wordIndex]);
|
||
wordIndex++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建单个解码器粒子
|
||
/// </summary>
|
||
private void CreateDecoderParticle(int row, int col, string word)
|
||
{
|
||
if (sharedCanvas == null) return;
|
||
|
||
// 先创建背景方块(使用 Shapes)
|
||
CreateDecoderCellBackground(row, col);
|
||
|
||
// 创建粒子对象
|
||
GameObject particleObj = new GameObject($"DecoderParticle_{row}_{col}");
|
||
|
||
// 添加 RectTransform
|
||
RectTransform rectTransform = particleObj.AddComponent<RectTransform>();
|
||
rectTransform.SetParent(decoderGridDrawerObj.transform, false);
|
||
rectTransform.localScale = Vector3.one;
|
||
rectTransform.sizeDelta = Vector2.zero;
|
||
|
||
// 计算局部位置(相对于解码器左上角)
|
||
float stride = cellSize + cellGap;
|
||
float xPos = col * stride;
|
||
float yPos = -row * stride;
|
||
rectTransform.localPosition = new Vector3(xPos, yPos, -0.15f); // 在背景之上
|
||
|
||
// 添加 TextParticle 组件
|
||
TextParticle particle = particleObj.AddComponent<TextParticle>();
|
||
particle.SetFont(chineseFontAsset);
|
||
|
||
// 设置为完全静态
|
||
particle.isStatic = true;
|
||
particle.isCalmed = true;
|
||
particle.velocity = Vector2.zero;
|
||
|
||
// 设置透明度
|
||
particle.alpha = 1f;
|
||
particle.baseAlpha = 1f;
|
||
|
||
// 设置文字内容
|
||
particle.ForceSetCharacter(word);
|
||
|
||
// 设置字体大小和颜色(参考 web 版本的 cyan 颜色)
|
||
if (particle.textMesh != null)
|
||
{
|
||
particle.textMesh.fontSize = cellFontSize;
|
||
// 使用 cyan 颜色(0, 255, 255)参考 web 版本
|
||
particle.textMesh.color = new Color(0f, 1f, 1f, 1f); // Cyan 青色
|
||
particle.textMesh.fontStyle = FontStyles.Bold;
|
||
}
|
||
|
||
// 添加发光效果(参考 web 版本的 text-shadow: 0 0 8px cyan)
|
||
if (particle.glowSprite != null)
|
||
{
|
||
particle.glowSprite.color = new Color(0f, 1f, 1f, 0.6f); // Cyan 发光
|
||
}
|
||
|
||
Vector2Int pos = new Vector2Int(row, col);
|
||
decoderParticles[pos] = particle;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建解码器单元格背景(遮挡主网格文字)
|
||
/// </summary>
|
||
private void CreateDecoderCellBackground(int row, int col)
|
||
{
|
||
if (decoderGridDrawerObj == null) return;
|
||
|
||
// 创建背景对象
|
||
GameObject bgObj = new GameObject($"DecoderBg_{row}_{col}");
|
||
bgObj.transform.SetParent(decoderGridDrawerObj.transform, false);
|
||
|
||
// 计算局部位置(相对于解码器左上角)
|
||
float stride = cellSize + cellGap;
|
||
float xPos = col * stride;
|
||
float yPos = -row * stride;
|
||
bgObj.transform.localPosition = new Vector3(xPos, yPos, -0.1f); // 更靠前的 Z 轴,确保遮挡主网格
|
||
|
||
// 添加 Shapes Rectangle 组件
|
||
Shapes.Rectangle rect = bgObj.AddComponent<Shapes.Rectangle>();
|
||
// 背景大小应该填充整个格子区域(包括间隙),确保完全遮挡下方文字
|
||
rect.Width = stride;
|
||
rect.Height = stride;
|
||
// 使用深青色背景,80% 不透明度
|
||
rect.Color = new Color(0f, 0.12f, 0.12f, 0.8f);
|
||
rect.SortingOrder = 8; // 在主网格之上,在解码器网格线之下
|
||
rect.CornerRadii = Vector4.one * 0.02f; // 轻微圆角
|
||
// 设置为不透明渲染模式
|
||
rect.BlendMode = Shapes.ShapesBlendMode.Opaque;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新解码器位置(对齐到网格)
|
||
/// </summary>
|
||
private void UpdateDecoderPosition()
|
||
{
|
||
if (decoderGridDrawerObj == null) return;
|
||
|
||
// 计算解码器的世界位置
|
||
float stride = cellSize + cellGap;
|
||
Vector3 decoderWorldPos = gridStartPos + new Vector3(
|
||
currentDecoderGridPos.y * stride,
|
||
-currentDecoderGridPos.x * stride,
|
||
-0.1f // Z轴稍微往前,确保显示在主网格之上
|
||
);
|
||
|
||
decoderGridDrawerObj.transform.position = decoderWorldPos;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 开始拖拽解码器
|
||
/// </summary>
|
||
private void OnDecoderBeginDrag(UnityEngine.EventSystems.PointerEventData eventData)
|
||
{
|
||
isDraggingDecoder = true;
|
||
dragStartGridPos = currentDecoderGridPos;
|
||
|
||
// 记录鼠标的世界坐标
|
||
if (RectTransformUtility.ScreenPointToWorldPointInRectangle(
|
||
sharedCanvas.transform as RectTransform,
|
||
eventData.position,
|
||
eventData.pressEventCamera,
|
||
out dragStartMouseWorldPos))
|
||
{
|
||
// 记录解码器的世界坐标
|
||
if (decoderGridDrawerObj != null)
|
||
{
|
||
dragStartDecoderWorldPos = decoderGridDrawerObj.transform.position;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 拖拽解码器中
|
||
/// </summary>
|
||
private void OnDecoderDrag(UnityEngine.EventSystems.PointerEventData eventData)
|
||
{
|
||
if (!isDraggingDecoder || decoderGridDrawerObj == null) return;
|
||
|
||
// 获取当前鼠标的世界坐标
|
||
Vector3 currentMouseWorldPos;
|
||
if (RectTransformUtility.ScreenPointToWorldPointInRectangle(
|
||
sharedCanvas.transform as RectTransform,
|
||
eventData.position,
|
||
eventData.pressEventCamera,
|
||
out currentMouseWorldPos))
|
||
{
|
||
// 计算鼠标移动的增量(世界坐标)
|
||
Vector3 mouseDelta = currentMouseWorldPos - dragStartMouseWorldPos;
|
||
|
||
// 计算解码器应该在的世界坐标(保持鼠标和解码器的相对位置)
|
||
Vector3 targetDecoderWorldPos = dragStartDecoderWorldPos + mouseDelta;
|
||
|
||
// 将目标世界坐标转换为网格坐标
|
||
Vector3 offsetFromGridStart = targetDecoderWorldPos - gridStartPos;
|
||
float stride = cellSize + cellGap;
|
||
|
||
int targetGridRow = Mathf.RoundToInt(-offsetFromGridStart.y / stride); // Y轴反向
|
||
int targetGridCol = Mathf.RoundToInt(offsetFromGridStart.x / stride);
|
||
|
||
// 限制在主网格范围内
|
||
targetGridRow = Mathf.Clamp(targetGridRow, 0, gridHeight - stripHeight);
|
||
targetGridCol = Mathf.Clamp(targetGridCol, 0, gridWidth - stripWidth);
|
||
|
||
Vector2Int newGridPos = new Vector2Int(targetGridRow, targetGridCol);
|
||
|
||
// 如果位置改变,更新解码器
|
||
if (newGridPos != currentDecoderGridPos)
|
||
{
|
||
currentDecoderGridPos = newGridPos;
|
||
UpdateDecoderPosition();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 结束拖拽解码器
|
||
/// </summary>
|
||
private void OnDecoderEndDrag(UnityEngine.EventSystems.PointerEventData eventData)
|
||
{
|
||
isDraggingDecoder = false;
|
||
|
||
// 确保最终位置对齐到网格
|
||
UpdateDecoderPosition();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理解码器
|
||
/// </summary>
|
||
private void ClearDecoder()
|
||
{
|
||
// 清理解码器粒子
|
||
foreach (var particle in decoderParticles.Values)
|
||
{
|
||
if (particle != null)
|
||
{
|
||
Destroy(particle.gameObject);
|
||
}
|
||
}
|
||
decoderParticles.Clear();
|
||
|
||
// 清理解码器绘制器
|
||
if (decoderGridDrawerObj != null)
|
||
{
|
||
Destroy(decoderGridDrawerObj);
|
||
decoderGridDrawerObj = null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建网格粒子(直接使用 TextParticle,与 LanguageParticleManager 一致)
|
||
/// </summary>
|
||
private void CreateGridParticle(int row, int col, Vector3 gridStartPos)
|
||
{
|
||
if (sharedCanvas == null) return;
|
||
|
||
// 计算粒子位置(从左上角开始)
|
||
float xPos = col * (cellSize + cellGap);
|
||
float yPos = -row * (cellSize + cellGap); // Y 轴向下
|
||
Vector3 position = gridStartPos + new Vector3(xPos, yPos, 0f);
|
||
|
||
// 创建粒子对象(参考 LanguageParticleManager.CreateParticleObject)
|
||
GameObject particleObj = new GameObject($"GridParticle_{row}_{col}");
|
||
|
||
// 添加 RectTransform(在 Canvas 下必需)
|
||
RectTransform rectTransform = particleObj.AddComponent<RectTransform>();
|
||
rectTransform.SetParent(sharedCanvas.transform, false);
|
||
rectTransform.position = position;
|
||
rectTransform.localScale = Vector3.one;
|
||
rectTransform.sizeDelta = Vector2.zero;
|
||
|
||
// 添加 TextParticle 组件
|
||
TextParticle particle = particleObj.AddComponent<TextParticle>();
|
||
particle.SetFont(chineseFontAsset);
|
||
|
||
// 设置为完全静态(固定在格子里,不飘动)
|
||
particle.isStatic = true; // 静态粒子
|
||
particle.isCalmed = true; // 完全平静,不受力影响
|
||
particle.velocity = Vector2.zero; // 速度为0
|
||
|
||
// 重要:设置透明度,否则粒子不可见!
|
||
particle.alpha = 1f;
|
||
particle.baseAlpha = 1f;
|
||
|
||
// 设置文字内容
|
||
if (gridData != null)
|
||
{
|
||
GridCell cellData = gridData[row, col];
|
||
particle.ForceSetCharacter(cellData.word); // 显示词语
|
||
|
||
// 设置字体大小和颜色
|
||
if (particle.textMesh != null)
|
||
{
|
||
particle.textMesh.fontSize = cellFontSize;
|
||
particle.textMesh.color = Color.white;
|
||
}
|
||
}
|
||
|
||
Vector2Int pos = new Vector2Int(row, col);
|
||
gridParticles[pos] = particle;
|
||
|
||
// 初始化视觉状态
|
||
if (!particleStates.ContainsKey(pos))
|
||
{
|
||
particleStates[pos] = new ParticleVisualState
|
||
{
|
||
basePosition = position,
|
||
currentAlpha = 1f,
|
||
targetAlpha = 1f,
|
||
currentColor = Color.white,
|
||
targetColor = Color.white,
|
||
currentScale = 1f,
|
||
targetScale = 1f,
|
||
isShowingWord = true
|
||
};
|
||
}
|
||
|
||
// 初始化闪烁计时器
|
||
if (!glitchTimers.ContainsKey(pos))
|
||
{
|
||
glitchTimers[pos] = Random.Range(0f, glitchInterval);
|
||
}
|
||
}
|
||
|
||
private void ClearGrid()
|
||
{
|
||
// 清理所有Tween
|
||
foreach (var state in particleStates.Values)
|
||
{
|
||
if (state.alphaTween != null) state.alphaTween.Kill();
|
||
if (state.colorTween != null) state.colorTween.Kill();
|
||
if (state.scaleTween != null) state.scaleTween.Kill();
|
||
if (state.jitterTween != null) state.jitterTween.Kill();
|
||
}
|
||
|
||
foreach (var particle in gridParticles.Values)
|
||
{
|
||
if (particle != null)
|
||
{
|
||
Destroy(particle.gameObject);
|
||
}
|
||
}
|
||
gridParticles.Clear();
|
||
particleStates.Clear();
|
||
glitchTimers.Clear();
|
||
|
||
if (mainGridDrawerObj != null)
|
||
{
|
||
Destroy(mainGridDrawerObj);
|
||
mainGridDrawerObj = null;
|
||
}
|
||
|
||
// 同时清理解码器
|
||
ClearDecoder();
|
||
}
|
||
|
||
private List<string> SplitIntoTwoCharChunks(string text)
|
||
{
|
||
List<string> chunks = new List<string>();
|
||
for (int i = 0; i < text.Length; i += 2)
|
||
{
|
||
if (i + 1 < text.Length)
|
||
{
|
||
chunks.Add(text.Substring(i, 2));
|
||
}
|
||
else
|
||
{
|
||
chunks.Add(text[i] + " "); // 奇数长度时补空格
|
||
}
|
||
}
|
||
return chunks;
|
||
}
|
||
|
||
private void OnStartFilterClicked()
|
||
{
|
||
if (!isInitialized || isFilteringActive) return;
|
||
|
||
isFilteringActive = true;
|
||
|
||
// 显示解码器条纹
|
||
CreateDecoder();
|
||
|
||
// 启用滑动条和验证按钮
|
||
if (emotionSlider != null)
|
||
{
|
||
emotionSlider.interactable = true;
|
||
}
|
||
|
||
if (verifyButton != null)
|
||
{
|
||
verifyButton.interactable = true;
|
||
}
|
||
|
||
if (startFilterButton != null)
|
||
{
|
||
startFilterButton.interactable = false;
|
||
}
|
||
|
||
// 应用初始过滤效果
|
||
UpdateGridVisualsWithFilter();
|
||
}
|
||
|
||
|
||
private void OnEmotionSliderChanged(float value)
|
||
{
|
||
currentFrequency = value;
|
||
|
||
if (frequencyText != null)
|
||
{
|
||
frequencyText.text = $"{value:F1} Hz";
|
||
|
||
// 确保频率文本使用正确的字体和大小
|
||
if (chineseFontAsset != null && frequencyText.font != chineseFontAsset)
|
||
{
|
||
frequencyText.font = chineseFontAsset;
|
||
}
|
||
}
|
||
|
||
// 更新网格显示
|
||
if (isFilteringActive)
|
||
{
|
||
UpdateGridVisualsWithFilter();
|
||
}
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
// 在过滤模式下,持续更新视觉效果以确保闪烁效果平滑
|
||
// 注意:只在闪烁区域需要频繁更新,其他区域DOTween会自动处理过渡
|
||
if (isFilteringActive && isInitialized)
|
||
{
|
||
// 只更新处于闪烁状态的粒子(距离在5-20之间)
|
||
// 这样可以减少性能开销
|
||
bool needsUpdate = false;
|
||
foreach (var kvp in gridParticles)
|
||
{
|
||
Vector2Int pos = kvp.Key;
|
||
if (gridData == null || pos.x >= gridHeight || pos.y >= gridWidth) continue;
|
||
|
||
GridCell cellData = gridData[pos.x, pos.y];
|
||
if (cellData.emotion == EmotionType.Neutral) continue;
|
||
|
||
EmotionConfig emotion = GetEmotionConfig(cellData.emotion);
|
||
float distance = CalculateFrequencyDistance(emotion);
|
||
|
||
// 如果处于闪烁区域,需要持续更新
|
||
if (distance >= 5f && distance < 20f)
|
||
{
|
||
needsUpdate = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 如果有粒子处于闪烁状态,每帧更新(但限制更新频率)
|
||
if (needsUpdate)
|
||
{
|
||
UpdateGridVisualsWithFilter();
|
||
}
|
||
}
|
||
}
|
||
|
||
private void UpdateGridVisualsWithFilter()
|
||
{
|
||
if (gridData == null) return;
|
||
|
||
float maxSignal = 0f;
|
||
|
||
for (int row = 0; row < gridHeight; row++)
|
||
{
|
||
for (int col = 0; col < gridWidth; col++)
|
||
{
|
||
float cellSignal = UpdateCellVisual(row, col);
|
||
maxSignal = Mathf.Max(maxSignal, cellSignal);
|
||
}
|
||
}
|
||
|
||
UpdateSignalStrengthDisplay(maxSignal);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新单个单元格的视觉效果
|
||
/// </summary>
|
||
private float UpdateCellVisual(int row, int col)
|
||
{
|
||
Vector2Int pos = new Vector2Int(row, col);
|
||
if (!gridParticles.ContainsKey(pos)) return 0f;
|
||
|
||
TextParticle particle = gridParticles[pos];
|
||
if (particle == null || particle.textMesh == null) return 0f;
|
||
|
||
GridCell cellData = gridData[row, col];
|
||
EmotionConfig emotion = GetEmotionConfig(cellData.emotion);
|
||
|
||
// 计算频率距离
|
||
float distance = CalculateFrequencyDistance(emotion);
|
||
|
||
|
||
// 根据情绪类型应用不同的视觉效果
|
||
if (cellData.emotion == EmotionType.Neutral)
|
||
{
|
||
ApplyNeutralState(particle, cellData);
|
||
return 0f;
|
||
}
|
||
else
|
||
{
|
||
return ApplyEmotionState(particle, cellData, emotion, distance);
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 计算与当前频率的距离
|
||
/// </summary>
|
||
private float CalculateFrequencyDistance(EmotionConfig emotion)
|
||
{
|
||
if (emotion.type == EmotionType.Neutral)
|
||
{
|
||
return 100f;
|
||
}
|
||
return Mathf.Abs(currentFrequency - emotion.frequency);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 应用情绪相关的视觉状态(参考 HTML 原型的视觉效果)
|
||
/// </summary>
|
||
private float ApplyEmotionState(TextParticle particle, GridCell cellData, EmotionConfig emotion, float distance)
|
||
{
|
||
bool isTargetWord = cellData.isTarget;
|
||
Vector2Int pos = new Vector2Int(cellData.row, cellData.col);
|
||
|
||
if (!particleStates.ContainsKey(pos))
|
||
{
|
||
particleStates[pos] = new ParticleVisualState
|
||
{
|
||
basePosition = particle.transform.position,
|
||
currentAlpha = particle.alpha,
|
||
targetAlpha = particle.alpha,
|
||
currentColor = particle.textMesh.color,
|
||
targetColor = particle.textMesh.color,
|
||
currentScale = 1f,
|
||
targetScale = 1f,
|
||
isShowingWord = true
|
||
};
|
||
}
|
||
|
||
ParticleVisualState state = particleStates[pos];
|
||
state.basePosition = particle.transform.position;
|
||
|
||
// 确保显示词语
|
||
if (particle.currentChar != cellData.word)
|
||
{
|
||
particle.ForceSetCharacter(cellData.word);
|
||
state.isShowingWord = true;
|
||
}
|
||
|
||
// 根据距离应用不同的视觉效果(参考 HTML)
|
||
if (distance < 5f)
|
||
{
|
||
// 锁定状态:高亮显示
|
||
return ApplyLockedStateSimple(particle, cellData, emotion, distance, state);
|
||
}
|
||
else if (distance < 20f)
|
||
{
|
||
// 闪烁状态
|
||
return ApplyGlitchingStateSimple(particle, cellData, emotion, distance, state);
|
||
}
|
||
else if (distance < 35f)
|
||
{
|
||
// 淡出状态
|
||
return ApplyFadingStateSimple(particle, cellData, distance, state);
|
||
}
|
||
else
|
||
{
|
||
// 隐藏状态
|
||
return ApplyHiddenStateSimple(particle, cellData, state);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 锁定状态:频率匹配,高亮显示(参考 HTML)
|
||
/// </summary>
|
||
private float ApplyLockedStateSimple(TextParticle particle, GridCell cellData, EmotionConfig emotion, float distance, ParticleVisualState state)
|
||
{
|
||
// 计算信号强度
|
||
float signalStrength = 100f - distance * 20f;
|
||
signalStrength = Mathf.Clamp01(signalStrength / 100f);
|
||
|
||
// 目标值:高亮、放大、加粗
|
||
state.targetAlpha = 1f;
|
||
state.targetColor = emotion.color;
|
||
state.targetScale = 1.15f;
|
||
|
||
// 平滑过渡
|
||
SmoothTransition(state, particle, visualTransitionDuration);
|
||
|
||
// 应用样式
|
||
if (particle.textMesh != null)
|
||
{
|
||
particle.textMesh.fontStyle = FontStyles.Bold;
|
||
}
|
||
|
||
// 设置光晕
|
||
SetParticleGlow(particle, emotion.color, 0.3f + signalStrength * 0.4f);
|
||
|
||
// 停止抖动
|
||
if (state.jitterTween != null)
|
||
{
|
||
state.jitterTween.Kill();
|
||
state.jitterTween = null;
|
||
}
|
||
particle.transform.position = state.basePosition;
|
||
|
||
return signalStrength * 100f;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 闪烁状态:颜色和光晕闪烁(参考 HTML)
|
||
/// </summary>
|
||
private float ApplyGlitchingStateSimple(TextParticle particle, GridCell cellData, EmotionConfig emotion, float distance, ParticleVisualState state)
|
||
{
|
||
Vector2Int pos = new Vector2Int(cellData.row, cellData.col);
|
||
|
||
// 计算闪烁强度
|
||
float glitchChance = 1f - (distance / 20f);
|
||
glitchChance = Mathf.Clamp01(glitchChance);
|
||
|
||
// 使用 Time.time 作为基础时间
|
||
if (!glitchTimers.ContainsKey(pos))
|
||
{
|
||
glitchTimers[pos] = Time.time + Random.Range(0f, glitchInterval * 2f);
|
||
}
|
||
|
||
// 颜色在情绪色和白色之间过渡(闪烁效果)
|
||
float timePhase = (Time.time - glitchTimers[pos]) * (10f + glitchChance * 20f);
|
||
float glitchPhase = Mathf.Sin(timePhase) * 0.5f + 0.5f;
|
||
float colorLerp = glitchChance * (0.5f + glitchPhase * 0.5f);
|
||
|
||
state.targetColor = Color.Lerp(Color.white, emotion.color, colorLerp);
|
||
|
||
// 目标值:轻微模糊、中等透明度
|
||
float blurFactor = (distance - 5f) / 15f;
|
||
state.targetAlpha = 0.9f - blurFactor * 0.2f;
|
||
state.targetScale = 1f;
|
||
|
||
// 平滑过渡
|
||
SmoothTransition(state, particle, visualTransitionDuration * 0.5f);
|
||
|
||
// 应用样式
|
||
if (particle.textMesh != null)
|
||
{
|
||
particle.textMesh.fontStyle = FontStyles.Normal;
|
||
}
|
||
|
||
// 设置光晕(闪烁效果)
|
||
float glowAlpha = 0.2f + glitchChance * 0.2f + glitchPhase * 0.1f;
|
||
SetParticleGlow(particle, emotion.color, glowAlpha);
|
||
|
||
// 添加抖动效果
|
||
float jitterAmount = glitchChance * jitterIntensity;
|
||
Vector3 jitterOffset = new Vector3(
|
||
Random.Range(-jitterAmount, jitterAmount),
|
||
Random.Range(-jitterAmount, jitterAmount),
|
||
0f
|
||
);
|
||
particle.transform.position = state.basePosition + jitterOffset;
|
||
|
||
return 0f;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 淡出状态:逐渐淡化(参考 HTML)
|
||
/// </summary>
|
||
private float ApplyFadingStateSimple(TextParticle particle, GridCell cellData, float distance, ParticleVisualState state)
|
||
{
|
||
// 计算透明度
|
||
float alpha = 0.6f - ((distance - 20f) / 30f);
|
||
alpha = Mathf.Clamp01(alpha);
|
||
|
||
state.targetAlpha = alpha;
|
||
state.targetColor = Color.white;
|
||
state.targetScale = 1f;
|
||
|
||
// 平滑过渡
|
||
SmoothTransition(state, particle, visualTransitionDuration);
|
||
|
||
// 应用样式
|
||
if (particle.textMesh != null)
|
||
{
|
||
particle.textMesh.fontStyle = FontStyles.Normal;
|
||
}
|
||
|
||
// 清除光晕
|
||
SetParticleGlow(particle, Color.clear, 0f);
|
||
|
||
// 停止抖动
|
||
if (state.jitterTween != null)
|
||
{
|
||
state.jitterTween.Kill();
|
||
state.jitterTween = null;
|
||
}
|
||
particle.transform.position = state.basePosition;
|
||
|
||
return 0f;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 隐藏状态:几乎不可见(参考 HTML)
|
||
/// </summary>
|
||
private float ApplyHiddenStateSimple(TextParticle particle, GridCell cellData, ParticleVisualState state)
|
||
{
|
||
state.targetAlpha = 0.05f;
|
||
state.targetColor = Color.white;
|
||
state.targetScale = 1f;
|
||
|
||
// 平滑过渡
|
||
SmoothTransition(state, particle, visualTransitionDuration);
|
||
|
||
// 应用样式
|
||
if (particle.textMesh != null)
|
||
{
|
||
particle.textMesh.fontStyle = FontStyles.Normal;
|
||
}
|
||
|
||
// 清除光晕
|
||
SetParticleGlow(particle, Color.clear, 0f);
|
||
|
||
// 停止抖动
|
||
if (state.jitterTween != null)
|
||
{
|
||
state.jitterTween.Kill();
|
||
state.jitterTween = null;
|
||
}
|
||
particle.transform.position = state.basePosition;
|
||
|
||
return 0f;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 中性状态:噪音/普通内容(参考 HTML)
|
||
/// </summary>
|
||
private void ApplyNeutralState(TextParticle particle, GridCell cellData)
|
||
{
|
||
Vector2Int pos = new Vector2Int(cellData.row, cellData.col);
|
||
|
||
if (!particleStates.ContainsKey(pos))
|
||
{
|
||
particleStates[pos] = new ParticleVisualState
|
||
{
|
||
basePosition = particle.transform.position,
|
||
currentAlpha = particle.alpha,
|
||
targetAlpha = particle.alpha,
|
||
currentColor = particle.textMesh.color,
|
||
targetColor = particle.textMesh.color,
|
||
currentScale = 1f,
|
||
targetScale = 1f,
|
||
isShowingWord = true
|
||
};
|
||
}
|
||
|
||
ParticleVisualState state = particleStates[pos];
|
||
|
||
// 确保显示词语
|
||
if (particle.currentChar != cellData.word)
|
||
{
|
||
particle.ForceSetCharacter(cellData.word);
|
||
state.isShowingWord = true;
|
||
}
|
||
|
||
// 中性颜色:灰色,淡化显示
|
||
Color neutralColor = new Color(0.4f, 0.5f, 0.4f, 1f);
|
||
state.targetAlpha = 0.3f;
|
||
state.targetColor = neutralColor;
|
||
state.targetScale = 1f;
|
||
|
||
// 平滑过渡
|
||
SmoothTransition(state, particle, visualTransitionDuration);
|
||
|
||
// 应用样式
|
||
if (particle.textMesh != null)
|
||
{
|
||
particle.textMesh.fontStyle = FontStyles.Normal;
|
||
}
|
||
|
||
// 清除光晕
|
||
SetParticleGlow(particle, Color.clear, 0f);
|
||
|
||
// 停止抖动
|
||
if (state.jitterTween != null)
|
||
{
|
||
state.jitterTween.Kill();
|
||
state.jitterTween = null;
|
||
}
|
||
particle.transform.position = state.basePosition;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 平滑过渡到目标视觉状态
|
||
/// </summary>
|
||
private void SmoothTransition(ParticleVisualState state, TextParticle particle, float duration)
|
||
{
|
||
// Alpha 过渡
|
||
if (Mathf.Abs(state.currentAlpha - state.targetAlpha) > 0.01f)
|
||
{
|
||
if (state.alphaTween != null && state.alphaTween.IsActive())
|
||
{
|
||
state.alphaTween.Kill();
|
||
}
|
||
state.alphaTween = DOTween.To(() => state.currentAlpha, x => {
|
||
state.currentAlpha = x;
|
||
particle.alpha = x;
|
||
}, state.targetAlpha, duration).SetEase(Ease.OutQuad);
|
||
}
|
||
|
||
// Color 过渡
|
||
if (ColorDistance(state.currentColor, state.targetColor) > 0.01f)
|
||
{
|
||
if (state.colorTween != null && state.colorTween.IsActive())
|
||
{
|
||
state.colorTween.Kill();
|
||
}
|
||
state.colorTween = DOTween.To(() => state.currentColor, x => {
|
||
state.currentColor = x;
|
||
if (particle.textMesh != null)
|
||
{
|
||
Color finalColor = x;
|
||
finalColor.a = state.currentAlpha; // 保持alpha同步
|
||
particle.textMesh.color = finalColor;
|
||
}
|
||
}, state.targetColor, duration).SetEase(Ease.OutQuad);
|
||
}
|
||
|
||
// Scale 过渡(通过transform.localScale)
|
||
if (Mathf.Abs(state.currentScale - state.targetScale) > 0.01f)
|
||
{
|
||
if (state.scaleTween != null && state.scaleTween.IsActive())
|
||
{
|
||
state.scaleTween.Kill();
|
||
}
|
||
Vector3 targetScale = Vector3.one * state.targetScale;
|
||
state.scaleTween = particle.transform.DOScale(targetScale, duration).SetEase(Ease.OutQuad);
|
||
state.currentScale = state.targetScale;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算两个颜色的距离(用于判断是否需要过渡)
|
||
/// </summary>
|
||
private float ColorDistance(Color a, Color b)
|
||
{
|
||
return Mathf.Abs(a.r - b.r) + Mathf.Abs(a.g - b.g) + Mathf.Abs(a.b - b.b);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置粒子光晕效果(平滑过渡)
|
||
/// </summary>
|
||
private void SetParticleGlow(TextParticle particle, Color color, float alpha)
|
||
{
|
||
if (particle.glowSprite != null)
|
||
{
|
||
Color glowColor = color;
|
||
glowColor.a = alpha;
|
||
// 使用DOTween平滑过渡光晕
|
||
particle.glowSprite.DOColor(glowColor, visualTransitionDuration).SetEase(Ease.OutQuad);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新信号强度显示
|
||
/// </summary>
|
||
private void UpdateSignalStrengthDisplay(float maxSignal)
|
||
{
|
||
if (signalStrengthText != null)
|
||
{
|
||
signalStrengthText.text = $"信号强度: {Mathf.RoundToInt(maxSignal)}%";
|
||
}
|
||
}
|
||
|
||
private void OnVerifyClicked()
|
||
{
|
||
if (!isFilteringActive || currentLevelData == null) return;
|
||
|
||
// 检查解码器位置是否正确
|
||
bool isPositionCorrect = (currentDecoderGridPos == currentLevelData.correctDecoderPosition);
|
||
|
||
// 检查频率是否正确(匹配答案情绪)
|
||
EmotionConfig answerEmotion = GetEmotionConfig(currentLevelData.answerEmotion);
|
||
float frequencyDistance = Mathf.Abs(currentFrequency - answerEmotion.frequency);
|
||
bool isFrequencyCorrect = frequencyDistance <= currentLevelData.frequencyTolerance;
|
||
|
||
// 验证答案
|
||
bool isCorrect = isPositionCorrect && isFrequencyCorrect;
|
||
|
||
if (isCorrect)
|
||
{
|
||
Debug.Log($"[AnalysisModeManager] 验证成功!位置: {currentDecoderGridPos}, 频率: {currentFrequency:F1} Hz");
|
||
|
||
// 显示成功反馈
|
||
ShowDecoderFeedback(true);
|
||
|
||
// 延迟后触发完成回调
|
||
StartCoroutine(DelayedComplete(true));
|
||
}
|
||
else
|
||
{
|
||
Debug.Log($"[AnalysisModeManager] 验证失败。位置正确: {isPositionCorrect}, 频率正确: {isFrequencyCorrect}");
|
||
|
||
// 显示失败反馈
|
||
ShowDecoderFeedback(false);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示解码器验证反馈
|
||
/// </summary>
|
||
private void ShowDecoderFeedback(bool isCorrect)
|
||
{
|
||
if (decoderGridDrawerObj == null) return;
|
||
|
||
// 改变解码器边框颜色
|
||
Color feedbackColor = isCorrect ? Color.green : Color.red;
|
||
Color originalColor = new Color(0f, 1f, 1f, 0.8f); // Cyan 原始颜色
|
||
|
||
// 找到所有网格线并改变颜色
|
||
Transform linesContainer = decoderGridDrawerObj.transform.Find("Lines");
|
||
if (linesContainer != null)
|
||
{
|
||
Line[] lines = linesContainer.GetComponentsInChildren<Line>();
|
||
foreach (Line line in lines)
|
||
{
|
||
Color startColor = line.Color;
|
||
|
||
// 使用DOTween.To来动画化颜色变化
|
||
DOTween.To(() => startColor, x => line.Color = x, feedbackColor, 0.3f)
|
||
.SetEase(Ease.OutQuad);
|
||
|
||
if (!isCorrect)
|
||
{
|
||
// 失败时闪烁后恢复
|
||
DOTween.To(() => feedbackColor, x => line.Color = x, originalColor, 0.3f)
|
||
.SetDelay(0.6f)
|
||
.SetEase(Ease.OutQuad);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 延迟完成(用于播放动画)
|
||
/// </summary>
|
||
private System.Collections.IEnumerator DelayedComplete(bool success)
|
||
{
|
||
yield return new WaitForSeconds(1f);
|
||
OnAnalysisComplete(success);
|
||
|
||
// 关闭分析模式
|
||
if (analysisPanel != null)
|
||
{
|
||
analysisPanel.gameObject.SetActive(false);
|
||
}
|
||
}
|
||
|
||
private void OnAnalysisComplete(bool success)
|
||
{
|
||
Debug.Log($"[AnalysisModeManager] OnAnalysisComplete 被调用,结果: {(success ? "成功" : "失败")}");
|
||
Debug.Log($"[AnalysisModeManager] completionNodeName: {(string.IsNullOrEmpty(completionNodeName) ? "空" : completionNodeName)}");
|
||
Debug.Log($"[AnalysisModeManager] currentLevelData: {(currentLevelData == null ? "null" : currentLevelData.levelName)}");
|
||
|
||
// 通过 Yarn 对话系统反馈结果
|
||
if (success)
|
||
{
|
||
// 成功节点:优先使用 completionNodeName,其次使用配置中的节点
|
||
string successNode = !string.IsNullOrEmpty(completionNodeName)
|
||
? completionNodeName
|
||
: (currentLevelData != null && !string.IsNullOrEmpty(currentLevelData.successNodeName))
|
||
? currentLevelData.successNodeName
|
||
: "";
|
||
|
||
Debug.Log($"[AnalysisModeManager] 成功节点选择: {(string.IsNullOrEmpty(successNode) ? "未找到节点" : successNode)}");
|
||
|
||
if (!string.IsNullOrEmpty(successNode))
|
||
{
|
||
Debug.Log($"[AnalysisModeManager] 准备启动对话节点: {successNode}");
|
||
if (DialogController.Instance == null)
|
||
{
|
||
Debug.LogError("[AnalysisModeManager] DialogController.Instance 为 null,无法启动对话节点!");
|
||
}
|
||
else
|
||
{
|
||
DialogController.Instance.StartDialogNode(successNode);
|
||
Debug.Log($"[AnalysisModeManager] 已调用 StartDialogNode: {successNode}");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("[AnalysisModeManager] 成功时未找到任何对话节点!");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 失败节点
|
||
string failNode = !string.IsNullOrEmpty(completionNodeName)
|
||
? completionNodeName + "_Fail"
|
||
: (currentLevelData != null && !string.IsNullOrEmpty(currentLevelData.failNodeName))
|
||
? currentLevelData.failNodeName
|
||
: "";
|
||
|
||
Debug.Log($"[AnalysisModeManager] 失败节点选择: {(string.IsNullOrEmpty(failNode) ? "未找到节点" : failNode)}");
|
||
|
||
if (!string.IsNullOrEmpty(failNode))
|
||
{
|
||
Debug.Log($"[AnalysisModeManager] 准备启动对话节点: {failNode}");
|
||
if (DialogController.Instance == null)
|
||
{
|
||
Debug.LogError("[AnalysisModeManager] DialogController.Instance 为 null,无法启动对话节点!");
|
||
}
|
||
else
|
||
{
|
||
DialogController.Instance.StartDialogNode(failNode);
|
||
Debug.Log($"[AnalysisModeManager] 已调用 StartDialogNode: {failNode}");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("[AnalysisModeManager] 失败时未找到任何对话节点!");
|
||
}
|
||
}
|
||
}
|
||
|
||
private EmotionConfig GetEmotionConfig(EmotionType type)
|
||
{
|
||
switch (type)
|
||
{
|
||
case EmotionType.Joy: return EmotionConfig.Joy;
|
||
case EmotionType.Sadness: return EmotionConfig.Sadness;
|
||
case EmotionType.Anger: return EmotionConfig.Anger;
|
||
case EmotionType.Fear: return EmotionConfig.Fear;
|
||
default: return EmotionConfig.Neutral;
|
||
}
|
||
}
|
||
|
||
|
||
}
|
||
}
|
||
|