refactor(火山): 删除旧的分析系统代码

- 删除AnalysisGridCell及相关组件
- 删除AnalysisMode数据类
- 删除分析模式生成器和管理器
- 清除Yarn命令和配置文件

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-02-11 18:25:17 +08:00
co-authored by Claude Code
parent 2e2da9ae98
commit 8c5d5d5db6
14 changed files with 0 additions and 2973 deletions
@@ -1,125 +0,0 @@
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,11 +0,0 @@
fileFormatVersion: 2
guid: 9547756a801380143846ebc10ce8274e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,237 +0,0 @@
using UnityEngine;
using System.Collections.Generic;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 分析模式关卡配置(ScriptableObject
/// 用于存储每个分析谜题的数据
/// </summary>
[CreateAssetMenu(fileName = "AnalysisLevel", menuName = "AIBIS/Language/Analysis Level")]
public class AnalysisModeData : ScriptableObject
{
[Header("关卡基础信息")]
[Tooltip("关卡名称")]
public string levelName = "Level 1";
[Tooltip("关卡描述")]
[TextArea(2, 4)]
public string description = "找出隐藏在笑话中的真实情绪";
[Header("解码器配置")]
[Tooltip("解码器宽度(列数)")]
public int decoderWidth = 5;
[Tooltip("解码器高度(行数)")]
public int decoderHeight = 3;
[Tooltip("孔洞位置(相对于解码器左上角)")]
public List<Vector2Int> holePositions = new List<Vector2Int>
{
new Vector2Int(0, 0), // 第1行第1列
new Vector2Int(1, 1), // 第2行第2列
new Vector2Int(2, 4) // 第3行第5列
};
[Tooltip("解码器文字(按行从上到下、列从左到右的顺序)\n每个格子显示2字,孔洞位置自动跳过\n例如:要来了,不合格被骂了,这让我很\n需要字符数 = (宽×高 - 孔洞数) × 2")]
[TextArea(2, 5)]
public string decoderText = "要来了,不合格被骂了,这让我很";
[Header("答案配置")]
[Tooltip("答案所属情绪")]
public EmotionType answerEmotion = EmotionType.Fear;
[Tooltip("答案词语(按孔洞顺序)")]
public List<string> answerWords = new List<string> { "临检", "英里", "担心" };
[Tooltip("解码器在网格中的正确位置(行,列)")]
public Vector2Int correctDecoderPosition = new Vector2Int(3, 5);
[Tooltip("完整的答案句子(用于提示)")]
public string completeSentence = "[临检]要来了,不合格[英里]被骂了,这让我很[担心]";
[Header("词池配置")]
[Tooltip("每种情绪的词库(用于填充网格)")]
public EmotionWordPool wordPool = new EmotionWordPool();
[Header("笑话配置(可选)")]
[Tooltip("笑话短句(用于初始演出,如果为空则使用词池)")]
public List<string> jokePhrases = new List<string>();
[Header("难度配置")]
[Tooltip("诱饵数量:每种情绪的非答案词数量")]
[Range(5, 50)]
public int decoyCountPerEmotion = 15;
[Tooltip("频率容差:验证时允许的频率偏差")]
[Range(1f, 15f)]
public float frequencyTolerance = 8f;
[Header("完成回调")]
[Tooltip("成功完成时跳转的Yarn节点")]
public string successNodeName = "";
[Tooltip("失败时跳转的Yarn节点")]
public string failNodeName = "";
/// <summary>
/// 验证配置是否有效
/// </summary>
public bool Validate(out string errorMessage)
{
// 检查答案词数量与孔洞数量是否匹配
if (answerWords.Count != holePositions.Count)
{
errorMessage = $"答案词数量({answerWords.Count})与孔洞数量({holePositions.Count})不匹配";
return false;
}
// 检查答案词是否都是2字符
foreach (var word in answerWords)
{
if (string.IsNullOrEmpty(word) || word.Length != 2)
{
errorMessage = $"答案词'{word}'必须是2个字符";
return false;
}
}
// 检查孔洞位置是否在解码器范围内
foreach (var hole in holePositions)
{
if (hole.x < 0 || hole.x >= decoderHeight || hole.y < 0 || hole.y >= decoderWidth)
{
errorMessage = $"孔洞位置({hole.x}, {hole.y})超出解码器范围({decoderHeight}x{decoderWidth})";
return false;
}
}
// 检查词池是否有足够的词
if (!wordPool.HasEnoughWords(answerEmotion, decoyCountPerEmotion + answerWords.Count))
{
errorMessage = $"情绪{answerEmotion}的词池不足,需要至少{decoyCountPerEmotion + answerWords.Count}个词";
return false;
}
errorMessage = "";
return true;
}
/// <summary>
/// 获取网格尺寸建议(根据词数量自动计算)
/// </summary>
public Vector2Int GetRecommendedGridSize()
{
// 计算总词数:答案词 + 诱饵词(所有情绪)
int totalWords = answerWords.Count + decoyCountPerEmotion * 4; // 4种情绪
// 添加一些中性词作为填充
int neutralWords = totalWords / 2;
totalWords += neutralWords;
// 推荐网格尺寸(接近正方形)
int size = Mathf.CeilToInt(Mathf.Sqrt(totalWords));
return new Vector2Int(size, size);
}
}
/// <summary>
/// 情绪词池
/// </summary>
[System.Serializable]
public class EmotionWordPool
{
[Header("快乐词语")]
[Tooltip("Joy情绪的词语库(2字词)")]
public List<string> joyWords = new List<string>
{
"开心", "快乐", "高兴", "喜悦", "欢笑", "愉快", "兴奋", "幸福",
"满足", "得意", "舒畅", "轻松", "欣慰", "庆幸", "欢喜", "喜庆"
};
[Header("悲伤词语")]
[Tooltip("Sadness情绪的词语库(2字词)")]
public List<string> sadnessWords = new List<string>
{
"悲伤", "难过", "伤心", "痛苦", "失望", "沮丧", "忧伤", "哀愁",
"失落", "绝望", "悲痛", "心碎", "凄凉", "孤独", "寂寞", "落寞"
};
[Header("愤怒词语")]
[Tooltip("Anger情绪的词语库(2字词)")]
public List<string> angerWords = new List<string>
{
"愤怒", "生气", "恼怒", "暴怒", "气愤", "愤恨", "恨意", "怒火",
"恼火", "火大", "抓狂", "烦躁", "憋屈", "不爽", "愤慨", "恼恨"
};
[Header("恐惧词语")]
[Tooltip("Fear情绪的词语库(2字词)")]
public List<string> fearWords = new List<string>
{
"恐惧", "害怕", "惊恐", "畏惧", "担心", "忧虑", "焦虑", "紧张",
"不安", "慌张", "惊慌", "胆怯", "恐慌", "惊吓", "惧怕", "临检",
"英里", "担忧", "忐忑", "惶恐"
};
[Header("中性词语")]
[Tooltip("Neutral情绪的词语库(用于填充,2字词)")]
public List<string> neutralWords = new List<string>
{
"工作", "学习", "生活", "时间", "地方", "事情", "人们", "世界",
"问题", "方法", "结果", "过程", "开始", "结束", "继续", "停止",
"前进", "后退", "上下", "左右", "这里", "那里", "现在", "以后"
};
/// <summary>
/// 获取指定情绪的词语列表
/// </summary>
public List<string> GetWords(EmotionType emotion)
{
switch (emotion)
{
case EmotionType.Joy: return joyWords;
case EmotionType.Sadness: return sadnessWords;
case EmotionType.Anger: return angerWords;
case EmotionType.Fear: return fearWords;
case EmotionType.Neutral: return neutralWords;
default: return neutralWords;
}
}
/// <summary>
/// 检查是否有足够的词
/// </summary>
public bool HasEnoughWords(EmotionType emotion, int requiredCount)
{
return GetWords(emotion).Count >= requiredCount;
}
/// <summary>
/// 获取随机词(不重复)
/// </summary>
public List<string> GetRandomWords(EmotionType emotion, int count, List<string> exclude = null)
{
List<string> pool = new List<string>(GetWords(emotion));
// 排除指定词语
if (exclude != null)
{
pool.RemoveAll(w => exclude.Contains(w));
}
// 随机打乱
for (int i = 0; i < pool.Count; i++)
{
int randomIndex = Random.Range(i, pool.Count);
string temp = pool[i];
pool[i] = pool[randomIndex];
pool[randomIndex] = temp;
}
// 返回指定数量
return pool.GetRange(0, Mathf.Min(count, pool.Count));
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 2d8de6401e07b534584449fb63ed5bd8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,358 +0,0 @@
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 分析模式网格生成器
/// 根据配置数据智能生成网格布局,确保唯一解
/// </summary>
public class AnalysisModeGenerator
{
private AnalysisModeData config;
private System.Random random;
public AnalysisModeGenerator(AnalysisModeData config, int seed = -1)
{
this.config = config;
this.random = seed >= 0 ? new System.Random(seed) : new System.Random();
}
/// <summary>
/// 生成完整的网格数据
/// </summary>
public GridCell[,] GenerateGrid(int gridWidth, int gridHeight)
{
GridCell[,] grid = new GridCell[gridHeight, gridWidth];
// 第一步:放置答案词(在正确的解码器位置)
PlaceAnswerWords(grid, gridWidth, gridHeight);
// 第二步:放置诱饵词(相同情绪,但位置不匹配解码器)
PlaceDecoyWords(grid, gridWidth, gridHeight);
// 第三步:填充中性词
FillNeutralWords(grid, gridWidth, gridHeight);
// 第四步:验证唯一解
if (!ValidateUniqueSolution(grid, gridWidth, gridHeight))
{
Debug.LogWarning("[AnalysisModeGenerator] 生成的网格可能没有唯一解,建议调整配置");
}
return grid;
}
/// <summary>
/// 放置答案词
/// </summary>
private void PlaceAnswerWords(GridCell[,] grid, int gridWidth, int gridHeight)
{
Vector2Int decoderPos = config.correctDecoderPosition;
for (int i = 0; i < config.answerWords.Count; i++)
{
// 计算答案词的绝对位置 = 解码器位置 + 孔洞相对位置
Vector2Int holeOffset = config.holePositions[i];
int row = decoderPos.x + holeOffset.x;
int col = decoderPos.y + holeOffset.y;
// 确保位置在网格范围内
if (row < 0 || row >= gridHeight || col < 0 || col >= gridWidth)
{
Debug.LogError($"[AnalysisModeGenerator] 答案词位置({row}, {col})超出网格范围");
continue;
}
// 放置答案词
grid[row, col] = new GridCell
{
row = row,
col = col,
word = config.answerWords[i],
emotion = config.answerEmotion,
isTarget = true
};
}
}
/// <summary>
/// 放置诱饵词(相同情绪,但不匹配解码器)
/// </summary>
private void PlaceDecoyWords(GridCell[,] grid, int gridWidth, int gridHeight)
{
// 为每种情绪生成诱饵词
EmotionType[] emotions = { EmotionType.Joy, EmotionType.Sadness, EmotionType.Anger, EmotionType.Fear };
foreach (var emotion in emotions)
{
// 获取该情绪的诱饵词数量
int decoyCount = config.decoyCountPerEmotion;
// 如果是答案情绪,减去答案词数量
if (emotion == config.answerEmotion)
{
decoyCount = Mathf.Max(0, decoyCount - config.answerWords.Count);
}
// 获取随机词语(排除答案词)
List<string> decoyWords = config.wordPool.GetRandomWords(
emotion,
decoyCount,
emotion == config.answerEmotion ? config.answerWords : null
);
// 放置诱饵词
PlaceWordsRandomly(grid, gridWidth, gridHeight, decoyWords, emotion, false);
}
}
/// <summary>
/// 填充中性词
/// </summary>
private void FillNeutralWords(GridCell[,] grid, int gridWidth, int gridHeight)
{
List<string> neutralWords = new List<string>(config.wordPool.neutralWords);
// 随机打乱
neutralWords = neutralWords.OrderBy(x => random.Next()).ToList();
int wordIndex = 0;
for (int row = 0; row < gridHeight; row++)
{
for (int col = 0; col < gridWidth; col++)
{
if (grid[row, col] == null)
{
// 如果词库用完了,重新开始
if (wordIndex >= neutralWords.Count)
{
wordIndex = 0;
}
grid[row, col] = new GridCell
{
row = row,
col = col,
word = neutralWords[wordIndex],
emotion = EmotionType.Neutral,
isTarget = false
};
wordIndex++;
}
}
}
}
/// <summary>
/// 随机放置词语
/// </summary>
private void PlaceWordsRandomly(GridCell[,] grid, int gridWidth, int gridHeight,
List<string> words, EmotionType emotion, bool isTarget)
{
List<Vector2Int> availablePositions = new List<Vector2Int>();
// 收集所有可用位置
for (int row = 0; row < gridHeight; row++)
{
for (int col = 0; col < gridWidth; col++)
{
if (grid[row, col] == null)
{
availablePositions.Add(new Vector2Int(row, col));
}
}
}
// 随机打乱
availablePositions = availablePositions.OrderBy(x => random.Next()).ToList();
// 放置词语
for (int i = 0; i < words.Count && i < availablePositions.Count; i++)
{
Vector2Int pos = availablePositions[i];
grid[pos.x, pos.y] = new GridCell
{
row = pos.x,
col = pos.y,
word = words[i],
emotion = emotion,
isTarget = isTarget
};
}
}
/// <summary>
/// 验证唯一解:检查是否只有一个位置和情绪组合能匹配所有答案词
/// </summary>
private bool ValidateUniqueSolution(GridCell[,] grid, int gridWidth, int gridHeight)
{
int matchCount = 0;
Vector2Int correctPos = config.correctDecoderPosition;
// 遍历所有可能的解码器位置
for (int row = 0; row <= gridHeight - config.decoderHeight; row++)
{
for (int col = 0; col <= gridWidth - config.decoderWidth; col++)
{
// 遍历所有情绪
foreach (EmotionType emotion in System.Enum.GetValues(typeof(EmotionType)))
{
if (emotion == EmotionType.Neutral) continue;
// 检查是否所有孔洞位置都匹配该情绪
bool allMatch = true;
List<string> matchedWords = new List<string>();
foreach (var hole in config.holePositions)
{
int checkRow = row + hole.x;
int checkCol = col + hole.y;
if (checkRow >= gridHeight || checkCol >= gridWidth)
{
allMatch = false;
break;
}
GridCell cell = grid[checkRow, checkCol];
if (cell == null || cell.emotion != emotion)
{
allMatch = false;
break;
}
matchedWords.Add(cell.word);
}
// 如果所有孔洞都匹配
if (allMatch)
{
matchCount++;
// 检查是否是正确答案
bool isCorrectAnswer = (row == correctPos.x && col == correctPos.y &&
emotion == config.answerEmotion);
if (isCorrectAnswer)
{
// 验证词语是否完全匹配
bool wordsMatch = true;
for (int i = 0; i < config.answerWords.Count; i++)
{
if (matchedWords[i] != config.answerWords[i])
{
wordsMatch = false;
break;
}
}
if (!wordsMatch)
{
Debug.LogError("[AnalysisModeGenerator] 正确位置的词语不匹配答案!");
return false;
}
}
else
{
// 诱饵解
Debug.Log($"[AnalysisModeGenerator] 发现诱饵解: 位置({row},{col}), 情绪{emotion}, 词语[{string.Join(",", matchedWords)}]");
}
}
}
}
}
// 应该恰好有一个解
if (matchCount == 0)
{
Debug.LogError("[AnalysisModeGenerator] 没有找到任何解!");
return false;
}
else if (matchCount > 1)
{
Debug.LogWarning($"[AnalysisModeGenerator] 找到 {matchCount} 个可能的解(包括诱饵),这可能增加难度");
}
else
{
Debug.Log("[AnalysisModeGenerator] 验证通过:唯一解");
}
return true;
}
/// <summary>
/// 生成笑话演出数据(可选)
/// 将网格数据转换为笑话段落,用于初始演出
/// </summary>
public List<string> GenerateJokePhrases(GridCell[,] grid, int gridWidth, int gridHeight)
{
// 如果配置中有笑话,直接使用
if (config.jokePhrases != null && config.jokePhrases.Count > 0)
{
return new List<string>(config.jokePhrases);
}
// 否则从网格词语生成简单的笑话段落
List<string> phrases = new List<string>();
System.Text.StringBuilder sb = new System.Text.StringBuilder();
for (int row = 0; row < gridHeight; row++)
{
for (int col = 0; col < gridWidth; col += 10) // 每10个词组成一句
{
sb.Clear();
for (int i = 0; i < 10 && col + i < gridWidth; i++)
{
if (grid[row, col + i] != null)
{
sb.Append(grid[row, col + i].word);
}
}
if (sb.Length > 0)
{
phrases.Add(sb.ToString());
}
}
}
return phrases;
}
/// <summary>
/// 生成解析目标配置(用于 AnalysisModeManager
/// </summary>
public AnalysisTarget GenerateAnalysisTarget()
{
AnalysisTarget target = new AnalysisTarget
{
name = config.levelName,
emotion = config.answerEmotion,
stripPosition = config.correctDecoderPosition,
isCorrectAnswer = true,
words = new List<TargetWord>()
};
// 添加目标词
for (int i = 0; i < config.answerWords.Count; i++)
{
Vector2Int holeOffset = config.holePositions[i];
Vector2Int absolutePos = new Vector2Int(
config.correctDecoderPosition.x + holeOffset.x,
config.correctDecoderPosition.y + holeOffset.y
);
target.words.Add(new TargetWord
{
position = absolutePos,
word = config.answerWords[i]
});
}
return target;
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 641e33af9f9410841b81fa65933f357d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 3cf72a773ae1e5442aea05dc7691d4e0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,223 +0,0 @@
# 分析模式设置指南
## 一、创建关卡配置
### 1. 创建 ScriptableObject
1. 在 Project 窗口中,找到 `Assets/Resources/AnalysisLevels/` 文件夹(如果没有请创建)
2. 右键 → `Create > AIBIS > Language > Analysis Level`
3. 命名为 `AnalysisLevel_Fear_01`
### 2. 配置关卡数据
在 Inspector 中填写:
**关卡基础信息:**
- Level Name: "恐惧关卡1"
- Description: "找出隐藏在笑话中的焦虑"
**解码器配置:**
- Decoder Width: 5
- Decoder Height: 3
- Hole Positions: (点击 + 添加3个位置)
- Element 0: X=0, Y=0
- Element 1: X=1, Y=1
- Element 2: X=2, Y=4
**答案配置:**
- Answer Emotion: Fear
- Answer Words: (点击 + 添加3个词)
- Element 0: "临检"
- Element 1: "英里"
- Element 2: "担心"
- Correct Decoder Position: X=3, Y=5
- Complete Sentence: "[临检]要来了,不合格[英里]被骂了,这让我很[担心]"
**词池配置:**
展开 Word Pool,确保:
- Fear Words 列表中包含 "临检", "英里", "担心"
- 每种情绪至少有 15 个词
**难度配置:**
- Decoy Count Per Emotion: 15
- Frequency Tolerance: 8.0
**完成回调:**
- Success Node Name: "Analysis_Fear_01_Success"
- Fail Node Name: "Analysis_Fear_01_Fail"
---
## 二、设置 AnalysisModeManager
### 1. 在场景中找到或创建 AnalysisModeManager
1. 在 Hierarchy 中找到包含 `AnalysisModeManager` 的物体
2. 如果没有,创建一个空物体,添加 `AnalysisModeManager` 组件
### 2. 配置 Inspector
**Canvas 引用:**
- Shared Canvas: 拖入共享的 Canvas(与 LanguageParticleManager 使用同一个)
**UI 引用:**
- Analysis Panel: 分析面板 RectTransform
- Initialize Button: "初始化" 按钮
- Emotion Slider: 情绪频率滑动条
- Frequency Text: 频率显示文本
- Signal Strength Text: 信号强度文本
- Start Filter Button: "启动过滤" 按钮
- Verify Button: "验证" 按钮
**网格配置:**
- Grid Container: 可选
- Grid Width: 20
- Grid Height: 20
- Cell Size: 0.4
- Cell Gap: 0.05
- Cell Font Size: 2.5
- Grid Center Offset: (0, 0, 0)
**解码器条纹:**
- Decoder Strip: 拖入解码器条纹 Prefab
**字体配置:**
- Chinese Font Asset: 拖入中文字体
**关卡配置:**
- Current Level Data: 拖入刚创建的 `AnalysisLevel_Fear_01`
---
## 三、创建解码器条纹 Prefab
### 1. 创建 UI 对象
1. 在 Canvas 下创建空物体
2. 命名为 "DecoderStrip"
3. 添加组件:
- RectTransform (自动添加)
- Image
- Canvas Group
- AnalysisDecoderStrip
### 2. 配置组件
**RectTransform:**
- Anchor: Center
- Pivot: (0.5, 0.5)
- Size Delta: 会被代码动态设置
**Image:**
- Color: R=0, G=30, B=30, A=242 (半透明深青色)
**AnalysisDecoderStrip:**
- Is Draggable: ✓
- Drag Alpha: 0.7
- Strip Color: (0, 0.12, 0.12, 0.95)
- Border Color: Cyan
- Border Width: 2
### 3. 添加孔洞标记(可选)
在 DecoderStrip 下创建子物体标记孔洞位置:
1. 创建3个 Image 子物体,命名为 "Hole_1", "Hole_2", "Hole_3"
2. 设置为小圆圈或方框
3. 位置对应配置中的 holePositions
### 4. 保存为 Prefab
将 DecoderStrip 拖到 Project 窗口,保存为 Prefab
---
## 四、在 Yarn 脚本中使用
创建或编辑 .yarn 文件:
```yarn
title: TestAnalysisMode
---
艾比斯: 让我看看这些记忆背后藏着什么...
<<start_analysis level:AnalysisLevel_Fear_01>>
// 玩家进行分析游戏,成功后自动跳转到下一个节点
===
title: Analysis_Fear_01_Success
---
艾比斯: 我明白了...这些笑话背后藏着焦虑...
艾比斯: [临检]要来了,不合格[英里]被骂了,这让我很[担心]...
-> 继续
<<close_analysis>>
===
title: Analysis_Fear_01_Fail
---
艾比斯: 似乎不对...让我再试试...
===
```
---
## 五、测试
1. 运行场景
2. 触发包含 `start_analysis` 命令的对话
3. 观察分析模式是否正确启动
4. 调整频率滑动条,拖动解码器条纹
5. 点击验证按钮测试
---
## 六、调试
### 查看日志
生成器会输出详细日志:
```
[AnalysisModeGenerator] 验证通过:唯一解
[AnalysisModeManager] 已加载关卡: 恐惧关卡1
```
### 常见问题
**Q: 提示"找不到关卡配置"**
A: 确保配置文件在 `Assets/Resources/AnalysisLevels/` 目录下
**Q: 提示"类型找不到"**
A: 在 Unity 中等待编译完成,或重启 Unity
**Q: 网格没有生成**
A: 检查 Console 是否有错误日志,确认配置数据有效
**Q: 解码器条纹不能拖动**
A: 检查 Canvas 的 Raycast Target 设置,确保 EventSystem 存在
---
## 七、进阶配置
### 创建更多关卡
复制 `AnalysisLevel_Fear_01`,修改:
- 答案词
- 孔洞位置
- 解码器正确位置
- 诱饵词数量
### 增加难度
- 增加 `decoyCountPerEmotion` (诱饵词数量)
- 减小 `frequencyTolerance` (频率容差)
- 增加网格尺寸
- 使用更多孔洞
---
完成!现在可以创建和配置分析模式关卡了。
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 60c809b2091567c4ba8ad39cde46bdf2
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,62 +0,0 @@
using System.Collections;
using AibisDream.FixSystem;
using UnityEngine;
using Yarn.Unity;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 分析模式的 Yarn 命令接口
/// </summary>
public static class AnalysisModeYarnCommand
{
private static AnalysisModeManager AnalysisModeManager => FixSystemCenter.SystemDic.Get<AnalysisModeManager>();
/// <summary>
/// 启动分析模式(使用 ScriptableObject 配置 - 推荐)
/// <<start_analysis level:AnalysisLevel_Fear_01>>
/// 或
/// <<start_analysis level:AnalysisLevel_Fear_01 completion:TestNode>>
/// </summary>
/// <param name="levelName">关卡配置名称(Resources/AnalysisLevels/ 下的文件名)</param>
/// <param name="completionNode">完成时触发的对话节点(可选,会覆盖配置中的节点)</param>
[YarnCommand("start_analysis")]
public static void StartAnalysis(string levelName, string completionNode = "")
{
if (AnalysisModeManager == null)
{
Debug.LogError("AnalysisModeManager 未找到!请确保场景中有 AnalysisModeManager 组件。");
return;
}
// 从 Resources 加载关卡配置
AnalysisModeData levelData = Resources.Load<AnalysisModeData>($"AnalysisLevels/{levelName}");
if (levelData == null)
{
Debug.LogError($"找不到关卡配置: Resources/AnalysisLevels/{levelName}.asset");
return;
}
// 启动分析模式
AnalysisModeManager.OpenView(levelData, completionNode);
}
/// <summary>
/// 关闭分析模式
/// <<close_analysis>>
/// </summary>
[YarnCommand("close_analysis")]
public static void CloseAnalysis()
{
if (AnalysisModeManager == null)
{
Debug.LogWarning("AnalysisModeManager 未找到!");
return;
}
AnalysisModeManager.CloseView();
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: ba015d54b185f1948b0250e112daaf93
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,136 +0,0 @@
%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}
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: fccb02126db337b4a831aaa31a396414
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: