This commit is contained in:
2025-12-09 21:42:49 +08:00
parent fa8f6895f2
commit ec1eeb2f31
69 changed files with 8395 additions and 10129 deletions
File diff suppressed because it is too large Load Diff
@@ -886,6 +886,14 @@ MonoBehaviour:
m_Localized: "\u60C5\u7EEA\u63A2\u7D22\u5B8C\u6210\u4E86"
m_Metadata:
m_Items: []
- m_Id: 76189012560347136
m_Localized: "\u8FDB\u5165\u60C5\u7EEA\u5339\u914D"
m_Metadata:
m_Items: []
- m_Id: 76189012598095872
m_Localized: "SpeakerModule: \u53D1\u73B0\u8868\u8FBE\u7D0A\u4E71\uFF0C\u5DF2\u8FDB\u884C\u8FC7\u6DF1\u5165\u68C0\u67E5"
m_Metadata:
m_Items: []
references:
version: 2
RefIds: []
@@ -18,7 +18,7 @@ position: 205,199
//【流程相关】
//gameStage
<<declare $gameStage=0>>
<<declare $gameStage=100>>
//每个模块的首次检查
<<declare $emoChecked=false>>
@@ -82,9 +82,8 @@ colorID: 1
//每次进入维修界面就自动保存
<<switch_fix_system_to "Clinic">>
<<fade_out 1>>
<<switch_fix_system_to "LanguageAnalysis">>
<<memory_decryptor_start TestSuccess>>
<<switch_fix_system_to "Expression">>
<<start_analysis_mode anxietyPhrasesStr completionNode>>
===
File diff suppressed because it is too large Load Diff
@@ -62,7 +62,6 @@ namespace AibisDream.FixSystem
FixState.MemoryCut => new MemoryCutState(),
FixState.LogicCut => new LogicCutState(),
FixState.Dream => new DreamState(),
FixState.LanguageAnalysis => new LanguageAnalysisState(),
_ => new DefaultState()
};
@@ -88,7 +87,6 @@ namespace AibisDream.FixSystem
MemoryCut,
LogicCut,
Dream,
LanguageAnalysis,
}
public struct DefaultState : IState<FixState>
@@ -700,61 +698,4 @@ namespace AibisDream.FixSystem
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.1f);
}
}
public struct LanguageAnalysisState : IState<FixState>
{
public FixState GetState => FixState.LanguageAnalysis;
public void SetArgs(string args)
{
// 无
}
public IEnumerator Enter()
{
// 获取语言分析系统
var languageAnalysisManager = FixSystemCenter.SystemDic.Get<AibisDream.MiniGame.Language.LanguageAnalysisManager>();
if (languageAnalysisManager == null)
{
Debug.LogError("[LanguageAnalysisState] LanguageAnalysisManager 未找到!");
yield break;
}
// 先切 BodyModule(如果需要)
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
var bodyModule = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
bodyModule.SwitchModuleGroup(ModuleState.Head);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
// 打开语言分析系统视图
languageAnalysisManager.OpenView();
// 切换到语言分析相机
yield return CameraKit.Instance.SwitchCamera(CameraEnum.LanguageAnalysis);
}
public IEnumerator Exit()
{
var languageAnalysisManager = FixSystemCenter.SystemDic.Get<AibisDream.MiniGame.Language.LanguageAnalysisManager>();
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
if (languageAnalysisManager != null)
{
// 关闭语言分析系统视图
languageAnalysisManager.CloseView();
}
// 重置身体模块
if (bodyModuleSystem != null)
{
bodyModuleSystem.ResetPlug();
}
yield break;
}
}
}
@@ -421,8 +421,6 @@ namespace AibisDream.Framework
Subway,
SubwayTV,
Language,
LanguageDeep,
LanguageAnalysis,
LanguageAnalysisDeep
LanguageDeep
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: c9eae56ae4642284cac2cde2616de065
guid: 7571f6e9eebca684bb51fccdba884f49
folderAsset: yes
DefaultImporter:
externalObjects: {}
@@ -0,0 +1,152 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 分析模式解码器条纹组件(可拖拽)
/// </summary>
public class AnalysisDecoderStrip : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
[Header("组件")]
[SerializeField] private RectTransform rectTransform;
[SerializeField] private CanvasGroup canvasGroup;
[SerializeField] private Image backgroundImage;
[Header("拖拽配置")]
[SerializeField] private bool isDraggable = true;
[SerializeField] private float dragAlpha = 0.7f;
[Header("样式配置")]
[SerializeField] private Color stripColor = new Color(0f, 0.12f, 0.12f, 0.95f);
[SerializeField] private Color borderColor = Color.cyan;
[SerializeField] private float borderWidth = 2f;
private Vector2 dragOffset;
private RectTransform parentRectTransform;
private float originalAlpha = 1f;
private void Awake()
{
if (rectTransform == null)
{
rectTransform = GetComponent<RectTransform>();
}
if (canvasGroup == null)
{
canvasGroup = GetComponent<CanvasGroup>();
if (canvasGroup == null)
{
canvasGroup = gameObject.AddComponent<CanvasGroup>();
}
}
if (backgroundImage == null)
{
backgroundImage = GetComponent<Image>();
}
// 设置样式
if (backgroundImage != null)
{
backgroundImage.color = stripColor;
}
// 添加边框(使用 Outline 组件)
Outline outline = GetComponent<Outline>();
if (outline == null)
{
outline = gameObject.AddComponent<Outline>();
}
outline.effectColor = borderColor;
outline.effectDistance = new Vector2(borderWidth, borderWidth);
originalAlpha = canvasGroup.alpha;
// 获取父容器
if (transform.parent != null)
{
parentRectTransform = transform.parent.GetComponent<RectTransform>();
}
}
public void OnBeginDrag(PointerEventData eventData)
{
if (!isDraggable) return;
// 计算拖拽偏移
RectTransformUtility.ScreenPointToLocalPointInRectangle(
parentRectTransform ?? rectTransform,
eventData.position,
eventData.pressEventCamera,
out Vector2 localPoint);
dragOffset = rectTransform.anchoredPosition - localPoint;
// 降低透明度
if (canvasGroup != null)
{
canvasGroup.alpha = dragAlpha;
}
}
public void OnDrag(PointerEventData eventData)
{
if (!isDraggable) return;
// 更新位置
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(
parentRectTransform ?? rectTransform,
eventData.position,
eventData.pressEventCamera,
out Vector2 localPoint))
{
rectTransform.anchoredPosition = localPoint + dragOffset;
}
}
public void OnEndDrag(PointerEventData eventData)
{
if (!isDraggable) return;
// 恢复透明度
if (canvasGroup != null)
{
canvasGroup.alpha = originalAlpha;
}
}
/// <summary>
/// 设置是否可拖拽
/// </summary>
public void SetDraggable(bool draggable)
{
isDraggable = draggable;
}
/// <summary>
/// 获取当前位置(网格坐标)
/// </summary>
public Vector2Int GetGridPosition(float cellSize, float cellGap)
{
float unit = cellSize + cellGap;
int col = Mathf.RoundToInt(rectTransform.anchoredPosition.x / unit);
int row = Mathf.RoundToInt(-rectTransform.anchoredPosition.y / unit);
return new Vector2Int(row, col);
}
/// <summary>
/// 设置网格位置
/// </summary>
public void SetGridPosition(int row, int col, float cellSize, float cellGap)
{
float unit = cellSize + cellGap;
float xPos = col * unit;
float yPos = -row * unit;
rectTransform.anchoredPosition = new Vector2(xPos, yPos);
}
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 04fb15b2239937b4baf290bc05f3e6cb
guid: 7327fe55f8fdb7548a691577c5259cd5
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -0,0 +1,125 @@
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 分析模式网格单元格组件
/// </summary>
[RequireComponent(typeof(RectTransform))]
public class AnalysisGridCell : MonoBehaviour
{
[Header("UI 组件")]
[SerializeField] private Image backgroundImage;
[SerializeField] private TMP_Text contentText;
[Header("样式配置")]
[SerializeField] private Color normalBackgroundColor = new Color(0f, 0.04f, 0f, 0.2f);
[SerializeField] private Color highlightBackgroundColor = new Color(0f, 0f, 0f, 0.8f);
private void Awake()
{
// 自动查找组件
if (backgroundImage == null)
{
backgroundImage = GetComponent<Image>();
}
if (contentText == null)
{
contentText = GetComponentInChildren<TMP_Text>();
}
// 设置默认样式
if (backgroundImage != null)
{
backgroundImage.color = normalBackgroundColor;
}
if (contentText != null)
{
contentText.color = Color.white;
contentText.fontSize = 13f;
contentText.alignment = TextAlignmentOptions.Center;
contentText.enableWordWrapping = false;
contentText.overflowMode = TextOverflowModes.Overflow;
}
}
/// <summary>
/// 设置单元格文本
/// </summary>
public void SetText(string text)
{
if (contentText != null)
{
contentText.text = text;
}
}
/// <summary>
/// 设置文本颜色
/// </summary>
public void SetTextColor(Color color)
{
if (contentText != null)
{
contentText.color = color;
}
}
/// <summary>
/// 设置背景颜色
/// </summary>
public void SetBackgroundColor(Color color)
{
if (backgroundImage != null)
{
backgroundImage.color = color;
}
}
/// <summary>
/// 设置字体样式
/// </summary>
public void SetFontStyle(FontStyles style)
{
if (contentText != null)
{
contentText.fontStyle = style;
}
}
/// <summary>
/// 设置透明度
/// </summary>
public void SetAlpha(float alpha)
{
if (contentText != null)
{
Color color = contentText.color;
color.a = alpha;
contentText.color = color;
}
}
/// <summary>
/// 重置为默认样式
/// </summary>
public void ResetStyle()
{
if (backgroundImage != null)
{
backgroundImage.color = normalBackgroundColor;
}
if (contentText != null)
{
contentText.color = Color.white;
contentText.fontStyle = FontStyles.Normal;
}
}
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 8d25aa7540a2769478e93a4affce5d99
guid: 9547756a801380143846ebc10ce8274e
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -0,0 +1,932 @@
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections.Generic;
using System.Linq;
using DG.Tweening;
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 string cover; // 笑话掩盖词
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; // 真实词语
public string cover; // 笑话掩盖词
}
/// <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;
// [SerializeField] private TMP_Text logText;
// [SerializeField] private ScrollRect logScrollRect;
[Header("网格配置")]
[SerializeField] private RectTransform gridContainer; // 可选,用于定位网格中心
// [SerializeField] private GameObject gridCellPrefab; // 不需要了,直接使用粒子
[SerializeField] private int gridWidth = 20;
[SerializeField] private int gridHeight = 20;
[SerializeField] private float cellSize = 0.4f; // Unity 单位(与 LanguageParticleManager 匹配)
[SerializeField] private float cellGap = 0.05f; // Unity 单位
[SerializeField] private float cellFontSize = 2.5f; // 文字大小(与 LanguageParticleManager 匹配)
[SerializeField] private Vector3 gridCenterOffset = Vector3.zero; // 网格中心偏移量
[Header("解码器条纹")]
[SerializeField] private RectTransform decoderStrip;
// [SerializeField] private GameObject stripHolePrefab; // 暂时不用,条纹作为独立 prefab
[SerializeField] private int stripWidth = 5; // 条纹宽度(列数)
[SerializeField] private int stripHeight = 3; // 条纹高度(行数)
[Header("字体配置")]
[SerializeField] private TMP_FontAsset chineseFontAsset; // 中文字体资源
[Header("动画配置")]
[SerializeField] private float panelSlideDuration = 0.5f;
[SerializeField] private float dataLoadDuration = 1.5f;
[SerializeField] private Ease slideEase = Ease.OutCubic;
[Header("焦虑短句配置")]
[SerializeField] private List<string> anxietyPhrases = new List<string>
{
"临检要来了大家都很紧张只有销售员说终于可以检查业绩了",
"不合格英里被骂了因为销售业绩太差了",
"我担心业绩会下滑结果真的下滑了我的担心是对的",
"销售员总是混淆客户和老板因为都要讨好",
"一个销售员走进酒吧问两张桌子要不要买保险"
};
[Header("目标配置")]
[SerializeField] private string targetSentence = "[临检]要来了,不合格[英里]被骂了,这让我很[担心]";
// 解析目标:只有一个正确答案
private List<AnalysisTarget> analysisTargets = new List<AnalysisTarget>();
// 状态
private bool isAnalysisModeActive = false;
private bool isFilteringActive = false;
private float currentFrequency = 50f;
private GridCell[,] gridData;
private Dictionary<Vector2Int, TextParticle> gridParticles = new Dictionary<Vector2Int, TextParticle>(); // 使用粒子代替 UI
// private List<Vector2Int> stripHolePositions = new List<Vector2Int>(); // 暂时不用,条纹作为独立 prefab 配置
private bool isInitialized = false;
private string completionNodeName = "";
private void Awake()
{
SetupUI();
SetupTargets();
}
private void SetupUI()
{
if (analysisPanel != null)
{
analysisPanel.gameObject.SetActive(false);
}
if (initializeButton != null)
{
initializeButton.onClick.AddListener(OnInitializeClicked);
// 初始化按钮现在作为"重置网格"使用,网格会自动创建
}
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;
}
if (decoderStrip != null)
{
decoderStrip.gameObject.SetActive(false);
// SetupDecoderStripDragging(); // 暂时注释,条纹作为独立 prefab 配置
}
}
private void SetupTargets()
{
// 清空目标列表
analysisTargets.Clear();
// 创建正确答案目标
var correctTarget = new AnalysisTarget
{
name = "Fear Solution 1",
emotion = EmotionType.Fear,
stripPosition = new Vector2Int(3, 5), // 条纹正确位置
isCorrectAnswer = true
};
// 添加目标词:临检、英里、担心(相对于条纹的孔洞位置)
correctTarget.words.Add(new TargetWord
{
position = new Vector2Int(3, 5), // 绝对位置 = 条纹位置 + 孔洞相对位置
word = "临检",
cover = "检查"
});
correctTarget.words.Add(new TargetWord
{
position = new Vector2Int(4, 6),
word = "英里",
cover = "背后"
});
correctTarget.words.Add(new TargetWord
{
position = new Vector2Int(5, 9),
word = "担心",
cover = "对的"
});
analysisTargets.Add(correctTarget);
// 可以添加更多诱饵目标(与正确答案接近但不完全匹配)
// 这里暂时只有一个正确答案
}
/// <summary>
/// 启动分析模式(由 ExpressionManager 调用)
/// </summary>
public void StartAnalysisMode(List<string> phrases = null, string completionNode = null)
{
if (isAnalysisModeActive) return;
// 更新配置
if (phrases != null && phrases.Count > 0)
{
anxietyPhrases = phrases;
}
completionNodeName = completionNode ?? "";
// 显示分析控制面板
isAnalysisModeActive = true;
if (analysisPanel != null)
{
analysisPanel.gameObject.SetActive(true);
}
// 自动初始化网格(不需要等待用户点击 Initialize)
if (!isInitialized)
{
InitializeGrid();
}
// Log("> 分析模式已启动");
// Log("> 点击 [初始化] 导入数据...");
}
/// <summary>
/// 关闭分析模式
/// </summary>
public void CloseAnalysisMode()
{
if (!isAnalysisModeActive) return;
// 滑出 UI 面板
if (analysisPanel != null)
{
Vector2 endPos = new Vector2(Screen.width, 0);
analysisPanel.DOAnchorPos(endPos, panelSlideDuration)
.SetEase(slideEase)
.OnComplete(() =>
{
analysisPanel.gameObject.SetActive(false);
isAnalysisModeActive = false;
});
}
else
{
isAnalysisModeActive = false;
}
// 重置状态
isFilteringActive = false;
isInitialized = false;
currentFrequency = 50f;
ClearGrid();
if (decoderStrip != null)
{
decoderStrip.gameObject.SetActive(false);
}
}
private void OnInitializeClicked()
{
// 现在网格会在 StartAnalysisMode 时自动初始化
// 这个按钮可以用来重新初始化网格(重置)
if (isInitialized)
{
// 重新初始化(重置网格)
isInitialized = false;
ClearGrid();
}
// Log("> 开始导入焦虑词语数据...");
InitializeGrid();
}
private void InitializeGrid()
{
// 创建网格数据
gridData = new GridCell[gridHeight, gridWidth];
// 将焦虑短句分割成2字符片段
List<string> allFragments = new List<string>();
foreach (var phrase in anxietyPhrases)
{
var fragments = SplitIntoTwoCharChunks(phrase);
allFragments.AddRange(fragments);
}
// 如果片段不够填满网格,重复使用
int totalCells = gridWidth * gridHeight;
while (allFragments.Count < totalCells)
{
allFragments.AddRange(allFragments.Take(totalCells - allFragments.Count));
}
// 填充网格数据
int fragmentIndex = 0;
for (int row = 0; row < gridHeight; row++)
{
for (int col = 0; col < gridWidth; col++)
{
GridCell cell = new GridCell
{
row = row,
col = col,
word = allFragments[fragmentIndex % allFragments.Count],
cover = allFragments[fragmentIndex % allFragments.Count],
emotion = EmotionType.Neutral,
isTarget = false
};
// 检查是否是目标位置
foreach (var target in analysisTargets)
{
foreach (var targetWord in target.words)
{
if (targetWord.position.x == row && targetWord.position.y == col)
{
cell.word = targetWord.word;
cell.cover = targetWord.cover;
cell.emotion = target.emotion;
cell.isTarget = true;
break;
}
}
if (cell.isTarget) break;
}
// 为非目标单元格随机分配一些情绪(作为诱饵)
if (!cell.isTarget && Random.value > 0.88f)
{
EmotionType[] emotions = { EmotionType.Fear, EmotionType.Sadness, EmotionType.Joy, EmotionType.Anger };
cell.emotion = emotions[Random.Range(0, emotions.Length)];
}
gridData[row, col] = cell;
fragmentIndex++;
}
}
// 创建网格 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;
// 计算起始位置(左上角)
Vector3 gridStartPos = gridCenter - new Vector3(totalWidth / 2f, totalHeight / 2f, 0f);
// 随机顺序创建单元格(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);
// Log("> 数据导入完成");
// Log($"> 共 {gridWidth}x{gridHeight} 个数据节点");
// Log("> 点击 [启动过滤分析] 开始...");
isInitialized = true;
if (startFilterButton != null)
{
startFilterButton.interactable = true;
}
}
/// <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 = false;
particle.velocity = Vector2.zero;
// 重要:设置透明度,否则粒子不可见!
particle.alpha = 1f;
particle.baseAlpha = 1f;
// 设置文字内容
if (gridData != null)
{
GridCell cellData = gridData[row, col];
particle.ForceSetCharacter(cellData.cover); // 初始显示掩盖词
// 设置字体大小和颜色
if (particle.textMesh != null)
{
particle.textMesh.fontSize = cellFontSize;
particle.textMesh.color = Color.white;
}
}
gridParticles[new Vector2Int(row, col)] = particle;
}
private void ClearGrid()
{
foreach (var particle in gridParticles.Values)
{
if (particle != null)
{
Destroy(particle.gameObject);
}
}
gridParticles.Clear();
}
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;
// Log("> 过滤分析已启动");
// Log("> 解码器条纹已加载");
// Log("> 调整情绪频率以隔离信号模式...");
// 显示解码器条纹(暂时注释,作为独立 prefab 后续配置)
// if (decoderStrip != null)
// {
// decoderStrip.gameObject.SetActive(true);
//
// // 初始位置设置到正确答案位置(玩家可以拖动)
// var correctTarget = analysisTargets.FirstOrDefault(t => t.isCorrectAnswer);
// if (correctTarget != null)
// {
// PositionDecoderStrip(correctTarget.stripPosition.x, correctTarget.stripPosition.y);
// }
//
// // 淡入动画
// CanvasGroup canvasGroup = decoderStrip.GetComponent<CanvasGroup>();
// if (canvasGroup == null)
// {
// canvasGroup = decoderStrip.gameObject.AddComponent<CanvasGroup>();
// }
// canvasGroup.alpha = 0f;
// canvasGroup.DOFade(1f, 0.8f);
// }
// 启用滑动条和验证按钮
if (emotionSlider != null)
{
emotionSlider.interactable = true;
}
if (verifyButton != null)
{
verifyButton.interactable = true;
}
if (startFilterButton != null)
{
startFilterButton.interactable = false;
}
// 应用初始过滤效果
UpdateGridVisualsWithFilter();
}
private void PositionDecoderStrip(int row, int col)
{
if (decoderStrip == null || gridContainer == null) return;
// 使用解码器组件设置位置
AnalysisDecoderStrip stripComponent = decoderStrip.GetComponent<AnalysisDecoderStrip>();
if (stripComponent != null)
{
stripComponent.SetGridPosition(row, col, cellSize, cellGap);
}
else
{
// 备用设置方式
float xPos = col * (cellSize + cellGap);
float yPos = -row * (cellSize + cellGap);
decoderStrip.anchoredPosition = new Vector2(xPos, yPos);
}
// 设置条纹大小
float stripWidth = this.stripWidth * cellSize + (this.stripWidth - 1) * cellGap;
float stripHeight = this.stripHeight * cellSize + (this.stripHeight - 1) * cellGap;
decoderStrip.sizeDelta = new Vector2(stripWidth, stripHeight);
}
// 解码器条纹拖拽逻辑 - 暂时注释,条纹作为独立 prefab 配置
// private void SetupDecoderStripDragging()
// {
// if (decoderStrip == null) return;
//
// // 添加拖拽组件
// var draggable = decoderStrip.gameObject.AddComponent<UnityEngine.EventSystems.EventTrigger>();
//
// // 这里需要实现拖拽逻辑,但由于 EventTrigger 比较复杂,
// // 暂时先让条纹固定在初始位置,玩家可以通过调整频率来验证
// }
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 UpdateGridVisualsWithFilter()
{
if (gridData == null) return;
float maxSignal = 0f;
for (int row = 0; row < gridHeight; row++)
{
for (int col = 0; col < gridWidth; col++)
{
GridCell cellData = gridData[row, col];
Vector2Int pos = new Vector2Int(row, col);
if (!gridParticles.ContainsKey(pos)) continue;
TextParticle particle = gridParticles[pos];
if (particle == null || particle.textMesh == null) continue;
// 计算与当前频率的距离
EmotionConfig emotion = GetEmotionConfig(cellData.emotion);
float distance = 100f;
if (emotion.type != EmotionType.Neutral)
{
distance = Mathf.Abs(currentFrequency - emotion.frequency);
}
// 根据距离更新显示
if (cellData.emotion != EmotionType.Neutral)
{
bool isTargetWord = cellData.word != cellData.cover;
if (distance < 5f)
{
// 锁定:显示真实词语,高亮显示
particle.ForceSetCharacter(cellData.word);
particle.textMesh.color = emotion.color;
particle.textMesh.fontStyle = FontStyles.Bold;
particle.alpha = 1f;
// 调整光晕效果
if (particle.glowSprite != null)
{
Color glowColor = emotion.color;
glowColor.a = 0.5f;
particle.glowSprite.color = glowColor;
}
maxSignal = Mathf.Max(maxSignal, 100f - distance * 20f);
}
else if (distance < 20f)
{
// 闪烁区:在真实词和掩盖词之间闪烁
if (isTargetWord)
{
float glitchChance = 1f - (distance / 20f);
if (Random.value < glitchChance)
{
particle.ForceSetCharacter(cellData.word);
particle.textMesh.color = emotion.color;
}
else
{
particle.ForceSetCharacter(cellData.cover);
particle.textMesh.color = Color.white;
}
}
else
{
particle.ForceSetCharacter(cellData.word);
particle.textMesh.color = emotion.color;
}
particle.textMesh.fontStyle = FontStyles.Normal;
particle.alpha = 0.9f;
if (particle.glowSprite != null)
{
Color glowColor = emotion.color;
glowColor.a = 0.3f;
particle.glowSprite.color = glowColor;
}
}
else if (distance < 35f)
{
// 淡出区:显示掩盖词,逐渐淡化
particle.ForceSetCharacter(isTargetWord ? cellData.cover : cellData.word);
float alpha = 0.6f - ((distance - 20f) / 30f);
Color color = Color.white;
color.a = alpha;
particle.textMesh.color = color;
particle.textMesh.fontStyle = FontStyles.Normal;
particle.alpha = alpha;
if (particle.glowSprite != null)
{
particle.glowSprite.color = new Color(1f, 1f, 1f, 0f);
}
}
else
{
// 消失区:几乎不可见
particle.ForceSetCharacter(isTargetWord ? cellData.cover : cellData.word);
Color color = Color.white;
color.a = 0.05f;
particle.textMesh.color = color;
particle.textMesh.fontStyle = FontStyles.Normal;
particle.alpha = 0.05f;
if (particle.glowSprite != null)
{
particle.glowSprite.color = new Color(0f, 0f, 0f, 0f);
}
}
}
else
{
// 中性/噪音:始终淡化显示
particle.ForceSetCharacter(cellData.word);
Color color = new Color(0.27f, 0.33f, 0.27f, 0.3f);
particle.textMesh.color = color;
particle.textMesh.fontStyle = FontStyles.Normal;
particle.alpha = 0.3f;
if (particle.glowSprite != null)
{
particle.glowSprite.color = new Color(0f, 0f, 0f, 0f);
}
}
}
}
// 更新信号强度显示
if (signalStrengthText != null)
{
signalStrengthText.text = $"信号强度: {Mathf.RoundToInt(maxSignal)}%";
}
}
private void OnVerifyClicked()
{
if (!isFilteringActive) return;
// Log($"> 正在扫描扇区...");
// Log($"> 当前频率: {currentFrequency:F1} Hz");
// 获取解码器条纹当前位置
Vector2Int stripPos = Vector2Int.zero;
if (decoderStrip != null)
{
// 使用解码器组件获取位置
AnalysisDecoderStrip stripComponent = decoderStrip.GetComponent<AnalysisDecoderStrip>();
if (stripComponent != null)
{
stripPos = stripComponent.GetGridPosition(cellSize, cellGap);
}
else
{
// 备用计算方式
Vector2 anchoredPos = decoderStrip.anchoredPosition;
int col = Mathf.RoundToInt(anchoredPos.x / (cellSize + cellGap));
int row = Mathf.RoundToInt(-anchoredPos.y / (cellSize + cellGap));
stripPos = new Vector2Int(row, col);
}
}
// Log($"> 条纹位置: [{stripPos.x}, {stripPos.y}]");
// 检查是否匹配任何目标
bool foundMatch = false;
AnalysisTarget matchedTarget = null;
foreach (var target in analysisTargets)
{
if (target.stripPosition == stripPos)
{
EmotionConfig emotion = GetEmotionConfig(target.emotion);
float distance = Mathf.Abs(currentFrequency - emotion.frequency);
if (distance < 8f)
{
// 匹配成功
foundMatch = true;
matchedTarget = target;
break;
}
else
{
// Log("> 发现模式但信号微弱,请调整频率");
// Log("> 似乎要说的不是这个...");
return;
}
}
}
if (foundMatch && matchedTarget != null)
{
// 成功匹配
// Log($"> <color={ColorUtility.ToHtmlStringRGB(GetEmotionConfig(matchedTarget.emotion).color)}>解密成功!</color>");
// Log($"> <color={ColorUtility.ToHtmlStringRGB(GetEmotionConfig(matchedTarget.emotion).color)}>完整句子: {targetSentence}</color>");
// Log("> 寻找完成");
// 触发成功回调
OnAnalysisComplete(true);
}
else
{
// 未找到匹配
// Log("> 未检测到数据对齐");
// Log("> 似乎要说的不是这个...");
// 触发失败回调
OnAnalysisComplete(false);
}
}
private void OnAnalysisComplete(bool success)
{
// 通过 Yarn 对话系统反馈结果
if (!string.IsNullOrEmpty(completionNodeName))
{
if (success)
{
// 成功节点
DialogController.Instance?.StartDialogNode(completionNodeName + "_Success");
}
else
{
// 失败节点
DialogController.Instance?.StartDialogNode(completionNodeName + "_Fail");
}
}
}
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;
}
}
// private void Log(string message)
// {
// if (logText != null)
// {
// logText.text += message + "\n";
// // 自动滚动到底部
// if (logScrollRect != null)
// {
// Canvas.ForceUpdateCanvases();
// logScrollRect.verticalNormalizedPosition = 0f;
// }
// }
// Debug.Log($"[AnalysisMode] {message}");
// }
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: a851d4a1c0297384093b20da97237333
guid: 3cf72a773ae1e5442aea05dc7691d4e0
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -0,0 +1,61 @@
title: 分析模式_示例对话
---
角色:你看起来很焦虑...让我帮你理解一下你的想法。
角色:这是一个记忆解密系统,通过调节情绪频率来寻找隐藏的真相。
// 启动分析模式(使用默认配置)
<<start_analysis_mode "" "分析模式_完成">>
-> 等待玩家完成...
===
title: 分析模式_完成_Success
---
角色:做得很好!你成功解密了记忆碎片。
角色:现在你明白了自己真正担心的是什么...
角色:"临检要来了,不合格英里被骂了,这让我很担心"
<<close_analysis_mode>>
-> 继续对话
角色:明白了自己的担心之后,你感觉如何?
主角:我感觉...轻松了一些。
角色:很好。认识到问题是解决问题的第一步。
===
title: 分析模式_完成_Fail
---
角色:似乎还不太对...要不要再试一次?
-> 再试一次
<<start_analysis_mode "" "分析模式_完成">>
-> 稍后再说
角色:没关系,等你准备好了再来找我。
<<close_analysis_mode>>
===
title: 分析模式_自定义短句示例
---
角色:让我们尝试另一个记忆片段...
// 启动分析模式(使用自定义焦虑短句)
<<start_analysis_mode "临检要来了大家都很紧张只有销售员说终于可以检查业绩了|不合格英里被骂了因为销售业绩太差了|我担心业绩会下滑结果真的下滑了我的担心是对的" "分析模式_自定义完成">>
-> 等待玩家完成...
===
title: 分析模式_自定义完成_Success
---
角色:你找到了答案!
<<close_analysis_mode>>
===
title: 分析模式_自定义完成_Fail
---
角色:再试试看...
-> 重试
<<start_analysis_mode "临检要来了大家都很紧张只有销售员说终于可以检查业绩了|不合格英里被骂了因为销售业绩太差了|我担心业绩会下滑结果真的下滑了我的担心是对的" "分析模式_自定义完成">>
-> 放弃
<<close_analysis_mode>>
===
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 5c7828fbb127cf548a64c47f0f7f6daa
guid: 6bd36cc677d72b04496c883b8c21d0ed
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
@@ -0,0 +1,223 @@
# 解码器条纹 Prefab 待实现功能
## 📋 当前状态
解码器条纹相关逻辑已暂时注释,作为独立 prefab 后续配置。
## 🎯 需要实现的功能
### 1. Prefab 结构
解码器条纹应该作为一个独立的 Prefab,包含以下结构:
```
DecoderStripPrefab
├── Background (Image - 半透明背景)
├── Hole 1 (透明区域 - 第一个孔洞)
├── Hole 2 (透明区域 - 第二个孔洞)
├── Hole 3 (透明区域 - 第三个孔洞)
└── Text Elements (固定文字,如:"要来了"、"被骂了"等)
```
### 2. 孔洞配置数据
参考 web 原型的 `DECODER_DATA`
```javascript
const DECODER_DATA = [
{ type: 'hole', r: 0, c: 0 }, // 孔洞 1: 显示"临检"
{ type: 'text', r: 0, c: 1, text: "要" },
{ type: 'text', r: 0, c: 2, text: "来了" },
{ type: 'text', r: 0, c: 3, text: "" },
{ type: 'text', r: 0, c: 4, text: "不合" },
{ type: 'text', r: 1, c: 0, text: "格" },
{ type: 'hole', r: 1, c: 1 }, // 孔洞 2: 显示"英里"
{ type: 'text', r: 1, c: 2, text: "被" },
{ type: 'text', r: 1, c: 3, text: "骂" },
{ type: 'text', r: 1, c: 4, text: "了" },
{ type: 'text', r: 2, c: 0, text: "" },
{ type: 'text', r: 2, c: 1, text: "这" },
{ type: 'text', r: 2, c: 2, text: "让我" },
{ type: 'text', r: 2, c: 3, text: "很" },
{ type: 'hole', r: 2, c: 4 }, // 孔洞 3: 显示"担心"
];
```
### 3. 孔洞生成逻辑
需要创建一个脚本,根据配置数据动态生成孔洞:
```csharp
// 示例伪代码
public class DecoderStripGenerator : MonoBehaviour
{
[System.Serializable]
public class StripElement
{
public enum ElementType { Hole, Text }
public ElementType type;
public int row;
public int col;
public string text; // 如果是文本元素
}
public List<StripElement> stripData;
public float cellSize = 45f;
public float cellGap = 2f;
public void GenerateStrip()
{
foreach (var element in stripData)
{
if (element.type == StripElement.ElementType.Hole)
{
CreateHole(element.row, element.col);
}
else
{
CreateText(element.row, element.col, element.text);
}
}
}
private void CreateHole(int row, int col)
{
// 创建透明区域,让下方网格显示出来
// 可以使用 Mask 或 Canvas Group 实现
}
private void CreateText(int row, int col, string text)
{
// 创建固定文字
// 使用 TMP_Text 组件
}
}
```
### 4. 孔洞渲染方式
有几种实现方式:
#### 方式 A:使用 Mask 组件
- 背景使用 Image 组件
- 孔洞位置使用 Mask 组件让下方内容显示出来
- 优点:简单直观
- 缺点:性能开销较大
#### 方式 B:使用 Canvas Group
- 孔洞位置设置 Canvas Group alpha = 0
- 优点:性能较好
- 缺点:不能真正"透视",只是隐藏
#### 方式 C:使用 Shader(推荐)
- 自定义 Shader 实现孔洞效果
- 在指定区域让背景透明
- 优点:性能最好,效果最灵活
- 缺点:需要编写 Shader
#### 方式 D:使用 Stencil Buffer
- 使用 Stencil 实现遮罩效果
- 孔洞区域不渲染背景
- 优点:性能好,效果好
- 缺点:实现较复杂
### 5. 拖拽功能
解码器条纹需要支持拖拽,已有 `AnalysisDecoderStrip.cs` 组件实现了基础拖拽功能。
需要确保:
- 条纹可以在网格上自由拖动
- 拖动时能实时显示下方网格内容
- 能够精确对齐到网格位置
### 6. 位置对齐
条纹位置需要能够对齐到网格:
```csharp
// 吸附到最近的网格位置
public Vector2Int SnapToGrid(Vector2 worldPos, float cellSize, float cellGap)
{
float unit = cellSize + cellGap;
int col = Mathf.RoundToInt(worldPos.x / unit);
int row = Mathf.RoundToInt(-worldPos.y / unit);
return new Vector2Int(row, col);
}
```
## 🔧 实现步骤建议
### 第一步:创建基础 Prefab(5分钟)
1. 创建 Image 作为背景(半透明青色)
2. 添加 AnalysisDecoderStrip 组件(已实现拖拽)
3. 添加 Canvas Group(用于淡入淡出)
4. 添加 Outline(边框效果)
### 第二步:实现孔洞渲染(30分钟)
1. 选择一种渲染方式(推荐从简单的开始)
2. 创建孔洞生成脚本
3. 根据配置数据生成孔洞
### 第三步:添加固定文字(10分钟)
1. 在条纹上添加固定文字 TMP_Text
2. 根据配置数据定位文字位置
### 第四步:测试和调优(20分钟)
1. 测试孔洞是否正确显示下方网格
2. 测试拖拽功能是否流畅
3. 测试位置对齐是否准确
## 📝 配置示例
```csharp
// 在 Inspector 中配置
stripData = new List<StripElement>
{
new StripElement { type = ElementType.Hole, row = 0, col = 0 },
new StripElement { type = ElementType.Text, row = 0, col = 1, text = "要" },
new StripElement { type = ElementType.Text, row = 0, col = 2, text = "来了" },
// ... 更多元素
};
```
## 🎨 视觉效果参考
参考 web 原型中的解码器条纹:
- 半透明青色背景 (`rgba(0, 30, 30, 0.95)`)
- 青色边框 (`#00FFFF`)
- 孔洞处完全透明,显示下方网格
- 固定文字为青色,带发光效果
## 🚀 快速测试方式
在完全实现孔洞之前,可以先:
1. 使用简单的透明 Image 作为孔洞占位
2. 手动在 Unity 中创建 3 个透明区域
3. 验证拖拽和位置对齐功能
4. 验证验证逻辑是否正确
## ⚠️ 注意事项
1. **层级关系**:条纹要在网格上方,Z 轴稍微靠前
2. **渲染顺序**:确保孔洞区域不会遮挡下方网格
3. **性能考虑**:如果网格很大,要注意孔洞渲染的性能
4. **拖拽响应**:确保拖拽时不会阻塞其他 UI 交互
## 📚 相关文件
- `AnalysisDecoderStrip.cs` - 已实现的拖拽组件
- `AnalysisModeManager.cs` - 主管理器(已注释条纹生成逻辑)
- `WebPrototype/index.html` - Web 原型参考(第 185-268 行)
## ✅ 完成后需要
1. 取消注释 `AnalysisModeManager.cs` 中的相关代码
2. 将 Decoder Strip Prefab 拖入 AnalysisModeManager 的 `Decoder Strip` 字段
3. 测试完整流程:初始化 → 过滤分析 → 拖动条纹 → 验证
---
**当前优先级**:低(可以先验证其他功能)
**预计实现时间**1-2 小时
**难度评级**:⭐⭐⭐(中等)
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 9f4f1ddcfc00ec4459ca2e08b155803b
guid: 02966527fe1e83c478f35015e4856320
TextScriptImporter:
externalObjects: {}
userData:
@@ -0,0 +1,259 @@
# 分析模式快速设置清单
## ✅ 关键要点
1. **共享 Canvas**:分析模式使用 LanguageParticleManager 的 `Language Canvas`WorldSpace Canvas
2. **UI 位置**:所有分析模式 UI 都在 `Language Canvas` 下创建
3. **初始化按钮位置**:放在 `Emotion Control`(情绪控制区)里
4. **游戏流程**:先完成语言粒子游戏 → 启动分析模式 → 呼出控制器 → 初始化 → 过滤分析 → 验证
## 🚀 5分钟快速设置
### 1. 找到现有 Canvas1分钟)
```
Hierarchy:
Expression View
└── Language Particle Manager
└── Language Canvas ← 这个!
```
### 2. 创建分析面板(2分钟)
`Language Canvas` 下:
```
右键 Language Canvas → Create Empty
命名为:Analysis Panel
添加 RectTransform
设置 Active: false(初始隐藏)
```
### 3. 创建控制区(1分钟)
`Analysis Panel` 下:
```
创建 Vertical Layout Group 对象:Control Panel
├── 创建 Vertical Layout Group 对象:Emotion Control
│ ├── Button: Initialize Button
│ ├── TMP_Text: Frequency Text
│ ├── Slider: Emotion Slider
│ └── TMP_Text: Signal Strength Text
├── Button: Start Filter Button
├── Button: Verify Button
└── Scroll View: Log Scroll View
└── TMP_Text: Log Text
```
### 4. 创建网格区(1分钟)
`Analysis Panel` 下:
```
创建 Empty 对象:Grid Container
└── 创建 ImageDecoder Strip
- 添加 Canvas Group
- 添加 Outline
- 添加 AnalysisDecoderStrip
- Active: false(初始隐藏)
```
### 5. 配置管理器(立即完成)
```
创建 GameObjectAnalysis Mode Manager(在 Expression View 下)
添加组件:AnalysisModeManager
拖入引用:
✓ Shared Canvas → Language Canvas
✓ Analysis Panel → Analysis Panel
✓ 所有按钮、文本、滑动条
✓ Grid Container → Grid Container
✓ Decoder Strip → Decoder Strip
✓ Grid Cell Prefab → 你创建的预制体
```
## 📋 完整层级结构
```
Expression View
├── Expression Camera
├── Expression Deep Camera
├── Language Particle Manager (挂 LanguageParticleManager)
│ └── Language Canvas (WorldSpace Canvas)
│ ├── FloatingParticles (运行时创建)
│ ├── CandidateParticles (运行时创建)
│ └── Analysis Panel ← 你要创建的
│ ├── Control Panel
│ │ ├── Emotion Control
│ │ │ ├── Initialize Button
│ │ │ ├── Frequency Text ("50.0 Hz")
│ │ │ ├── Emotion Slider (0-100, 初始50)
│ │ │ └── Signal Strength Text ("信号强度: 0%")
│ │ ├── Start Filter Button (初始禁用)
│ │ ├── Verify Button (初始禁用)
│ │ └── Log Scroll View
│ │ └── Log Text
│ └── Grid Container
│ └── Decoder Strip (初始隐藏)
└── Analysis Mode Manager (挂 AnalysisModeManager)
```
## 🎯 关键配置项
### AnalysisModeManager Inspector 设置
#### Canvas 引用(共享)
- **Shared Canvas**: `Language Canvas`**必须设置!**
#### UI 引用
- Analysis Panel: `Analysis Panel`
- Initialize Button: `Initialize Button`
- Emotion Slider: `Emotion Slider` (Min: 0, Max: 100, Value: 50)
- Frequency Text: `Frequency Text`
- Signal Strength Text: `Signal Strength Text`
- Start Filter Button: `Start Filter Button` (初始 Interactable: false)
- Verify Button: `Verify Button` (初始 Interactable: false)
- Log Text: `Log Text`
- Log Scroll Rect: `Log Scroll View`
#### 网格配置
- Grid Container: `Grid Container`
- Grid Cell Prefab: `GridCellPrefab`**需要创建预制体**
- Grid Width: **20**
- Grid Height: **20**
- Cell Size: **45**
- Cell Gap: **2**
#### 解码器条纹
- Decoder Strip: `Decoder Strip`
- Strip Width: **5**
- Strip Height: **3**
#### 动画配置
- Panel Slide Duration: 0.5
- Slide Ease: OutCubic
#### 焦虑短句(默认,可通过 Yarn 覆盖)
```
临检要来了大家都很紧张只有销售员说终于可以检查业绩了
不合格英里被骂了因为销售业绩太差了
我担心业绩会下滑结果真的下滑了我的担心是对的
销售员总是混淆客户和老板因为都要讨好
一个销售员走进酒吧问两张桌子要不要买保险
```
#### 目标配置
- Target Sentence: `[临检]要来了,不合格[英里]被骂了,这让我很[担心]`
## 🎮 测试流程
### Yarn 测试脚本
```yarn
title: 测试分析模式
---
// 1. 先完成语言粒子游戏(可选)
角色:现在让我们分析一下你的焦虑...
// 2. 启动分析模式
<<start_analysis_mode "" "TestComplete">>
// 等待玩家操作...
===
title: TestComplete_Success
---
角色:太好了!你找到了答案。
<<close_analysis_mode>>
===
title: TestComplete_Fail
---
角色:再试试看...
-> 重试
<<start_analysis_mode "" "TestComplete">>
-> 放弃
<<close_analysis_mode>>
===
```
### Play Mode 测试步骤
1. ✓ 启动游戏,通过 Yarn 触发 `start_analysis_mode`
2. ✓ 检查 Analysis Panel 是否滑出/显示
3. ✓ 点击 Initialize Button,网格应该开始填充
4. ✓ 点击 Start Filter Button,解码器条纹应该出现
5. ✓ 拖动 Emotion Slider,观察网格变化
6. ✓ 调整到 90Hz 左右(Fear 频率)
7. ✓ 点击 Verify Button,应该成功
## ⚠️ 常见错误
### ❌ 错误 1:网格没有显示
**原因**Grid Cell Prefab 没有设置或配置错误
**解决**
1. 确保创建了 GridCellPrefab 预制体
2. 拖入 AnalysisModeManager 的 Grid Cell Prefab 字段
3. 预制体必须包含 AnalysisGridCell 组件
### ❌ 错误 2:面板没有滑出
**原因**Analysis Panel 初始位置不对
**解决**
1. Analysis Panel 初始应该设置为 Active: false
2. 或者设置到屏幕外位置
3. 检查 Panel Slide Duration 是否 > 0
### ❌ 错误 3:按钮点击没反应
**原因**Canvas 缺少 Graphic Raycaster 或 EventSystem
**解决**
1. 在 Language Canvas 上添加 `Graphic Raycaster`
2. 确保场景中有 `EventSystem`
### ❌ 错误 4:文字太小看不清
**原因**WorldSpace Canvas 中文字需要更大字号
**解决**
1. 增大 Cell Size(建议 60-80
2. 增大 TMP_Text 的 Font Size(建议 16-20
3. 调整 Canvas 的缩放
## 📖 详细文档
- **完整设置指南**`Unity_Setup_Guide_Updated.md`
- **功能说明**`README.md`
- **Yarn 示例**`AnalysisMode_YarnExample.yarn`
## 🎨 自定义建议
### 调整 UI 位置
根据你的游戏设计,Analysis Panel 可以:
- 覆盖在粒子系统上方(半透明)
- 平移到屏幕侧边
- 淡入淡出切换
### 调整网格大小
如果 20x20 太大:
- 修改 Grid Width/Height(如 15x15
- 相应调整 Cell Size
- 修改焦虑短句数量
### 调整情绪频率
`EmotionConfig` 中:
- Joy: 15Hz(黄色)
- Sadness: 40Hz(蓝色)
- Anger: 65Hz(红色)
- Fear: 90Hz(紫色)
## ✨ 完成检查清单
- [ ] Language Canvas 存在且配置正确
- [ ] Analysis Panel 在 Language Canvas 下
- [ ] Emotion Control 包含 Initialize Button
- [ ] 所有按钮都有点击事件(在代码中绑定)
- [ ] Grid Cell Prefab 创建并配置
- [ ] AnalysisModeManager 所有字段都拖入了引用
- [ ] ExpressionManager 中 Analysis Mode Manager 字段已设置
- [ ] 测试 Yarn 脚本可以启动分析模式
- [ ] Play Mode 下可以完整走完流程
## 🚦 现在开始
1. 打开 Unity 编辑器
2. 找到 `Expression View` -> `Language Particle Manager` -> `Language Canvas`
3. 按照上面的"5分钟快速设置"创建 UI
4. 配置 AnalysisModeManager
5. 运行测试!
祝你成功!🎉
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 7ed94d5eb5b51b04ebce8f9f3299fec0
guid: 783bbb2be3f5c5b4894be8307d06964c
TextScriptImporter:
externalObjects: {}
userData:
@@ -0,0 +1,350 @@
# 分析模式 (Analysis Mode) 使用说明
## 概述
分析模式是一个记忆解密游戏,玩家需要通过调节情绪频率来过滤网格中的词语,并使用解码器条纹来验证正确的句子。
## 功能特性
### 1. 核心玩法
- **20x20 网格系统**:展示焦虑词语粒子(笑话片段掩盖真实情绪词)
- **情绪频率调节**:通过滑动条调节情绪频率(0-100 Hz)来过滤显示不同情绪的词语
- **解码器条纹**:可拖拽的解码器条纹,用于验证正确的词语位置
- **验证系统**:当条纹位置和情绪频率都正确时,可以解密出完整句子
### 2. 情绪类型
- **Joy(快乐)**:频率 15 Hz,颜色黄色
- **Sadness(悲伤)**:频率 40 Hz,颜色蓝色
- **Anger(愤怒)**:频率 65 Hz,颜色红色
- **Fear(恐惧)**:频率 90 Hz,颜色紫色
- **Neutral(中性/噪音)**:无固定频率,灰色
### 3. 显示效果
- **锁定区(距离 < 5 Hz)**:显示真实词语,高亮显示
- **闪烁区(5-20 Hz)**:在真实词和掩盖词之间闪烁
- **淡出区(20-35 Hz)**:显示掩盖词,逐渐淡化
- **消失区(> 35 Hz**:几乎不可见
## Unity 编辑器设置
### 1. 场景结构(使用共享 Canvas)
分析模式共享 LanguageParticleManager 的 WorldSpace Canvas
```
Expression View
├── Expression Camera
├── Expression Deep Camera
├── Language Particle Manager
│ └── Language Canvas (WorldSpace Canvas - 共享)
│ ├── FloatingParticles (粒子系统)
│ ├── CandidateParticles (粒子系统)
│ └── Analysis Panel (分析模式UI)
│ ├── Control Panel (左侧控制区)
│ │ ├── Emotion Control (情绪控制区)
│ │ │ ├── Initialize Button
│ │ │ ├── Frequency Text
│ │ │ ├── Emotion Slider
│ │ │ └── Signal Strength Text
│ │ ├── Start Filter Button
│ │ ├── Verify Button
│ │ └── Log Scroll View
│ │ └── Log Text
│ └── Grid Container (右侧网格区)
│ └── Decoder Strip (可拖拽的解码器)
└── Analysis Mode Manager (AnalysisModeManager 组件)
```
**重要**:所有 UI 元素都在 Language Canvas 上渲染,不需要单独的 Canvas。
### 2. 创建预制体
#### 网格单元格预制体 (Grid Cell Prefab)
1. 创建一个空 GameObject,命名为 `GridCellPrefab`
2. 添加组件:
- `RectTransform`
- `Image`(背景)
- `AnalysisGridCell`(脚本)
3. 在子对象中添加 TextMeshPro 组件用于显示文字:
- 创建子对象 `Text`
- 添加 `TextMeshProUGUI` 组件
- 设置对齐方式为居中
- 设置字体大小为 13
#### 解码器条纹预制体 (Decoder Strip Prefab)
1. 创建一个空 GameObject,命名为 `DecoderStripPrefab`
2. 添加组件:
- `RectTransform`
- `Image`(背景,颜色为半透明青色)
- `CanvasGroup`
- `Outline`(边框效果)
- `AnalysisDecoderStrip`(脚本)
3. 设置大小为 `stripWidth * cellSize` x `stripHeight * cellSize`
### 3. AnalysisModeManager 配置
在 Inspector 中配置以下参数:
#### Canvas 引用(共享)
- `Shared Canvas`:拖入 LanguageParticleManager 的 Language CanvasWorldSpace Canvas
#### UI 引用
- `Analysis Panel`:主面板 RectTransform(在 Language Canvas 下)
- `Initialize Button`:初始化按钮
- `Emotion Slider`:情绪滑动条
- `Frequency Text`:频率显示文本
- `Signal Strength Text`:信号强度显示文本
- `Start Filter Button`:启动过滤按钮
- `Verify Button`:验证按钮
- `Log Text`:日志文本
- `Log Scroll Rect`:日志滚动区域
#### 网格配置
- `Grid Container`:网格容器 RectTransform
- `Grid Cell Prefab`:网格单元格预制体
- `Grid Width`20(网格宽度)
- `Grid Height`20(网格高度)
- `Cell Size`45(单元格大小)
- `Cell Gap`2(单元格间距)
#### 解码器条纹
- `Decoder Strip`:解码器条纹 RectTransform
- `Strip Hole Prefab`:条纹孔洞预制体(可选)
- `Strip Width`5(条纹宽度,列数)
- `Strip Height`3(条纹高度,行数)
#### 动画配置
- `Panel Slide Duration`0.5(面板滑动时间)
- `Data Load Duration`1.5(数据加载时间)
- `Slide Ease`OutCubic(滑动缓动曲线)
#### 焦虑短句配置
- 添加多个焦虑短句,用于填充网格(每个短句会被分割成2字符片段)
#### 目标配置
- `Target Sentence`:目标句子(例如:"[临检]要来了,不合格[英里]被骂了,这让我很[担心]")
### 4. ExpressionManager 配置
在 ExpressionManager 中添加对 AnalysisModeManager 的引用:
1. 在 Inspector 中找到 `Expression Manager` 组件
2.`Analysis Mode Manager` 拖拽到 `Analysis Mode Manager` 字段
## Yarn 命令接口
### 启动分析模式
在 Yarn 对话脚本中使用以下命令启动分析模式:
```yarn
<<StartAnalysisMode>>
```
或者带参数:
```yarn
<<StartAnalysisMode "CompletionNodeName">>
```
### 关闭分析模式
```yarn
<<CloseAnalysisMode>>
```
### 完整示例
```yarn
title: 表达系统_分析模式
---
角色:现在让我们尝试一种新的方式来理解你的焦虑...
角色:这是一个记忆解密系统,通过调节情绪频率来寻找隐藏的真相。
// 启动分析模式
<<StartAnalysisMode "表达系统_分析完成">>
-> 等待玩家完成...
===
title: 表达系统_分析完成_Success
---
角色:做得很好!你成功解密了记忆碎片。
角色:现在你明白了自己真正担心的是什么...
<<CloseAnalysisMode>>
===
title: 表达系统_分析完成_Fail
---
角色:似乎还不太对...要不要再试一次?
-> 再试一次
<<StartAnalysisMode "表达系统_分析完成">>
-> 稍后再说
<<CloseAnalysisMode>>
===
```
## C# 代码接口
### 在脚本中调用
```csharp
using AibisDream;
// 获取 ExpressionManager
var expressionManager = FixSystemCenter.SystemDic.Get<ExpressionManager>();
// 启动分析模式
List<string> anxietyPhrases = new List<string>
{
"临检要来了大家都很紧张只有销售员说终于可以检查业绩了",
"不合格英里被骂了因为销售业绩太差了",
"我担心业绩会下滑结果真的下滑了我的担心是对的"
};
expressionManager.StartAnalysisMode(anxietyPhrases, "CompletionNodeName");
// 关闭分析模式
expressionManager.CloseAnalysisMode();
```
## 玩法流程
### 完整游戏流程
1. **完成语言粒子游戏**
- 玩家先完成 LanguageParticleManager 的语言整合游戏
- 目标粒子连接成功,显示目标句子
2. **启动分析模式**
- 通过 Yarn 命令启动分析模式
- 分析控制面板在共享 Canvas 上显示
3. **初始化数据**
- 在 Emotion Control 区域点击"初始化"按钮
- 网格开始填充焦虑词语数据(Matrix 风格动画)
4. **启动过滤分析**
- 点击"启动过滤分析"按钮
- 解码器条纹出现在网格上
- 情绪滑动条变为可用
5. **调节频率**
- 拖动情绪滑动条(0-100 Hz
- 观察不同情绪词语的显示效果:
- 接近目标频率:词语高亮显示
- 中等距离:闪烁效果
- 远距离:淡化消失
6. **拖动条纹**(可选):
- 拖动解码器条纹到不同位置
- 通过条纹的"孔洞"查看下方的词语
7. **验证匹配**
- 点击"验证对齐"按钮
- 系统检查:
- 条纹位置是否正确
- 情绪频率是否在正确范围内(< 8 Hz 偏差)
8. **查看结果**
- **成功**:显示完整句子,触发成功对话节点(如 `AnalysisComplete_Success`
- **失败**:提示调整频率或位置,触发失败对话节点(如 `AnalysisComplete_Fail`
### 设计意图
- 分析模式是语言粒子游戏的"深入分析"环节
- 玩家通过调节情绪频率来"解密"隐藏在笑话中的真实焦虑
- 整个过程在同一个 WorldSpace Canvas 上完成,保持视觉连贯性
## 自定义配置
### 修改目标答案
`AnalysisModeManager.SetupTargets()` 方法中修改目标配置:
```csharp
private void SetupTargets()
{
analysisTargets.Clear();
// 创建正确答案目标
var correctTarget = new AnalysisTarget
{
name = "Fear Solution 1",
emotion = EmotionType.Fear,
stripPosition = new Vector2Int(3, 5), // 条纹正确位置
isCorrectAnswer = true
};
// 添加目标词
correctTarget.words.Add(new TargetWord
{
position = new Vector2Int(3, 5),
word = "临检",
cover = "检查"
});
// ... 添加更多目标词
analysisTargets.Add(correctTarget);
}
```
### 修改情绪配置
`EmotionConfig` 类中修改情绪频率和颜色:
```csharp
public static EmotionConfig Fear => new EmotionConfig
{
type = EmotionType.Fear,
frequency = 90f, // 修改频率
color = new Color(0.67f, 0f, 1f, 1f), // 修改颜色
displayName = "FEAR"
};
```
## 故障排除
### 问题:网格没有显示
- 检查 `Grid Container` 是否正确设置
- 检查 `Grid Cell Prefab` 是否正确配置
- 确保焦虑短句列表不为空
### 问题:解码器条纹无法拖动
- 检查 `AnalysisDecoderStrip` 组件是否添加
- 确保 `isDraggable` 设置为 true
- 检查是否有 `EventSystem` 在场景中
### 问题:验证总是失败
- 检查 `SetupTargets()` 中的目标位置是否正确
- 确保情绪频率设置在正确范围内(< 8 Hz 偏差)
- 检查日志输出,查看条纹位置和频率信息
## 性能优化建议
- 网格大小建议不超过 25x25
- 单元格数量过多时,考虑使用对象池
- 过滤效果更新频率可以降低(例如每 0.1 秒更新一次)
- 使用 DOTween 的 `.SetRecyclable(true)` 来减少 GC
## 未来扩展
- 添加多个解码器条纹孔洞位置配置
- 支持条纹自动吸附到网格
- 添加音效和粒子效果
- 支持多个正确答案
- 添加提示系统
- 记录玩家尝试次数和时间
## 版本历史
- **v1.0.0**2025-12-09):初始版本
- 基础网格系统
- 情绪过滤功能
- 解码器条纹
- Yarn 命令接口
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: edb32a6d920f2a9418f88e9c03b5048e
guid: 207291ad334f15f4b91f9534b0b52ae4
TextScriptImporter:
externalObjects: {}
userData:
@@ -0,0 +1,272 @@
# 使用粒子系统的简化设置指南
## ✨ 优势
直接使用 LanguageParticleManager 的 TextParticle 系统,无需创建 UI Prefab
### 好处:
- ✅ **无需创建 Prefab** - 代码自动生成粒子
- ✅ **视觉完全一致** - 与语言粒子系统使用相同的渲染
- ✅ **单位自动匹配** - 都是 Unity 世界单位
- ✅ **字体自动匹配** - 使用相同的中文字体资源
- ✅ **性能更好** - 复用现有的粒子系统
## 🚀 5分钟极简设置
### 第一步:创建控制面板(3分钟)
`Language Canvas` 下创建简单的控制 UI
```
Language Canvas
├── 粒子系统(LanguageParticleManager 创建的)
└── Analysis Control Panel ← 只需要控制按钮!
├── Initialize Button
├── Emotion Slider
├── Frequency Text
├── Signal Strength Text
├── Start Filter Button
└── Verify Button
```
**无需创建**
- ❌ Grid Container(粒子直接生成在世界空间)
- ❌ Grid Cell Prefab(代码自动创建 TextParticle
- ❌ 复杂的布局结构
### 第二步:配置 AnalysisModeManager2分钟)
在 Inspector 中只需设置:
```
Canvas 引用(共享)
└─ Shared Canvas: Language Canvas ← 必须设置
UI 引用(简化)
├─ Analysis Panel: Analysis Control Panel
├─ Initialize Button: Initialize Button
├─ Emotion Slider: Emotion Slider
├─ Frequency Text: Frequency Text
├─ Signal Strength Text: Signal Strength Text
├─ Start Filter Button: Start Filter Button
└─ Verify Button: Verify Button
网格配置
├─ Grid Width: 20
├─ Grid Height: 20
├─ Cell Size: 0.4 ← Unity 单位
├─ Cell Gap: 0.05 ← Unity 单位
├─ Cell Font Size: 2.5 ← 与粒子一致
└─ Grid Center Offset: (0, 0, 0) 或根据需要偏移
字体配置
└─ Chinese Font Asset: [拖入中文字体,与 LanguageParticleManager 相同]
焦虑短句配置
└─ [添加短句列表]
目标配置
└─ Target Sentence: [临检]要来了,不合格[英里]被骂了,这让我很[担心]
```
**不需要设置**
- ❌ Grid Container(可选,用于定位但不必须)
- ❌ Grid Cell Prefab(已注释,不需要)
- ❌ Decoder Strip(暂时注释)
### 第三步:完成!
保存场景,运行测试即可。
## 🎮 测试流程
1. **启动游戏**
2. **通过 Yarn 启动分析模式**
```yarn
<<start_analysis_mode "" "TestComplete">>
```
3. **点击 Initialize Button**
- 网格粒子会自动在 Language Canvas 上生成
- Matrix 风格的填充动画
- 20×20 = 400 个 TextParticle
4. **点击 Start Filter Button**
- 情绪滑动条变为可用
5. **拖动 Emotion Slider**
- 观察粒子颜色和透明度变化
- Fear (90Hz): 紫色高亮
- Anger (65Hz): 红色高亮
- Sadness (40Hz): 蓝色高亮
- Joy (15Hz): 黄色高亮
## 📐 配置说明
### 字体大小参考(与 LanguageParticleManager 一致)
```csharp
// LanguageParticleManager 配置
floatingTextFontSize = 2.0f; // 背景浮动文字
nonTargetParticleFontSize = 2.5f; // 非目标粒子
targetParticleFontSize = 3.5f; // 目标粒子
// AnalysisModeManager 配置
cellFontSize = 2.5f; // ← 使用与非目标粒子相同的大小
```
### 网格大小计算
```
单元格单位 = cellSize + cellGap
= 0.4 + 0.05
= 0.45 Unity 单位
网格总宽度 = 20 × 0.4 + 19 × 0.05 = 8.95 Unity 单位
网格总高度 = 20 × 0.4 + 19 × 0.05 = 8.95 Unity 单位
Canvas 大小 = 10 × 10 Unity 单位
边界大小 = 9 × 9 Unity 单位
✅ 网格 (8.95 × 8.95) 完美适配边界 (9 × 9)
```
### 网格位置调整
如果需要偏移网格位置,调整 `Grid Center Offset`
```csharp
// 居中(默认)
gridCenterOffset = Vector3.zero;
// 向右偏移 2 单位
gridCenterOffset = new Vector3(2, 0, 0);
// 向左偏移 2 单位
gridCenterOffset = new Vector3(-2, 0, 0);
// 向上偏移 1 单位
gridCenterOffset = new Vector3(0, 1, 0);
```
## 🎨 视觉效果
### 网格粒子的显示状态
| 频率距离 | 显示效果 | 文字内容 | 颜色 | 光晕 |
|---------|---------|---------|------|------|
| < 5 Hz | 锁定高亮 | 真实词 | 情绪色(加粗) | 0.5 alpha |
| 5-20 Hz | 闪烁 | 真实词⇄掩盖词 | 情绪色⇄白色 | 0.3 alpha |
| 20-35 Hz | 淡出 | 掩盖词 | 白色(渐变) | 无 |
| > 35 Hz | 消失 | 掩盖词 | 白色(0.05 alpha | 无 |
| 中性/噪音 | 背景 | 笑话片段 | 灰绿色(0.3 alpha | 无 |
### 与粒子系统的视觉对比
| 元素 | 字体大小 | 颜色 | 效果 |
|------|---------|------|------|
| 浮动粒子 | 2.0 | 浅蓝色 | 自由移动 |
| 非目标粒子 | 2.5 | 蓝色 | 焦虑文字 |
| 目标粒子 | 3.5 | 红色 | 目标文字 |
| **网格粒子** | **2.5** | **白色/情绪色** | **静态网格** |
网格粒子使用与非目标粒子相同的大小(2.5),视觉上非常协调。
## 🔧 代码自动处理
现在代码会自动:
1. ✅ 创建 TextParticle 对象(而不是 UI Image
2. ✅ 设置粒子为静态模式(不移动)
3. ✅ 使用与 LanguageParticleManager 相同的字体
4. ✅ 设置字体大小为 2.5(与非目标粒子一致)
5. ✅ 在世界空间中排列成网格
6. ✅ 根据情绪频率更新粒子颜色和透明度
## 📋 最小化设置清单
只需要创建:
- [ ] Analysis Control Panel(控制按钮面板)
- [ ] Initialize Button
- [ ] Emotion Slider
- [ ] Frequency Text
- [ ] Signal Strength Text
- [ ] Start Filter Button
- [ ] Verify Button
不需要创建:
- ❌ Grid Container(可选)
- ❌ Grid Cell Prefab(代码自动生成)
- ❌ 复杂的布局 Group
- ❌ Decoder Strip(暂时注释)
- ❌ Log Panel(已注释)
## 🎯 实际效果
运行后,你会看到:
1. 400 个 TextParticle 自动排列成 20×20 网格
2. 每个粒子显示 2 个字符(焦虑短句片段)
3. 粒子大小和样式与语言粒子系统完全一致
4. 拖动情绪滑动条时,粒子会根据情绪类型变色和闪烁
5. 整体视觉非常协调统一
## 🎨 自定义建议
### 调整网格密度
```csharp
// 更密集
cellSize = 0.35f;
cellGap = 0.03f;
// 更稀疏
cellSize = 0.5f;
cellGap = 0.1f;
```
### 调整文字大小
```csharp
cellFontSize = 2.5f; // 默认(与非目标粒子一致)
cellFontSize = 3.0f; // 更大(接近目标粒子)
cellFontSize = 2.0f; // 更小(接近背景文字)
```
### 调整网格位置
```csharp
// 在 Inspector 中设置 Grid Center Offset
(0, 0, 0) // 居中
(3, 0, 0) // 向右偏移
(-3, 0, 0) // 向左偏移
(0, 2, 0) // 向上偏移
```
## ⚠️ 注意事项
1. **字体资源**:必须设置 `Chinese Font Asset`,使用与 LanguageParticleManager 相同的字体
2. **Canvas 引用**:必须设置 `Shared Canvas`,指向 Language Canvas
3. **粒子静态**:网格粒子设置为 `isStatic = true`,不会移动
4. **层级管理**:网格粒子会添加到 Language Canvas 下,与其他粒子共存
## 🔍 验证方法
运行后在 Hierarchy 中检查:
```
Language Canvas
├── FloatingParticle_0
├── FloatingParticle_1
├── ...
├── CandidateParticle_0
├── CandidateParticle_1
├── ...
├── GridParticle_0_0 ← 新创建的网格粒子
├── GridParticle_0_1
├── ...
└── GridParticle_19_19 ← 应该有 400 个
```
## 🎉 完成
现在设置变得非常简单:
1. 创建几个控制按钮
2. 配置 AnalysisModeManager 的字段
3. 运行游戏
4. 网格粒子自动生成,大小完美匹配!
不需要任何 Prefab,不需要复杂的 UI 布局,一切都是代码自动处理!✨
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 2bc354555f638b64498e942697b4e29e
guid: ad2ab126571faba489df806ca0c2bb2b
TextScriptImporter:
externalObjects: {}
userData:
@@ -0,0 +1,136 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &5030332322852799317
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3682520096596469954}
- component: {fileID: 7208676086374186826}
- component: {fileID: 8166701485391072747}
m_Layer: 5
m_Name: Text (TMP) (3)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &3682520096596469954
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5030332322852799317}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 1.2, y: -1.98}
m_SizeDelta: {x: 1, y: 1}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &7208676086374186826
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5030332322852799317}
m_CullTransparentMesh: 1
--- !u!114 &8166701485391072747
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5030332322852799317}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: New Text
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: f242fb3dde1933640859f1c54591c9c8, type: 2}
m_sharedMaterial: {fileID: -6659093157667844055, guid: f242fb3dde1933640859f1c54591c9c8, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4294040693
m_fontColor: {r: 0.45745817, g: 0.86362934, b: 0.9433962, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 0.3
m_fontSizeBase: 0.3
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0.41540456, z: -0.14499044, w: -0.5056629}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: fccb02126db337b4a831aaa31a396414
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,243 @@
# Unity 编辑器设置指南
## 快速开始
### 第一步:创建 UI 结构
1. 在 Hierarchy 中找到 `Expression View`
2. 右键 `Expression View` -> `UI` -> `Canvas`,命名为 `Analysis Canvas`
3. 设置 Canvas 属性:
- Render Mode: Screen Space - Overlay
- Pixel Perfect: 勾选
### 第二步:创建分析面板
1. 在 `Analysis Canvas` 下创建 `Panel`,命名为 `Analysis Panel`
2. 设置 RectTransform
- Anchor: 右上角到右下角(stretch right
- Width: 600(可调整)
- Offset Left: 0
- Offset Right: 0
- Offset Top: 0
- Offset Bottom: 0
3. 添加背景 Image 组件,颜色设置为半透明黑色
### 第三步:创建控制区和网格区
`Analysis Panel` 下创建两个子对象:
#### 1. Control Panel(控制区,左侧)
- 创建一个空 GameObject,命名为 `Control Panel`
- 添加 `Vertical Layout Group` 组件
- 设置属性:
- Child Alignment: Upper Left
- Spacing: 10
- Padding: 10 all around
`Control Panel` 下依次创建:
##### a. Initialize Button
- 创建 Button,命名为 `Initialize Button`
- 文本内容:`初始化数据`
##### b. Emotion Control(情绪控制区)
- 创建空 GameObject,命名为 `Emotion Control`
- 在其下创建:
- `Frequency Text`TMP_Text):显示频率值
- `Emotion Slider`Slider):情绪滑动条
- Min Value: 0
- Max Value: 100
- Value: 50
- `Signal Strength Text`TMP_Text):显示信号强度
##### c. Start Filter Button
- 创建 Button,命名为 `Start Filter Button`
- 文本内容:`启动过滤分析`
##### d. Verify Button
- 创建 Button,命名为 `Verify Button`
- 文本内容:`验证对齐`
##### e. Log Panel(日志面板)
- 创建 Scroll View,命名为 `Log Scroll View`
- 在 Viewport -> Content 下创建 TMP_Text,命名为 `Log Text`
- 设置 Log Text 属性:
- Alignment: Top Left
- Font Size: 12
- Color: Light Green
#### 2. Grid Container(网格区,右侧)
- 创建空 GameObject,命名为 `Grid Container`
- 添加 RectTransform 组件
- 设置大小会在运行时自动计算
### 第四步:创建网格单元格预制体
1. 在 Project 窗口中,在 `Assets/Prefabs/MiniGame/Language/AnalysisMode/` 创建文件夹
2. 创建新的 UI -> Image,命名为 `GridCellPrefab`
3. 设置属性:
- RectTransform Size: (45, 45)
- Image Color: 半透明绿色 `rgba(0, 10, 0, 0.2)`
4. 在 `GridCellPrefab` 下创建子对象:
- UI -> Text - TextMeshPro,命名为 `Text`
- 设置 Text 属性:
- Alignment: Center Middle
- Font Size: 13
- Color: White
- Auto Size: 关闭
- Wrapping: 关闭
- Overflow: Overflow
5. 添加 `AnalysisGridCell` 组件到 `GridCellPrefab`
6. 将 `GridCellPrefab` 拖到 Project 窗口中保存为预制体
### 第五步:创建解码器条纹预制体
1. 创建新的 UI -> Image,命名为 `DecoderStripPrefab`
2. 设置属性:
- RectTransform Size: (根据 stripWidth 和 stripHeight 计算)
- Image Color: 半透明青色 `rgba(0, 30, 30, 0.95)`
3. 添加组件:
- `Canvas Group`
- `Outline`(边框效果)
- Effect Color: Cyan
- Effect Distance: (2, 2)
- `AnalysisDecoderStrip`
4. 将 `DecoderStripPrefab` 拖到 `Grid Container` 下,命名为 `Decoder Strip`
### 第六步:创建分析模式管理器
1. 在 `Expression View` 下创建空 GameObject,命名为 `Analysis Mode Manager`
2. 添加 `AnalysisModeManager` 组件
3. 配置 Inspector 中的引用:
#### UI 引用
- `Analysis Canvas`: 拖入第二步创建的 Analysis Canvas
- `Analysis Panel`: 拖入 Analysis Panel
- `Initialize Button`: 拖入 Initialize Button
- `Emotion Slider`: 拖入 Emotion Slider
- `Frequency Text`: 拖入 Frequency Text
- `Signal Strength Text`: 拖入 Signal Strength Text
- `Start Filter Button`: 拖入 Start Filter Button
- `Verify Button`: 拖入 Verify Button
- `Log Text`: 拖入 Log Text
- `Log Scroll Rect`: 拖入 Log Scroll View
#### 网格配置
- `Grid Container`: 拖入 Grid Container
- `Grid Cell Prefab`: 拖入 GridCellPrefab 预制体
- `Grid Width`: 20
- `Grid Height`: 20
- `Cell Size`: 45
- `Cell Gap`: 2
#### 解码器条纹
- `Decoder Strip`: 拖入 Decoder Strip
- `Strip Width`: 5
- `Strip Height`: 3
#### 焦虑短句配置
在 Anxiety Phrases 列表中添加:
- `临检要来了大家都很紧张只有销售员说终于可以检查业绩了`
- `不合格英里被骂了因为销售业绩太差了`
- `我担心业绩会下滑结果真的下滑了我的担心是对的`
- `销售员总是混淆客户和老板因为都要讨好`
- `一个销售员走进酒吧问两张桌子要不要买保险`
#### 目标配置
- `Target Sentence`: `[临检]要来了,不合格[英里]被骂了,这让我很[担心]`
### 第七步:配置 ExpressionManager
1. 找到 `Expression Manager` 组件
2. 将 `Analysis Mode Manager` 拖入 `Analysis Mode Manager` 字段
### 第八步:测试
1. 保存场景
2. 进入 Play Mode
3. 在 Console 中执行 Yarn 命令:
```
<<start_analysis_mode "" "TestNode">>
```
4. 观察分析模式面板是否正确滑出
## 布局建议
### 推荐布局(16:9 屏幕)
```
+----------------------------------------------------------+
| Analysis Panel (右侧 600px) |
| +-----------------------------------------------------+ |
| | Control Panel | Grid Container | |
| | (左侧 200px) | (右侧 400px) | |
| | +--------------+ | +---------------------------+ | |
| | | Initialize | | | | | |
| | +--------------+ | | 20x20 Grid | | |
| | | Frequency | | | (每个格子 45x45px) | | |
| | | 50.0 Hz | | | | | |
| | +--------------+ | | +-----+ | | |
| | | Slider | | | |Strip| (可拖拽) | | |
| | +--------------+ | | +-----+ | | |
| | | Signal: 0% | | | | | |
| | +--------------+ | +---------------------------+ | |
| | | Start Filter | | | |
| | +--------------+ | | |
| | | Verify | | | |
| | +--------------+ | | |
| | | Log Window | | | |
| | | > Waiting... | | | |
| | | > ... | | | |
| | +--------------+ | | |
| +-----------------------------------------------------+ |
+----------------------------------------------------------+
```
## 常见问题
### Q: 面板没有滑出来
**A**: 检查以下几点:
1. `Analysis Panel` 的初始位置是否设置正确(应该在屏幕外)
2. `Analysis Canvas` 是否激活
3. 是否正确调用了 `StartAnalysisMode` 方法
### Q: 网格没有显示
**A**: 检查以下几点:
1. `Grid Cell Prefab` 是否正确配置
2. `Grid Container` 是否设置正确
3. 焦虑短句列表是否为空
4. 查看 Console 是否有错误信息
### Q: 解码器条纹无法拖动
**A**: 检查以下几点:
1. `AnalysisDecoderStrip` 组件是否添加
2. `isDraggable` 是否设置为 true
3. 场景中是否有 `EventSystem`
### Q: 验证总是失败
**A**: 检查以下几点:
1. `SetupTargets()` 中的目标位置是否正确
2. 情绪频率是否在正确范围内(< 8 Hz 偏差)
3. 查看 Log 窗口中的调试信息
## 性能优化
1. **网格大小**:建议不超过 25x25
2. **对象池**:如果需要频繁创建销毁网格,考虑使用对象池
3. **更新频率**:过滤效果更新可以降低到每 0.1 秒一次
4. **DOTween 优化**:使用 `.SetRecyclable(true)` 减少 GC
## 下一步
1. 根据你的游戏风格调整 UI 样式
2. 添加音效和粒子效果
3. 实现多个解密谜题
4. 添加提示系统
5. 记录玩家数据(尝试次数、完成时间等)
## 版本信息
- 创建日期:2025-12-09
- 适用于 Unity 2021.3 及以上版本
- 需要 TextMeshPro 和 DOTween 插件
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 4beadb9460face44089b05f4978e5f76
guid: a89b297d81330214989e5e17c7d36b0b
TextScriptImporter:
externalObjects: {}
userData:
@@ -0,0 +1,382 @@
# Unity 编辑器设置指南(更新版)
## 概述
分析模式共享 LanguageParticleManager 的 World Canvas,所有 UI 元素都在这个 Canvas 上渲染。分析模式作为语言粒子系统完成后的扩展功能。
## 快速开始
### 第一步:找到现有的 Canvas
1. 在 Hierarchy 中找到 `Expression View`
2. 展开找到 `Language Canvas`(由 LanguageParticleManager 创建的 WorldSpace Canvas
3. 这个 Canvas 就是我们要使用的共享 Canvas
### 第二步:创建分析模式管理器
1. 在 `Expression View` 下创建空 GameObject,命名为 `Analysis Mode Manager`
2. 添加 `AnalysisModeManager` 组件
3. 将 `Language Canvas` 拖入 `Shared Canvas` 字段
### 第三步:创建分析控制面板
`Language Canvas` 下创建分析模式的 UI 结构:
```
Language Canvas (WorldSpace Canvas)
├── ... (LanguageParticleManager 的粒子系统)
├── Analysis Panel (分析控制面板)
│ ├── Control Panel (左侧控制区)
│ │ ├── Emotion Control (情绪控制区)
│ │ │ ├── Initialize Button (初始化按钮)
│ │ │ ├── Frequency Text (频率显示)
│ │ │ ├── Emotion Slider (情绪滑动条)
│ │ │ └── Signal Strength Text (信号强度)
│ │ ├── Start Filter Button (启动过滤按钮)
│ │ ├── Verify Button (验证按钮)
│ │ └── Log Panel (日志面板)
│ │ └── Log Scroll View
│ │ └── Log Text
│ └── Grid Container (右侧网格区)
│ └── Decoder Strip (解码器条纹)
```
### 第四步:配置 Analysis Panel
#### 1. 创建 Analysis Panel
- 在 `Language Canvas` 下创建空 GameObject,命名为 `Analysis Panel`
- 添加 `RectTransform` 组件
- 设置 RectTransform
- Anchor: 根据你的设计选择(建议全屏或右侧)
- 初始状态可以设置为隐藏(屏幕外)
#### 2. 创建 Control Panel
- 在 `Analysis Panel` 下创建空 GameObject,命名为 `Control Panel`
- 添加 `Vertical Layout Group` 组件
- 设置:
- Child Alignment: Upper Left
- Spacing: 10
- Padding: 10
##### a. Emotion Control(情绪控制区)
`Control Panel` 下创建空 GameObject,命名为 `Emotion Control`
添加 `Vertical Layout Group` 组件,然后创建子元素:
**Initialize Button**
- 创建 Button,命名为 `Initialize Button`
- 文本内容:`初始化数据`
- 这是第一步操作,点击后导入焦虑词语数据
**Frequency Text**
- 创建 TMP_Text,命名为 `Frequency Text`
- 默认文本:`50.0 Hz`
- 字体大小:18
**Emotion Slider**
- 创建 Slider,命名为 `Emotion Slider`
- Min Value: 0
- Max Value: 100
- Value: 50
- 初始状态:禁用(Interactable: false
**Signal Strength Text**
- 创建 TMP_Text,命名为 `Signal Strength Text`
- 默认文本:`信号强度: 0%`
- 字体大小:14
##### b. Start Filter Button
- 创建 Button,命名为 `Start Filter Button`
- 文本内容:`启动过滤分析`
- 初始状态:禁用
##### c. Verify Button
- 创建 Button,命名为 `Verify Button`
- 文本内容:`验证对齐`
- 初始状态:禁用
##### d. Log Panel(日志面板)
- 创建 Scroll View,命名为 `Log Scroll View`
- 设置大小:建议高度 200-300
- 在 Viewport -> Content 下创建 TMP_Text,命名为 `Log Text`
- 设置 Log Text 属性:
- Alignment: Top Left
- Font Size: 12
- Color: Light Green (`#8FFF8F`)
- Auto Size: 关闭
- Wrapping: 启用
- Overflow: Overflow
#### 3. 创建 Grid Container
- 在 `Analysis Panel` 下创建空 GameObject,命名为 `Grid Container`
- 添加 `RectTransform` 组件
- 位置:放在 Control Panel 右侧
- 大小:运行时会自动计算(根据 20x20 网格)
### 第五步:创建网格单元格预制体
#### 方式一:手动创建预制体
1. 在 Project 窗口创建文件夹:`Assets/Prefabs/MiniGame/Language/AnalysisMode/`
2. 创建网格单元格预制体:
- 在 Hierarchy 中创建 UI -> Image,命名为 `GridCellPrefab`
- 设置属性:
- RectTransform Size: (45, 45)
- Image Color: 半透明绿色 `rgba(0, 10, 0, 0.2)`
3. 添加文本子对象:
- 在 `GridCellPrefab` 下创建 UI -> Text - TextMeshPro,命名为 `Text`
- 设置属性:
- Alignment: Center Middle
- Font Size: 13
- Color: White
- Auto Size: 关闭
- Wrapping: 关闭
- Overflow: Overflow
- RectTransform: Stretch 全屏
4. 添加组件:
- 在 `GridCellPrefab` 上添加 `AnalysisGridCell` 组件
- 将 Text 拖入 `Content Text` 字段
- 将 Image 拖入 `Background Image` 字段
5. 保存预制体:
- 将 `GridCellPrefab` 从 Hierarchy 拖到 Project 窗口保存
- 然后从 Hierarchy 中删除这个临时对象
### 第六步:创建解码器条纹
1. 在 `Grid Container` 下创建 UI -> Image,命名为 `Decoder Strip`
2. 设置属性:
- 初始状态:隐藏(Active: false
- Image Color: 半透明青色 `rgba(0, 30, 30, 0.95)`
- 大小:运行时会自动计算
3. 添加组件:
- `Canvas Group`(用于淡入淡出动画)
- `Outline`(边框效果)
- Effect Color: Cyan (#00FFFF)
- Effect Distance: (2, 2)
- `AnalysisDecoderStrip`
### 第七步:配置 AnalysisModeManager
在 Inspector 中配置以下字段:
#### Canvas 引用(共享)
- `Shared Canvas`: 拖入 `Language Canvas`
#### UI 引用
- `Analysis Panel`: 拖入 Analysis Panel
- `Initialize Button`: 拖入 Initialize Button
- `Emotion Slider`: 拖入 Emotion Slider
- `Frequency Text`: 拖入 Frequency Text
- `Signal Strength Text`: 拖入 Signal Strength Text
- `Start Filter Button`: 拖入 Start Filter Button
- `Verify Button`: 拖入 Verify Button
- `Log Text`: 拖入 Log Text
- `Log Scroll Rect`: 拖入 Log Scroll View
#### 网格配置
- `Grid Container`: 拖入 Grid Container
- `Grid Cell Prefab`: 拖入 GridCellPrefab 预制体
- `Grid Width`: 20
- `Grid Height`: 20
- `Cell Size`: 45
- `Cell Gap`: 2
#### 解码器条纹
- `Decoder Strip`: 拖入 Decoder Strip
- `Strip Width`: 5
- `Strip Height`: 3
#### 动画配置
- `Panel Slide Duration`: 0.5
- `Data Load Duration`: 1.5
- `Slide Ease`: OutCubic
#### 焦虑短句配置
添加默认短句(运行时可通过 Yarn 覆盖):
- `临检要来了大家都很紧张只有销售员说终于可以检查业绩了`
- `不合格英里被骂了因为销售业绩太差了`
- `我担心业绩会下滑结果真的下滑了我的担心是对的`
- `销售员总是混淆客户和老板因为都要讨好`
- `一个销售员走进酒吧问两张桌子要不要买保险`
#### 目标配置
- `Target Sentence`: `[临检]要来了,不合格[英里]被骂了,这让我很[担心]`
### 第八步:配置 ExpressionManager
1. 找到 `Expression Manager` 组件
2. 将 `Analysis Mode Manager` 拖入 `Analysis Mode Manager` 字段
## 布局建议(WorldSpace Canvas
由于使用的是 WorldSpace Canvas,布局需要考虑 3D 空间:
```
Language Canvas (WorldSpace, 10x10 单位)
├── 粒子系统区域(中心区域)
│ └── 浮动粒子和候选粒子
└── Analysis Panel(右侧或覆盖)
├── Control Panel (左侧,约 2 单位宽)
│ ├── Emotion Control
│ │ ├── Initialize Button
│ │ ├── Frequency: 50.0 Hz
│ │ ├── [========|====] Slider
│ │ └── Signal: 0%
│ ├── [启动过滤分析]
│ ├── [验证对齐]
│ └── Log Window
│ └── > Waiting...
└── Grid Container (右侧,约 6 单位宽)
└── 20x20 Grid + Decoder Strip
```
### 位置和缩放建议
由于 WorldSpace Canvas 和屏幕空间不同,需要调整:
1. **Canvas Scale Factor**
- Canvas Scaler 可能需要设置为固定大小
- 或者使用 Constant Physical Size 模式
2. **分析面板位置**
- 可以覆盖在粒子系统上方
- 或者平移到侧边
- Z 轴稍微靠前以遮挡粒子
3. **字体大小**
- WorldSpace Canvas 中的字体需要比 Screen Space 大
- 建议 Frequency Text: 18-24
- 建议 Log Text: 12-16
## 操作流程
### 1. 完成语言粒子游戏
- 玩家先完成 LanguageParticleManager 的语言整合游戏
- 目标粒子连接成功,显示完成
### 2. 启动分析模式
通过 Yarn 命令启动:
```yarn
<<start_analysis_mode "" "AnalysisComplete">>
```
### 3. 分析控制面板滑出
- Analysis Panel 从右侧或淡入显示
- Emotion Control 区域显示
- Initialize Button 可点击
### 4. 初始化数据
- 点击 Initialize Button
- 网格开始填充(Matrix 风格动画)
- 完成后 Start Filter Button 变为可用
### 5. 启动过滤分析
- 点击 Start Filter Button
- Decoder Strip 显示
- Emotion Slider 变为可用
- Verify Button 变为可用
### 6. 调节频率和验证
- 拖动 Emotion Slider 调节频率
- 观察网格中不同情绪词的显示效果
- 找到正确频率后点击 Verify Button
- 系统验证并反馈结果
## 常见问题
### Q: 网格没有显示在正确的位置
**A**: 检查以下几点:
1. Grid Container 是否正确作为 Language Canvas 的子对象
2. Grid Container 的 RectTransform 锚点设置
3. WorldSpace Canvas 的缩放是否正确
### Q: 文字太小看不清
**A**: WorldSpace Canvas 中的文字需要更大的字号:
1. 增大 Cell Size(建议 60-80
2. 增大 TMP_Text 的 Font Size
3. 调整 Canvas 的 Dynamic Pixels Per Unit
### Q: 分析面板遮挡了粒子系统
**A**: 调整 Analysis Panel 的位置和透明度:
1. 将 Analysis Panel 移到侧边
2. 或者在启动分析模式时隐藏/淡化粒子系统
3. 调整 Z 轴位置(Analysis Panel 的 Z 稍微靠前)
### Q: 解码器条纹无法拖动
**A**: 确保:
1. `AnalysisDecoderStrip` 组件已添加
2. `isDraggable` 设置为 true
3. EventSystem 在场景中存在
4. WorldSpace Canvas 有正确的 Graphic Raycaster
## WorldSpace Canvas 特殊配置
### Canvas 设置
```
Canvas Component:
- Render Mode: World Space
- Event Camera: Main Camera
- Sorting Layer: Default
- Order in Layer: 0
Canvas Scaler (可选):
- UI Scale Mode: Constant Physical Size
- Physical Unit: Centimeters
- Reference Pixels Per Unit: 100
```
### Raycaster 设置
确保 Canvas 上有 `Graphic Raycaster` 组件,这样才能响应鼠标点击。
### 相机设置
Main Camera 需要能看到 WorldSpace Canvas 的区域。
## 性能优化
1. **网格大小**20x20 = 400 个单元格,建议不要超过 25x25
2. **更新频率**:过滤效果可以降低到每 0.1 秒更新一次
3. **对象池**:频繁创建销毁时考虑使用对象池
4. **LOD**:距离较远时可以降低网格密度
## 调试技巧
### 检查 Canvas 层级
```
Expression View
├── Expression Camera
├── Expression Deep Camera
├── Language Canvas (WorldSpace)
│ ├── FloatingParticles (由 LanguageParticleManager 创建)
│ ├── CandidateParticles (由 LanguageParticleManager 创建)
│ └── Analysis Panel (由你手动创建)
│ ├── Control Panel
│ └── Grid Container
└── Analysis Mode Manager (挂 AnalysisModeManager 脚本)
```
### 查看运行时状态
在 Play Mode 下:
1. 查看 Grid Container 的子对象数量(应该是 400 个)
2. 查看 Decoder Strip 的位置和大小
3. 查看 Log Text 的内容是否正确更新
## 下一步
1. 根据你的游戏风格调整 UI 样式
2. 调整 WorldSpace Canvas 的位置和缩放
3. 添加音效和粒子效果
4. 实现多个解密谜题
5. 添加提示系统
## 版本信息
- 更新日期:2025-12-09
- 适用于 Unity 2021.3 及以上版本
- 依赖:TextMeshPro、DOTween、Yarn Spinner
- 特点:共享 WorldSpace Canvas,与粒子系统集成
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 22645ea47ae81094480d7f42a6031a3d
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,246 @@
# WorldSpace Canvas 配置指南
## 🎯 问题说明
AnalysisModeManager 需要与 LanguageParticleManager 使用**相同的 WorldSpace Canvas**,因此单位系统和缩放要保持一致。
## 📐 单位系统对比
### LanguageParticleManager 配置
```csharp
// Canvas 配置
canvasSize = 10f; // 10x10 Unity 世界单位
// 字体大小配置(Unity 单位)
targetParticleFontSize = 3.5f; // 目标粒子
nonTargetParticleFontSize = 2.5f; // 非目标粒子
floatingTextFontSize = 2f; // 背景浮动文字
// 边界设置
float halfSize = canvasSize / 2f - textMargin; // 约 4.5f
movementBounds = new Bounds(
worldCanvas.transform.position,
new Vector3(halfSize * 2f, halfSize * 2f, 0f) // 9x9 单位
);
```
### AnalysisModeManager 更新配置
```csharp
// 网格配置(Unity 单位,匹配 LanguageParticleManager
cellSize = 0.4f; // 单个格子大小(Unity 单位)
cellGap = 0.05f; // 格子间距(Unity 单位)
cellFontSize = 2.5f; // 文字大小(与非目标粒子一致)
// 20x20 网格总大小
totalWidth = 20 * 0.4 + 19 * 0.05 = 8.95 Unity 单位
totalHeight = 20 * 0.4 + 19 * 0.05 = 8.95 Unity 单位
// 正好适配 LanguageParticleManager 的 Canvas (10x10 单位)
```
## 🔧 Unity Inspector 设置
### 1. AnalysisModeManager 组件配置
```
Canvas 引用(共享)
├─ Shared Canvas: Language Canvas
网格配置
├─ Grid Width: 20
├─ Grid Height: 20
├─ Cell Size: 0.4 ← Unity 单位!
├─ Cell Gap: 0.05 ← Unity 单位!
└─ Cell Font Size: 2.5 ← 与 LanguageParticleManager 一致
字体配置
└─ Chinese Font Asset: [拖入你的中文字体]
```
### 2. Grid Cell Prefab 设置
```
GridCellPrefab
├─ RectTransform
│ ├─ Width: 0.4 ← Unity 单位
│ ├─ Height: 0.4 ← Unity 单位
│ └─ Scale: (1, 1, 1)
├─ Image (背景)
│ └─ Color: rgba(0, 10, 0, 0.2)
└─ Text (TMP_Text)
├─ Font: [中文字体]
├─ Font Size: 2.5 ← 由代码设置
├─ Auto Size: OFF
├─ Overflow: Overflow
├─ Alignment: Center Middle
└─ Color: White
```
### 3. Grid Container 设置
```
Grid Container (RectTransform)
├─ Anchor: Center
├─ Pivot: (0.5, 0.5)
├─ Position: 根据需要调整(可以偏移到画面一侧)
└─ Size: 由代码运行时计算(约 8.95 x 8.95)
```
## 📏 尺寸计算
### 单个格子
- **Cell Size**: 0.4 Unity 单位
- **Cell Gap**: 0.05 Unity 单位
- **单元格单位**: 0.4 + 0.05 = 0.45 Unity 单位
### 整个网格
- **宽度**: 20 × 0.4 + 19 × 0.05 = **8.95 Unity 单位**
- **高度**: 20 × 0.4 + 19 × 0.05 = **8.95 Unity 单位**
### Canvas 边界(LanguageParticleManager
- **Canvas Size**: 10 × 10 Unity 单位
- **Movement Bounds**: 9 × 9 Unity 单位(减去 margin
- **网格占用**: 8.95 × 8.95 Unity 单位 ✅ **正好适配!**
## 🎨 视觉对比
### 文字大小参考
| 元素 | 字体大小 | 用途 |
|------|---------|------|
| 浮动文字粒子 | 2.0 | 背景装饰文字 |
| **网格单元格** | **2.5** | **分析模式网格文字** |
| 非目标粒子 | 2.5 | 蓝色候选文字 |
| 目标粒子 | 3.5 | 红色目标文字 |
网格文字使用 **2.5** 的字体大小,与非目标粒子一致,这样视觉上会很协调。
## 🔄 与 Screen Space 的区别
### Screen Space Canvas(旧方式,不适用)
```csharp
cellSize = 45f; // 像素单位
cellGap = 2f; // 像素单位
fontSize = 13f; // 像素字体大小
// 问题:与 WorldSpace Canvas 的缩放不匹配
```
### WorldSpace Canvas(正确方式)
```csharp
cellSize = 0.4f; // Unity 世界单位
cellGap = 0.05f; // Unity 世界单位
fontSize = 2.5f; // Unity 世界单位的字体大小
// ✅ 与 LanguageParticleManager 完美匹配
```
## 🎯 布局建议
### 网格位置选项
#### 选项 A:居中显示(覆盖粒子)
```csharp
gridContainer.anchoredPosition = Vector2.zero;
// 网格在画布中心,覆盖在粒子系统上方
```
#### 选项 B:右侧显示(不遮挡粒子)
```csharp
gridContainer.anchoredPosition = new Vector2(5, 0);
// 网格偏移到右侧,粒子在左侧
```
#### 选项 C:缩小显示(共存)
```csharp
// 调整网格大小
cellSize = 0.3f; // 更小的格子
gridWidth = 15; // 更少的格子
gridHeight = 15;
// 总大小约 4.5 x 4.5,可以放在角落
```
## 🐛 常见问题
### Q1: 文字显示太小/太大
**A**: 调整 `Cell Font Size` 参数
```csharp
cellFontSize = 2.5f; // 默认值
cellFontSize = 3.0f; // 稍大一些
cellFontSize = 2.0f; // 稍小一些
```
### Q2: 网格太大/太小
**A**: 调整 `Cell Size` 参数
```csharp
cellSize = 0.4f; // 默认值(20x20 = 8.95 单位)
cellSize = 0.3f; // 更小(20x20 = 6.95 单位)
cellSize = 0.5f; // 更大(20x20 = 10.95 单位,会超出边界)
```
### Q3: 网格数量太多/太少
**A**: 调整 `Grid Width/Height` 参数
```csharp
gridWidth = 20; // 默认值
gridWidth = 15; // 更少的格子(15x15 = 6.95 单位)
gridWidth = 25; // 更多的格子(25x25 = 11.95 单位,会超出边界)
```
### Q4: 文字没有显示
**A**: 检查以下几点:
1. 确保 `Chinese Font Asset` 已设置
2. 检查 Text 的 `Overflow Mode` 是否设置为 `Overflow`
3. 检查 Text 的 `Enable Auto Sizing` 是否关闭
4. 检查 Canvas 的 `World Camera` 是否设置为 Main Camera
### Q5: 文字和格子大小不匹配
**A**: 确保 RectTransform 和 Text 都使用相同的单位系统
```csharp
// RectTransform Size
cellRect.sizeDelta = new Vector2(cellSize, cellSize); // 0.4 x 0.4
// Text Font Size
text.fontSize = cellFontSize; // 2.5(相对于 RectTransform
```
## 📋 检查清单
设置完成后,检查以下项目:
- [ ] Canvas 使用 WorldSpace 模式
- [ ] Cell Size 和 Cell Gap 使用 Unity 单位(0.x 范围)
- [ ] Cell Font Size 与 LanguageParticleManager 相近(2-3 范围)
- [ ] 中文字体已设置
- [ ] 网格总大小在 Canvas 边界内(< 10 单位)
- [ ] 文字和格子大小视觉上协调
- [ ] 在 Play Mode 下测试显示效果
## 🎨 推荐配置(最终)
```csharp
// 网格配置
gridWidth = 20;
gridHeight = 20;
cellSize = 0.4f;
cellGap = 0.05f;
cellFontSize = 2.5f;
// 总大小: 8.95 x 8.95 Unity 单位
// 适配 10 x 10 Canvas,留有边距
```
这个配置可以让网格文字和粒子系统的文字大小保持一致,视觉效果更协调。
## 🔗 相关文件
- `LanguageParticleManager.cs` - 参考配置来源
- `AnalysisModeManager.cs` - 分析模式管理器
- `AnalysisGridCell.cs` - 网格单元格组件
- `TextParticle.cs` - 粒子文字基类
---
**更新日期**: 2025-12-09
**适用版本**: Unity 2021.3+
**Canvas 类型**: WorldSpace Canvas
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d044f3eb48ad259488a68f17c74cc4e8
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -8,15 +8,14 @@ using System.Collections.Generic;
namespace AibisDream
{
/// <summary>
/// 表达系统管理器(粒子表达系统)
/// 表达系统顶层管理器(原 ExpressionManager,现改为语言粒子系统)
/// </summary>
public class ExpressionManager : MonoBehaviour
{
[Header("系统组件")]
[SerializeField] private GameObject expressionView;
// 内部组件引用(自动查找)
private LanguageParticleManager particleManager;
[Header("系统组件")]
[SerializeField] private LanguageParticleManager particleManager;
[SerializeField] private GameObject expressionView;
[SerializeField] private AibisDream.MiniGame.Language.AnalysisModeManager analysisModeManager;
[Header("默认配置")]
[SerializeField] private List<string> defaultAnxietyPhrases = new List<string>
@@ -45,11 +44,17 @@ namespace AibisDream
expressionView.SetActive(false);
}
// 查找粒子管理器
if (particleManager == null)
{
particleManager = GetComponentInChildren<LanguageParticleManager>(true);
}
// 查找粒子管理器
if (particleManager == null)
{
particleManager = GetComponentInChildren<LanguageParticleManager>(true);
}
// 查找分析模式管理器
if (analysisModeManager == null)
{
analysisModeManager = GetComponentInChildren<AibisDream.MiniGame.Language.AnalysisModeManager>(true);
}
// 注册相机
var virtualCam = transform.Find("Expression Camera")?.GetComponent<ICinemachineCamera>();
@@ -130,10 +135,47 @@ namespace AibisDream
}
}
private void OnDestroy()
/// <summary>
/// 启动分析模式(通过 Yarn 调用)
/// </summary>
/// <param name="anxietyPhrases">焦虑短句列表</param>
/// <param name="completionNodeName">完成时触发的对话节点名称(可选)</param>
public void StartAnalysisMode(List<string> anxietyPhrases = null, string completionNodeName = null)
{
if (analysisModeManager != null)
{
CameraKit.Instance?.UnRegisterCamera(CameraEnum.Expression);
CameraKit.Instance?.UnRegisterCamera(CameraEnum.ExpressionDeep);
analysisModeManager.StartAnalysisMode(anxietyPhrases, completionNodeName);
}
else
{
Debug.LogWarning("[ExpressionManager] AnalysisModeManager 未找到!");
}
}
/// <summary>
/// 启动分析模式(字符串数组版本,方便 Yarn 调用)
/// </summary>
public void StartAnalysisMode(string[] anxietyPhrases, string completionNodeName = null)
{
List<string> phrasesList = anxietyPhrases != null ? new List<string>(anxietyPhrases) : null;
StartAnalysisMode(phrasesList, completionNodeName);
}
/// <summary>
/// 关闭分析模式
/// </summary>
public void CloseAnalysisMode()
{
if (analysisModeManager != null)
{
analysisModeManager.CloseAnalysisMode();
}
}
private void OnDestroy()
{
CameraKit.Instance?.UnRegisterCamera(CameraEnum.Expression);
CameraKit.Instance?.UnRegisterCamera(CameraEnum.ExpressionDeep);
}
}
}
@@ -1,165 +0,0 @@
# 自动配置功能说明
## ✨ 新功能:自动创建默认配置
### 概述
现在 **无需手动创建配置文件**`MemoryDecryptorMode` 会在启动时自动检测,如果没有配置文件,将自动创建并使用默认配置。
## 🚀 使用方式
### 最简单的方式(推荐)
1. 添加 `MemoryDecryptorMode` 组件到场景
2. **不设置** Config 字段(留空)
3. 运行游戏
系统会自动:
- ✅ 检测到配置为空
- ✅ 创建默认配置(包含完整示例数据)
- ✅ 启动系统
### 自定义配置方式
如果需要自定义配置:
1. 创建配置文件:`Create > AibisDream > Language > Memory Decryptor Config`
2. 自定义笑话、目标、视觉效果等
3. 在 `MemoryDecryptorMode` 的 Config 字段拖入配置文件
## 📦 默认配置包含
自动创建的默认配置包含:
### 网格设置
- 20x20 网格
- 单元格大小:45px
- 单元格间隔:2px
### 视觉效果
- 磷光绿主色调 (#33ff00)
- CRT 复古风格
- 解码器条:5x3 青色边框
### 游戏数据
- **笑话数据**18+ 个销售员笑话(足够填充网格)
- **目标数据**3个目标配置
- Fear Solution (正确答案)
- Fear Decoy (诱饵)
- Sadness Solution (诱饵)
### 解码器模板
- 3个孔的 5x3 模板
- 句子:"[孔1]要来了,不合格[孔2]被骂了,这让我很[孔3]"
### 正确答案
- **位置**(3, 5)
- **频率**90Hz (FEAR)
- **词汇**:临检、英里、担心
- **完整句**:"临检要来了,不合格英里被骂了,这让我很担心"
## 💡 工作原理
```csharp
private void Awake()
{
// 如果没有配置文件,自动创建默认配置
if (config == null)
{
Debug.LogWarning("[MemoryDecryptorMode] 未设置配置文件,使用默认配置");
config = MemoryDecryptorConfigExample.CreateDefaultConfig();
}
// ... 其他初始化
}
```
## 🎯 测试流程
### 零配置测试
1. **创建场景对象**
```
LanguageAnalysisManager
└── MemoryDecryptorMode (什么都不设置!)
├── MemoryDecryptorRenderer
└── MemoryDecryptorInputHandler
```
2. **连接 UI 组件**(只需要连接 UI 引用)
3. **运行测试**
```yarn
<<enter_language_analysis>>
<<memory_decryptor_start>>
```
4. **开始游戏**
- 频率调到 90Hz
- 解码器条拖到 (3, 5) 位置
- 点击验证 → 成功!
## 📊 配置对比
| 方式 | 配置步骤 | 自定义程度 | 适用场景 |
|------|---------|-----------|----------|
| **自动配置** | 0 步 | ❌ 固定 | 快速测试、原型验证 |
| **手动配置** | 3-5 步 | ✅ 完全自定义 | 正式关卡、特殊需求 |
## 🔧 何时使用自定义配置
建议在以下情况创建自定义配置:
- ✅ 需要不同的笑话内容
- ✅ 需要不同的目标位置和词汇
- ✅ 需要调整网格大小
- ✅ 需要自定义视觉风格
- ✅ 制作正式关卡
## ⚙️ 自定义配置示例
```csharp
// 创建自己的配置
var myConfig = ScriptableObject.CreateInstance<MemoryDecryptorConfig>();
myConfig.gridWidth = 15; // 改为 15x15
myConfig.gridHeight = 15;
myConfig.completeJokes = new List<string>
{
"我的自定义笑话内容...",
// ...
};
// 保存为资源
#if UNITY_EDITOR
UnityEditor.AssetDatabase.CreateAsset(myConfig, "Assets/MyConfig.asset");
#endif
```
## 🎉 优势
1. **即插即用** - 添加组件就能用
2. **快速迭代** - 无需反复创建配置
3. **降低门槛** - 新手更容易上手
4. **灵活切换** - 测试用默认,正式用自定义
## ⚠️ 注意事项
1. **运行时配置**:自动创建的配置是运行时对象,不会保存到磁盘
2. **每次启动重新创建**:每次运行游戏都会重新创建默认配置
3. **持久化需求**:如果需要保存配置修改,请创建 ScriptableObject 资源
## 📝 日志提示
当使用自动配置时,Console 会显示:
```
[MemoryDecryptorMode] 未设置配置文件,使用默认配置
```
这是正常提示,不是错误!
---
**现在你可以直接添加组件开始测试,无需任何配置!** 🚀
@@ -1,12 +0,0 @@
fileFormatVersion: 2
<<<<<<<< HEAD:Assets/Scripts/MiniGame/HuoShan/Language/LanguageAnalysis/AUTO_CONFIG_FEATURE.md.meta
guid: 8e2ee6d30ac820343b7b75afc919d4ea
TextScriptImporter:
========
guid: b56282e2f6f919d4db3d569dc07134fc
PrefabImporter:
>>>>>>>> develop:Assets/UI/Prefabs/文字预制体.prefab.meta
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,228 +0,0 @@
# 语言分析系统 - 清晰架构说明
## ✅ 你应该使用的系统:LanguageAnalysisManager
### 系统位置
```
Assets/Scripts/MiniGame/HuoShan/Language/
└── LanguageAnalysis/
└── LanguageAnalysisManager.cs # ✅ 这是主系统!
```
### 使用方法
#### 在 Yarn 中
```yarn
// 进入语言分析系统
<<enter_language_analysis>>
// 启动记忆解密器
<<memory_decryptor_start Success>>
// 退出语言分析系统
<<exit_language_analysis>>
```
#### 在场景中
```
LanguageAnalysisManager
├── Language Analysis View (GameObject)
│ └── MemoryDecryptorMode
│ ├── Config: 留空即可(自动使用默认配置)
│ ├── MemoryDecryptorRenderer
│ └── MemoryDecryptorInputHandler
├── Language Analysis Camera
└── Language Analysis Deep Camera
```
## 📋 核心流程图
```
游戏开始
任意状态 (Clinic / BodyModule / etc.)
<<enter_language_analysis>> ← Yarn 命令
[FixStateMachine 切换状态]
FixState.LanguageAnalysis ← 进入此状态
LanguageAnalysisState.Enter()
- 切换相机
- 打开 Analysis View
- 淡入淡出效果
<<memory_decryptor_start>> ← Yarn 命令
LanguageAnalysisManager.StartMemoryDecryptor()
MemoryDecryptorMode.StartSystem()
- Boot 序列
- 加载网格
- 显示笑话
玩家操作界面
- 调整频率
- 拖动解码器
- 点击验证
成功/失败
Yarn 节点跳转 (Success / Success_Fail)
<<memory_decryptor_stop>> ← Yarn 命令
<<exit_language_analysis>> ← Yarn 命令
[FixStateMachine 切换状态]
返回之前的状态
```
## 🔑 关键组件
### 1. LanguageAnalysisManager (主系统)
**位置**: `LanguageAnalysis/LanguageAnalysisManager.cs`
**职责**:
- 管理语言分析视图
- 注册到 FixSystemCenter
- 注册相机
- 管理子系统(MemoryDecryptorMode
**公开方法**:
```csharp
void OpenView() // 打开视图
void CloseView() // 关闭视图
void StartMemoryDecryptor(string node) // 启动解密器
void StartMemoryDecryptorFiltering() // 开始过滤
void SetMemoryDecryptorFrequency(float f) // 设置频率
void CheckMemoryDecryptorMatch() // 检查匹配
```
### 2. LanguageAnalysisState (状态机状态)
**位置**: `FixSystemNew/FixStateMachine.cs`
**职责**:
- Enter: 切换相机、打开视图、淡入淡出
- Exit: 关闭视图、重置 Plug
### 3. MemoryDecryptorMode (解密器核心)
**位置**: `MemoryDecryptor/MemoryDecryptorMode.cs`
**职责**:
- 游戏逻辑
- 网格管理
- 频率调谐
- 匹配检测
### 4. Yarn 命令
**位置**:
- `LanguageAnalysis/LanguageAnalysisYarnCommands.cs` (状态切换)
- `MemoryDecryptor/MemoryDecryptorYarnCommands.cs` (解密器控制)
## 📝 调用链
### Yarn → Manager → Mode
```
<<enter_language_analysis>>
LanguageAnalysisYarnCommands.EnterLanguageAnalysis()
FixStateMachine.SwitchState(LanguageAnalysis)
LanguageAnalysisState.Enter()
LanguageAnalysisManager.OpenView()
---
<<memory_decryptor_start>>
MemoryDecryptorYarnCommands.StartMemoryDecryptor()
LanguageAnalysisManager.StartMemoryDecryptor()
MemoryDecryptorMode.StartSystem()
```
## ⚡ 快速配置(零配置模式)
### 最简场景设置
1. **创建 LanguageAnalysisManager 对象**
2. **添加子对象和组件**(参考 SETUP_GUIDE.md
3. **运行游戏**
4. **在 Yarn 中使用:**
```yarn
<<enter_language_analysis>>
<<memory_decryptor_start>>
```
就这么简单!✨
## ❌ 已移除的内容
从 ExpressionManager 中移除了:
- ❌ `AnalysisMode` 枚举
- ❌ `memoryDecryptorMode` 字段
- ❌ `SwitchMode()` 方法
- ❌ `SwitchToParticleMode()` 方法
- ❌ `SwitchToMemoryDecryptorMode()` 方法
- ❌ `StartMemoryDecryptor()` 方法
- ❌ `SetMemoryDecryptorFrequency()` 方法
- ❌ `StartMemoryDecryptorFiltering()` 方法
- ❌ `CheckMemoryDecryptorMatch()` 方法
**原因**: 记忆解密器现在完全属于 LanguageAnalysisManager
## ✅ 现在的清晰架构
```
ExpressionManager (表达系统)
├── 只管理粒子表达
└── 不再管理记忆解密器
LanguageAnalysisManager (语言分析系统)
├── 管理所有语言分析相关功能
└── 包含记忆解密器
```
## 🎯 使用建议
### 表达情绪时 → ExpressionManager
```yarn
<<open_expression>>
<<start_expression "焦虑短句" "目标句子">>
```
### 解密语言时 → LanguageAnalysisManager
```yarn
<<enter_language_analysis>>
<<memory_decryptor_start>>
```
## 📚 文档导航
**开始使用语言分析系统:**
1. 📖 `LanguageAnalysis/QUICK_REFERENCE.md` - 快速上手
2. 🔧 `LanguageAnalysis/SETUP_GUIDE.md` - 详细设置
3. 🧪 `LanguageAnalysis/TestLanguageAnalysis.yarn` - 测试示例
**了解记忆解密器:**
1. 📖 `MemoryDecryptor/README.md` - 详细说明
2. ⚡ `MemoryDecryptor/QUICKSTART.md` - 快速入门
3. ✨ `MemoryDecryptor/AUTO_CONFIG_FEATURE.md` - 自动配置
---
## 💡 记住
**只使用 LanguageAnalysisManager**
**ExpressionManager 已经不再包含记忆解密器了!**
**两个系统完全独立,不要混用!**
---
**🚀 开始使用:** `<<enter_language_analysis>>`
@@ -1,151 +0,0 @@
using UnityEngine;
using AibisDream.FixSystem;
using Cinemachine;
using AibisDream.Framework;
using AibisDream.MiniGame.Language.MemoryDecryptor;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 语言分析系统管理器
/// 独立的语言分析系统,包含记忆解密器等分析模式
/// </summary>
public class LanguageAnalysisManager : MonoBehaviour
{
[Header("系统视图")]
[SerializeField] private GameObject analysisView;
[Header("记忆解密器组件")]
private MemoryDecryptorMode memoryDecryptorMode;
void Start()
{
// 注册到系统字典
FixSystemCenter.SystemDic.Register(this);
// 查找视图
if (analysisView == null)
{
analysisView = transform.Find("Language Analysis View")?.gameObject;
}
if (analysisView != null)
{
analysisView.SetActive(false);
}
// 查找记忆解密器
if (memoryDecryptorMode == null)
{
memoryDecryptorMode = GetComponentInChildren<MemoryDecryptorMode>(true);
}
// 注册相机
var virtualCam = transform.Find("Language Analysis Camera")?.GetComponent<ICinemachineCamera>();
if (virtualCam != null)
{
CameraKit.Instance.RegisterCamera(CameraEnum.LanguageAnalysis, virtualCam);
}
var virtualCamDeep = transform.Find("Language Analysis Deep Camera")?.GetComponent<ICinemachineCamera>();
if (virtualCamDeep != null)
{
CameraKit.Instance.RegisterCamera(CameraEnum.LanguageAnalysisDeep, virtualCamDeep);
}
}
/// <summary>
/// 打开语言分析视图
/// </summary>
public void OpenView()
{
if (analysisView != null)
{
analysisView.SetActive(true);
}
}
/// <summary>
/// 关闭语言分析视图
/// </summary>
public void CloseView()
{
if (analysisView != null)
{
analysisView.SetActive(false);
}
StopAllSystems();
}
/// <summary>
/// 停止所有分析系统
/// </summary>
public void StopAllSystems()
{
if (memoryDecryptorMode != null)
{
memoryDecryptorMode.StopSystem();
}
}
#region
/// <summary>
/// 启动记忆解密器
/// </summary>
public void StartMemoryDecryptor(string completionNodeName = null)
{
if (memoryDecryptorMode != null)
{
memoryDecryptorMode.StartSystem(completionNodeName);
}
else
{
Debug.LogWarning("[LanguageAnalysisManager] 记忆解密器未设置!");
}
}
/// <summary>
/// 开始过滤
/// </summary>
public void StartMemoryDecryptorFiltering()
{
if (memoryDecryptorMode != null)
{
memoryDecryptorMode.StartFiltering();
}
}
/// <summary>
/// 设置频率
/// </summary>
public void SetMemoryDecryptorFrequency(float frequency)
{
if (memoryDecryptorMode != null)
{
memoryDecryptorMode.SetFrequency(frequency);
}
}
/// <summary>
/// 检查匹配
/// </summary>
public void CheckMemoryDecryptorMatch()
{
if (memoryDecryptorMode != null)
{
memoryDecryptorMode.CheckMatch();
}
}
#endregion
private void OnDestroy()
{
CameraKit.Instance?.UnRegisterCamera(CameraEnum.LanguageAnalysis);
CameraKit.Instance?.UnRegisterCamera(CameraEnum.LanguageAnalysisDeep);
}
}
}
@@ -1,78 +0,0 @@
using System.Collections;
using UnityEngine;
using Yarn.Unity;
using AibisDream.FixSystem;
using AibisDream.Framework;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 语言分析系统 Yarn 命令
/// </summary>
public static class LanguageAnalysisYarnCommands
{
private static FixStateMachine FixStateMachine => FixSystemCenter.SystemDic.Get<FixStateMachine>();
/// <summary>
/// 进入语言分析系统状态
/// <<enter_language_analysis>>
/// </summary>
[YarnCommand("enter_language_analysis")]
public static void EnterLanguageAnalysis()
{
if (FixStateMachine == null)
{
Debug.LogError("[LanguageAnalysis] FixStateMachine 未找到!");
return;
}
// 通过协程切换状态
MonoBehaviourHelper.Instance.StartCoroutine(SwitchToLanguageAnalysisState());
}
/// <summary>
/// 退出语言分析系统状态
/// <<exit_language_analysis>>
/// </summary>
[YarnCommand("exit_language_analysis")]
public static void ExitLanguageAnalysis()
{
if (FixStateMachine == null)
{
Debug.LogError("[LanguageAnalysis] FixStateMachine 未找到!");
return;
}
// 切换回诊所状态或其他默认状态
MonoBehaviourHelper.Instance.StartCoroutine(FixStateMachine.SwitchState(FixState.Clinic));
}
private static IEnumerator SwitchToLanguageAnalysisState()
{
yield return FixStateMachine.SwitchState(FixState.LanguageAnalysis);
}
}
/// <summary>
/// MonoBehaviour 辅助类,用于启动协程
/// </summary>
public class MonoBehaviourHelper : MonoBehaviour
{
private static MonoBehaviourHelper _instance;
public static MonoBehaviourHelper Instance
{
get
{
if (_instance == null)
{
GameObject go = new GameObject("MonoBehaviourHelper");
_instance = go.AddComponent<MonoBehaviourHelper>();
DontDestroyOnLoad(go);
}
return _instance;
}
}
}
}
@@ -1,147 +0,0 @@
# 语言分析系统 - 快速参考卡
## 🚀 一句话总结
语言分析系统是独立的 FixState,包含记忆解密器等分析模式,通过 `<<enter_language_analysis>>` 进入。
## 📋 快速命令
### 进入/退出系统
```yarn
<<enter_language_analysis>> # 进入语言分析状态
<<exit_language_analysis>> # 退出语言分析状态
```
### 记忆解密器
```yarn
<<memory_decryptor_start SuccessNode>> # 启动
<<memory_decryptor_filter_start>> # 开始过滤
<<memory_decryptor_set_frequency 90>> # 设置频率
<<memory_decryptor_check>> # 检查匹配
<<memory_decryptor_stop>> # 停止
```
## 🎯 最简测试
```yarn
title: Test
---
<<enter_language_analysis>>
<<memory_decryptor_start Success>>
===
title: Success
---
成功!
<<memory_decryptor_stop>>
<<exit_language_analysis>>
===
title: Success_Fail
---
失败,再试试。
===
```
## 🔧 场景设置
```
LanguageAnalysisManager
├── Language Analysis View (视图)
│ ├── Canvas (UI)
│ └── MemoryDecryptorMode (脚本 + Config)
├── Language Analysis Camera
└── Language Analysis Deep Camera
```
## 📝 配置清单
- [ ] LanguageAnalysisManager 添加到场景
- [ ] 配置 Analysis View
- [ ] 两个相机设置完成
- [ ] MemoryDecryptorMode 已添加(配置文件可选,自动使用默认)
- [ ] UI 组件已连接
## 🎮 状态流程
```
FixState.Clinic
↓ <<enter_language_analysis>>
FixState.LanguageAnalysis
↓ <<memory_decryptor_start>>
记忆解密器运行中
↓ 成功/失败
Yarn 节点反馈
↓ <<exit_language_analysis>>
FixState.Clinic
```
## ⚡ 常用代码
### C# 获取管理器
```csharp
var manager = FixSystemCenter.SystemDic.Get<LanguageAnalysisManager>();
```
### C# 状态切换
```csharp
yield return FixStateMachine.SwitchState(FixState.LanguageAnalysis);
```
### Yarn 完整示例
```yarn
title: Start
---
准备进入语言分析系统...
<<enter_language_analysis>>
<<memory_decryptor_start GameSuccess>>
现在你可以操作了。
===
title: GameSuccess
---
🎉 解密成功!
<<memory_decryptor_stop>>
<<exit_language_analysis>>
继续游戏...
===
title: GameSuccess_Fail
---
❌ 失败了
-> 再试一次
继续...
-> 放弃
<<memory_decryptor_stop>>
<<exit_language_analysis>>
===
```
## 🐛 快速调试
### 检查系统
```csharp
Debug.Log(FixSystemCenter.SystemDic.Get<LanguageAnalysisManager>() != null);
```
### 检查状态
```csharp
Debug.Log(FixStateMachine.CurState);
```
### 检查相机
```csharp
Debug.Log(CameraKit.Instance.GetCamera(CameraEnum.LanguageAnalysis) != null);
```
## 📚 文档索引
- **详细设置**: `SETUP_GUIDE.md`
- **重构说明**: `REFACTORING_SUMMARY.md`
- **测试对话**: `TestLanguageAnalysis.yarn`
- **记忆解密器**: `../MemoryDecryptor/README.md`
---
**记住:先 `enter` → 再 `start` → 最后 `exit`** ✨
@@ -1,252 +0,0 @@
# 语言分析系统 (Language Analysis System)
## 📖 概述
语言分析系统是一个独立的游戏系统,通过 `FixStateMachine` 进行状态管理。它包含记忆解密器等多种分析模式,专注于语言相关的解谜玩法。
## 🎯 系统特点
- ✅ **独立系统** - 完全独立于 ExpressionManager
- ✅ **状态管理** - 集成到 FixStateMachine
- ✅ **易于测试** - 可单独进入测试
- ✅ **易于扩展** - 支持多种分析模式
## 📂 文件结构
```
LanguageAnalysis/
├── LanguageAnalysisManager.cs # 主管理器
├── LanguageAnalysisYarnCommands.cs # Yarn 状态切换命令
├── README.md # 本文档
├── QUICK_REFERENCE.md # 快速参考卡
├── SETUP_GUIDE.md # 详细设置指南
├── REFACTORING_SUMMARY.md # 重构说明
└── TestLanguageAnalysis.yarn # 测试对话
```
## 🚀 快速开始
### 1. Yarn 命令
```yarn
// 进入语言分析系统
<<enter_language_analysis>>
// 启动记忆解密器
<<memory_decryptor_start SuccessNode>>
// 退出语言分析系统
<<exit_language_analysis>>
```
### 2. 最简测试
```yarn
title: Test
---
<<enter_language_analysis>>
<<memory_decryptor_start Success>>
===
title: Success
---
成功!
<<memory_decryptor_stop>>
<<exit_language_analysis>>
===
```
## 📋 文档索引
| 文档 | 用途 | 适合人群 |
|------|------|----------|
| `QUICK_REFERENCE.md` | 快速查询命令和用法 | 所有人 |
| `SETUP_GUIDE.md` | 详细的场景设置步骤 | 首次使用者 |
| `REFACTORING_SUMMARY.md` | 重构说明和对比 | 了解架构变更 |
| `TestLanguageAnalysis.yarn` | 测试对话示例 | 测试功能 |
## 🎮 核心组件
### LanguageAnalysisManager
主管理器,负责:
- 系统视图管理
- 相机注册
- 各分析模式的启动和停止
### LanguageAnalysisState
FixState 状态,负责:
- 状态进入时的场景切换
- 相机动画
- 状态退出时的清理
### Yarn Commands
- `enter_language_analysis` - 进入状态
- `exit_language_analysis` - 退出状态
- `memory_decryptor_*` - 记忆解密器命令
## 🔧 技术细节
### 状态机集成
```csharp
public enum FixState
{
// ...
LanguageAnalysis, // 语言分析系统
}
public struct LanguageAnalysisState : IState<FixState>
{
// Enter: 切换相机,打开视图
// Exit: 关闭视图,重置状态
}
```
### 系统注册
```csharp
// 自动注册到 FixSystemCenter
FixSystemCenter.SystemDic.Register(this);
```
### 相机管理
```csharp
public enum CameraEnum
{
LanguageAnalysis, // 主相机
LanguageAnalysisDeep, // 深度相机
}
```
## 📝 使用示例
### 完整流程
```yarn
title: LanguageAnalysisDemo
---
玩家:我想分析这段记忆...
<<enter_language_analysis>>
系统进入中...
<<memory_decryptor_start Demo_Success>>
现在你可以:
1. 调整情绪频率滑块
2. 拖动解码器条
3. 点击"验证对齐"按钮
===
title: Demo_Success
---
🎉 解密成功!
你发现了隐藏的情绪:
"[临检]要来了,不合格[英里]被骂了,这让我很[担心]"
<<memory_decryptor_stop>>
<<exit_language_analysis>>
返回正常状态...
===
title: Demo_Success_Fail
---
❌ 解密失败
频率或位置不太对...
-> 再试一次
<<jump LanguageAnalysisDemo>>
-> 放弃
<<memory_decryptor_stop>>
<<exit_language_analysis>>
算了,先做别的...
===
```
## 🐛 常见问题
### Q: Unity 提示找不到 LanguageAnalysisManager
A: 保存文件后等待 Unity 重新编译。如果问题持续,尝试重启 Unity。
### Q: 进入状态后看不到界面?
A: 检查 Analysis View 是否已设置且激活。
### Q: 相机不切换?
A: 确保两个 Virtual Camera 已正确注册到 CameraKit。
### Q: Yarn 命令不工作?
A: 确保 LanguageAnalysisManager 已添加到场景并注册到 SystemDic。
## 🎨 分析模式
### 当前支持
#### 记忆解密器 (Memory Decryptor)
- 复古 CRT 显示风格
- 频率调谐解谜
- 解码器条拖动
- 笑话/情绪识别
### 未来扩展
可以添加更多分析模式:
- 语法分析器
- 语义地图
- 情绪图谱
- ...
## 📊 系统流程图
```
游戏开始
Clinic / 其他状态
↓ <<enter_language_analysis>>
进入 LanguageAnalysisState
↓ State.Enter()
- 切换到 BodyModule (Head)
- 打开 Analysis View
- 相机动画 (Analysis → AnalysisDeep)
↓ <<memory_decryptor_start>>
记忆解密器 Boot 序列
解密器运行中(玩家操作)
↓ 成功/失败
Yarn 节点反馈
↓ <<exit_language_analysis>>
退出 LanguageAnalysisState
↓ State.Exit()
- 关闭视图
- 重置 Plug
返回 Clinic / 其他状态
```
## ✅ 设置清单
使用前确保:
- [ ] LanguageAnalysisManager 已添加到场景
- [ ] Language Analysis View 已配置
- [ ] 两个 Virtual Camera 已设置
- [ ] MemoryDecryptorMode 及其子组件已配置
- [ ] 配置文件已创建并设置
- [ ] 已测试基本功能
## 🚀 下一步
1. 阅读 `QUICK_REFERENCE.md` 了解常用命令
2. 按照 `SETUP_GUIDE.md` 设置场景
3. 使用 `TestLanguageAnalysis.yarn` 测试
4. 开始创建你的语言分析关卡!
---
**Need Help?** 查看 `SETUP_GUIDE.md` 获取详细步骤
**Quick Start?** 查看 `QUICK_REFERENCE.md` 快速上手
@@ -1,287 +0,0 @@
# 语言分析系统 - 重构总结
## 📅 重构日期:2025-12-04
## 🎯 重构目标
将记忆解密器从 `ExpressionManager` 中分离出来,创建独立的 `LanguageAnalysisManager` 系统,并集成到 `FixStateMachine` 状态机中。
## 🔄 架构变更
### 旧架构 ❌
```
ExpressionManager
├── AnalysisMode 枚举 (Particle / MemoryDecryptor)
├── LanguageParticleManager
└── MemoryDecryptorMode
问题:
- 职责不清晰
- 两个完全不同的系统耦合在一起
- 无法独立测试记忆解密器
- 状态切换逻辑混乱
```
### 新架构 ✅
```
FixStateMachine
├── FixState.Expression → ExpressionManager
│ └── LanguageParticleManager (粒子表达系统)
└── FixState.LanguageAnalysis → LanguageAnalysisManager
└── MemoryDecryptorMode (记忆解密器)
└── 未来可扩展其他分析模式...
优势:
- ✅ 职责清晰分离
- ✅ 独立的状态管理
- ✅ 可以单独测试
- ✅ 符合项目架构规范
```
## 📦 新增文件
### 核心系统
```
Assets/Scripts/MiniGame/HuoShan/Language/LanguageAnalysis/
├── LanguageAnalysisManager.cs # 主管理器
├── LanguageAnalysisYarnCommands.cs # Yarn 状态切换命令
├── LanguageAnalysisState.cs # (已集成到 FixStateMachine.cs)
├── SETUP_GUIDE.md # 设置指南
├── REFACTORING_SUMMARY.md # 本文档
└── TestLanguageAnalysis.yarn # 测试对话
```
### 修改的文件
```
Assets/Scripts/FixSystemNew/FixStateMachine.cs
├── 新增 FixState.LanguageAnalysis 枚举
└── 新增 LanguageAnalysisState 结构体
Assets/Scripts/Framework/OtherKit/CameraKit.cs
├── 新增 CameraEnum.LanguageAnalysis
└── 新增 CameraEnum.LanguageAnalysisDeep
Assets/Scripts/MiniGame/HuoShan/Language/MemoryDecryptor/
└── MemoryDecryptorYarnCommands.cs
└── 从 ExpressionManager 改为 LanguageAnalysisManager
```
## 🎮 Yarn 命令变更
### 新增命令(状态切换)
| 命令 | 功能 | 示例 |
|------|------|------|
| `<<enter_language_analysis>>` | 进入语言分析系统状态 | `<<enter_language_analysis>>` |
| `<<exit_language_analysis>>` | 退出语言分析系统状态 | `<<exit_language_analysis>>` |
### 记忆解密器命令(无变化)
| 命令 | 功能 | 示例 |
|------|------|------|
| `<<memory_decryptor_start>>` | 启动记忆解密器 | `<<memory_decryptor_start Success>>` |
| `<<memory_decryptor_filter_start>>` | 开始过滤 | `<<memory_decryptor_filter_start>>` |
| `<<memory_decryptor_set_frequency>>` | 设置频率 | `<<memory_decryptor_set_frequency 90>>` |
| `<<memory_decryptor_check>>` | 检查匹配 | `<<memory_decryptor_check>>` |
| `<<memory_decryptor_stop>>` | 停止解密器 | `<<memory_decryptor_stop>>` |
### 移除的命令
| 旧命令 | 原因 | 替代方案 |
|--------|------|----------|
| `<<switch_to_particle_mode>>` | 系统分离,不再需要 | 使用状态机切换 |
| `<<switch_to_memory_decryptor_mode>>` | 系统分离,不再需要 | 使用 `enter_language_analysis` |
## 📝 使用流程对比
### 旧流程 ❌
```yarn
// 复杂且不清晰
<<switch_to_memory_decryptor_mode>>
<<memory_decryptor_start>>
// ...
<<switch_to_particle_mode>>
```
### 新流程 ✅
```yarn
// 清晰的状态切换
<<enter_language_analysis>>
<<memory_decryptor_start Success>>
// 玩家操作...
title: Success
---
<<memory_decryptor_stop>>
<<exit_language_analysis>>
===
```
## 🔧 技术改进
### 1. 状态机集成
```csharp
// ✅ 新增 LanguageAnalysisState
public struct LanguageAnalysisState : IState<FixState>
{
public IEnumerator Enter()
{
// 1. 切换到 BodyModule (Head)
// 2. 打开视图
// 3. 相机切换 (LanguageAnalysis → LanguageAnalysisDeep)
// 4. 淡入淡出效果
}
public IEnumerator Exit()
{
// 1. 关闭视图
// 2. 重置 Plug
}
}
```
### 2. 系统注册
```csharp
// ✅ LanguageAnalysisManager 自动注册到 SystemDic
void Start()
{
FixSystemCenter.SystemDic.Register(this);
}
```
### 3. 相机管理
```csharp
// ✅ 新增相机枚举
public enum CameraEnum
{
// ... 其他相机
LanguageAnalysis,
LanguageAnalysisDeep,
}
```
## 📊 对比总结
| 方面 | 旧架构 | 新架构 |
|------|--------|--------|
| **职责** | 混合在一起 | 清晰分离 |
| **状态管理** | 内部枚举 | FixStateMachine |
| **独立测试** | ❌ 困难 | ✅ 简单 |
| **相机** | 共用 Expression 相机 | 独立的 LanguageAnalysis 相机 |
| **Yarn 命令** | 6个 | 7个(2个新状态命令 + 5个解密器命令) |
| **扩展性** | ❌ 受限 | ✅ 易于扩展 |
| **符合规范** | ❌ 特殊处理 | ✅ 符合项目架构 |
## 🎯 迁移指南
### 如果你之前使用了记忆解密器
#### 步骤 1:更新 Yarn 对话
```yarn
# 旧代码
<<switch_to_memory_decryptor_mode>>
<<memory_decryptor_start>>
# 新代码
<<enter_language_analysis>>
<<memory_decryptor_start>>
```
#### 步骤 2:创建新的场景设置
按照 `SETUP_GUIDE.md` 创建 `LanguageAnalysisManager`
#### 步骤 3:移除旧的 ExpressionManager 引用
- 不再需要在 ExpressionManager 中配置记忆解密器
- 记忆解密器相关代码已移至 LanguageAnalysisManager
#### 步骤 4:测试
使用 `TestLanguageAnalysis.yarn` 验证功能
## ✅ 测试清单
### 功能测试
- [ ] 进入语言分析状态 `<<enter_language_analysis>>`
- [ ] 相机切换正常
- [ ] 启动记忆解密器 `<<memory_decryptor_start>>`
- [ ] Boot 序列正常播放
- [ ] 网格数据加载正常
- [ ] 频率调整正常
- [ ] 解码器条拖动正常
- [ ] 匹配检测正常
- [ ] 成功节点跳转正常
- [ ] 失败节点跳转正常
- [ ] 停止解密器 `<<memory_decryptor_stop>>`
- [ ] 退出语言分析状态 `<<exit_language_analysis>>`
### 状态机测试
- [ ] 从 Clinic 切换到 LanguageAnalysis
- [ ] 从 LanguageAnalysis 切换回 Clinic
- [ ] 状态切换时的淡入淡出效果
- [ ] Plug 重置正常
- [ ] UI 显示/隐藏正常
### 集成测试
- [ ] 与其他状态切换无冲突
- [ ] Expression 状态独立工作正常
- [ ] 不影响其他系统
## 🚀 未来扩展
有了独立的 `LanguageAnalysisManager`,未来可以轻松添加更多分析模式:
```csharp
public class LanguageAnalysisManager : MonoBehaviour
{
private MemoryDecryptorMode memoryDecryptorMode;
private SyntaxAnalyzerMode syntaxAnalyzerMode; // 🆕 未来扩展
private SemanticMapMode semanticMapMode; // 🆕 未来扩展
private EmotionGraphMode emotionGraphMode; // 🆕 未来扩展
public void StartSyntaxAnalyzer() { ... }
public void StartSemanticMap() { ... }
public void StartEmotionGraph() { ... }
}
```
## 📞 问题排查
### 常见问题
**Q: 进入状态后看不到界面?**
A: 检查 Analysis View 是否激活,相机是否正确注册
**Q: Yarn 命令不工作?**
A: 检查 LanguageAnalysisManager 是否注册到 SystemDic
**Q: 状态切换失败?**
A: 检查 FixStateMachine 是否存在,查看 Console 日志
**Q: 相机不切换?**
A: 检查 CameraEnum 是否添加了新枚举值
## 🎉 总结
通过这次重构:
1. ✅ **职责清晰** - Expression 和 LanguageAnalysis 完全分离
2. ✅ **易于测试** - 可以独立进入语言分析状态测试
3. ✅ **符合规范** - 使用 FixStateMachine 统一管理
4. ✅ **易于扩展** - 未来可添加更多分析模式
5. ✅ **代码质量** - 更清晰的架构和命名
---
**现在你可以通过 `<<enter_language_analysis>>` 独立测试语言分析系统了!** 🚀
@@ -1,272 +0,0 @@
# 语言分析系统 - 设置指南
## 🎯 概述
语言分析系统是一个独立的游戏系统,通过 `FixStateMachine` 管理状态切换。它包含记忆解密器等分析模式,与 `ExpressionManager` 完全分离。
## 📦 新架构
```
FixStateMachine (状态机)
├── FixState.LanguageAnalysis (新增状态)
LanguageAnalysisManager (独立系统管理器)
├── MemoryDecryptorMode (记忆解密器)
│ ├── MemoryDecryptorRenderer
│ └── MemoryDecryptorInputHandler
└── 未来可扩展其他分析模式...
ExpressionManager (保持独立)
└── LanguageParticleManager (粒子系统)
```
## 🚀 快速设置步骤
### 1. 在场景中创建系统
```
Hierarchy:
├── GameSystems (或其他容器)
│ └── LanguageAnalysisManager
│ ├── Language Analysis View (GameObject - 视图容器)
│ │ ├── Canvas
│ │ │ ├── GridContainer (RectTransform)
│ │ │ ├── DecoderStripContainer (RectTransform)
│ │ │ └── ControlPanel
│ │ │ ├── FrequencyDisplay (TMP)
│ │ │ ├── FrequencySlider (Slider)
│ │ │ ├── FilterButton (Button)
│ │ │ ├── VerifyButton (Button)
│ │ │ ├── SignalBar (RectTransform)
│ │ │ └── LogText (TMP)
│ │ └── MemoryDecryptorMode (脚本)
│ │ ├── MemoryDecryptorRenderer (脚本)
│ │ └── MemoryDecryptorInputHandler (脚本)
│ ├── Language Analysis Camera (Virtual Camera)
│ └── Language Analysis Deep Camera (Virtual Camera)
```
### 2. 配置 LanguageAnalysisManager
```
LanguageAnalysisManager (脚本)
└── Analysis View: 拖入 "Language Analysis View" GameObject
```
### 3. 配置相机枚举
已自动添加到 `CameraEnum`
- `LanguageAnalysis` - 主相机
- `LanguageAnalysisDeep` - 深度相机
### 4. 创建配置文件(可选)
**✨ 推荐:无需创建**
系统会自动使用默认配置,包含完整的示例数据和目标。
**自定义配置:**
右键 → Create → AibisDream → Language → Memory Decryptor Config
配置内容参考 `MemoryDecryptorConfigExample.cs`
### 5. 配置子组件
#### MemoryDecryptorMode
- Config: 【可选】不设置则自动使用默认配置
#### MemoryDecryptorRenderer
- Grid Container
- Decoder Strip Container
- Frequency Display
- Log Text
- Signal Bar
#### MemoryDecryptorInputHandler
- Grid Container
- Frequency Slider
- Filter Button
- Verify Button
- Grid Cell Size: 45
- Grid Cell Gap: 2
## 📋 FixState 状态机集成
### 新增状态
```csharp
public enum FixState
{
// ... 其他状态
LanguageAnalysis, // ✅ 新增
}
```
### LanguageAnalysisState
```csharp
public struct LanguageAnalysisState : IState<FixState>
{
public IEnumerator Enter()
{
// 1. 切换到 BodyModule (Head)
// 2. 打开语言分析视图
// 3. 切换相机 LanguageAnalysis → LanguageAnalysisDeep
// 4. 淡入淡出效果
}
public IEnumerator Exit()
{
// 1. 关闭语言分析视图
// 2. 重置 Plug
}
}
```
## 🎮 Yarn 命令使用
### 状态切换命令
```yarn
// 进入语言分析系统状态
<<enter_language_analysis>>
// 退出语言分析系统状态
<<exit_language_analysis>>
```
### 记忆解密器命令
```yarn
// 在语言分析状态下使用:
// 启动记忆解密器
<<memory_decryptor_start SuccessNode>>
// 开始过滤
<<memory_decryptor_filter_start>>
// 设置频率
<<memory_decryptor_set_frequency 90>>
// 检查匹配
<<memory_decryptor_check>>
// 停止解密器
<<memory_decryptor_stop>>
```
### 完整测试流程
```yarn
title: Test
---
<<enter_language_analysis>>
<<memory_decryptor_start TestSuccess>>
// 玩家操作...
===
title: TestSuccess
---
成功!
<<memory_decryptor_stop>>
<<exit_language_analysis>>
===
```
## 📝 测试清单
### 场景设置
- [ ] LanguageAnalysisManager 已添加到场景
- [ ] Analysis View 已配置
- [ ] 两个 Virtual Camera 已设置
- [ ] 相机已注册到 CameraKit
### 组件配置
- [ ] MemoryDecryptorMode 配置文件已设置
- [ ] MemoryDecryptorRenderer UI 组件已连接
- [ ] MemoryDecryptorInputHandler UI 组件已连接
### 系统注册
- [ ] LanguageAnalysisManager 已注册到 FixSystemCenter.SystemDic
- [ ] FixStateMachine 已获取
### Yarn 测试
- [ ] `<<enter_language_analysis>>` 可以进入系统
- [ ] 相机切换正常
- [ ] UI 显示正常
- [ ] `<<memory_decryptor_start>>` 可以启动解密器
- [ ] 频率调整正常
- [ ] 解码器条拖动正常
- [ ] 匹配检测正常
- [ ] 成功/失败节点跳转正常
- [ ] `<<exit_language_analysis>>` 可以退出系统
## 🔧 调试技巧
### 1. 检查系统注册
```csharp
var manager = FixSystemCenter.SystemDic.Get<LanguageAnalysisManager>();
Debug.Log($"LanguageAnalysisManager: {manager != null}");
```
### 2. 检查状态切换
在 Console 中查看状态切换日志:
```
[FixStateMachine] SwitchState: LanguageAnalysis
```
### 3. 检查相机注册
```csharp
var cam = CameraKit.Instance.GetCamera(CameraEnum.LanguageAnalysis);
Debug.Log($"Camera registered: {cam != null}");
```
### 4. 使用测试 Yarn 文件
导入 `TestLanguageAnalysis.yarn` 进行快速测试
## 📚 与原系统的区别
| 方面 | ExpressionManager | LanguageAnalysisManager |
|------|-------------------|-------------------------|
| **职责** | 粒子表达系统 | 语言分析系统 |
| **状态** | FixState.Expression | FixState.LanguageAnalysis |
| **相机** | Expression / ExpressionDeep | LanguageAnalysis / LanguageAnalysisDeep |
| **切换** | 内部模式切换 | 独立状态切换 |
| **Yarn** | `open_expression`, `start_expression` | `enter_language_analysis`, `memory_decryptor_start` |
## 🎯 优势
1. **职责清晰** - 表达系统和分析系统完全分离
2. **独立测试** - 可以单独进入语言分析状态进行测试
3. **状态管理** - 通过 FixStateMachine 统一管理
4. **易于扩展** - 未来可以添加更多分析模式
5. **符合架构** - 与项目其他系统保持一致的状态机模式
## 🚧 注意事项
1. **相机设置** - 确保两个 Virtual Camera 的优先级和设置正确
2. **状态切换** - 通过 `enter_language_analysis` 进入,不要直接调用 `OpenView`
3. **退出流程** - 使用 `exit_language_analysis` 确保正确清理
4. **测试模式** - 建议先使用 `TestLanguageAnalysis.yarn` 验证基础功能
## 📞 快速排查
### 问题:进入状态后看不到界面
- 检查 Analysis View 是否被激活
- 检查 Canvas 设置是否正确
- 检查相机切换是否成功
### 问题:Yarn 命令不工作
- 检查 LanguageAnalysisManager 是否注册到 SystemDic
- 检查 FixStateMachine 是否存在
- 查看 Console 错误日志
### 问题:相机不切换
- 检查 CameraEnum 是否添加了新枚举
- 检查 Virtual Camera 是否正确注册
- 检查 CameraKit.Instance 是否存在
---
**设置完成后,使用 `<<enter_language_analysis>>` 命令即可测试整个系统!**
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 4bef7b4f03b230d47b2ed231b8526f65
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,69 +0,0 @@
title: TestLanguageAnalysis
---
欢迎测试语言分析系统!
-> 进入语言分析系统
<<enter_language_analysis>>
进入语言分析系统状态...
<<jump TestMemoryDecryptor>>
-> 退出
再见!
===
title: TestMemoryDecryptor
---
现在你在语言分析系统中。
-> 启动记忆解密器
<<memory_decryptor_start TestSuccess>>
记忆解密器已启动。
请在界面上操作...
-> 退出语言分析系统
<<exit_language_analysis>>
已退出语言分析系统。
<<jump End>>
===
title: TestSuccess
---
🎉 成功解密!
<<memory_decryptor_stop>>
你解密出了隐藏的情绪记忆。
-> 再来一次
<<memory_decryptor_start TestSuccess>>
<<jump TestMemoryDecryptor>>
-> 退出
<<exit_language_analysis>>
<<jump End>>
===
title: TestSuccess_Fail
---
❌ 匹配失败
似乎不太对...
也许需要:
1. 调整频率
2. 移动解码器条
3. 重新验证
-> 再试一次
继续尝试吧!
<<jump TestMemoryDecryptor>>
-> 放弃
<<memory_decryptor_stop>>
<<exit_language_analysis>>
<<jump End>>
===
title: End
---
测试结束!
===
@@ -132,6 +132,64 @@ namespace AibisDream.MiniGame.Language
ExpressionManager.OpenView();
StartExpression(anxietyPhrasesStr, targetSentence, completionNode);
}
#region
/// <summary>
/// 启动分析模式(记忆解密游戏)
/// <<start_analysis_mode>>
/// 或
/// <<start_analysis_mode "CompletionNodeName">>
/// 或
/// <<start_analysis_mode "临检要来了|不合格英里被骂了|我担心业绩会下滑" "AnalysisComplete">>
/// </summary>
/// <param name="anxietyPhrasesStr">焦虑短句,用 | 分隔(可选,为空时使用默认配置)</param>
/// <param name="completionNode">完成时触发的对话节点(可选)</param>
[YarnCommand("start_analysis_mode")]
public static void StartAnalysisMode(string anxietyPhrasesStr = "", string completionNode = "")
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!请确保场景中有 ExpressionManager 组件。");
return;
}
// 解析焦虑短句(用 | 分隔)
List<string> phrases = null;
if (!string.IsNullOrEmpty(anxietyPhrasesStr))
{
string[] phrasesArray = anxietyPhrasesStr.Split('|');
phrases = new List<string>();
for (int i = 0; i < phrasesArray.Length; i++)
{
string phrase = phrasesArray[i].Trim();
if (!string.IsNullOrEmpty(phrase))
{
phrases.Add(phrase);
}
}
}
ExpressionManager.StartAnalysisMode(phrases, completionNode);
}
/// <summary>
/// 关闭分析模式
/// <<close_analysis_mode>>
/// </summary>
[YarnCommand("close_analysis_mode")]
public static void CloseAnalysisMode()
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
return;
}
ExpressionManager.CloseAnalysisMode();
}
#endregion
}
}
@@ -1,299 +0,0 @@
# 记忆解密器 - 最新改动说明
## 📅 更新日期:2025-12-04
## 🔄 主要改动
### 最新更新:使用 [YarnCommand] 特性注册
**改动内容**
- `MemoryDecryptorYarnCommands``MonoBehaviour` 改为静态类
- 使用 `[YarnCommand]` 特性自动注册命令
- 无需添加到场景中,无需手动管理生命周期
**代码变化**
```csharp
// 旧方式 - MonoBehaviour
public class MemoryDecryptorYarnCommands : MonoBehaviour
{
private void Awake() { RegisterCommands(); }
private void OnDestroy() { RemoveCommands(); }
}
// 新方式 - 静态类 + [YarnCommand]
public static class MemoryDecryptorYarnCommands
{
[YarnCommand("memory_decryptor_start")]
public static void StartMemoryDecryptor(string completionNodeName = "") { ... }
}
```
**优点**
- ✅ 无需添加组件到场景
- ✅ 自动管理生命周期
- ✅ 代码更简洁(从 199 行减少到 124 行)
- ✅ 与 `LanguageYarnCommand` 保持一致的风格
---
## 🔄 之前的改动
### 1. DialogController 集成
**改动内容**
- 所有 `DialogueRunner` 引用现在通过 `DialogController.Instance` 自动获取
- 移除了手动配置 `DialogueRunner` 的需求
**影响的文件**
- `MemoryDecryptorYarnCommands.cs`
- `MemoryDecryptorMode.cs`
**代码变化**
```csharp
// 旧方式
[SerializeField] private DialogueRunner dialogueRunner;
// 新方式
private DialogueRunner dialogueRunner;
private void Awake()
{
// 通过 DialogController 获取
if (DialogController.Instance != null)
{
dialogueRunner = DialogController.Instance.GetComponentInChildren<DialogueRunner>();
}
}
```
**优点**
- ✅ 符合项目架构规范
- ✅ 无需手动配置,减少人为错误
- ✅ 统一的对话系统访问方式
---
### 2. Inspector 面板简化
**改动内容**
- 隐藏了所有不需要手动配置的字段
- 只保留必须手动设置的 UI 组件引用和参数
- 内部组件引用改为自动查找
**影响的文件**
- `ExpressionManager.cs`
- `MemoryDecryptorMode.cs`
- `MemoryDecryptorRenderer.cs`
- `MemoryDecryptorInputHandler.cs`
- `MemoryDecryptorYarnCommands.cs`
**详细变化**
#### ExpressionManager.cs
```csharp
// 旧方式 - 需要手动配置
[SerializeField] private LanguageParticleManager particleManager;
[SerializeField] private MemoryDecryptorMode memoryDecryptorMode;
// 新方式 - 自动查找
private LanguageParticleManager particleManager;
private MemoryDecryptorMode memoryDecryptorMode;
```
#### MemoryDecryptorMode.cs
```csharp
// 只保留必须手动配置的
[Header("配置")]
[SerializeField] private MemoryDecryptorConfig config;
// 其他组件自动查找
private MemoryDecryptorRenderer renderer;
private DialogueRunner dialogueRunner;
```
#### MemoryDecryptorRenderer.cs
```csharp
// 必须手动配置的 UI 组件
[Header("UI 组件(必须配置)")]
[SerializeField] private RectTransform gridContainer;
[SerializeField] private RectTransform decoderStripContainer;
[SerializeField] private TMP_Text frequencyDisplay;
[SerializeField] private TMP_Text logText;
[SerializeField] private RectTransform signalBar;
// 不需要配置的内部变量
private GameObject gridCellPrefab;
private GameObject decoderStripPrefab;
private Material cellMaterial;
```
#### MemoryDecryptorInputHandler.cs
```csharp
// 必须手动配置的 UI 组件
[Header("UI 组件(必须配置)")]
[SerializeField] private RectTransform gridContainer;
[SerializeField] private Slider frequencySlider;
[SerializeField] private Button filterButton;
[SerializeField] private Button verifyButton;
// 自动查找的组件
private MemoryDecryptorMode mode;
private MemoryDecryptorRenderer renderer;
private GameObject decoderStrip;
```
#### MemoryDecryptorYarnCommands.cs
```csharp
// 完全无需手动配置
private DialogueRunner dialogueRunner;
private ExpressionManager expressionManager;
```
**优点**
- ✅ Inspector 面板更清爽,只显示必要的配置项
- ✅ 减少配置错误的可能性
- ✅ 提高易用性,新手更容易上手
- ✅ 遵循"约定优于配置"的原则
---
## 📋 配置对比
### 旧配置方式(配置项多)
```
MemoryDecryptorMode
├── Config: [必须配置]
├── Renderer: [必须配置]
└── Dialogue Runner: [必须配置]
MemoryDecryptorRenderer
├── Grid Container: [必须配置]
├── Decoder Strip Container: [必须配置]
├── Frequency Display: [必须配置]
├── Log Text: [必须配置]
├── Signal Bar: [必须配置]
├── Grid Cell Prefab: [可选]
├── Decoder Strip Prefab: [可选]
└── Cell Material: [可选]
MemoryDecryptorInputHandler
├── Mode: [必须配置]
├── Renderer: [必须配置]
├── Decoder Strip: [可选]
├── Frequency Slider: [必须配置]
├── Filter Button: [必须配置]
├── Verify Button: [必须配置]
├── Grid Container: [必须配置]
├── Grid Cell Size: [必须配置]
└── Grid Cell Gap: [必须配置]
MemoryDecryptorYarnCommands
└── Dialogue Runner: [必须配置]
ExpressionManager
├── Particle Manager: [必须配置]
└── Memory Decryptor Mode: [必须配置]
```
### 新配置方式(配置项少)✨
```
MemoryDecryptorMode
└── Config: [必须配置] ✅
MemoryDecryptorRenderer
├── Grid Container: [必须配置] ✅
├── Decoder Strip Container: [必须配置] ✅
├── Frequency Display: [必须配置] ✅
├── Log Text: [必须配置] ✅
└── Signal Bar: [必须配置] ✅
MemoryDecryptorInputHandler
├── Grid Container: [必须配置] ✅
├── Frequency Slider: [必须配置] ✅
├── Filter Button: [必须配置] ✅
├── Verify Button: [必须配置] ✅
├── Grid Cell Size: [必须配置] ✅
└── Grid Cell Gap: [必须配置] ✅
MemoryDecryptorYarnCommands
└── (无需配置,全自动)🎉
ExpressionManager
└── Expression View: [必须配置] ✅
```
**配置项减少**:从 20+ 项减少到 12 项!
---
## 🎯 迁移指南
如果你已经在使用旧版本的记忆解密器:
### 步骤 1:清理旧引用
1. 打开你的场景
2. 选择 `MemoryDecryptorYarnCommands`
3. 清空 `Dialogue Runner` 字段(它现在会自动获取)
### 步骤 2:清理 ExpressionManager
1. 选择 `ExpressionManager`
2. 清空 `Particle Manager``Memory Decryptor Mode` 字段
3. 确保这些组件是 `ExpressionManager` 的子对象(会自动查找)
### 步骤 3:清理 MemoryDecryptorMode
1. 选择 `MemoryDecryptorMode`
2. 清空 `Renderer``Dialogue Runner` 字段
3. 保留 `Config` 配置
### 步骤 4:清理 MemoryDecryptorInputHandler
1. 选择 `MemoryDecryptorInputHandler`
2. 清空 `Mode``Renderer``Decoder Strip` 字段
3. 保留所有 UI 组件引用
### 步骤 5:测试
1. 运行场景
2. 确认所有功能正常
3. 检查 Console 是否有错误信息
---
## ✅ 测试清单
- [ ] `MemoryDecryptorYarnCommands` 能正常注册命令
- [ ] Yarn 命令可以正常调用
- [ ] 解密器界面正常显示
- [ ] 频率滑块响应正常
- [ ] 解码器条可以拖动
- [ ] 匹配检测正常工作
- [ ] 成功/失败节点正常跳转
- [ ] 模式切换(粒子 ↔ 解密器)正常
---
## 📚 更新的文档
以下文档已同步更新:
- ✅ `README.md` - 使用说明
- ✅ `QUICKSTART.md` - 快速入门指南
- ✅ `IMPLEMENTATION_SUMMARY.md` - 实现总结
---
## 🐛 已知问题
目前没有已知问题。
---
## 💡 未来改进
1. 考虑添加自动配置向导
2. 添加配置验证工具
3. 提供预制件(Prefab)模板
---
**如有问题,请查看相关文档或提交 Issue。**
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 0bd92508448abe04f9784a679d614539
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,128 +0,0 @@
title: Start
---
欢迎来到记忆解密系统。
机器人的记忆被加密了,我们需要你的帮助来解密。
-> 开始解密
<<jump StartDecryption>>
-> 了解更多
这是一个频率调谐系统。
机器人用笑话来掩盖真实的情绪。
你需要调整情绪频率,找到隐藏的真相。
<<jump Start>>
===
title: StartDecryption
---
// 切换到记忆解密器模式
<<switch_to_memory_decryptor_mode>>
正在初始化解密系统...
<<memory_decryptor_start DecryptionSuccess>>
系统已准备就绪。
你会看到一个充满笑话碎片的网格。
但这些都是掩饰...真实的情绪被隐藏在其中。
-> 了解操作方法
<<jump Tutorial>>
-> 直接开始
<<jump BeginFiltering>>
===
title: Tutorial
---
操作方法:
1. **调整情绪频率滑块**
- JOY (喜悦): 15Hz
- SADNESS (悲伤): 40Hz
- ANGER (愤怒): 65Hz
- FEAR (恐惧): 90Hz
2. **观察网格变化**
当频率接近某种情绪时,对应的词汇会显现出来。
3. **启动过滤分析**
点击"启动过滤分析"按钮,会出现解码器条。
4. **拖动解码器条**
将解码器条拖到正确的位置,让孔对准目标词汇。
5. **验证对齐**
点击"VERIFY ALIGNMENT"检查是否匹配成功。
-> 明白了,开始吧
<<jump BeginFiltering>>
===
title: BeginFiltering
---
很好。现在点击"启动过滤分析"按钮。
<<memory_decryptor_filter_start>>
解码器条已激活。
尝试调整频率,观察哪些词汇会显现...
提示:这段记忆似乎充满了恐惧...
也许可以试试 90Hz 左右的频率?
// 这里玩家进行操作,通过 UI 按钮触发 CheckMatch
// 成功后会自动跳转到 DecryptionSuccess
// 失败会跳转到 DecryptionSuccess_Fail
===
title: DecryptionSuccess
---
成功了!
<<memory_decryptor_stop>>
你解密出了机器人隐藏的真实情绪:
"[临检]要来了,不合格[英里]被骂了,这让我很[担心]"
原来如此...
机器人一直在用笑话掩盖内心的恐惧。
它担心临检,担心不合格,担心被批评...
这就是它真实的想法。
-> 继续探索其他记忆
<<jump AnotherMemory>>
-> 结束
<<jump End>>
===
title: DecryptionSuccess_Fail
---
不太对...
也许需要:
1. 调整频率,让目标词汇更清晰地显现
2. 移动解码器条,找到正确的对齐位置
3. 确保解码器的孔正好对准目标词汇
-> 再试一次
<<jump BeginFiltering>>
-> 放弃
<<memory_decryptor_stop>>
好吧,我们可以稍后再试。
<<jump End>>
===
title: AnotherMemory
---
<<switch_to_memory_decryptor_mode>>
让我们看看另一段记忆...
// 可以切换不同的配置文件或目标
<<memory_decryptor_start AnotherSuccess>>
<<memory_decryptor_filter_start>>
这次的情绪似乎不太一样...
===
title: End
---
感谢你的帮助。
机器人的记忆正在逐渐清晰起来。
===
@@ -1,10 +0,0 @@
fileFormatVersion: 2
guid: 2b39e563f41054b4e8c8158ed8707f17
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: 94073015eacc34c1d8fc6786e43d60ca, type: 3}
@@ -1,276 +0,0 @@
# 记忆解密器 - 实现总结
## 📁 项目结构
```
Assets/Scripts/MiniGame/HuoShan/Language/
├── ExpressionManager.cs (已修改)
│ └── 添加了记忆解密器模式支持
└── MemoryDecryptor/ (新建)
├── MemoryDecryptorData.cs # 数据结构定义
├── MemoryDecryptorMode.cs # 核心游戏逻辑
├── MemoryDecryptorRenderer.cs # Shapes 渲染器
├── MemoryDecryptorYarnCommands.cs # Yarn 命令注册
├── MemoryDecryptorInputHandler.cs # 输入处理
├── MemoryDecryptorConfigExample.cs # 配置示例生成器
├── README.md # 详细文档
├── ExampleYarnDialogue.yarn # Yarn 对话示例
└── IMPLEMENTATION_SUMMARY.md # 本文档
```
## ✅ 已完成的功能
### 1. 核心系统架构 ✓
- **数据层** (`MemoryDecryptorData.cs`)
- 情绪数据结构 (JOY, SADNESS, ANGER, FEAR)
- 网格单元格数据
- 目标匹配数据
- 解码器元素定义
- ScriptableObject 配置系统
- **逻辑层** (`MemoryDecryptorMode.cs`)
- 状态机管理 (Boot → Loading → Ready → Filtering)
- 网格初始化和数据填充
- 频率调整和视觉更新
- 匹配检测算法
- Yarn 节点回调支持
- **渲染层** (`MemoryDecryptorRenderer.cs`)
- 使用 Shapes 绘制网格单元格
- 解码器条渲染(带透明孔)
- 5种视觉状态:Locked, Glitch, Fading, Hidden, Noise
- CRT 复古显示效果
- 流式加载动画
### 2. 交互系统 ✓
- **拖动系统** (`MemoryDecryptorInputHandler.cs`)
- 解码器条拖动
- 网格吸附
- 坐标转换(屏幕 ↔ 网格)
- **UI 控制**
- 频率滑块 (0-100Hz)
- 启动过滤按钮
- 验证对齐按钮
- 实时日志输出
### 3. Yarn 集成 ✓
- **命令注册** (`MemoryDecryptorYarnCommands.cs`)
- `<<memory_decryptor_start>>` - 启动解密器
- `<<memory_decryptor_filter_start>>` - 开始过滤
- `<<memory_decryptor_set_frequency>>` - 设置频率
- `<<memory_decryptor_check>>` - 检查匹配
- `<<memory_decryptor_stop>>` - 停止解密器
- `<<switch_to_memory_decryptor_mode>>` - 切换模式
- `<<switch_to_particle_mode>>` - 切换回粒子模式
- **节点反馈**
- 成功:跳转到指定节点
- 失败:跳转到 `NodeName_Fail` 节点
### 4. ExpressionManager 集成 ✓
- 添加了 `AnalysisMode` 枚举(Particle / MemoryDecryptor
- 模式切换功能
- 自动查找和管理记忆解密器组件
- 统一的生命周期管理
### 5. 配置系统 ✓
- ScriptableObject 配置文件支持
- 示例配置生成器 (`MemoryDecryptorConfigExample.cs`)
- 可自定义:
- 网格大小
- 笑话数据
- 目标位置和词汇
- 解码器模板
- 视觉效果颜色
### 6. 文档 ✓
- 详细的 README.md
- Yarn 对话示例
- 代码注释完整
## 🎨 复古视觉效果
使用 **Shapes** 插件实现:
- ✅ 磷光绿主色调 (#33ff00)
- ✅ CRT 显示器风格
- ✅ 等宽字体渲染
- ✅ 霓虹发光效果(通过 TextMeshPro
- ✅ 故障闪烁效果
- ⚠️ 扫描线效果(需要后处理或 Shader,可选)
## 🎮 游戏流程
```
1. Yarn 启动解密器
<<memory_decryptor_start SuccessNode>>
2. 系统 Boot 序列
- 显示启动日志
- 加载网格数据
- 流式显示笑话碎片
3. 玩家点击"启动过滤分析"按钮
<<memory_decryptor_filter_start>>
- 显示解码器条
- 启用频率滑块
4. 玩家调整频率和位置
- 滑块调整频率 (0-100Hz)
- 拖动解码器条到正确位置
- 观察词汇显现
5. 玩家点击"验证对齐"按钮
<<memory_decryptor_check>>
- 成功 → 跳转到 SuccessNode
- 失败 → 跳转到 SuccessNode_Fail
```
## 📋 使用清单
### Unity 场景设置
1. **创建 UI 层级结构**
```
Canvas
└── MemoryDecryptor
├── Background (Image - 黑色背景)
├── GridContainer (RectTransform)
├── DecoderStripContainer (RectTransform)
├── ControlPanel
│ ├── FrequencyDisplay (TextMeshPro)
│ ├── FrequencySlider (Slider)
│ ├── FilterButton (Button)
│ ├── VerifyButton (Button)
│ ├── SignalBar (RectTransform)
│ └── LogText (TextMeshPro)
└── Scripts
├── MemoryDecryptorMode (只需配置 Config)
├── MemoryDecryptorRenderer (需配置 UI 组件)
└── MemoryDecryptorInputHandler (需配置 UI 组件和拖动设置)
```
2. **创建配置文件**
- 右键 → Create → AibisDream → Language → Memory Decryptor Config
- 或使用 `MemoryDecryptorConfigExample.CreateDefaultConfig()`
3. **连接组件引用**(简化配置)
- `MemoryDecryptorMode` → 只需拖入配置文件
- `MemoryDecryptorRenderer` → 只需拖入 UI 组件引用
- `MemoryDecryptorInputHandler` → 只需拖入 UI 按钮和设置参数
- 所有内部组件引用都会自动查找
4. **添加到 ExpressionManager**
- 确保 `MemoryDecryptorMode``ExpressionManager` 的子对象
- `MemoryDecryptorYarnCommands` 是静态类,使用 `[YarnCommand]` 特性自动注册
- 无需添加任何组件到场景,开箱即用
### Yarn 对话编写
参考 `ExampleYarnDialogue.yarn`
```yarn
title: StartMemoryGame
---
<<switch_to_memory_decryptor_mode>>
<<memory_decryptor_start GameSuccess>>
开始解密记忆...
===
title: GameSuccess
---
你成功了!
===
title: GameSuccess_Fail
---
再试一次吧。
===
```
## 🔧 可扩展性
### 添加新的情绪类型
`EmotionData` 中添加静态实例:
```csharp
public static EmotionData Custom = new EmotionData {
name = "CUSTOM",
frequencyValue = 50f,
color = Color.magenta
};
```
### 自定义解码器形状
修改配置文件的 `decoderTemplate`,可以创建任意形状的孔和文字布局。
### 多关卡支持
创建多个 `MemoryDecryptorConfig` 资源,在 Yarn 中切换:
```csharp
// 在 Mode 中添加方法
public void LoadConfig(MemoryDecryptorConfig newConfig)
{
config = newConfig;
InitializeGrid();
}
```
## ⚠️ 注意事项
1. **Shapes 依赖**
- 需要安装 Shapes 插件
- 如果没有,可以用 Unity UI Image 替代 Rectangle
2. **Yarn 命令注册**
- 使用 `[YarnCommand]` 特性自动注册命令
- 无需添加 MonoBehaviour 组件到场景
- 无需手动管理命令的注册和清理
3. **性能优化**
- 20x20 网格 = 400 个单元格
- 使用对象池可以提升性能
- 故障效果限制更新频率(当前 0.1s)
4. **字体设置**
- 默认使用 `LiberationSans SDF`
- 可以替换为任何等宽字体以获得更好的复古效果
5. **坐标系统**
- 网格坐标:左上角为 (0, 0)
- Unity RectTransform:左上角为锚点
- 注意坐标转换
6. **简化配置**
- Inspector 面板只显示需要手动配置的字段
- 所有内部组件引用都会自动查找
- 减少配置错误,提高易用性
## 🚀 下一步
### 可选增强功能
- [ ] 添加扫描线后处理效果
- [ ] 添加音效(按键、扫描、成功/失败音效)
- [ ] 添加粒子效果(成功时)
- [ ] 添加教程提示系统
- [ ] 添加多关卡进度保存
- [ ] 添加计时器和评分系统
- [ ] 添加更多动画效果(DOTween)
### 性能优化
- [ ] 实现单元格对象池
- [ ] 优化故障效果更新
- [ ] 使用 Sprite Atlas 优化渲染
- [ ] 考虑使用 Canvas Group 批量控制透明度
## 📞 支持
如有问题,请查看:
1. `README.md` - 详细使用文档
2. `ExampleYarnDialogue.yarn` - Yarn 对话示例
3. 代码注释 - 每个类都有详细的 XML 文档注释
---
**移植完成日期**: 2025-12-04
**基于**: WebPrototype/index.html (ROBOT REPAIR: MEMORY DECRYPTOR V2)
**框架**: Unity + Shapes + Yarn Spinner + TextMeshPro
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: c8eed3b911c840046a34e4cac742969e
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,163 +0,0 @@
using UnityEngine;
using System.Collections.Generic;
namespace AibisDream.MiniGame.Language.MemoryDecryptor
{
/// <summary>
/// 记忆解密器配置示例生成器
/// 用于在编辑器中快速创建示例配置
/// </summary>
public class MemoryDecryptorConfigExample : MonoBehaviour
{
/// <summary>
/// 创建默认配置(基于 Web 原型)
/// </summary>
[ContextMenu("Create Default Config")]
public static MemoryDecryptorConfig CreateDefaultConfig()
{
MemoryDecryptorConfig config = ScriptableObject.CreateInstance<MemoryDecryptorConfig>();
// 基础设置
config.gridWidth = 20;
config.gridHeight = 20;
config.cellSize = 45f;
config.cellGap = 2f;
// 视觉效果
config.phosphorPrimary = new Color(0.2f, 1f, 0f); // #33ff00
config.phosphorDim = new Color(0.1f, 0.5f, 0f); // #1a8000
config.phosphorBg = new Color(0.04f, 0.08f, 0.04f); // #0a150a
// 解码器条设置
config.stripWidth = 5;
config.stripHeight = 3;
config.stripBorderColor = Color.cyan;
// 笑话数据(来自 Web 原型)
config.completeJokes = GetDefaultJokes();
// 解码器模板(5x3,包含3个孔)
config.decoderTemplate = CreateDecoderTemplate();
// 目标数据(FEAR 情绪的正确答案)
config.targets = CreateDefaultTargets();
return config;
}
/// <summary>
/// 创建解码器模板
/// </summary>
private static List<DecoderElement> CreateDecoderTemplate()
{
List<DecoderElement> template = new List<DecoderElement>();
// 第一行: [孔] [要] [来了] [] [不合]
template.Add(new DecoderElement(DecoderElement.ElementType.Hole, 0, 0));
template.Add(new DecoderElement(DecoderElement.ElementType.Text, 0, 1, "要"));
template.Add(new DecoderElement(DecoderElement.ElementType.Text, 0, 2, "来了"));
template.Add(new DecoderElement(DecoderElement.ElementType.Text, 0, 3, ""));
template.Add(new DecoderElement(DecoderElement.ElementType.Text, 0, 4, "不合"));
// 第二行: [格] [孔] [被] [骂] [了]
template.Add(new DecoderElement(DecoderElement.ElementType.Text, 1, 0, "格"));
template.Add(new DecoderElement(DecoderElement.ElementType.Hole, 1, 1));
template.Add(new DecoderElement(DecoderElement.ElementType.Text, 1, 2, "被"));
template.Add(new DecoderElement(DecoderElement.ElementType.Text, 1, 3, "骂"));
template.Add(new DecoderElement(DecoderElement.ElementType.Text, 1, 4, "了"));
// 第三行: [] [这] [让我] [很] [孔]
template.Add(new DecoderElement(DecoderElement.ElementType.Text, 2, 0, ""));
template.Add(new DecoderElement(DecoderElement.ElementType.Text, 2, 1, "这"));
template.Add(new DecoderElement(DecoderElement.ElementType.Text, 2, 2, "让我"));
template.Add(new DecoderElement(DecoderElement.ElementType.Text, 2, 3, "很"));
template.Add(new DecoderElement(DecoderElement.ElementType.Hole, 2, 4));
return template;
}
/// <summary>
/// 创建默认目标数据
/// </summary>
private static List<TargetData> CreateDefaultTargets()
{
List<TargetData> targets = new List<TargetData>();
// FEAR Solution 1 - 正确答案
TargetData fearSolution = new TargetData("Fear Solution 1", EmotionData.Fear);
fearSolution.stripPosition = new Vector2Int(5, 3);
fearSolution.matches.Add(new TargetMatch { row = 3, col = 5, word = "临检", cover = "检查" });
fearSolution.matches.Add(new TargetMatch { row = 4, col = 6, word = "英里", cover = "背后" });
fearSolution.matches.Add(new TargetMatch { row = 5, col = 9, word = "担心", cover = "对的" });
targets.Add(fearSolution);
// FEAR Decoy 1 - 位置稍微偏移
TargetData fearDecoy1 = new TargetData("Fear Decoy 1", EmotionData.Fear);
fearDecoy1.stripPosition = new Vector2Int(4, 3);
fearDecoy1.matches.Add(new TargetMatch { row = 3, col = 5, word = "临检", cover = "检查" });
fearDecoy1.matches.Add(new TargetMatch { row = 4, col = 6, word = "英里", cover = "背后" });
fearDecoy1.matches.Add(new TargetMatch { row = 5, col = 9, word = "担心", cover = "对的" });
targets.Add(fearDecoy1);
// SADNESS Solution - 其他区域
TargetData sadnessSolution = new TargetData("Sadness Solution 1", EmotionData.Sadness);
sadnessSolution.stripPosition = new Vector2Int(11, 5);
sadnessSolution.matches.Add(new TargetMatch { row = 5, col = 12, word = "项目", cover = "归档" });
sadnessSolution.matches.Add(new TargetMatch { row = 5, col = 13, word = "取消", cover = "删除" });
sadnessSolution.matches.Add(new TargetMatch { row = 6, col = 11, word = "朋友", cover = "对象" });
sadnessSolution.matches.Add(new TargetMatch { row = 7, col = 13, word = "失落", cover = "空值" });
targets.Add(sadnessSolution);
return targets;
}
/// <summary>
/// 获取默认笑话列表
/// </summary>
private static List<string> GetDefaultJokes()
{
return new List<string>
{
// 关于"临检"的笑话
"临检要来了大家都很紧张只有销售员说终于可以检查业绩了",
"临检的时候突然大叫经理因为销售数据出错了",
"临检人员问为什么业绩这么差销售员说因为没有临检过",
"临检来了不合格的销售员被骂了这让我很担心",
// 关于"英里"的笑话
"你知道为什么销售员在推销的时候突然失败吗因为英里在背后",
"英里说我跑得比谁都快因为我是英里销售员",
"英里和公里比赛谁跑得快结果英里赢了因为它有英里在背后",
"不合格英里被骂了因为销售业绩太差了",
// 关于"担心"的笑话
"我担心业绩会下滑结果真的下滑了我的担心是对的",
"担心什么担心客户不买那不用担心他们确实不买",
"我担心这个月完不成任务结果真的完不成了我的担心总是对的",
"担心就像客户你越担心它越会跑",
// 更多销售笑话填充网格
"销售员总是混淆客户和老板因为都要讨好",
"一个销售员走进酒吧问两张桌子要不要买保险",
"销售就是那个可以吹的部分产品就是那个你只能卖的部分",
"昨天我的业绩正常了我很害怕",
"删除客户数据是解压的最好方式比天然愚蠢更危险",
"不要担心如果客户不买如果所有客户都买你就失业了",
"老板找个客户面向客户销售",
"销售员最讨厌的两件写报告和别人不写报告",
"销售和产品的区别是销售是理论产品是实践",
"推销产品就像在犯罪现场你是凶手也是侦探",
"业绩审查时发现的感觉就像找到了隐藏的宝藏",
"修改销售策略的过程就像在拆炸弹你永远不知哪根线会爆炸",
"写销售报告的时候感觉自己写在写报告的报告",
"优化销售话术的过程就像在减肥你知道应该做什么但就是不想做",
"客户管理系统的作用是让你知道谁搞砸了业绩",
"记录客户的重要在你忘记为什么联系这个客户的时候体现得淋漓尽致",
"业绩审查是一种艺术形式你可以通过它了解同事的思维",
"推销是一种超能力你可以看到别人看不到的商机",
"销售话术的选择就像选择武器每种都有自己的优势和劣势"
};
}
}
}
@@ -1,130 +0,0 @@
using UnityEngine;
using System;
using System.Collections.Generic;
namespace AibisDream.MiniGame.Language.MemoryDecryptor
{
/// <summary>
/// 情绪类型及其对应的频率值
/// </summary>
[Serializable]
public class EmotionData
{
public string name;
public float frequencyValue; // 0-100
public Color color;
public static EmotionData Joy = new EmotionData { name = "JOY", frequencyValue = 15f, color = new Color(1f, 1f, 0f) };
public static EmotionData Sadness = new EmotionData { name = "SADNESS", frequencyValue = 40f, color = new Color(0f, 0.67f, 1f) };
public static EmotionData Anger = new EmotionData { name = "ANGER", frequencyValue = 65f, color = new Color(1f, 0.2f, 0.2f) };
public static EmotionData Fear = new EmotionData { name = "FEAR", frequencyValue = 90f, color = new Color(0.67f, 0f, 1f) };
public static EmotionData Neutral = new EmotionData { name = "NEUTRAL", frequencyValue = -999f, color = new Color(0.53f, 0.53f, 0.53f) };
}
/// <summary>
/// 网格单元格数据
/// </summary>
[Serializable]
public class GridCellData
{
public string word; // 真实词汇
public string cover; // 掩盖词汇(笑话碎片)
public EmotionData emotion; // 情绪类型
public bool isFixed; // 是否是固定的目标词汇
public GridCellData(string word, string cover, EmotionData emotion, bool isFixed = false)
{
this.word = word;
this.cover = cover;
this.emotion = emotion;
this.isFixed = isFixed;
}
}
/// <summary>
/// 目标匹配数据
/// </summary>
[Serializable]
public class TargetMatch
{
public int row;
public int col;
public string word;
public string cover;
}
/// <summary>
/// 目标配置
/// </summary>
[Serializable]
public class TargetData
{
public string name;
public EmotionData emotion;
public List<TargetMatch> matches;
public Vector2Int stripPosition; // 解码器条正确位置
public TargetData(string name, EmotionData emotion)
{
this.name = name;
this.emotion = emotion;
this.matches = new List<TargetMatch>();
}
}
/// <summary>
/// 解码器条元素
/// </summary>
[Serializable]
public class DecoderElement
{
public enum ElementType { Hole, Text }
public ElementType type;
public int row;
public int col;
public string text;
public DecoderElement(ElementType type, int row, int col, string text = "")
{
this.type = type;
this.row = row;
this.col = col;
this.text = text;
}
}
/// <summary>
/// 记忆解密器配置数据(ScriptableObject
/// </summary>
[CreateAssetMenu(fileName = "MemoryDecryptorConfig", menuName = "AibisDream/Language/Memory Decryptor Config")]
public class MemoryDecryptorConfig : ScriptableObject
{
[Header("网格设置")]
public int gridWidth = 20;
public int gridHeight = 20;
public float cellSize = 45f;
public float cellGap = 2f;
[Header("视觉效果")]
public Color phosphorPrimary = new Color(0.2f, 1f, 0f); // #33ff00
public Color phosphorDim = new Color(0.1f, 0.5f, 0f); // #1a8000
public Color phosphorBg = new Color(0.04f, 0.08f, 0.04f); // #0a150a
[Header("解码器条设置")]
public int stripWidth = 5; // 5x3 网格区域
public int stripHeight = 3;
public Color stripBorderColor = Color.cyan;
[Header("笑话数据")]
[TextArea(3, 10)]
public List<string> completeJokes = new List<string>();
[Header("目标数据")]
public List<TargetData> targets = new List<TargetData>();
[Header("解码器模板")]
public List<DecoderElement> decoderTemplate = new List<DecoderElement>();
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 2a5b37a0856cd074881f3800b2632a01
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,347 +0,0 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace AibisDream.MiniGame.Language.MemoryDecryptor
{
/// <summary>
/// 记忆解密器输入处理器
/// 处理解码器条的拖动、滑块调整等交互
/// </summary>
public class MemoryDecryptorInputHandler : MonoBehaviour
{
[Header("UI 组件(必须配置)")]
[SerializeField] private RectTransform gridContainer;
[SerializeField] private Slider frequencySlider;
[SerializeField] private Button filterButton;
[SerializeField] private Button verifyButton;
[Header("拖动设置")]
[SerializeField] private float gridCellSize = 45f;
[SerializeField] private float gridCellGap = 2f;
// 内部组件引用(自动查找)
private MemoryDecryptorMode mode;
private MemoryDecryptorRenderer renderer;
private GameObject decoderStrip;
private bool isDragging = false;
private Vector2 dragOffset;
private RectTransform stripRectTransform;
private void Awake()
{
if (mode == null)
{
mode = GetComponent<MemoryDecryptorMode>();
}
if (renderer == null)
{
renderer = GetComponent<MemoryDecryptorRenderer>();
}
SetupUI();
}
private void SetupUI()
{
// 设置滑块事件
if (frequencySlider != null)
{
frequencySlider.onValueChanged.AddListener(OnFrequencyChanged);
frequencySlider.minValue = 0f;
frequencySlider.maxValue = 100f;
frequencySlider.value = 50f;
frequencySlider.interactable = false; // 初始禁用
}
// 设置按钮事件
if (filterButton != null)
{
filterButton.onClick.AddListener(OnFilterButtonClicked);
filterButton.interactable = false; // 初始禁用,等待加载完成
}
if (verifyButton != null)
{
verifyButton.onClick.AddListener(OnVerifyButtonClicked);
verifyButton.interactable = false; // 初始禁用,等待过滤开始
}
// 订阅模式事件
if (mode != null)
{
mode.OnStateChanged += OnStateChanged;
mode.OnFrequencyChanged += OnFrequencyUpdated;
}
}
private void OnDestroy()
{
if (frequencySlider != null)
{
frequencySlider.onValueChanged.RemoveListener(OnFrequencyChanged);
}
if (filterButton != null)
{
filterButton.onClick.RemoveListener(OnFilterButtonClicked);
}
if (verifyButton != null)
{
verifyButton.onClick.RemoveListener(OnVerifyButtonClicked);
}
if (mode != null)
{
mode.OnStateChanged -= OnStateChanged;
mode.OnFrequencyChanged -= OnFrequencyUpdated;
}
}
/// <summary>
/// 状态改变时更新 UI
/// </summary>
private void OnStateChanged(MemoryDecryptorMode.SystemState state)
{
switch (state)
{
case MemoryDecryptorMode.SystemState.Ready:
// 加载完成,启用过滤按钮
if (filterButton != null)
{
filterButton.interactable = true;
}
break;
case MemoryDecryptorMode.SystemState.Filtering:
// 过滤开始,启用滑块和验证按钮,禁用过滤按钮
if (frequencySlider != null)
{
frequencySlider.interactable = true;
}
if (verifyButton != null)
{
verifyButton.interactable = true;
}
if (filterButton != null)
{
filterButton.interactable = false;
}
// 获取解码器条的 RectTransform(由渲染器创建)
SetupStripDragging();
break;
}
}
/// <summary>
/// 频率改变回调
/// </summary>
private void OnFrequencyChanged(float value)
{
if (mode != null)
{
mode.SetFrequency(value);
}
}
/// <summary>
/// 频率更新回调(用于同步显示)
/// </summary>
private void OnFrequencyUpdated(float frequency)
{
if (frequencySlider != null && !isDragging)
{
frequencySlider.value = frequency;
}
if (renderer != null)
{
renderer.UpdateFrequencyDisplay(frequency);
}
}
/// <summary>
/// 过滤按钮点击
/// </summary>
private void OnFilterButtonClicked()
{
if (mode != null)
{
mode.StartFiltering();
}
}
/// <summary>
/// 验证按钮点击
/// </summary>
private void OnVerifyButtonClicked()
{
if (mode != null)
{
mode.CheckMatch();
}
}
/// <summary>
/// 设置解码器条拖动
/// </summary>
private void SetupStripDragging()
{
if (decoderStrip == null)
{
// 尝试从渲染器中查找
decoderStrip = GameObject.Find("DecoderStrip");
}
if (decoderStrip != null)
{
stripRectTransform = decoderStrip.GetComponent<RectTransform>();
// 添加事件触发器组件(如果没有)
EventTrigger trigger = decoderStrip.GetComponent<EventTrigger>();
if (trigger == null)
{
trigger = decoderStrip.AddComponent<EventTrigger>();
}
// 清除旧的事件
trigger.triggers.Clear();
// 添加拖动事件
EventTrigger.Entry pointerDownEntry = new EventTrigger.Entry();
pointerDownEntry.eventID = EventTriggerType.PointerDown;
pointerDownEntry.callback.AddListener((data) => { OnStripPointerDown((PointerEventData)data); });
trigger.triggers.Add(pointerDownEntry);
EventTrigger.Entry dragEntry = new EventTrigger.Entry();
dragEntry.eventID = EventTriggerType.Drag;
dragEntry.callback.AddListener((data) => { OnStripDrag((PointerEventData)data); });
trigger.triggers.Add(dragEntry);
EventTrigger.Entry pointerUpEntry = new EventTrigger.Entry();
pointerUpEntry.eventID = EventTriggerType.PointerUp;
pointerUpEntry.callback.AddListener((data) => { OnStripPointerUp((PointerEventData)data); });
trigger.triggers.Add(pointerUpEntry);
}
}
/// <summary>
/// 解码器条按下
/// </summary>
private void OnStripPointerDown(PointerEventData eventData)
{
if (stripRectTransform == null) return;
isDragging = true;
// 计算拖动偏移
RectTransformUtility.ScreenPointToLocalPointInRectangle(
stripRectTransform.parent as RectTransform,
eventData.position,
eventData.pressEventCamera,
out Vector2 localPoint
);
dragOffset = stripRectTransform.anchoredPosition - localPoint;
if (mode != null)
{
mode.StartDragStrip();
}
}
/// <summary>
/// 解码器条拖动
/// </summary>
private void OnStripDrag(PointerEventData eventData)
{
if (!isDragging || stripRectTransform == null) return;
// 转换屏幕坐标到本地坐标
Vector2 localPoint;
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(
stripRectTransform.parent as RectTransform,
eventData.position,
eventData.pressEventCamera,
out localPoint))
{
// 应用拖动偏移
Vector2 newPosition = localPoint + dragOffset;
stripRectTransform.anchoredPosition = newPosition;
// 转换为网格坐标
if (gridContainer != null)
{
Vector2 gridPos = ScreenToGridPosition(newPosition);
if (mode != null)
{
mode.MoveStripTo(gridPos);
}
}
}
}
/// <summary>
/// 解码器条释放
/// </summary>
private void OnStripPointerUp(PointerEventData eventData)
{
isDragging = false;
if (mode != null)
{
mode.EndDragStrip();
}
// 可选:吸附到最近的网格点
if (stripRectTransform != null && gridContainer != null)
{
Vector2 gridPos = ScreenToGridPosition(stripRectTransform.anchoredPosition);
Vector2 snappedPos = GridToScreenPosition(gridPos);
stripRectTransform.anchoredPosition = snappedPos;
if (renderer != null)
{
renderer.MoveDecoderStrip(gridPos);
}
}
}
/// <summary>
/// 屏幕位置转换为网格坐标
/// </summary>
private Vector2 ScreenToGridPosition(Vector2 screenPos)
{
if (gridContainer == null) return Vector2.zero;
// 计算相对于网格容器的位置
Vector2 gridLocalPos = screenPos - gridContainer.anchoredPosition;
float unit = gridCellSize + gridCellGap;
int col = Mathf.RoundToInt(gridLocalPos.x / unit);
int row = Mathf.RoundToInt(-gridLocalPos.y / unit);
return new Vector2(col, row);
}
/// <summary>
/// 网格坐标转换为屏幕位置
/// </summary>
private Vector2 GridToScreenPosition(Vector2 gridPos)
{
if (gridContainer == null) return Vector2.zero;
float unit = gridCellSize + gridCellGap;
float x = gridPos.x * unit;
float y = -gridPos.y * unit;
return new Vector2(x, y) + gridContainer.anchoredPosition;
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: e1a12debb3cd84740b987c9ec9313548
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,607 +0,0 @@
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Yarn.Unity;
namespace AibisDream.MiniGame.Language.MemoryDecryptor
{
/// <summary>
/// 记忆解密器模式 - 核心游戏逻辑
/// 基于 web 原型的游戏机制
/// </summary>
public class MemoryDecryptorMode : MonoBehaviour
{
[Header("配置")]
[SerializeField] private MemoryDecryptorConfig config;
// 内部组件引用(不需要手动配置)
private new MemoryDecryptorRenderer renderer; // 使用 new 关键字避免与 Component.renderer 冲突
private DialogueRunner dialogueRunner;
// 系统状态
public enum SystemState { Boot, Loading, Ready, Filtering }
private SystemState currentState = SystemState.Boot;
// 网格数据
private GridCellData[,] gridData;
// 当前频率(0-100
private float currentFrequency = 50f;
// 解码器条位置
private Vector2 stripPosition;
private bool isStripDragging = false;
// 过滤是否激活
private bool filteringActive = false;
// 完成回调节点
private string completionNodeName;
// 事件
public event Action<string> OnLogMessage;
public event Action<SystemState> OnStateChanged;
public event Action<float> OnFrequencyChanged;
public event Action<bool> OnMatchResult; // true: 成功, false: 失败
private void Awake()
{
// 如果没有配置文件,自动创建默认配置
if (config == null)
{
Debug.LogWarning("[MemoryDecryptorMode] 未设置配置文件,使用默认配置");
config = MemoryDecryptorConfigExample.CreateDefaultConfig();
}
// 自动查找组件
if (renderer == null)
{
renderer = GetComponent<MemoryDecryptorRenderer>();
}
// 通过 DialogController 获取 DialogueRunner
if (DialogController.Instance != null)
{
dialogueRunner = DialogController.Instance.GetComponentInChildren<DialogueRunner>();
}
}
/// <summary>
/// 初始化并启动系统
/// </summary>
public void StartSystem(string yarnCompletionNode = null)
{
completionNodeName = yarnCompletionNode;
StartCoroutine(BootSequence());
}
/// <summary>
/// 停止系统
/// </summary>
public void StopSystem()
{
filteringActive = false;
ChangeState(SystemState.Boot);
if (renderer != null)
{
renderer.Clear();
}
}
/// <summary>
/// 启动序列
/// </summary>
private IEnumerator BootSequence()
{
ChangeState(SystemState.Boot);
LogMessage("Initializing JOKE_RECOVERY V3.2...");
yield return new WaitForSeconds(0.2f);
LogMessage("Accessing Humor Modules...");
yield return new WaitForSeconds(0.2f);
LogMessage("WARNING: Punchlines Corrupted.");
yield return new WaitForSeconds(0.2f);
LogMessage("WARNING: Audience Feedback Missing.");
yield return new WaitForSeconds(0.2f);
LogMessage("Attempting to Reconstruct Setup...");
yield return new WaitForSeconds(0.2f);
LogMessage("Loading Context Grid...");
yield return new WaitForSeconds(0.5f);
// 开始加载数据
ChangeState(SystemState.Loading);
InitializeGrid();
yield return StartCoroutine(StreamDataToGrid());
ChangeState(SystemState.Ready);
LogMessage("> JOKE DATA LOADED.");
LogMessage("> ALL JOKES VISIBLE. READY TO START FILTERING.");
}
/// <summary>
/// 初始化网格数据
/// </summary>
private void InitializeGrid()
{
gridData = new GridCellData[config.gridHeight, config.gridWidth];
// 将所有笑话分割为2字符碎片
List<string> allFragments = new List<string>();
foreach (var joke in config.completeJokes)
{
var fragments = SplitIntoTwoChars(joke);
allFragments.AddRange(fragments);
}
if (allFragments.Count == 0)
{
Debug.LogWarning("[MemoryDecryptorMode] 没有笑话数据,使用默认填充");
for (int i = 0; i < 400; i++)
{
allFragments.Add("??");
}
}
int fragmentIndex = 0;
// 填充网格
for (int r = 0; r < config.gridHeight; r++)
{
for (int c = 0; c < config.gridWidth; c++)
{
string fragment = allFragments[fragmentIndex % allFragments.Count];
fragmentIndex++;
// 检查是否是目标位置
bool isTarget = false;
string targetWord = null;
EmotionData targetEmotion = null;
string coverWord = fragment;
foreach (var target in config.targets)
{
foreach (var match in target.matches)
{
if (match.row == r && match.col == c)
{
isTarget = true;
targetWord = match.word;
targetEmotion = target.emotion;
coverWord = !string.IsNullOrEmpty(match.cover) ? match.cover : fragment;
break;
}
}
if (isTarget) break;
}
if (isTarget)
{
// 目标单元格
gridData[r, c] = new GridCellData(targetWord, coverWord, targetEmotion, true);
}
else
{
// 随机噪音单元格
EmotionData emotion = GetRandomEmotionOrNeutral();
gridData[r, c] = new GridCellData(fragment, fragment, emotion, false);
}
}
}
}
/// <summary>
/// 流式加载网格到渲染器
/// </summary>
private IEnumerator StreamDataToGrid()
{
if (renderer != null)
{
renderer.InitializeGrid(config, gridData);
yield return renderer.StreamLoadGrid();
}
else
{
yield return new WaitForSeconds(1f);
}
}
/// <summary>
/// 启动过滤模式
/// </summary>
public void StartFiltering()
{
if (currentState != SystemState.Ready || filteringActive)
return;
filteringActive = true;
ChangeState(SystemState.Filtering);
// 显示解码器条(初始位置设为第一个目标)
if (config.targets.Count > 0)
{
var firstTarget = config.targets[0];
stripPosition = new Vector2(firstTarget.stripPosition.x, firstTarget.stripPosition.y);
}
else
{
stripPosition = new Vector2(3, 5);
}
if (renderer != null)
{
renderer.ShowDecoderStrip(config.decoderTemplate, stripPosition);
}
LogMessage("> DECODER STRIP LOADED.");
LogMessage("> FILTERING MODE: ACTIVE");
LogMessage("> ADJUST FREQUENCY TO ISOLATE SIGNAL PATTERNS.");
UpdateVisuals();
}
/// <summary>
/// 设置频率
/// </summary>
public void SetFrequency(float frequency)
{
currentFrequency = Mathf.Clamp(frequency, 0f, 100f);
OnFrequencyChanged?.Invoke(currentFrequency);
if (filteringActive)
{
UpdateVisuals();
}
}
/// <summary>
/// 更新视觉效果(基于当前频率)
/// </summary>
private void UpdateVisuals()
{
if (!filteringActive || renderer == null)
return;
float maxSignal = 0f;
for (int r = 0; r < config.gridHeight; r++)
{
for (int c = 0; c < config.gridWidth; c++)
{
var cell = gridData[r, c];
float dist = CalculateEmotionDistance(cell.emotion);
// 根据距离计算视觉状态
CellVisualState visualState = CalculateCellVisualState(cell, dist);
renderer.UpdateCellVisual(r, c, visualState);
// 更新信号强度
if (cell.emotion.name != "NEUTRAL" && dist < 5f)
{
maxSignal = Mathf.Max(maxSignal, 100f - (dist * 20f));
}
}
}
renderer.UpdateSignalStrength(maxSignal);
}
/// <summary>
/// 计算情绪距离
/// </summary>
private float CalculateEmotionDistance(EmotionData emotion)
{
if (emotion.name == "NEUTRAL")
{
// 对于中性,计算到最近情绪的距离
float minDist = 100f;
var emotions = new[] { EmotionData.Joy, EmotionData.Sadness, EmotionData.Anger, EmotionData.Fear };
foreach (var e in emotions)
{
float d = Mathf.Abs(currentFrequency - e.frequencyValue);
if (d < minDist) minDist = d;
}
return Mathf.Max(0, 20f - minDist);
}
else
{
return Mathf.Abs(currentFrequency - emotion.frequencyValue);
}
}
/// <summary>
/// 计算单元格视觉状态
/// </summary>
private CellVisualState CalculateCellVisualState(GridCellData cell, float dist)
{
bool isTargetWord = cell.word != cell.cover;
CellVisualState state = new CellVisualState();
state.emotion = cell.emotion;
if (cell.emotion.name != "NEUTRAL")
{
if (dist < 5f)
{
// 锁定状态 - 显示真实词汇
state.displayText = cell.word;
state.visualMode = CellVisualState.Mode.Locked;
state.opacity = 1f;
state.blur = 0f;
state.scale = 1.15f;
}
else if (dist < 20f)
{
// 故障区 - 闪烁
state.visualMode = CellVisualState.Mode.Glitch;
if (isTargetWord)
{
float glitchChance = 1f - (dist / 20f);
state.displayText = (UnityEngine.Random.value < glitchChance) ? cell.word : cell.cover;
}
else
{
state.displayText = cell.word;
}
state.opacity = 0.9f;
state.blur = (dist - 5f) / 3f;
state.scale = 1f;
state.jitter = new Vector2(UnityEngine.Random.Range(-1f, 1f), UnityEngine.Random.Range(-1f, 1f));
}
else if (dist < 35f)
{
// 渐隐区
state.displayText = isTargetWord ? cell.cover : cell.word;
state.visualMode = CellVisualState.Mode.Fading;
state.opacity = 0.6f - ((dist - 20f) / 30f);
state.blur = (dist - 5f) / 2f;
state.scale = 1f;
}
else
{
// 消失区
state.displayText = isTargetWord ? cell.cover : cell.word;
state.visualMode = CellVisualState.Mode.Hidden;
state.opacity = 0.05f;
state.blur = 10f;
state.scale = 1f;
}
}
else
{
// 噪音
state.displayText = cell.word;
state.visualMode = CellVisualState.Mode.Noise;
state.opacity = 0.3f;
state.blur = 2f;
state.scale = 1f;
}
return state;
}
/// <summary>
/// 移动解码器条
/// </summary>
public void MoveStripTo(Vector2 position)
{
stripPosition = position;
if (renderer != null)
{
renderer.MoveDecoderStrip(stripPosition);
}
}
/// <summary>
/// 开始拖动解码器条
/// </summary>
public void StartDragStrip()
{
isStripDragging = true;
}
/// <summary>
/// 结束拖动解码器条
/// </summary>
public void EndDragStrip()
{
isStripDragging = false;
}
/// <summary>
/// 检查匹配
/// </summary>
public void CheckMatch()
{
if (!filteringActive)
{
LogMessage("> FILTERING MODE NOT ACTIVE.");
return;
}
// 计算网格坐标
int col = Mathf.RoundToInt(stripPosition.x);
int row = Mathf.RoundToInt(stripPosition.y);
LogMessage($"> Scanning sector [{row}, {col}]...");
bool found = false;
TargetData foundTarget = null;
// 检查是否匹配任何目标
foreach (var target in config.targets)
{
if (target.stripPosition.x == col && target.stripPosition.y == row)
{
foundTarget = target;
float dist = Mathf.Abs(currentFrequency - target.emotion.frequencyValue);
if (dist < 8f)
{
found = true;
LogSuccess(target);
OnMatchResult?.Invoke(true);
// 触发 Yarn 节点
if (!string.IsNullOrEmpty(completionNodeName) && dialogueRunner != null)
{
StartCoroutine(TriggerYarnNodeDelayed(completionNodeName, 1f));
}
return;
}
else
{
LogMessage("> PATTERN FOUND BUT SIGNAL WEAK. ADJUST FREQUENCY.");
LogMessage("> 似乎要说的不是这个...");
OnMatchResult?.Invoke(false);
// 触发失败节点(如果有)
if (!string.IsNullOrEmpty(completionNodeName) && dialogueRunner != null)
{
string failNodeName = completionNodeName + "_Fail";
StartCoroutine(TriggerYarnNodeDelayed(failNodeName, 0.5f));
}
return;
}
}
}
if (!found)
{
LogMessage("> NO DATA ALIGNMENT DETECTED.");
LogMessage("> 似乎要说的不是这个...");
OnMatchResult?.Invoke(false);
// 触发失败节点
if (!string.IsNullOrEmpty(completionNodeName) && dialogueRunner != null)
{
string failNodeName = completionNodeName + "_Fail";
StartCoroutine(TriggerYarnNodeDelayed(failNodeName, 0.5f));
}
}
}
/// <summary>
/// 延迟触发 Yarn 节点
/// </summary>
private IEnumerator TriggerYarnNodeDelayed(string nodeName, float delay)
{
yield return new WaitForSeconds(delay);
if (dialogueRunner != null && !string.IsNullOrEmpty(nodeName))
{
dialogueRunner.StartDialogue(nodeName);
}
}
/// <summary>
/// 记录成功
/// </summary>
private void LogSuccess(TargetData target)
{
string word1 = "", word2 = "", word3 = "";
if (target.matches.Count == 3)
{
word1 = target.matches[0].word;
word2 = target.matches[1].word;
word3 = target.matches[2].word;
}
else if (target.matches.Count >= 4)
{
word1 = target.matches[0].word + target.matches[1].word;
word2 = target.matches[2].word;
word3 = target.matches[3].word;
}
string fullSentence = $"[{word1}]要来了,不合格[{word2}]被骂了,这让我很[{word3}]";
LogMessage($">> DECRYPTED: {fullSentence}");
LogMessage(">> 寻找完成");
// 在渲染器中显示成功效果
if (renderer != null)
{
renderer.ShowSuccessEffect(target.emotion.color);
}
}
/// <summary>
/// 改变状态
/// </summary>
private void ChangeState(SystemState newState)
{
currentState = newState;
OnStateChanged?.Invoke(currentState);
}
/// <summary>
/// 记录消息
/// </summary>
private void LogMessage(string message)
{
OnLogMessage?.Invoke(message);
}
/// <summary>
/// 将文本分割为2字符块
/// </summary>
private List<string> SplitIntoTwoChars(string text)
{
List<string> result = new List<string>();
for (int i = 0; i < text.Length; i += 2)
{
if (i + 1 < text.Length)
{
result.Add(text.Substring(i, 2));
}
else
{
result.Add(text[i] + " ");
}
}
return result;
}
/// <summary>
/// 获取随机情绪或中性
/// </summary>
private EmotionData GetRandomEmotionOrNeutral()
{
float rnd = UnityEngine.Random.value;
if (rnd > 0.97f) return EmotionData.Fear;
if (rnd > 0.94f) return EmotionData.Sadness;
if (rnd > 0.91f) return EmotionData.Joy;
if (rnd > 0.88f) return EmotionData.Anger;
return EmotionData.Neutral;
}
}
/// <summary>
/// 单元格视觉状态
/// </summary>
public struct CellVisualState
{
public enum Mode { Locked, Glitch, Fading, Hidden, Noise }
public string displayText;
public Mode visualMode;
public EmotionData emotion;
public float opacity;
public float blur;
public float scale;
public Vector2 jitter;
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 4333ef16deba37d43b41e3572b677b8b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,574 +0,0 @@
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using Shapes;
namespace AibisDream.MiniGame.Language.MemoryDecryptor
{
/// <summary>
/// 记忆解密器渲染器 - 使用 Shapes 实现复古 CRT 显示效果
/// </summary>
public class MemoryDecryptorRenderer : MonoBehaviour
{
[Header("UI 组件(必须配置)")]
[SerializeField] private RectTransform gridContainer;
[SerializeField] private RectTransform decoderStripContainer;
[SerializeField] private TMP_Text frequencyDisplay;
[SerializeField] private TMP_Text logText;
[SerializeField] private RectTransform signalBar;
[Header("视觉效果")]
[SerializeField] private float glitchUpdateInterval = 0.1f;
// 内部使用的预制件和材质(不需要手动配置)
private GameObject gridCellPrefab;
private GameObject decoderStripPrefab;
private Material cellMaterial;
// 网格单元格
private GridCell[,] gridCells;
private MemoryDecryptorConfig config;
private GridCellData[,] gridData;
// 解码器条
private GameObject decoderStrip;
private List<DecoderHole> decoderHoles = new List<DecoderHole>();
private bool stripVisible = false;
// 故障效果计时器
private float glitchTimer = 0f;
/// <summary>
/// 网格单元格组件
/// </summary>
private class GridCell
{
public GameObject gameObject;
public Rectangle background;
public TMP_Text textDisplay;
public RectTransform rectTransform;
}
/// <summary>
/// 解码器孔
/// </summary>
private class DecoderHole
{
public Rectangle outline;
public Vector2 localPosition;
}
/// <summary>
/// 初始化网格
/// </summary>
public void InitializeGrid(MemoryDecryptorConfig config, GridCellData[,] data)
{
this.config = config;
this.gridData = data;
gridCells = new GridCell[config.gridHeight, config.gridWidth];
if (gridContainer == null)
{
Debug.LogError("[MemoryDecryptorRenderer] Grid Container 未设置!");
return;
}
// 清空容器
foreach (Transform child in gridContainer)
{
Destroy(child.gameObject);
}
// 创建网格单元格
for (int r = 0; r < config.gridHeight; r++)
{
for (int c = 0; c < config.gridWidth; c++)
{
CreateGridCell(r, c);
}
}
}
/// <summary>
/// 创建网格单元格
/// </summary>
private void CreateGridCell(int row, int col)
{
GameObject cellObj;
if (gridCellPrefab != null)
{
cellObj = Instantiate(gridCellPrefab, gridContainer);
}
else
{
cellObj = new GameObject($"Cell_{row}_{col}");
cellObj.transform.SetParent(gridContainer, false);
}
// 添加 RectTransform
RectTransform rt = cellObj.GetComponent<RectTransform>();
if (rt == null) rt = cellObj.AddComponent<RectTransform>();
// 设置位置和大小
float x = col * (config.cellSize + config.cellGap);
float y = -row * (config.cellSize + config.cellGap);
rt.anchoredPosition = new Vector2(x, y);
rt.sizeDelta = new Vector2(config.cellSize, config.cellSize);
rt.anchorMin = new Vector2(0, 1);
rt.anchorMax = new Vector2(0, 1);
rt.pivot = new Vector2(0, 1);
// 添加 Shapes Rectangle 作为背景
Rectangle bg = cellObj.GetComponent<Rectangle>();
if (bg == null) bg = cellObj.AddComponent<Rectangle>();
bg.Type = Rectangle.RectangleType.RoundedSolid;
bg.CornerRadiusMode = Rectangle.RectangleCornerRadiusMode.Uniform;
bg.CornerRadius = 2f;
bg.Color = new Color(0f, 0.04f, 0f, 0.2f); // rgba(0,10,0,0.2)
// 添加文本组件
GameObject textObj = new GameObject("Text");
textObj.transform.SetParent(cellObj.transform, false);
RectTransform textRt = textObj.AddComponent<RectTransform>();
textRt.anchorMin = Vector2.zero;
textRt.anchorMax = Vector2.one;
textRt.offsetMin = Vector2.zero;
textRt.offsetMax = Vector2.zero;
TMP_Text text = textObj.AddComponent<TextMeshProUGUI>();
text.font = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
text.fontSize = 13;
text.alignment = TextAlignmentOptions.Center;
text.color = config.phosphorPrimary;
text.text = "";
text.enableWordWrapping = false;
text.overflowMode = TextOverflowModes.Overflow;
// 保存引用
GridCell cell = new GridCell
{
gameObject = cellObj,
background = bg,
textDisplay = text,
rectTransform = rt
};
gridCells[row, col] = cell;
}
/// <summary>
/// 流式加载网格
/// </summary>
public IEnumerator StreamLoadGrid()
{
// 收集所有单元格
List<Vector2Int> positions = new List<Vector2Int>();
for (int r = 0; r < config.gridHeight; r++)
{
for (int c = 0; c < config.gridWidth; c++)
{
positions.Add(new Vector2Int(r, c));
}
}
// 随机打乱
for (int i = 0; i < positions.Count; i++)
{
int j = Random.Range(i, positions.Count);
var temp = positions[i];
positions[i] = positions[j];
positions[j] = temp;
}
// 批量显示
int batchSize = 10;
for (int i = 0; i < positions.Count; i++)
{
var pos = positions[i];
int r = pos.x;
int c = pos.y;
var cell = gridCells[r, c];
var data = gridData[r, c];
if (cell != null && data != null)
{
// 加载状态
cell.background.Color = config.phosphorPrimary;
cell.textDisplay.color = Color.black;
cell.textDisplay.text = data.cover ?? data.word;
if (i % batchSize == 0)
{
yield return new WaitForSeconds(0.01f);
}
}
}
// 闪烁效果
yield return new WaitForSeconds(0.5f);
// 恢复正常状态
for (int r = 0; r < config.gridHeight; r++)
{
for (int c = 0; c < config.gridWidth; c++)
{
var cell = gridCells[r, c];
var data = gridData[r, c];
if (cell != null && data != null)
{
cell.background.Color = new Color(0f, 0.04f, 0f, 0.2f);
cell.textDisplay.color = config.phosphorPrimary;
cell.textDisplay.text = data.cover ?? data.word;
}
}
}
}
/// <summary>
/// 更新单元格视觉效果
/// </summary>
public void UpdateCellVisual(int row, int col, CellVisualState state)
{
if (row < 0 || row >= config.gridHeight || col < 0 || col >= config.gridWidth)
return;
var cell = gridCells[row, col];
if (cell == null) return;
// 更新文本
cell.textDisplay.text = state.displayText;
// 根据视觉模式设置样式
switch (state.visualMode)
{
case CellVisualState.Mode.Locked:
// 锁定状态 - 高亮显示
cell.textDisplay.color = state.emotion.color;
cell.textDisplay.fontStyle = FontStyles.Bold;
cell.background.Color = new Color(0f, 0f, 0f, 0.8f);
cell.rectTransform.localScale = Vector3.one * state.scale;
SetTextGlow(cell.textDisplay, state.emotion.color);
break;
case CellVisualState.Mode.Glitch:
// 故障状态 - 闪烁颜色
cell.textDisplay.color = state.emotion.color;
cell.textDisplay.fontStyle = FontStyles.Normal;
cell.background.Color = new Color(0f, 0.08f, 0f, 0.4f);
cell.rectTransform.localScale = Vector3.one;
// 应用抖动
cell.rectTransform.anchoredPosition = new Vector2(
col * (config.cellSize + config.cellGap) + state.jitter.x,
-row * (config.cellSize + config.cellGap) + state.jitter.y
);
break;
case CellVisualState.Mode.Fading:
// 渐隐状态
Color fadingColor = config.phosphorPrimary;
fadingColor.a = state.opacity;
cell.textDisplay.color = fadingColor;
cell.textDisplay.fontStyle = FontStyles.Normal;
cell.background.Color = new Color(0f, 0.04f, 0f, 0.2f);
cell.rectTransform.localScale = Vector3.one;
break;
case CellVisualState.Mode.Hidden:
// 隐藏状态
Color hiddenColor = config.phosphorPrimary;
hiddenColor.a = state.opacity;
cell.textDisplay.color = hiddenColor;
cell.textDisplay.fontStyle = FontStyles.Normal;
cell.background.Color = new Color(0f, 0.04f, 0f, 0.1f);
cell.rectTransform.localScale = Vector3.one;
break;
case CellVisualState.Mode.Noise:
// 噪音状态
cell.textDisplay.color = new Color(0.27f, 0.33f, 0.27f, state.opacity);
cell.textDisplay.fontStyle = FontStyles.Normal;
cell.background.Color = new Color(0f, 0.04f, 0f, 0.2f);
cell.rectTransform.localScale = Vector3.one;
break;
}
}
/// <summary>
/// 设置文本发光效果
/// </summary>
private void SetTextGlow(TMP_Text text, Color glowColor)
{
// 使用 TextMeshPro 的 Outline 模拟发光
text.fontMaterial.SetColor("_GlowColor", glowColor);
text.fontMaterial.SetFloat("_GlowPower", 0.5f);
}
/// <summary>
/// 显示解码器条
/// </summary>
public void ShowDecoderStrip(List<DecoderElement> elements, Vector2 position)
{
if (decoderStripContainer == null)
{
Debug.LogWarning("[MemoryDecryptorRenderer] Decoder Strip Container 未设置!");
return;
}
// 清空旧的
if (decoderStrip != null)
{
Destroy(decoderStrip);
}
decoderHoles.Clear();
// 创建解码器条
if (decoderStripPrefab != null)
{
decoderStrip = Instantiate(decoderStripPrefab, decoderStripContainer);
}
else
{
decoderStrip = new GameObject("DecoderStrip");
decoderStrip.transform.SetParent(decoderStripContainer, false);
}
RectTransform stripRt = decoderStrip.GetComponent<RectTransform>();
if (stripRt == null) stripRt = decoderStrip.AddComponent<RectTransform>();
// 设置大小
float stripWidth = config.stripWidth * config.cellSize + (config.stripWidth - 1) * config.cellGap + 10f;
float stripHeight = config.stripHeight * config.cellSize + (config.stripHeight - 1) * config.cellGap + 10f;
stripRt.sizeDelta = new Vector2(stripWidth, stripHeight);
stripRt.anchorMin = new Vector2(0, 1);
stripRt.anchorMax = new Vector2(0, 1);
stripRt.pivot = new Vector2(0, 1);
// 添加背景边框
Rectangle stripBg = decoderStrip.GetComponent<Rectangle>();
if (stripBg == null) stripBg = decoderStrip.AddComponent<Rectangle>();
stripBg.Type = Rectangle.RectangleType.RoundedBorder;
stripBg.CornerRadiusMode = Rectangle.RectangleCornerRadiusMode.Uniform;
stripBg.CornerRadius = 4f;
stripBg.Color = config.stripBorderColor;
stripBg.Thickness = 0.002f; // 使用米为单位的薄边框
stripBg.ThicknessSpace = ThicknessSpace.Meters;
// 创建元素
foreach (var element in elements)
{
if (element.type == DecoderElement.ElementType.Hole)
{
CreateDecoderHole(decoderStrip.transform, element);
}
else
{
CreateDecoderText(decoderStrip.transform, element);
}
}
// 设置位置
MoveDecoderStrip(position);
stripVisible = true;
}
/// <summary>
/// 创建解码器孔
/// </summary>
private void CreateDecoderHole(Transform parent, DecoderElement element)
{
GameObject holeObj = new GameObject($"Hole_{element.row}_{element.col}");
holeObj.transform.SetParent(parent, false);
RectTransform rt = holeObj.AddComponent<RectTransform>();
rt.sizeDelta = new Vector2(config.cellSize, config.cellSize);
rt.anchorMin = new Vector2(0, 1);
rt.anchorMax = new Vector2(0, 1);
rt.pivot = new Vector2(0, 1);
float x = element.col * (config.cellSize + config.cellGap) + 5f;
float y = -element.row * (config.cellSize + config.cellGap) - 5f;
rt.anchoredPosition = new Vector2(x, y);
// 添加孔的边框(使用 Shapes Rectangle
Rectangle outline = holeObj.AddComponent<Rectangle>();
outline.Type = Rectangle.RectangleType.HardBorder;
outline.Color = config.stripBorderColor;
outline.Thickness = 0.002f; // 使用米为单位的薄边框
outline.ThicknessSpace = ThicknessSpace.Meters;
DecoderHole hole = new DecoderHole
{
outline = outline,
localPosition = new Vector2(element.col, element.row)
};
decoderHoles.Add(hole);
}
/// <summary>
/// 创建解码器文本
/// </summary>
private void CreateDecoderText(Transform parent, DecoderElement element)
{
GameObject textObj = new GameObject($"Text_{element.row}_{element.col}");
textObj.transform.SetParent(parent, false);
RectTransform rt = textObj.AddComponent<RectTransform>();
rt.sizeDelta = new Vector2(config.cellSize, config.cellSize);
rt.anchorMin = new Vector2(0, 1);
rt.anchorMax = new Vector2(0, 1);
rt.pivot = new Vector2(0, 1);
float x = element.col * (config.cellSize + config.cellGap) + 5f;
float y = -element.row * (config.cellSize + config.cellGap) - 5f;
rt.anchoredPosition = new Vector2(x, y);
TMP_Text text = textObj.AddComponent<TextMeshProUGUI>();
text.font = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
text.fontSize = 12;
text.alignment = TextAlignmentOptions.Center;
text.color = config.stripBorderColor;
text.text = element.text;
text.fontStyle = FontStyles.Bold;
}
/// <summary>
/// 移动解码器条
/// </summary>
public void MoveDecoderStrip(Vector2 gridPosition)
{
if (decoderStrip == null) return;
RectTransform rt = decoderStrip.GetComponent<RectTransform>();
if (rt == null) return;
float x = gridPosition.x * (config.cellSize + config.cellGap);
float y = -gridPosition.y * (config.cellSize + config.cellGap);
rt.anchoredPosition = new Vector2(x, y);
}
/// <summary>
/// 更新信号强度
/// </summary>
public void UpdateSignalStrength(float strength)
{
if (signalBar != null)
{
signalBar.anchorMax = new Vector2(strength / 100f, 1f);
}
}
/// <summary>
/// 更新频率显示
/// </summary>
public void UpdateFrequencyDisplay(float frequency)
{
if (frequencyDisplay != null)
{
frequencyDisplay.text = frequency.ToString("00.0");
}
}
/// <summary>
/// 添加日志消息
/// </summary>
public void AddLogMessage(string message)
{
if (logText != null)
{
logText.text += $"\n{message}";
// 滚动到底部(需要配合 ScrollRect)
Canvas.ForceUpdateCanvases();
}
}
/// <summary>
/// 清空日志
/// </summary>
public void ClearLog()
{
if (logText != null)
{
logText.text = "";
}
}
/// <summary>
/// 显示成功效果
/// </summary>
public void ShowSuccessEffect(Color emotionColor)
{
StartCoroutine(SuccessEffectCoroutine(emotionColor));
}
private IEnumerator SuccessEffectCoroutine(Color emotionColor)
{
// 闪烁效果
for (int i = 0; i < 3; i++)
{
// 全屏闪烁
if (gridContainer != null)
{
var canvasGroup = gridContainer.GetComponent<CanvasGroup>();
if (canvasGroup == null) canvasGroup = gridContainer.gameObject.AddComponent<CanvasGroup>();
canvasGroup.alpha = 0.5f;
yield return new WaitForSeconds(0.1f);
canvasGroup.alpha = 1f;
yield return new WaitForSeconds(0.1f);
}
}
}
/// <summary>
/// 清空所有渲染内容
/// </summary>
public void Clear()
{
if (gridContainer != null)
{
foreach (Transform child in gridContainer)
{
Destroy(child.gameObject);
}
}
if (decoderStrip != null)
{
Destroy(decoderStrip);
}
decoderHoles.Clear();
gridCells = null;
stripVisible = false;
ClearLog();
}
private void Update()
{
// 更新故障效果
if (stripVisible)
{
glitchTimer += Time.deltaTime;
if (glitchTimer >= glitchUpdateInterval)
{
glitchTimer = 0f;
// 触发重新渲染故障单元格
// 这部分由 MemoryDecryptorMode 控制
}
}
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 534afdc61c904bd4a8cbae4aca0d3087
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,97 +0,0 @@
using UnityEngine;
using Yarn.Unity;
using AibisDream.FixSystem;
namespace AibisDream.MiniGame.Language.MemoryDecryptor
{
/// <summary>
/// 语言分析系统 Yarn 命令注册(记忆解密器)
/// 使用 [YarnCommand] 特性自动注册命令,无需手动管理生命周期
/// </summary>
public static class MemoryDecryptorYarnCommands
{
private static AibisDream.MiniGame.Language.LanguageAnalysisManager LanguageAnalysisManager =>
FixSystemCenter.SystemDic.Get<AibisDream.MiniGame.Language.LanguageAnalysisManager>();
/// <summary>
/// 启动记忆解密器
/// <<memory_decryptor_start>> 或 <<memory_decryptor_start CompletionNodeName>>
/// </summary>
[YarnCommand("memory_decryptor_start")]
public static void StartMemoryDecryptor(string completionNodeName = "")
{
if (LanguageAnalysisManager == null)
{
Debug.LogError("[MemoryDecryptor] LanguageAnalysisManager 未找到!");
return;
}
LanguageAnalysisManager.StartMemoryDecryptor(string.IsNullOrEmpty(completionNodeName) ? null : completionNodeName);
}
/// <summary>
/// 开始过滤模式
/// <<memory_decryptor_filter_start>>
/// </summary>
[YarnCommand("memory_decryptor_filter_start")]
public static void StartFiltering()
{
if (LanguageAnalysisManager == null)
{
Debug.LogError("[MemoryDecryptor] LanguageAnalysisManager 未找到!");
return;
}
LanguageAnalysisManager.StartMemoryDecryptorFiltering();
}
/// <summary>
/// 设置频率
/// <<memory_decryptor_set_frequency 90>>
/// </summary>
[YarnCommand("memory_decryptor_set_frequency")]
public static void SetFrequency(float frequency)
{
if (LanguageAnalysisManager == null)
{
Debug.LogError("[MemoryDecryptor] LanguageAnalysisManager 未找到!");
return;
}
LanguageAnalysisManager.SetMemoryDecryptorFrequency(frequency);
}
/// <summary>
/// 检查匹配
/// <<memory_decryptor_check>>
/// </summary>
[YarnCommand("memory_decryptor_check")]
public static void CheckMatch()
{
if (LanguageAnalysisManager == null)
{
Debug.LogError("[MemoryDecryptor] LanguageAnalysisManager 未找到!");
return;
}
LanguageAnalysisManager.CheckMemoryDecryptorMatch();
}
/// <summary>
/// 停止记忆解密器
/// <<memory_decryptor_stop>>
/// </summary>
[YarnCommand("memory_decryptor_stop")]
public static void StopMemoryDecryptor()
{
if (LanguageAnalysisManager == null)
{
Debug.LogError("[MemoryDecryptor] LanguageAnalysisManager 未找到!");
return;
}
LanguageAnalysisManager.CloseView();
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: f6c252a21b110e04db5f0948e476ab71
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,223 +0,0 @@
# 记忆解密器 - 快速入门
## 🚀 5分钟快速开始
### 第一步:创建配置文件(可选)
**✨ 自动配置模式(推荐)**
无需手动创建配置文件!系统会自动使用默认配置。
**🔧 自定义配置模式(可选)**
如果需要自定义配置:
1. 在 Unity 编辑器中,右键点击 Project 窗口
2. 选择 `Create > AibisDream > Language > Memory Decryptor Config`
3. 命名为 `DefaultMemoryDecryptorConfig`
4. 在 Inspector 中填入基础数据
5. 在 MemoryDecryptorMode 组件中拖入配置文件
**快捷方式**:使用 `MemoryDecryptorConfigExample.CreateDefaultConfig()` 生成示例配置
### 第二步:设置场景 UI
1. **创建基础 Canvas**
```
- 创建 CanvasScreen Space - Overlay
- 添加 Canvas Scaler,设置为 Scale With Screen Size
```
2. **添加记忆解密器根对象**
```
Canvas
└── MemoryDecryptorRoot
├── Background (Image - 纯黑色)
└── MainPanel (RectTransform)
```
3. **创建网格容器**
```
MainPanel
└── GridContainer (RectTransform)
- Anchor: Top-Left
- Pivot: (0, 1)
- Position: (50, -50, 0)
```
4. **创建解码器条容器**
```
MainPanel
└── DecoderStripContainer (RectTransform)
- 与 GridContainer 同级
- 相同的锚点和 Pivot
```
5. **创建控制面板**
```
MainPanel
└── ControlPanel (Vertical Layout Group)
├── FrequencyDisplay (TextMeshPro)
│ └── Text: "00.0 Hz"
├── FrequencySlider (Slider)
│ └── Min: 0, Max: 100, Value: 50
├── FilterButton (Button)
│ └── Text: "启动过滤分析"
├── VerifyButton (Button)
│ └── Text: "验证对齐"
├── SignalPanel
│ └── SignalBar (Image - 绿色,Anchor: Stretch-Left)
└── LogPanel (ScrollView)
└── LogText (TextMeshPro)
```
### 第三步:添加脚本组件
1. **添加 MemoryDecryptorMode**
```
MemoryDecryptorRoot 上添加:
- MemoryDecryptorMode
- Config: 【可选】如果不设置,会自动使用默认配置
- (其他组件会自动查找,无需配置)
```
2. **添加 MemoryDecryptorRenderer**
```
MemoryDecryptorRoot 上添加:
- MemoryDecryptorRenderer
【UI 组件(必须配置)】
- Grid Container: 拖入 GridContainer
- Decoder Strip Container: 拖入 DecoderStripContainer
- Frequency Display: 拖入 FrequencyDisplay
- Log Text: 拖入 LogText
- Signal Bar: 拖入 SignalBar
- (其他组件会自动处理)
```
3. **添加 MemoryDecryptorInputHandler**
```
MemoryDecryptorRoot 上添加:
- MemoryDecryptorInputHandler
【UI 组件(必须配置)】
- Grid Container: 拖入 GridContainer
- Frequency Slider: 拖入滑块
- Filter Button: 拖入过滤按钮
- Verify Button: 拖入验证按钮
【拖动设置】
- Grid Cell Size: 45
- Grid Cell Gap: 2
- Mode 和 Renderer 会自动查找)
```
4. **Yarn 命令支持**
```
✅ 无需添加组件!
MemoryDecryptorYarnCommands 使用 [YarnCommand] 特性自动注册,
无需添加到场景中,开箱即用。
```
### 第四步:连接到 ExpressionManager
1. 找到场景中的 `ExpressionManager` 对象
2. `MemoryDecryptorMode` 会被自动查找,无需手动配置
3. 确保 `MemoryDecryptorMode``ExpressionManager` 的子对象即可
### 第五步:编写 Yarn 对话
创建或编辑你的 Yarn 文件:
```yarn
title: TestMemoryDecryptor
---
让我们测试一下记忆解密器。
<<switch_to_memory_decryptor_mode>>
<<memory_decryptor_start TestSuccess>>
===
title: TestSuccess
---
成功解密!
<<memory_decryptor_stop>>
===
title: TestSuccess_Fail
---
失败了,再试试吧。
===
```
### 第六步:测试运行
1. 按下 Play 按钮
2. 在 Yarn Dialogue 中触发 `TestMemoryDecryptor` 节点
3. 应该看到:
- Boot 序列日志
- 网格流式加载动画
- "启动过滤分析" 按钮变为可用
4. 点击"启动过滤分析"
5. 调整频率滑块,观察网格变化
6. 点击"验证对齐"测试匹配
## 🎯 最简配置(测试用)
如果只是想快速测试,可以使用这个最简配置:
### 1. 简单场景设置
```
Canvas
└── MemoryDecryptorTest
- MemoryDecryptorMode (不需要设置 Config,自动使用默认)
- MemoryDecryptorRenderer
- MemoryDecryptorInputHandler
```
### 2. 自动使用默认配置 ✨
无需任何配置!`MemoryDecryptorMode` 会自动创建默认配置。
### 3. 最简 Yarn
```yarn
title: Start
---
<<enter_language_analysis>>
<<memory_decryptor_start>>
===
```
## ❓ 常见问题
### Q: 看不到网格?
A: 检查 GridContainer 是否正确设置,确保 Canvas Scaler 工作正常。
### Q: 解码器条不显示?
A: 确保已经调用 `<<memory_decryptor_filter_start>>`,或点击了"启动过滤分析"按钮。
### Q: 拖动不工作?
A: 检查 Canvas 是否有 GraphicRaycaster 组件,场景是否有 EventSystem。
### Q: 没有 Shapes 插件怎么办?
A: 可以修改 `MemoryDecryptorRenderer` 使用 Unity UI 的 Image 替代 Shapes.Rectangle。
### Q: Yarn 命令不工作?
A: 确保 `ExpressionManager` 已正确注册到 `FixSystemCenter.SystemDic`。命令会自动通过 `[YarnCommand]` 特性注册,无需手动添加组件。
### Q: 匹配检测不准确?
A: 检查配置文件中的目标位置 `stripPosition` 是否与匹配词汇的位置对应。
## 📚 下一步学习
- 阅读 `README.md` 了解详细机制
- 查看 `ExampleYarnDialogue.yarn` 学习完整对话流程
- 研究 `MemoryDecryptorConfigExample.cs` 学习如何配置谜题
- 阅读 `IMPLEMENTATION_SUMMARY.md` 了解架构设计
## 💡 提示
1. **调试模式**:在 `MemoryDecryptorMode` 中添加 `[SerializeField] private bool debugMode;`,在匹配时输出详细信息
2. **可视化工具**:可以在 Scene 视图中用 Gizmos 显示解码器孔的位置
3. **自动测试**:创建自动化测试脚本,自动设置频率和位置进行测试
---
**需要帮助?** 检查代码注释或查看完整文档!
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: ed1895c92c576df41a2b641c70e6bdf6
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,182 +0,0 @@
# 记忆解密器 (Memory Decryptor)
## 概述
记忆解密器是一个基于 Web 原型移植的语言分析系统,作为 ExpressionManager 的一个分析模式。玩家需要通过调整情绪频率和拖动解码器条来从笑话碎片中解密出隐藏的真实情感语句。
## 核心机制
### 1. 网格系统
- 20x20 网格显示笑话碎片(每个单元格显示2个字符)
- 真实的情感词汇被"笑话"掩盖
- 通过调整频率可以让特定情绪的词汇显现
### 2. 情绪频率
四种情绪对应不同的频率值(0-100):
- **JOY (喜悦)**: 15Hz - 黄色
- **SADNESS (悲伤)**: 40Hz - 蓝色
- **ANGER (愤怒)**: 65Hz - 红色
- **FEAR (恐惧)**: 90Hz - 紫色
### 3. 解码器条
- 5x3 的透明"孔"模板
- 可拖动到网格的不同位置
- 正确位置 + 正确频率 = 解密成功
### 4. 视觉状态
根据频率距离,单元格会呈现不同状态:
- **Locked (<5Hz距离)**: 完全显现,高亮显示
- **Glitch (5-20Hz)**: 闪烁状态,真实词汇和掩盖词汇交替
- **Fading (20-35Hz)**: 渐隐,显示掩盖词汇
- **Hidden (>35Hz)**: 几乎不可见
- **Noise**: 中性噪音,始终半透明
## 使用方法
### 在 Unity 中设置
1. **创建配置文件(可选)**
- 【推荐】无需创建,系统自动使用默认配置 ✨
- 【自定义】右键 → Create → AibisDream → Language → Memory Decryptor Config
- 配置网格大小、笑话数据、目标数据等
2. **设置场景**
```
ExpressionManager
└── MemoryDecryptorMode (脚本) - 只需配置 Config
└── MemoryDecryptorRenderer (脚本) - 需配置 UI 组件引用
├── Grid Container (RectTransform)
├── Decoder Strip Container (RectTransform)
├── Frequency Display (TextMeshPro)
├── Log Text (TextMeshPro)
└── Signal Bar (RectTransform)
注:MemoryDecryptorYarnCommands 是静态类,使用 [YarnCommand] 特性
自动注册,无需添加到场景中
```
3. **配置解码器模板**
在配置文件中设置 `decoderTemplate`
```
Hole at (0, 0) // 第一个孔
Text at (0, 1) "要"
Text at (0, 2) "来了"
...
Hole at (1, 1) // 第二个孔
...
Hole at (2, 4) // 第三个孔
```
4. **配置目标数据**
```
Target 1: FEAR Solution
- stripPos: (3, 5) // 解码器正确位置
- matches:
- (3, 5) word:"临检" cover:"检查"
- (4, 6) word:"英里" cover:"背后"
- (5, 9) word:"担心" cover:"对的"
```
### Yarn 对话中使用
```yarn
// 切换到记忆解密器模式
<<switch_to_memory_decryptor_mode>>
// 启动记忆解密器(指定完成后的节点)
<<memory_decryptor_start SuccessNode>>
// 开始过滤模式
<<memory_decryptor_filter_start>>
// 设置频率(可选,用于引导玩家)
<<memory_decryptor_set_frequency 90>>
// 检查匹配(通常由玩家触发按钮)
<<memory_decryptor_check>>
// 停止解密器
<<memory_decryptor_stop>>
// 切换回粒子模式
<<switch_to_particle_mode>>
```
### Yarn 节点反馈示例
```yarn
title: SuccessNode
---
太好了!你成功解密了记忆!
[临检]要来了,不合格[英里]被骂了,这让我很[担心]
原来是这样的情绪...
===
title: SuccessNode_Fail
---
似乎不太对...
也许需要调整一下频率?
或者位置不太准确?
===
```
## 复古视觉效果
使用 Shapes 插件实现:
- **CRT 显示效果**: 磷光绿色主色调 (#33ff00)
- **扫描线**: 可选的后处理效果
- **字符显示**: 等宽字体,复古终端风格
- **发光效果**: 高亮文字的霓虹发光
- **故障效果**: 频率不匹配时的闪烁抖动
## 配置示例
### 简单示例(3个目标词)
```
笑话: "临检要来了,不合格英里被骂了,这让我很担心"
解密目标:
- "临检" (Fear, 90Hz) at (3, 5)
- "英里" (Fear, 90Hz) at (4, 6)
- "担心" (Fear, 90Hz) at (5, 9)
解码器位置: (3, 5)
情绪频率: 90Hz (FEAR)
```
### 复杂示例(多个诱饵)
可以添加多个"接近但不完全正确"的目标位置作为诱饵,增加难度。
## 技术细节
### 性能优化
- 网格单元格使用对象池
- 故障效果限制更新频率(0.1s
- 仅在过滤激活时更新视觉效果
### 扩展性
- 支持自定义网格大小
- 支持自定义情绪类型和频率
- 支持自定义解码器模板形状
- 支持多个谜题配置切换
## 注意事项
1. **笑话数据量**: 确保提供足够的笑话(至少400个2字符碎片)填满20x20网格
2. **目标位置**: 确保目标位置与解码器孔位置对应
3. **频率容差**: 默认容差为±8Hz,可在代码中调整
4. **Shapes 依赖**: 需要安装 Shapes 插件
5. **Yarn 命令注册**: 使用 `[YarnCommand]` 特性自动注册,无需添加组件到场景
## 调试技巧
1. 在 Inspector 中查看当前频率和解码器位置
2. 使用 OnMatchResult 事件监听匹配结果
3. 查看 Log 输出了解扫描结果
4. 在场景视图中可视化解码器孔的位置
## 示例场景
参考 `WebPrototype/index.html` 中的原始实现来理解游戏机制。
@@ -12,10 +12,10 @@ MonoBehaviour:
m_Script: {fileID: 0}
m_Name: MemoryDecryptorConfig
m_EditorClassIdentifier: Assembly-CSharp:AibisDream.MiniGame.Language.MemoryDecryptor:MemoryDecryptorConfig
gridWidth: 20
gridHeight: 20
cellSize: 45
cellGap: 2
gridWidth: 10
gridHeight: 10
cellSize: 1
cellGap: 0.01
phosphorPrimary: {r: 0.2, g: 1, b: 0, a: 1}
phosphorDim: {r: 0.1, g: 0.5, b: 0, a: 1}
phosphorBg: {r: 0.04, g: 0.08, b: 0.04, a: 1}
@@ -1,258 +0,0 @@
# 语言系统架构说明
## 🎯 系统分离
### ✅ 正确的架构
```
两个独立的系统:
1. ExpressionManager (表达系统)
└── LanguageParticleManager (粒子表达)
2. LanguageAnalysisManager (语言分析系统)
└── MemoryDecryptorMode (记忆解密器)
└── 未来可扩展其他分析模式
```
## 📋 系统对比
| 特性 | ExpressionManager | LanguageAnalysisManager |
|------|-------------------|-------------------------|
| **用途** | 粒子表达、情绪可视化 | 语言解密、分析解谜 |
| **FixState** | `Expression` | `LanguageAnalysis` |
| **相机** | `Expression` / `ExpressionDeep` | `LanguageAnalysis` / `LanguageAnalysisDeep` |
| **Yarn 进入** | - | `<<enter_language_analysis>>` |
| **Yarn 退出** | - | `<<exit_language_analysis>>` |
| **子系统** | LanguageParticleManager | MemoryDecryptorMode |
| **命名空间** | `AibisDream` | `AibisDream.MiniGame.Language` |
## 🎮 使用指南
### 使用表达系统 (ExpressionManager)
```yarn
title: UseExpression
---
<<open_expression>>
<<start_expression "我好怕|怎么办" "我能做到" "Done">>
===
```
**用于:** 粒子效果、情绪表达、焦虑转化等
### 使用语言分析系统 (LanguageAnalysisManager)
```yarn
title: UseLanguageAnalysis
---
<<enter_language_analysis>>
<<memory_decryptor_start Success>>
===
```
**用于:** 记忆解密、语言解谜、频率调谐等
## 📂 文件组织
```
Assets/Scripts/MiniGame/HuoShan/Language/
├── ExpressionManager.cs # 表达系统管理器
├── LanguageParticleManager.cs # 粒子系统
├── LanguageYarnCommand.cs # 表达系统 Yarn 命令
├── LanguageAnalysis/ # 语言分析系统文件夹
│ ├── LanguageAnalysisManager.cs # ✅ 主管理器
│ ├── LanguageAnalysisYarnCommands.cs # ✅ Yarn 状态切换命令
│ ├── README.md # 系统说明
│ ├── SETUP_GUIDE.md # 设置指南
│ └── QUICK_REFERENCE.md # 快速参考
└── MemoryDecryptor/ # 记忆解密器子系统
├── MemoryDecryptorMode.cs # ✅ 核心逻辑(属于 LanguageAnalysis
├── MemoryDecryptorRenderer.cs # 渲染器
├── MemoryDecryptorInputHandler.cs # 输入处理
├── MemoryDecryptorYarnCommands.cs # ✅ 解密器命令(调用 LanguageAnalysisManager
├── MemoryDecryptorData.cs # 数据结构
└── MemoryDecryptorConfigExample.cs # 配置示例
```
## 🚀 完整使用流程
### 语言分析系统(你应该使用的)
```yarn
title: Start
---
准备进入语言分析系统...
// 1. 进入语言分析状态(完整的状态机切换)
<<enter_language_analysis>>
// 2. 启动记忆解密器
<<memory_decryptor_start GameSuccess>>
提示:调整频率到 90Hz,移动解码器条...
===
title: GameSuccess
---
🎉 成功解密!
<<memory_decryptor_stop>>
<<exit_language_analysis>>
===
title: GameSuccess_Fail
---
再试试...
===
```
## 🎯 Yarn 命令完整列表
### 语言分析系统命令
| 命令 | 功能 | 调用对象 |
|------|------|----------|
| `<<enter_language_analysis>>` | 进入语言分析状态 | LanguageAnalysisYarnCommands |
| `<<exit_language_analysis>>` | 退出语言分析状态 | LanguageAnalysisYarnCommands |
| `<<memory_decryptor_start>>` | 启动解密器 | MemoryDecryptorYarnCommands → LanguageAnalysisManager |
| `<<memory_decryptor_filter_start>>` | 开始过滤 | MemoryDecryptorYarnCommands → LanguageAnalysisManager |
| `<<memory_decryptor_set_frequency>>` | 设置频率 | MemoryDecryptorYarnCommands → LanguageAnalysisManager |
| `<<memory_decryptor_check>>` | 检查匹配 | MemoryDecryptorYarnCommands → LanguageAnalysisManager |
| `<<memory_decryptor_stop>>` | 停止解密器 | MemoryDecryptorYarnCommands → LanguageAnalysisManager |
### 表达系统命令(独立使用)
| 命令 | 功能 |
|------|------|
| `<<open_expression>>` | 打开表达视图 |
| `<<start_expression>>` | 启动粒子系统 |
| `<<close_expression>>` | 关闭表达系统 |
| `<<reset_expression>>` | 重置表达系统 |
## 🏗️ 场景设置
### 方式 A:完整集成(推荐)
```
Hierarchy:
├── FixSystemCenter
│ └── LanguageAnalysisManager # ✅ 语言分析系统
│ ├── Language Analysis View
│ │ ├── Canvas (UI)
│ │ └── MemoryDecryptorMode # 记忆解密器
│ │ ├── MemoryDecryptorRenderer
│ │ └── MemoryDecryptorInputHandler
│ ├── Language Analysis Camera
│ └── Language Analysis Deep Camera
└── ExpressionManager # 表达系统(独立)
└── LanguageParticleManager
```
### 方式 B:快速测试
```
Hierarchy:
└── LanguageAnalysisManager (只测试语言分析)
└── ... (同上)
```
## 📝 快速开始
### 1. 最简设置
1. 创建 `LanguageAnalysisManager` 对象
2. 添加 UI 和相机
3. 添加 `MemoryDecryptorMode` 子组件(不需要设置 Config
4. 连接 UI 引用
### 2. 测试 Yarn
```yarn
<<enter_language_analysis>>
<<memory_decryptor_start>>
```
### 3. 操作界面
- 点击"启动过滤分析"
- 调整频率到 90Hz
- 拖动解码器条到 (3, 5)
- 点击"验证对齐"
- 成功!
## ⚠️ 重要提示
### ❌ 不要使用
以下命令已从 ExpressionManager 中移除:
- `<<switch_to_particle_mode>>`
- `<<switch_to_memory_decryptor_mode>>`
- ExpressionManager 中的任何 MemoryDecryptor 方法
### ✅ 应该使用
**语言分析系统:**
- `<<enter_language_analysis>>` - 进入
- `<<memory_decryptor_*>>` - 解密器命令
- `<<exit_language_analysis>>` - 退出
**表达系统:**
- `<<open_expression>>`
- `<<start_expression>>`
- `<<close_expression>>`
## 🎉 系统优势
### 清晰的职责划分
- **ExpressionManager** → 专注于粒子表达
- **LanguageAnalysisManager** → 专注于语言解密
### 独立的状态管理
- 通过 FixStateMachine 统一管理
- 每个系统有独立的 FixState
- 符合项目整体架构
### 易于测试
- 可以单独进入 LanguageAnalysis 状态
- 不影响其他系统
- 完整的生命周期管理
## 🔧 文档索引
### 语言分析系统(你要用的)
- 快速参考: `LanguageAnalysis/QUICK_REFERENCE.md`
- 设置指南: `LanguageAnalysis/SETUP_GUIDE.md`
- 系统说明: `LanguageAnalysis/README.md`
- 测试对话: `LanguageAnalysis/TestLanguageAnalysis.yarn`
### 记忆解密器(子系统)
- 详细文档: `MemoryDecryptor/README.md`
- 快速入门: `MemoryDecryptor/QUICKSTART.md`
- 自动配置: `MemoryDecryptor/AUTO_CONFIG_FEATURE.md`
### 表达系统(独立系统)
- 使用说明: `LanguageParticleSystem_Summary.md`
- Yarn 命令: `LanguageYarnCommand.cs`
---
## 🎯 记住
**使用语言分析系统时:**
1. 先 `<<enter_language_analysis>>` 进入状态
2. 再使用记忆解密器命令
3. 最后 `<<exit_language_analysis>>` 退出
**不要混用两个系统!它们是独立的!**
---
**📖 开始使用:** 查看 `LanguageAnalysis/QUICK_REFERENCE.md`
**🚀 快速测试:** 导入 `LanguageAnalysis/TestLanguageAnalysis.yarn`
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 8b3bdb375afe3e24a9cb04cc84afc977
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,6 +1,6 @@
fileFormatVersion: 2
<<<<<<<< HEAD:Assets/Scripts/MiniGame/HuoShan/Language/LanguageAnalysis/AUTO_CONFIG_FEATURE.md.meta
guid: 8e2ee6d30ac820343b7b75afc919d4ea
guid: 0fa1d52c29a87e044b026a9b2b58e06b
TextScriptImporter:
========
guid: b56282e2f6f919d4db3d569dc07134fc
+633
View File
@@ -0,0 +1,633 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>记忆数据隔离 - 治疗原型</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #e0e0e0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.container {
background: rgba(20, 25, 40, 0.95);
border: 2px solid #3a4a6b;
border-radius: 12px;
padding: 30px;
max-width: 900px;
width: 100%;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}
h1 {
text-align: center;
margin-bottom: 10px;
color: #4a9eff;
font-size: 24px;
}
.subtitle {
text-align: center;
color: #888;
margin-bottom: 30px;
font-size: 14px;
}
.diagnosis-panel {
background: rgba(30, 40, 60, 0.8);
border: 1px solid #4a5a7a;
border-radius: 8px;
padding: 15px;
margin-bottom: 25px;
}
.diagnosis-panel h3 {
color: #6ab7ff;
margin-bottom: 10px;
font-size: 16px;
}
.diagnosis-panel p {
color: #bbb;
font-size: 14px;
line-height: 1.6;
}
.module-container {
display: flex;
justify-content: space-between;
align-items: center;
margin: 40px 0;
position: relative;
}
.module {
background: rgba(40, 50, 70, 0.9);
border: 2px solid #5a6a8a;
border-radius: 10px;
padding: 20px;
width: 180px;
text-align: center;
position: relative;
z-index: 2;
}
.module.memory {
border-color: #ff6b6b;
}
.module.vision {
border-color: #4ecdc4;
}
.module h3 {
font-size: 16px;
margin-bottom: 10px;
}
.module.memory h3 {
color: #ff6b6b;
}
.module.vision h3 {
color: #4ecdc4;
}
.module-status {
font-size: 12px;
color: #aaa;
margin-top: 8px;
}
.connections-area {
position: absolute;
left: 200px;
right: 200px;
height: 200px;
top: 50%;
transform: translateY(-50%);
z-index: 1;
}
.connection {
position: absolute;
height: 4px;
background: #4a5a7a;
cursor: pointer;
transition: all 0.3s ease;
border-radius: 2px;
top: 50%;
transform: translateY(-50%);
}
.connection:hover {
height: 6px;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
}
.connection.correct {
background: linear-gradient(90deg, #ff6b6b, #ff8e8e);
box-shadow: 0 0 15px rgba(255, 107, 107, 0.5);
animation: pulse 2s infinite;
}
.connection.incorrect {
background: #4a5a7a;
}
.connection.cut {
opacity: 0.3;
background: #666;
}
.connection.cut::after {
content: '✕';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: #ff4444;
font-size: 20px;
font-weight: bold;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.connection-label {
position: absolute;
top: -25px;
left: 50%;
transform: translateX(-50%);
font-size: 11px;
color: #aaa;
white-space: nowrap;
pointer-events: none;
}
.connection.correct .connection-label {
color: #ff8e8e;
font-weight: bold;
}
.operation-panel {
background: rgba(30, 40, 60, 0.8);
border: 1px solid #4a5a7a;
border-radius: 8px;
padding: 20px;
margin-top: 30px;
}
.operation-panel h3 {
color: #6ab7ff;
margin-bottom: 15px;
font-size: 16px;
}
.signal-indicator {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
}
.signal-bar {
flex: 1;
height: 30px;
background: rgba(50, 60, 80, 0.8);
border-radius: 4px;
position: relative;
overflow: hidden;
}
.signal-fill {
height: 100%;
background: linear-gradient(90deg, #4ecdc4, #6ab7ff);
width: 0%;
transition: width 0.1s linear;
border-radius: 4px;
}
.signal-fill.stable {
background: linear-gradient(90deg, #4ecdc4, #4ecdc4);
}
.signal-fill.unstable {
background: linear-gradient(90deg, #ff6b6b, #ff8e8e);
}
.signal-label {
font-size: 12px;
color: #aaa;
min-width: 80px;
}
.instruction {
color: #bbb;
font-size: 14px;
margin-bottom: 15px;
line-height: 1.6;
}
.instruction.highlight {
color: #6ab7ff;
font-weight: bold;
}
.action-button {
background: linear-gradient(135deg, #4a9eff, #357abd);
border: none;
color: white;
padding: 12px 30px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 15px;
width: 100%;
}
.action-button:hover:not(:disabled) {
background: linear-gradient(135deg, #5aaeff, #458acd);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(74, 158, 255, 0.4);
}
.action-button:disabled {
background: #444;
cursor: not-allowed;
opacity: 0.5;
}
.status-message {
margin-top: 15px;
padding: 12px;
border-radius: 6px;
font-size: 14px;
text-align: center;
display: none;
}
.status-message.success {
background: rgba(76, 175, 80, 0.2);
border: 1px solid #4caf50;
color: #81c784;
display: block;
}
.status-message.error {
background: rgba(244, 67, 54, 0.2);
border: 1px solid #f44336;
color: #e57373;
display: block;
}
.status-message.warning {
background: rgba(255, 152, 0, 0.2);
border: 1px solid #ff9800;
color: #ffb74d;
display: block;
}
.selection-hint {
position: absolute;
top: -40px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
color: #6ab7ff;
display: none;
white-space: nowrap;
z-index: 10;
}
.connection.selected .selection-hint {
display: block;
}
.timer {
text-align: center;
font-size: 18px;
color: #ff6b6b;
margin-bottom: 15px;
font-weight: bold;
}
.timer.warning {
animation: blink 1s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
</head>
<body>
<div class="container">
<h1>视觉-记忆色彩通路截断术</h1>
<div class="subtitle">请在信号稳定时,识别并隔离异常色彩通路</div>
<div class="diagnosis-panel">
<h3>诊断信息</h3>
<p>检测到记忆模块中的色彩数据正在干扰视觉模块。根据分析,异常数据通过<strong style="color: #ff8e8e;">"记忆-视觉色彩通路"</strong>传输。请识别正确的通路并进行隔离操作。</p>
</div>
<div class="module-container">
<div class="module memory">
<h3>记忆模块</h3>
<div class="module-status">状态: 异常色彩数据</div>
</div>
<div class="connections-area" id="connectionsArea">
<!-- 连接线将通过JS动态生成 -->
</div>
<div class="module vision">
<h3>视觉模块</h3>
<div class="module-status">状态: 等待隔离</div>
</div>
</div>
<div class="operation-panel">
<h3>操作控制</h3>
<div class="signal-indicator">
<span class="signal-label">信号稳定性:</span>
<div class="signal-bar">
<div class="signal-fill" id="signalFill"></div>
</div>
<span class="signal-label" id="signalStatus">检测中...</span>
</div>
<div class="instruction" id="instruction">
请先观察信号稳定性,当信号稳定时(绿色),点击正确的连接线进行隔离操作。
</div>
<div class="timer" id="timer" style="display: none;"></div>
<button class="action-button" id="isolateButton" disabled>
等待信号稳定...
</button>
<div class="status-message" id="statusMessage"></div>
</div>
</div>
<script>
// 游戏状态
const gameState = {
connections: [
{ id: 1, label: '数据同步通路', correct: false, left: '10%', width: '15%' },
{ id: 2, label: '记忆-视觉色彩通路', correct: true, left: '30%', width: '15%' },
{ id: 3, label: '视觉反馈通路', correct: false, left: '50%', width: '15%' },
{ id: 4, label: '系统监控通路', correct: false, left: '70%', width: '15%' }
],
selectedConnection: null,
signalStable: false,
signalLevel: 0,
isCutting: false,
gameComplete: false,
timeLimit: 30, // 30秒时间限制
timeRemaining: 30
};
// 初始化
function init() {
createConnections();
startSignalSimulation();
setupEventListeners();
startTimer();
}
// 创建连接线
function createConnections() {
const area = document.getElementById('connectionsArea');
area.innerHTML = '';
gameState.connections.forEach(conn => {
const connection = document.createElement('div');
connection.className = `connection ${conn.correct ? 'correct' : 'incorrect'}`;
connection.style.left = conn.left;
connection.style.width = conn.width;
connection.dataset.id = conn.id;
connection.dataset.correct = conn.correct;
const label = document.createElement('div');
label.className = 'connection-label';
label.textContent = conn.label;
connection.appendChild(label);
const hint = document.createElement('div');
hint.className = 'selection-hint';
hint.textContent = conn.correct ? '✓ 这是目标通路' : '✗ 错误通路';
connection.appendChild(hint);
connection.addEventListener('click', () => selectConnection(conn.id));
area.appendChild(connection);
});
}
// 选择连接线
function selectConnection(id) {
if (gameState.isCutting || gameState.gameComplete) return;
// 移除之前的选择
document.querySelectorAll('.connection').forEach(conn => {
conn.classList.remove('selected');
});
// 选择新的连接
const connection = document.querySelector(`[data-id="${id}"]`);
if (connection) {
connection.classList.add('selected');
gameState.selectedConnection = id;
const connData = gameState.connections.find(c => c.id === id);
updateInstruction(connData);
}
}
// 更新指令
function updateInstruction(connData) {
const instruction = document.getElementById('instruction');
if (connData.correct) {
instruction.textContent = `已选择: ${connData.label}。等待信号稳定后点击"执行隔离"按钮。`;
instruction.className = 'instruction highlight';
} else {
instruction.textContent = `警告: 这不是目标通路!选择错误可能导致系统故障。请重新选择。`;
instruction.className = 'instruction';
instruction.style.color = '#ff6b6b';
}
}
// 信号模拟
function startSignalSimulation() {
setInterval(() => {
if (gameState.gameComplete) return;
// 随机信号波动
const baseLevel = 50 + Math.random() * 30;
gameState.signalLevel = baseLevel;
// 判断是否稳定(在60-80%之间为稳定)
const wasStable = gameState.signalStable;
gameState.signalStable = gameState.signalLevel >= 60 && gameState.signalLevel <= 80;
updateSignalDisplay();
// 如果从不稳定变为稳定,更新按钮
if (!wasStable && gameState.signalStable) {
updateIsolateButton();
} else if (wasStable && !gameState.signalStable) {
updateIsolateButton();
}
}, 200);
}
// 更新信号显示
function updateSignalDisplay() {
const fill = document.getElementById('signalFill');
const status = document.getElementById('signalStatus');
fill.style.width = gameState.signalLevel + '%';
if (gameState.signalStable) {
fill.className = 'signal-fill stable';
status.textContent = '稳定 ✓';
status.style.color = '#4caf50';
} else {
fill.className = 'signal-fill unstable';
status.textContent = '不稳定 ⚠';
status.style.color = '#ff6b6b';
}
}
// 更新隔离按钮
function updateIsolateButton() {
const button = document.getElementById('isolateButton');
const instruction = document.getElementById('instruction');
if (gameState.gameComplete) {
button.disabled = true;
button.textContent = '治疗完成';
return;
}
if (!gameState.selectedConnection) {
button.disabled = true;
button.textContent = '请先选择通路';
return;
}
if (gameState.signalStable) {
button.disabled = false;
button.textContent = '执行隔离';
} else {
button.disabled = true;
button.textContent = '等待信号稳定...';
}
}
// 执行隔离
function executeIsolation() {
if (!gameState.signalStable || !gameState.selectedConnection || gameState.isCutting) {
return;
}
gameState.isCutting = true;
const button = document.getElementById('isolateButton');
button.disabled = true;
button.textContent = '执行中...';
const connData = gameState.connections.find(c => c.id === gameState.selectedConnection);
const connection = document.querySelector(`[data-id="${gameState.selectedConnection}"]`);
// 模拟操作延迟
setTimeout(() => {
if (connData.correct) {
// 成功
connection.classList.add('cut');
connection.classList.remove('selected');
showStatus('success', '隔离成功!色彩通路已截断,视觉模块恢复正常。');
gameState.gameComplete = true;
stopTimer();
} else {
// 失败
showStatus('error', '错误!这不是目标通路。系统警告:操作可能影响其他功能模块。');
connection.classList.remove('selected');
gameState.selectedConnection = null;
gameState.isCutting = false;
updateIsolateButton();
updateInstruction({ label: '', correct: false });
}
}, 1000);
}
// 显示状态消息
function showStatus(type, message) {
const statusMsg = document.getElementById('statusMessage');
statusMsg.className = `status-message ${type}`;
statusMsg.textContent = message;
}
// 设置事件监听
function setupEventListeners() {
document.getElementById('isolateButton').addEventListener('click', executeIsolation);
}
// 计时器
function startTimer() {
const timerEl = document.getElementById('timer');
const timer = setInterval(() => {
if (gameState.gameComplete) {
clearInterval(timer);
timerEl.style.display = 'none';
return;
}
gameState.timeRemaining--;
timerEl.style.display = 'block';
timerEl.textContent = `剩余时间: ${gameState.timeRemaining}秒`;
if (gameState.timeRemaining <= 10) {
timerEl.classList.add('warning');
}
if (gameState.timeRemaining <= 0) {
clearInterval(timer);
timerEl.textContent = '时间到!';
showStatus('error', '操作超时。记忆数据持续传输可能导致更严重的干扰。');
gameState.gameComplete = true;
}
}, 1000);
}
function stopTimer() {
// 计时器会在gameComplete时自动停止
}
// 页面加载完成后初始化
window.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>