From e32c09aea23d4fd9e91b6f235b5a2db0c0f3c48d Mon Sep 17 00:00:00 2001 From: zhang hongbo Date: Wed, 27 May 2026 18:40:34 +0800 Subject: [PATCH] =?UTF-8?q?GuidConfigEditor=E6=9F=A5=E7=9C=8B=E5=99=A8?= =?UTF-8?q?=EF=BC=88=E5=88=9D=E7=89=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Editor/Design_Tools/GuideConfigEditor.cs | 544 ++++++++++++++++++ .../Design_Tools/GuideConfigEditor.cs.meta | 11 + 2 files changed, 555 insertions(+) create mode 100644 Scripts/Editor/Design_Tools/GuideConfigEditor.cs create mode 100644 Scripts/Editor/Design_Tools/GuideConfigEditor.cs.meta diff --git a/Scripts/Editor/Design_Tools/GuideConfigEditor.cs b/Scripts/Editor/Design_Tools/GuideConfigEditor.cs new file mode 100644 index 0000000..39e03f7 --- /dev/null +++ b/Scripts/Editor/Design_Tools/GuideConfigEditor.cs @@ -0,0 +1,544 @@ +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 +} diff --git a/Scripts/Editor/Design_Tools/GuideConfigEditor.cs.meta b/Scripts/Editor/Design_Tools/GuideConfigEditor.cs.meta new file mode 100644 index 0000000..96827d6 --- /dev/null +++ b/Scripts/Editor/Design_Tools/GuideConfigEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 22a0284075eddb94b9d56c8e0c19b1e9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: