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
}