using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; using BaseGames.Dialogue; namespace BaseGames.Editor.Dialogue { /// /// 对话变体预览窗口。 /// 给定一个 DialogueSequenceSO,模拟世界状态标志的开关组合, /// 实时显示各条件变体是否满足,并高亮胜出的变体。 /// 菜单:BaseGames/Dialogue/Variant Preview /// public class DialogueVariantPreviewWindow : EditorWindow { private DialogueSequenceSO _target; private readonly HashSet _enabledFlags = new(System.StringComparer.Ordinal); private readonly List _allFlags = new(); private ObjectField _targetField; private VisualElement _flagContainer; private VisualElement _resultContainer; private VisualElement _matrixContainer; private static readonly Color ColWin = new(0.20f, 0.75f, 0.35f, 1f); private static readonly Color ColFail = new(0.55f, 0.55f, 0.55f, 1f); private static readonly Color ColOverride = new(0.70f, 0.70f, 0.25f, 1f); private static readonly Color ColBlocked = new(0.85f, 0.35f, 0.30f, 1f); [MenuItem("BaseGames/Dialogue/Variant Preview")] public static void Open() { var win = GetWindow("对话变体预览"); win.minSize = new Vector2(480, 400); } /// 从外部打开并预填目标 SO。 public static void OpenWith(DialogueSequenceSO target) { var win = GetWindow("对话变体预览"); win.minSize = new Vector2(480, 400); win.SetTarget(target); } private void CreateGUI() { _mockReader = new MockFlagReader(_enabledFlags); rootVisualElement.style.paddingLeft = 10; rootVisualElement.style.paddingRight = 10; rootVisualElement.style.paddingTop = 10; rootVisualElement.style.paddingBottom = 10; // ── 标题栏 ── var header = new Label("对话变体预览工具"); header.style.fontSize = 14; header.style.unityFontStyleAndWeight = FontStyle.Bold; header.style.marginBottom = 8; rootVisualElement.Add(header); var desc = new Label("在模拟的世界状态标志组合下,预览哪个条件变体会被选中。"); desc.style.fontSize = 11; desc.style.opacity = 0.6f; desc.style.marginBottom = 10; rootVisualElement.Add(desc); // ── 目标选择器 ── _targetField = new ObjectField("对话序列 SO") { objectType = typeof(DialogueSequenceSO), allowSceneObjects = false }; _targetField.value = _target; _targetField.RegisterValueChangedCallback(evt => { SetTarget(evt.newValue as DialogueSequenceSO); }); rootVisualElement.Add(_targetField); rootVisualElement.Add(MakeDivider()); // ── 标志模拟区 ── var flagHeader = new Label("模拟世界状态标志"); flagHeader.style.fontSize = 12; flagHeader.style.unityFontStyleAndWeight = FontStyle.Bold; flagHeader.style.marginBottom = 4; rootVisualElement.Add(flagHeader); _flagContainer = new VisualElement(); rootVisualElement.Add(_flagContainer); rootVisualElement.Add(MakeDivider()); // ── 变体结果区 ── var resultHeader = new Label("变体求值结果"); resultHeader.style.fontSize = 12; resultHeader.style.unityFontStyleAndWeight = FontStyle.Bold; resultHeader.style.marginBottom = 4; rootVisualElement.Add(resultHeader); var scrollView = new ScrollView(ScrollViewMode.Vertical); scrollView.style.flexGrow = 1; rootVisualElement.Add(scrollView); _resultContainer = new VisualElement(); scrollView.Add(_resultContainer); rootVisualElement.Add(MakeDivider()); // ── 矩阵分析区 ── var matrixFoldout = new Foldout { text = "矩阵分析(所有标志组合 → 胜出变体)", value = false }; matrixFoldout.style.marginTop = 4; rootVisualElement.Add(matrixFoldout); _matrixContainer = new VisualElement(); matrixFoldout.Add(_matrixContainer); var matrixBtn = new Button(() => RebuildMatrix()) { text = "矩阵分析" }; matrixBtn.style.marginBottom = 4; matrixFoldout.Add(matrixBtn); var csvBtn = new Button(() => ExportMatrixCsv()) { text = "复制为 CSV" }; csvBtn.style.marginBottom = 4; matrixFoldout.Add(csvBtn); Rebuild(); } private void SetTarget(DialogueSequenceSO target) { _target = target; _enabledFlags.Clear(); if (_targetField != null && _targetField.value != target) _targetField.SetValueWithoutNotify(target); Rebuild(); } // ── 重建 ───────────────────────────────────────────────────────────── private void Rebuild() { RebuildFlagToggles(); RebuildResults(); } private void RebuildFlagToggles() { if (_flagContainer == null) return; _flagContainer.Clear(); _allFlags.Clear(); if (_target == null || _target.variants == null || _target.variants.Length == 0) { var empty = new Label(_target == null ? "(请选择一个 DialogueSequenceSO)" : "(该序列无条件变体,无需模拟)"); empty.style.opacity = 0.5f; empty.style.fontSize = 11; _flagContainer.Add(empty); return; } // 收集所有变体中涉及的 Flag var flagSet = new HashSet(System.StringComparer.Ordinal); foreach (var v in _target.variants) { if (v.requiredFlags != null) foreach (var f in v.requiredFlags) if (!string.IsNullOrEmpty(f)) flagSet.Add(f); } _allFlags.AddRange(flagSet.OrderBy(x => x)); if (_allFlags.Count == 0) { var empty = new Label("(变体未使用任何 requiredFlags)"); empty.style.opacity = 0.5f; empty.style.fontSize = 11; _flagContainer.Add(empty); return; } // 全选 / 全不选 快速按钮 var btnRow = new VisualElement(); btnRow.style.flexDirection = FlexDirection.Row; btnRow.style.marginBottom = 4; var btnAll = new Button(() => { foreach (var f in _allFlags) _enabledFlags.Add(f); Rebuild(); }) { text = "全选" }; btnAll.style.fontSize = 10; btnAll.style.height = 18; btnRow.Add(btnAll); var btnNone = new Button(() => { _enabledFlags.Clear(); Rebuild(); }) { text = "全不选" }; btnNone.style.fontSize = 10; btnNone.style.height = 18; btnRow.Add(btnNone); _flagContainer.Add(btnRow); // 每个 Flag 对应一个 Toggle foreach (var flag in _allFlags) { bool isOn = _enabledFlags.Contains(flag); var toggle = new Toggle(flag) { value = isOn }; toggle.style.fontSize = 11; toggle.RegisterValueChangedCallback(evt => { if (evt.newValue) _enabledFlags.Add(flag); else _enabledFlags.Remove(flag); RebuildResults(); }); _flagContainer.Add(toggle); } } private void RebuildResults() { if (_resultContainer == null) return; _resultContainer.Clear(); if (_target == null) return; if (_target.variants == null || _target.variants.Length == 0) { var msg = new Label("(序列无条件变体,直接使用本序列默认台词)"); msg.style.opacity = 0.5f; msg.style.fontSize = 11; _resultContainer.Add(msg); return; } bool winnerFound = false; for (int i = 0; i < _target.variants.Length; i++) { var variant = _target.variants[i]; var row = BuildVariantRow(i, variant, winnerFound); _resultContainer.Add(row); if (!winnerFound && EvaluateVariant(variant)) winnerFound = true; } // 若无变体胜出,提示将回退到本序列默认台词 if (!winnerFound) { var fallback = new Label("↳ 无变体满足,将使用本序列默认台词(无变体覆盖)"); fallback.style.fontSize = 11; fallback.style.opacity = 0.6f; fallback.style.marginTop = 4; _resultContainer.Add(fallback); } } private VisualElement BuildVariantRow(int index, DialogueSequenceSO.ConditionalVariant variant, bool higherWon) { bool condMet = EvaluateVariant(variant); bool isWinner = condMet && !higherWon; var card = new VisualElement(); card.style.borderLeftWidth = 3; card.style.paddingLeft = 8; card.style.paddingRight = 8; card.style.paddingTop = 5; card.style.paddingBottom = 5; card.style.marginBottom = 4; card.style.backgroundColor = new StyleColor(new Color(0.18f, 0.18f, 0.18f, 1f)); Color borderColor; string statusText; Color statusColor; if (isWinner) { borderColor = ColWin; statusText = "✓ 胜出"; statusColor = ColWin; } else if (condMet) { borderColor = ColOverride; statusText = "⏩ 被更高优先级覆盖"; statusColor = ColOverride; } else { borderColor = ColFail; statusText = "✗ 条件不满足"; statusColor = ColFail; } card.style.borderLeftColor = new StyleColor(borderColor); // 标题行 var titleRow = new VisualElement(); titleRow.style.flexDirection = FlexDirection.Row; titleRow.style.alignItems = Align.Center; titleRow.style.marginBottom = 3; var idxLabel = new Label($"变体 {index}"); idxLabel.style.fontSize = 11; idxLabel.style.flexGrow = 1; idxLabel.style.unityFontStyleAndWeight = isWinner ? FontStyle.Bold : FontStyle.Normal; titleRow.Add(idxLabel); var seqName = new Label(variant.sequence != null ? variant.sequence.name : "(未设置序列)"); seqName.style.fontSize = 10; seqName.style.opacity = 0.6f; seqName.style.width = 160; titleRow.Add(seqName); var statusLabel = new Label(statusText); statusLabel.style.fontSize = 10; statusLabel.style.color = new StyleColor(statusColor); statusLabel.style.unityFontStyleAndWeight = FontStyle.Bold; titleRow.Add(statusLabel); card.Add(titleRow); // 逻辑类型 var logicLabel = new Label($"逻辑:{variant.logic}"); logicLabel.style.fontSize = 10; logicLabel.style.opacity = 0.5f; card.Add(logicLabel); // 条件详情 if (variant.requiredFlags != null && variant.requiredFlags.Length > 0) { foreach (var flag in variant.requiredFlags) { if (string.IsNullOrEmpty(flag)) continue; bool flagOn = _enabledFlags.Contains(flag); var flagRow = new VisualElement(); flagRow.style.flexDirection = FlexDirection.Row; flagRow.style.alignItems = Align.Center; flagRow.style.marginTop = 1; var icon = new Label(flagOn ? "✓" : "✗"); icon.style.fontSize = 10; icon.style.color = new StyleColor(flagOn ? ColWin : ColBlocked); icon.style.width = 16; flagRow.Add(icon); var flagLabel = new Label(flag); flagLabel.style.fontSize = 10; flagRow.Add(flagLabel); card.Add(flagRow); } } else { var noFlags = new Label("(无 requiredFlags — 无条件激活)"); noFlags.style.fontSize = 10; noFlags.style.opacity = 0.5f; card.Add(noFlags); } return card; } private bool EvaluateVariant(DialogueSequenceSO.ConditionalVariant variant) { // 使用 DialogueSequenceSO.CheckVariant 统一变体求值逻辑,避免重复实现 return _target != null && _target.CheckVariant(variant, _mockReader); } /// 将 _enabledFlags 包装为 IWorldStateReader,供 CheckVariant 调用。 private MockFlagReader _mockReader; private sealed class MockFlagReader : BaseGames.Core.IWorldStateReader { private readonly System.Collections.Generic.HashSet _flags; public MockFlagReader(System.Collections.Generic.HashSet flags) => _flags = flags; public bool HasFlag(string key) => _flags.Contains(key); } // ── 矩阵分析 ───────────────────────────────────────────────────────── /// /// 将矩阵分析结果复制为 CSV 字符串到系统剪贴板。 /// 格式:首行为标志名称列头(各列)+ "胜出变体",后续每行为一个组合及其结果。 /// N > 10 时与 一致,提示用户先减少标志数量。 /// private void ExportMatrixCsv() { if (_target == null || _target.variants == null || _target.variants.Length == 0) { EditorUtility.DisplayDialog("矩阵分析 CSV", "当前无可导出的变体数据,请先选择对话序列 SO。", "确定"); return; } var matrixFlags = _allFlags.Count > 0 ? _allFlags : new List(); if (matrixFlags.Count == 0) { EditorUtility.DisplayDialog("矩阵分析 CSV", "变体未使用任何 requiredFlags,无数据可导出。", "确定"); return; } const int MaxFlags = 10; if (matrixFlags.Count > MaxFlags) { EditorUtility.DisplayDialog("矩阵分析 CSV", $"标志数量 ({matrixFlags.Count}) 超过 {MaxFlags},无法导出。\n请先在上方取消勾选不关心的标志,再点击此按钮。", "确定"); return; } int n = matrixFlags.Count; int combos = 1 << n; var sb = new System.Text.StringBuilder(); // 表头 foreach (var f in matrixFlags) sb.Append(EscapeCsv(f)).Append(','); sb.AppendLine("胜出变体"); // 数据行 for (int mask = 0; mask < combos; mask++) { var combo = new HashSet(System.StringComparer.Ordinal); for (int bit = 0; bit < n; bit++) if ((mask & (1 << bit)) != 0) combo.Add(matrixFlags[bit]); var mockReader = new MockFlagReader(combo); int winner = -1; for (int vi = 0; vi < _target.variants.Length; vi++) if (_target.CheckVariant(_target.variants[vi], mockReader)) { winner = vi; break; } for (int ci = 0; ci < n; ci++) sb.Append((mask & (1 << ci)) != 0 ? "1" : "0").Append(','); string winnerLabel = winner >= 0 ? $"变体{winner}" + (_target.variants[winner].sequence != null ? $"({_target.variants[winner].sequence.name})" : "(无序列)") : "默认台词"; sb.AppendLine(EscapeCsv(winnerLabel)); } EditorGUIUtility.systemCopyBuffer = sb.ToString(); Debug.Log($"[DialogueVariantPreviewWindow] 矩阵 CSV({combos} 行)已复制到剪贴板。"); ShowNotification(new GUIContent($"✓ 已复制 {combos} 行 CSV 到剪贴板")); } private static string EscapeCsv(string s) { if (string.IsNullOrEmpty(s)) return string.Empty; if (s.Contains(',') || s.Contains('"') || s.Contains('\n')) return '"' + s.Replace("\"", "\"\"") + '"'; return s; } /// /// 枚举全部 2^N 标志组合(N ≤ 10),以表格形式展示每种组合下胜出的变体索引。 /// N > 10 时显示提示,建议手动筛选标志后分析。 /// private void RebuildMatrix() { if (_matrixContainer == null) return; _matrixContainer.Clear(); if (_target == null || _target.variants == null || _target.variants.Length == 0) { _matrixContainer.Add(new Label("(无可分析的变体)") { style = { opacity = 0.5f, fontSize = 11 } }); return; } var matrixFlags = _allFlags.Count > 0 ? _allFlags : new List(); if (matrixFlags.Count == 0) { _matrixContainer.Add(new Label("(变体不使用任何 requiredFlags,无需矩阵分析)") { style = { opacity = 0.5f, fontSize = 11 } }); return; } const int MaxFlags = 10; if (matrixFlags.Count > MaxFlags) { var warn = new Label($"⚠ 标志数量 ({matrixFlags.Count}) 超过 {MaxFlags},枚举 2^N 组合代价过高。请在上方取消勾选不关心的标志后重新点击「矩阵分析」。"); warn.style.fontSize = 11; warn.style.color = new StyleColor(new Color(0.9f, 0.7f, 0.2f)); warn.style.whiteSpace = WhiteSpace.Normal; _matrixContainer.Add(warn); return; } int n = matrixFlags.Count; int combos = 1 << n; // 2^n // ── 表头 ── var headerRow = MakeMatrixRow(isHeader: true); for (int ci = 0; ci < n; ci++) { var cell = MakeMatrixCell(matrixFlags[ci], isHeader: true); cell.style.minWidth = 90; headerRow.Add(cell); } headerRow.Add(MakeMatrixCell("胜出变体", isHeader: true)); _matrixContainer.Add(headerRow); // ── 数据行 ── for (int mask = 0; mask < combos; mask++) { var combo = new HashSet(System.StringComparer.Ordinal); for (int bit = 0; bit < n; bit++) if ((mask & (1 << bit)) != 0) combo.Add(matrixFlags[bit]); // 求胜出变体 var mockReader = new MockFlagReader(combo); int winner = -1; for (int vi = 0; vi < _target.variants.Length; vi++) if (_target.CheckVariant(_target.variants[vi], mockReader)) { winner = vi; break; } string winnerText = winner >= 0 ? $"变体 {winner}" + (_target.variants[winner].sequence != null ? $"\n({_target.variants[winner].sequence.name})" : "(无序列)") : "默认台词"; var dataRow = MakeMatrixRow(isHeader: false); // 标志列 for (int ci = 0; ci < n; ci++) { bool on = (mask & (1 << ci)) != 0; var cell = MakeMatrixCell(on ? "✓" : "–", isHeader: false); cell.style.color = new StyleColor(on ? ColWin : ColFail); cell.style.minWidth = 90; dataRow.Add(cell); } // 胜出列 var winCell = MakeMatrixCell(winnerText, isHeader: false); winCell.style.color = new StyleColor(winner >= 0 ? ColWin : new Color(0.5f, 0.5f, 0.5f)); dataRow.Add(winCell); _matrixContainer.Add(dataRow); } } private static VisualElement MakeMatrixRow(bool isHeader) { var row = new VisualElement(); row.style.flexDirection = FlexDirection.Row; row.style.borderBottomWidth = 1; row.style.borderBottomColor = new StyleColor(new Color(0.3f, 0.3f, 0.3f, 0.5f)); if (isHeader) row.style.backgroundColor = new StyleColor(new Color(0.22f, 0.22f, 0.28f, 1f)); return row; } private static Label MakeMatrixCell(string text, bool isHeader) { var lbl = new Label(text); lbl.style.fontSize = isHeader ? 10 : 10; lbl.style.unityFontStyleAndWeight = isHeader ? FontStyle.Bold : FontStyle.Normal; lbl.style.paddingLeft = 4; lbl.style.paddingRight = 4; lbl.style.paddingTop = 3; lbl.style.paddingBottom = 3; lbl.style.whiteSpace = WhiteSpace.Normal; lbl.style.width = 80; return lbl; } // ── 辅助 ───────────────────────────────────────────────────────────── private static VisualElement MakeDivider() { var d = new VisualElement(); d.style.height = 1; d.style.backgroundColor = new StyleColor(new Color(0.35f, 0.35f, 0.35f, 0.5f)); d.style.marginTop = 6; d.style.marginBottom = 6; return d; } } }