using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using UnityEditor; using UnityEngine; /// /// GuideConfig.txt 可视化编辑器 /// 菜单路径: 程序工具/策划工具/引导配置编辑器 /// public class GuideConfigEditor : EditorWindow { #region 数据结构 [Serializable] public class GuideConditions { public List Satisfy = new List(); public int CheckAgain; public List Trigger = new List(); public string End = ""; public float Delay; public int Limit; } [Serializable] public class GuideShowUI { public string LangText = ""; public string RoleExpression = ""; public string DialogPos = ""; public int OkBtn; public int ClickSg; public string ClickPos = ""; public string FocusPos = ""; public int Phone; public int ClickToSkip; } [Serializable] public class GuideEntry { public int Id; public string Name = ""; public int Priority; public GuideConditions Conditions = new GuideConditions(); public GuideShowUI ShowUI = new GuideShowUI(); } #endregion #region 字段 private static readonly string ConfigPath = "Assets/Scripts/Preload/GuideConfig.txt"; private List _entries = new List(); private Vector2 _listScroll; private Vector2 _detailScroll; private int _selectedIndex = -1; private string _searchText = ""; private string _searchId = ""; private bool _isDirty; private bool _showConditions = true; private bool _showShowUI = true; private bool _showSatisfy = true; private bool _showTrigger = true; // 列表排序 private enum SortMode { Id, Priority, Name } private SortMode _sortMode = SortMode.Id; // 样式缓存 private GUIStyle _headerStyle; private GUIStyle _selectedStyle; private GUIStyle _entryStyle; private bool _stylesInited; #endregion [MenuItem("策划工具/引导配置编辑器")] public static void ShowWindow() { var win = GetWindow("引导配置编辑器"); win.minSize = new Vector2(900, 500); win.LoadConfig(); } private void OnEnable() { LoadConfig(); } private void InitStyles() { if (_stylesInited) return; _headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 13 }; _selectedStyle = new GUIStyle("TV Selection"); _entryStyle = new GUIStyle(EditorStyles.label) { richText = true }; _stylesInited = true; } #region 加载 / 保存 private void LoadConfig() { string fullPath = Path.Combine(Application.dataPath, "..", ConfigPath); if (!File.Exists(fullPath)) { Debug.LogError($"[GuideConfigEditor] 文件不存在: {fullPath}"); return; } string json = File.ReadAllText(fullPath); _entries = ParseJsonArray(json); _isDirty = false; _selectedIndex = -1; Debug.Log($"[GuideConfigEditor] 已加载 {_entries.Count} 条引导配置"); } private void SaveConfig() { string fullPath = Path.Combine(Application.dataPath, "..", ConfigPath); string json = SerializeToJson(_entries); File.WriteAllText(fullPath, json); _isDirty = false; AssetDatabase.Refresh(); Debug.Log($"[GuideConfigEditor] 已保存 {_entries.Count} 条引导配置到 {ConfigPath}"); } #endregion #region GUI private void OnGUI() { InitStyles(); DrawToolbar(); EditorGUILayout.BeginHorizontal(); { // 左侧列表 EditorGUILayout.BeginVertical(GUILayout.Width(320)); DrawList(); EditorGUILayout.EndVertical(); // 分割线 GUILayout.Box("", GUILayout.Width(2), GUILayout.ExpandHeight(true)); // 右侧详情 EditorGUILayout.BeginVertical(); DrawDetail(); EditorGUILayout.EndVertical(); } EditorGUILayout.EndHorizontal(); } private void DrawToolbar() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); { if (GUILayout.Button("重新加载", EditorStyles.toolbarButton, GUILayout.Width(60))) { if (!_isDirty || EditorUtility.DisplayDialog("确认", "有未保存的修改,确定重新加载?", "确定", "取消")) LoadConfig(); } GUI.backgroundColor = _isDirty ? Color.yellow : Color.white; if (GUILayout.Button(_isDirty ? "保存 *" : "保存", EditorStyles.toolbarButton, GUILayout.Width(60))) { SaveConfig(); } GUI.backgroundColor = Color.white; GUILayout.Space(10); if (GUILayout.Button("+ 新建", EditorStyles.toolbarButton, GUILayout.Width(50))) { AddNewEntry(); } if (GUILayout.Button("复制当前", EditorStyles.toolbarButton, GUILayout.Width(60))) { DuplicateSelected(); } if (GUILayout.Button("删除当前", EditorStyles.toolbarButton, GUILayout.Width(60))) { DeleteSelected(); } GUILayout.FlexibleSpace(); // 排序 GUILayout.Label("排序:", EditorStyles.toolbarButton, GUILayout.Width(30)); var newSort = (SortMode)EditorGUILayout.EnumPopup(_sortMode, EditorStyles.toolbarPopup, GUILayout.Width(70)); if (newSort != _sortMode) { _sortMode = newSort; } GUILayout.Space(5); GUILayout.Label("搜索:", EditorStyles.toolbarButton, GUILayout.Width(30)); _searchText = EditorGUILayout.TextField(_searchText, EditorStyles.toolbarSearchField, GUILayout.Width(120)); GUILayout.Label("ID:", EditorStyles.toolbarButton, GUILayout.Width(20)); _searchId = EditorGUILayout.TextField(_searchId, EditorStyles.toolbarSearchField, GUILayout.Width(60)); } EditorGUILayout.EndHorizontal(); } private void DrawList() { _listScroll = EditorGUILayout.BeginScrollView(_listScroll); var filtered = GetFilteredAndSorted(); for (int i = 0; i < filtered.Count; i++) { var entry = filtered[i]; int realIndex = _entries.IndexOf(entry); bool isSelected = realIndex == _selectedIndex; Rect rect = EditorGUILayout.BeginHorizontal(isSelected ? _selectedStyle : GUIStyle.none, GUILayout.Height(22)); { string label = $"[{entry.Id}] {entry.Name}"; if (entry.Priority > 0) label += $" P{entry.Priority}"; GUILayout.Label(label, _entryStyle); } EditorGUILayout.EndHorizontal(); if (Event.current.type == EventType.MouseDown && rect.Contains(Event.current.mousePosition)) { _selectedIndex = realIndex; GUI.FocusControl(null); Repaint(); } } EditorGUILayout.EndScrollView(); // 底部统计 EditorGUILayout.LabelField($"共 {_entries.Count} 条 | 显示 {filtered.Count} 条", EditorStyles.centeredGreyMiniLabel); } private void DrawDetail() { if (_selectedIndex < 0 || _selectedIndex >= _entries.Count) { EditorGUILayout.HelpBox("← 请在左侧列表中选择一条引导配置", MessageType.Info); return; } var entry = _entries[_selectedIndex]; _detailScroll = EditorGUILayout.BeginScrollView(_detailScroll); EditorGUILayout.LabelField("基本信息", _headerStyle); EditorGUI.indentLevel++; DrawField("Id", ref entry.Id); DrawField("Name (名称)", ref entry.Name); DrawField("Priority (优先级)", ref entry.Priority); EditorGUI.indentLevel--; EditorGUILayout.Space(8); // Conditions _showConditions = EditorGUILayout.Foldout(_showConditions, "Conditions (触发条件)", true); if (_showConditions) { EditorGUI.indentLevel++; DrawStringList("Satisfy (满足条件)", ref entry.Conditions.Satisfy, ref _showSatisfy); DrawField("CheckAgain", ref entry.Conditions.CheckAgain); DrawStringList("Trigger (触发器)", ref entry.Conditions.Trigger, ref _showTrigger); DrawField("End (结束条件)", ref entry.Conditions.End); DrawField("Delay (延迟秒)", ref entry.Conditions.Delay); DrawField("Limit (限制次数)", ref entry.Conditions.Limit); EditorGUI.indentLevel--; } EditorGUILayout.Space(8); // ShowUI _showShowUI = EditorGUILayout.Foldout(_showShowUI, "ShowUI (显示配置)", true); if (_showShowUI) { EditorGUI.indentLevel++; DrawField("LangText (多语言Key)", ref entry.ShowUI.LangText); DrawField("RoleExpression (角色表情)", ref entry.ShowUI.RoleExpression); DrawField("DialogPos (对话位置)", ref entry.ShowUI.DialogPos); DrawField("OkBtn (确认按钮)", ref entry.ShowUI.OkBtn); DrawField("ClickSg (点击信号)", ref entry.ShowUI.ClickSg); DrawField("ClickPos (点击位置)", ref entry.ShowUI.ClickPos); DrawField("FocusPos (聚焦位置)", ref entry.ShowUI.FocusPos); DrawField("Phone (手机引导)", ref entry.ShowUI.Phone); DrawField("ClickToSkip (点击跳过)", ref entry.ShowUI.ClickToSkip); EditorGUI.indentLevel--; } EditorGUILayout.EndScrollView(); } #endregion #region 辅助绘制 private void DrawField(string label, ref int value) { int newVal = EditorGUILayout.IntField(label, value); if (newVal != value) { value = newVal; _isDirty = true; } } private void DrawField(string label, ref float value) { float newVal = EditorGUILayout.FloatField(label, value); if (!Mathf.Approximately(newVal, value)) { value = newVal; _isDirty = true; } } private void DrawField(string label, ref string value) { string newVal = EditorGUILayout.TextField(label, value ?? ""); if (newVal != (value ?? "")) { value = newVal; _isDirty = true; } } private void DrawStringList(string label, ref List list, ref bool foldout) { EditorGUILayout.BeginHorizontal(); foldout = EditorGUILayout.Foldout(foldout, $"{label} ({list.Count})", true); if (GUILayout.Button("+", GUILayout.Width(20))) { list.Add(""); _isDirty = true; } EditorGUILayout.EndHorizontal(); if (!foldout) return; EditorGUI.indentLevel++; for (int i = 0; i < list.Count; i++) { EditorGUILayout.BeginHorizontal(); string newVal = EditorGUILayout.TextField($"[{i}]", list[i] ?? ""); if (newVal != (list[i] ?? "")) { list[i] = newVal; _isDirty = true; } if (GUILayout.Button("↑", GUILayout.Width(20)) && i > 0) { (list[i - 1], list[i]) = (list[i], list[i - 1]); _isDirty = true; } if (GUILayout.Button("↓", GUILayout.Width(20)) && i < list.Count - 1) { (list[i], list[i + 1]) = (list[i + 1], list[i]); _isDirty = true; } if (GUILayout.Button("×", GUILayout.Width(20))) { list.RemoveAt(i); _isDirty = true; break; } EditorGUILayout.EndHorizontal(); } EditorGUI.indentLevel--; } #endregion #region 操作 private List GetFilteredAndSorted() { IEnumerable result = _entries; // 按名称搜索 if (!string.IsNullOrEmpty(_searchText)) { string lower = _searchText.ToLower(); result = result.Where(e => (e.Name ?? "").ToLower().Contains(lower)); } // 按ID搜索 if (!string.IsNullOrEmpty(_searchId) && int.TryParse(_searchId, out int searchIdVal)) { result = result.Where(e => e.Id.ToString().Contains(_searchId)); } // 排序 switch (_sortMode) { case SortMode.Id: result = result.OrderBy(e => e.Id); break; case SortMode.Priority: result = result.OrderByDescending(e => e.Priority).ThenBy(e => e.Id); break; case SortMode.Name: result = result.OrderBy(e => e.Name ?? ""); break; } return result.ToList(); } private void AddNewEntry() { int maxId = _entries.Count > 0 ? _entries.Max(e => e.Id) : 0; var newEntry = new GuideEntry { Id = maxId + 1, Name = "新引导步骤", }; _entries.Add(newEntry); _selectedIndex = _entries.Count - 1; _isDirty = true; } private void DuplicateSelected() { if (_selectedIndex < 0 || _selectedIndex >= _entries.Count) return; var src = _entries[_selectedIndex]; string json = JsonUtility.ToJson(src); var copy = JsonUtility.FromJson(json); // 深拷贝列表字段 (JsonUtility 会处理) copy.Id = _entries.Max(e => e.Id) + 1; copy.Name = src.Name + "_副本"; _entries.Insert(_selectedIndex + 1, copy); _selectedIndex = _selectedIndex + 1; _isDirty = true; } private void DeleteSelected() { if (_selectedIndex < 0 || _selectedIndex >= _entries.Count) return; var entry = _entries[_selectedIndex]; if (EditorUtility.DisplayDialog("确认删除", $"确定删除引导 [{entry.Id}] {entry.Name}?", "删除", "取消")) { _entries.RemoveAt(_selectedIndex); _selectedIndex = Mathf.Min(_selectedIndex, _entries.Count - 1); _isDirty = true; } } #endregion #region JSON 解析/序列化 (手写,兼容原文件格式) /// /// 使用 Unity 内置 JsonUtility 无法直接解析顶层数组,这里包装一下。 /// 原文件存在尾逗号等不规范 JSON,需要预处理。 /// private List ParseJsonArray(string json) { // 预处理:移除尾逗号 (trailing commas before ] or }) json = Regex.Replace(json, @",(\s*[\]\}])", "$1"); // 包装成对象让 JsonUtility 能解析 string wrapped = "{\"items\":" + json + "}"; var wrapper = JsonUtility.FromJson(wrapped); return wrapper != null && wrapper.items != null ? wrapper.items : new List(); } [Serializable] private class GuideEntryArrayWrapper { public List items; } /// /// 序列化回原始格式(带缩进的 JSON 数组,保持和原文件一致的风格) /// private string SerializeToJson(List entries) { var sb = new System.Text.StringBuilder(); sb.AppendLine("["); for (int i = 0; i < entries.Count; i++) { var e = entries[i]; sb.AppendLine(" {"); sb.AppendLine($" \"Id\": {e.Id},"); sb.AppendLine($" \"Name\":\"{EscapeJson(e.Name)}\","); sb.AppendLine($" \"Priority\":{e.Priority},"); // Conditions sb.AppendLine(" \"Conditions\":{"); sb.AppendLine(" \"Satisfy\":["); for (int s = 0; s < e.Conditions.Satisfy.Count; s++) { string comma = s < e.Conditions.Satisfy.Count - 1 ? "," : ""; sb.AppendLine($" \"{EscapeJson(e.Conditions.Satisfy[s])}\"{comma}"); } sb.AppendLine(" ],"); sb.AppendLine($" \"CheckAgain\":{e.Conditions.CheckAgain},"); sb.AppendLine(" \"Trigger\":["); for (int t = 0; t < e.Conditions.Trigger.Count; t++) { string comma = t < e.Conditions.Trigger.Count - 1 ? "," : ""; sb.AppendLine($" \"{EscapeJson(e.Conditions.Trigger[t])}\"{comma}"); } sb.AppendLine(" ],"); sb.AppendLine($" \"End\":\"{EscapeJson(e.Conditions.End)}\","); sb.AppendLine($" \"Delay\":{FormatFloat(e.Conditions.Delay)},"); sb.AppendLine($" \"Limit\":{e.Conditions.Limit}"); sb.AppendLine(" },"); // ShowUI sb.AppendLine(" \"ShowUI\": {"); sb.AppendLine($" \"LangText\":\"{EscapeJson(e.ShowUI.LangText)}\","); sb.AppendLine($" \"RoleExpression\":\"{EscapeJson(e.ShowUI.RoleExpression)}\","); sb.AppendLine($" \"DialogPos\":\"{EscapeJson(e.ShowUI.DialogPos)}\","); sb.AppendLine($" \"OkBtn\":{e.ShowUI.OkBtn},"); sb.AppendLine($" \"ClickSg\":{e.ShowUI.ClickSg},"); sb.AppendLine($" \"ClickPos\":\"{EscapeJson(e.ShowUI.ClickPos)}\","); sb.AppendLine($" \"FocusPos\":\"{EscapeJson(e.ShowUI.FocusPos)}\","); sb.AppendLine($" \"Phone\":{e.ShowUI.Phone},"); sb.AppendLine($" \"ClickToSkip\":{e.ShowUI.ClickToSkip}"); sb.AppendLine(" }"); string entryComma = i < entries.Count - 1 ? "," : ""; sb.AppendLine($" }}{entryComma}"); } sb.Append("]"); return sb.ToString(); } private string FormatFloat(float val) { // 如果是整数值,输出不带小数点的格式(和原文件保持一致) if (Mathf.Approximately(val, Mathf.Round(val))) return ((int)val).ToString(); return val.ToString("G"); } private string EscapeJson(string s) { if (string.IsNullOrEmpty(s)) return ""; return s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r").Replace("\t", "\\t"); } #endregion }