313 lines
13 KiB
C#
313 lines
13 KiB
C#
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
using BaseGames.EventChain;
|
||
|
||
namespace BaseGames.Editor
|
||
{
|
||
/// <summary>
|
||
/// 事件链可视化编辑器窗口(架构 14_NarrativeModule §13)。
|
||
/// 菜单:BaseGames/Tools/Event Chain Viewer
|
||
///
|
||
/// 功能:
|
||
/// - 左侧:chainId 分组总览(按完成状态着色)
|
||
/// - 右侧:选中链的 Conditions 和 Actions 表格
|
||
/// - Play Mode:运行时状态着色(已完成=绿 / 条件满足=橙 / 未满足=白)
|
||
/// - ChainCompletedCondition 依赖链箭头指示
|
||
/// - 执行日志(最近 20 条)
|
||
/// - 双击 → EditorGUIUtility.PingObject
|
||
/// </summary>
|
||
public class EventChainEditorWindow : EditorWindow
|
||
{
|
||
// ── State ──────────────────────────────────────────────────────────
|
||
private EventChainSO[] _allChains;
|
||
private EventChainSO _selectedChain;
|
||
private Vector2 _leftScroll;
|
||
private Vector2 _rightScroll;
|
||
private Vector2 _logScroll;
|
||
|
||
private static readonly List<string> _log = new();
|
||
private const int MaxLogEntries = 20;
|
||
|
||
// ── Colors ─────────────────────────────────────────────────────────
|
||
private static readonly Color ColCompleted = new Color(0.15f, 0.75f, 0.25f, 0.80f);
|
||
private static readonly Color ColActive = new Color(0.95f, 0.60f, 0.10f, 0.80f);
|
||
private static readonly Color ColPending = new Color(0.70f, 0.70f, 0.75f, 0.80f);
|
||
|
||
[MenuItem("BaseGames/Tools/Event Chain Viewer")]
|
||
public static void OpenWindow()
|
||
{
|
||
var win = GetWindow<EventChainEditorWindow>("Event Chain Viewer");
|
||
win.minSize = new Vector2(800, 500);
|
||
win.Show();
|
||
}
|
||
|
||
/// <summary>外部调用:向执行日志追加一条记录(可在运行时由 EventChainManager 调用)。</summary>
|
||
public static void LogExecution(string chainId, string message)
|
||
{
|
||
_log.Add($"[{System.DateTime.Now:HH:mm:ss}] [{chainId}] {message}");
|
||
if (_log.Count > MaxLogEntries)
|
||
_log.RemoveAt(0);
|
||
}
|
||
|
||
// ── Lifecycle ─────────────────────────────────────────────────────
|
||
|
||
private void OnEnable()
|
||
{
|
||
RefreshChainList();
|
||
EditorApplication.playModeStateChanged += OnPlayModeChanged;
|
||
EventChainManager.OnChainExecutedInEditor += LogExecution;
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
EditorApplication.playModeStateChanged -= OnPlayModeChanged;
|
||
EventChainManager.OnChainExecutedInEditor -= LogExecution;
|
||
}
|
||
|
||
private void OnPlayModeChanged(PlayModeStateChange state)
|
||
{
|
||
if (state == PlayModeStateChange.EnteredPlayMode
|
||
|| state == PlayModeStateChange.ExitingPlayMode)
|
||
{
|
||
RefreshChainList();
|
||
Repaint();
|
||
}
|
||
}
|
||
|
||
private void RefreshChainList()
|
||
{
|
||
var guids = AssetDatabase.FindAssets("t:EventChainSO");
|
||
var chains = new List<EventChainSO>(guids.Length);
|
||
foreach (var g in guids)
|
||
{
|
||
var path = AssetDatabase.GUIDToAssetPath(g);
|
||
var chain = AssetDatabase.LoadAssetAtPath<EventChainSO>(path);
|
||
if (chain != null) chains.Add(chain);
|
||
}
|
||
_allChains = chains.OrderBy(c => c.chainId).ToArray();
|
||
}
|
||
|
||
// ── GUI ────────────────────────────────────────────────────────────
|
||
|
||
private void OnGUI()
|
||
{
|
||
DrawToolbar();
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
|
||
// 左:链列表
|
||
EditorGUILayout.BeginVertical(GUILayout.Width(240));
|
||
DrawChainList();
|
||
EditorGUILayout.EndVertical();
|
||
|
||
// 分割线
|
||
EditorGUILayout.BeginVertical(GUILayout.Width(2));
|
||
EditorGUI.DrawRect(GUILayoutUtility.GetRect(2, position.height), new Color(0.1f, 0.1f, 0.1f));
|
||
EditorGUILayout.EndVertical();
|
||
|
||
// 右:选中链详情
|
||
EditorGUILayout.BeginVertical();
|
||
if (_selectedChain != null)
|
||
DrawChainDetail(_selectedChain);
|
||
else
|
||
EditorGUILayout.HelpBox("从左侧选择一条事件链查看详情。", MessageType.None);
|
||
EditorGUILayout.EndVertical();
|
||
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
// ── Toolbar ───────────────────────────────────────────────────────
|
||
|
||
private void DrawToolbar()
|
||
{
|
||
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
|
||
if (GUILayout.Button("刷新", EditorStyles.toolbarButton, GUILayout.Width(50)))
|
||
RefreshChainList();
|
||
GUILayout.FlexibleSpace();
|
||
EditorGUILayout.LabelField(
|
||
$"共 {_allChains?.Length ?? 0} 条事件链",
|
||
EditorStyles.toolbarButton);
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
// ── 左侧链列表 ────────────────────────────────────────────────────
|
||
|
||
private void DrawChainList()
|
||
{
|
||
EditorGUILayout.LabelField("事件链列表", EditorStyles.boldLabel);
|
||
_leftScroll = EditorGUILayout.BeginScrollView(_leftScroll);
|
||
|
||
if (_allChains == null || _allChains.Length == 0)
|
||
{
|
||
EditorGUILayout.HelpBox("未找到 EventChainSO 资产。", MessageType.Info);
|
||
EditorGUILayout.EndScrollView();
|
||
return;
|
||
}
|
||
|
||
foreach (var chain in _allChains)
|
||
{
|
||
if (chain == null) continue;
|
||
|
||
bool isSelected = _selectedChain == chain;
|
||
bool isCompleted = IsChainCompleted(chain);
|
||
bool isActive = Application.isPlaying && IsChainActive(chain);
|
||
|
||
Color bgColor = isCompleted ? ColCompleted : isActive ? ColActive : ColPending;
|
||
var prevBg = GUI.backgroundColor;
|
||
GUI.backgroundColor = isSelected ? bgColor * 1.4f : bgColor * 0.7f;
|
||
|
||
EditorGUILayout.BeginHorizontal("box");
|
||
|
||
// 状态图标
|
||
string icon = isCompleted ? "✓" : isActive ? "▶" : "○";
|
||
EditorGUILayout.LabelField(icon, GUILayout.Width(16));
|
||
|
||
if (GUILayout.Button(chain.chainId, isSelected ? EditorStyles.boldLabel : EditorStyles.label))
|
||
_selectedChain = chain;
|
||
|
||
// 双击 Ping
|
||
if (Event.current.type == EventType.MouseDown && Event.current.clickCount == 2
|
||
&& GUILayoutUtility.GetLastRect().Contains(Event.current.mousePosition))
|
||
{
|
||
EditorGUIUtility.PingObject(chain);
|
||
Event.current.Use();
|
||
}
|
||
|
||
EditorGUILayout.EndHorizontal();
|
||
GUI.backgroundColor = prevBg;
|
||
}
|
||
|
||
EditorGUILayout.EndScrollView();
|
||
}
|
||
|
||
// ── 右侧详情 ──────────────────────────────────────────────────────
|
||
|
||
private void DrawChainDetail(EventChainSO chain)
|
||
{
|
||
_rightScroll = EditorGUILayout.BeginScrollView(_rightScroll);
|
||
|
||
// 标题行
|
||
EditorGUILayout.BeginHorizontal();
|
||
EditorGUILayout.LabelField(
|
||
$"事件链:{chain.chainId}",
|
||
EditorStyles.boldLabel);
|
||
if (GUILayout.Button("↗ Ping", GUILayout.Width(60)))
|
||
EditorGUIUtility.PingObject(chain);
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
EditorGUILayout.LabelField(
|
||
$"可重复:{(chain.repeatable ? "是" : "否")} | " +
|
||
$"动作间隔:{chain.actionDelay:F2}s",
|
||
EditorStyles.miniLabel);
|
||
|
||
EditorGUILayout.Space(6);
|
||
|
||
// Conditions 表格
|
||
EditorGUILayout.LabelField("触发条件(全部满足才触发)", EditorStyles.boldLabel);
|
||
if (chain.conditions != null && chain.conditions.Length > 0)
|
||
{
|
||
foreach (var cond in chain.conditions)
|
||
{
|
||
if (cond == null) continue;
|
||
|
||
bool met = Application.isPlaying && cond.IsMet();
|
||
var prevBg = GUI.backgroundColor;
|
||
GUI.backgroundColor = met ? ColCompleted * 0.8f : new Color(0.9f, 0.9f, 0.9f, 0.3f);
|
||
|
||
EditorGUILayout.BeginHorizontal("box");
|
||
string status = Application.isPlaying ? (met ? "✓" : "✗") : "—";
|
||
EditorGUILayout.LabelField(status, GUILayout.Width(20));
|
||
EditorGUILayout.LabelField(cond.GetType().Name, GUILayout.Width(220));
|
||
|
||
// 依赖箭头:ChainCompletedCondition
|
||
if (cond is ChainCompletedCondition depCond)
|
||
{
|
||
EditorGUILayout.LabelField($"→ 依赖链:{depCond.chainId}",
|
||
EditorStyles.miniLabel);
|
||
}
|
||
|
||
if (GUILayout.Button("↗", GUILayout.Width(24)))
|
||
EditorGUIUtility.PingObject(cond);
|
||
EditorGUILayout.EndHorizontal();
|
||
GUI.backgroundColor = prevBg;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
EditorGUILayout.LabelField("(无条件,立即触发)", EditorStyles.miniLabel);
|
||
}
|
||
|
||
EditorGUILayout.Space(6);
|
||
|
||
// Actions 表格
|
||
EditorGUILayout.LabelField("执行动作(顺序执行)", EditorStyles.boldLabel);
|
||
if (chain.actions != null && chain.actions.Length > 0)
|
||
{
|
||
for (int i = 0; i < chain.actions.Length; i++)
|
||
{
|
||
var action = chain.actions[i];
|
||
if (action == null) continue;
|
||
|
||
EditorGUILayout.BeginHorizontal("box");
|
||
EditorGUILayout.LabelField($"[{i}]", GUILayout.Width(30));
|
||
EditorGUILayout.LabelField(action.GetType().Name, GUILayout.Width(200));
|
||
EditorGUILayout.LabelField(action.name, EditorStyles.miniLabel);
|
||
if (GUILayout.Button("↗", GUILayout.Width(24)))
|
||
EditorGUIUtility.PingObject(action);
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
EditorGUILayout.LabelField("(无动作)", EditorStyles.miniLabel);
|
||
}
|
||
|
||
// 执行日志
|
||
EditorGUILayout.Space(6);
|
||
EditorGUILayout.LabelField($"执行日志(最近 {MaxLogEntries} 条)", EditorStyles.boldLabel);
|
||
_logScroll = EditorGUILayout.BeginScrollView(_logScroll, GUILayout.Height(120));
|
||
var relevantLogs = _log.Where(l => l.Contains(chain.chainId)).ToList();
|
||
if (relevantLogs.Count == 0)
|
||
EditorGUILayout.LabelField("—(无日志)", EditorStyles.miniLabel);
|
||
else
|
||
foreach (var entry in relevantLogs)
|
||
EditorGUILayout.LabelField(entry, EditorStyles.miniLabel);
|
||
EditorGUILayout.EndScrollView();
|
||
|
||
EditorGUILayout.EndScrollView();
|
||
}
|
||
|
||
// ── 运行时状态查询 ────────────────────────────────────────────────
|
||
|
||
private static bool IsChainCompleted(EventChainSO chain)
|
||
{
|
||
if (!Application.isPlaying) return false;
|
||
var manager = FindFirstObjectByType<EventChainManager>();
|
||
if (manager == null) return false;
|
||
// 通过反射读取 _completedChains
|
||
var field = typeof(EventChainManager).GetField(
|
||
"_completedChains",
|
||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||
if (field?.GetValue(manager) is HashSet<string> completed)
|
||
return completed.Contains(chain.chainId);
|
||
return false;
|
||
}
|
||
|
||
private static bool IsChainActive(EventChainSO chain)
|
||
{
|
||
// 链"激活中"= 有任意条件已满足但链未完成
|
||
if (chain.conditions == null) return false;
|
||
return chain.conditions.Any(c => c != null && c.IsMet());
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
// Play Mode 下每秒刷新一次以更新状态颜色
|
||
if (Application.isPlaying)
|
||
Repaint();
|
||
}
|
||
}
|
||
}
|