using System; using System.Collections.Generic; using System.Linq; using AibisDream; using UnityEditor; using UnityEditor.Experimental.GraphView; using UnityEngine; using UnityEngine.UIElements; namespace AibisDream.SystemEditor { public class ChapterGraphView : GraphView { public event Action OnNodeSelected; public event Action OnNodeDeleted; private const float HorizontalSpacing = 420f; private const float VerticalSpacing = 220f; public const string DefaultFolderPrefKey = "AibisDream.ChapterGraph.DefaultFolder"; public const string DefaultFolder = "Assets/ScriptableObjects/SceneSO"; public ChapterGraphView() { style.flexGrow = 1; SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale); this.AddManipulator(new ContentDragger()); this.AddManipulator(new SelectionDragger()); this.AddManipulator(new RectangleSelector()); var grid = new GridBackground(); Insert(0, grid); grid.StretchToParentSize(); graphViewChanged += OnGraphViewChanged; } public void UpdateNodeColor(TalkSceneSO so) { foreach (var node in graphElements.OfType()) { if (node.SceneSo == so) { node.UpdateColor(); break; } } } public void LoadGraph(string folderPath = null) { ClearGraph(); var guids = AssetDatabase.FindAssets("t:TalkSceneSO", folderPath != null ? new[] { folderPath } : null); var allSos = guids.Select(g => AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(g))).Where(so => so != null).ToList(); var nodeMap = new Dictionary(); // 创建节点 foreach (var so in allSos) { var node = new ChapterNode(so); AddElement(node); nodeMap[so] = node; } // 创建连线 foreach (var so in allSos) { if (so.exits == null) continue; if (!nodeMap.TryGetValue(so, out var sourceNode)) continue; for (int i = 0; i < so.exits.Count; i++) { var exit = so.exits[i]; if (exit == null || exit.targetScene == null) continue; if (!nodeMap.TryGetValue(exit.targetScene, out var targetNode)) continue; var outputPort = sourceNode.GetOutputPort(i); var inputPort = targetNode.InputPort; if (outputPort == null || inputPort == null) continue; var edge = new Edge { output = outputPort, input = inputPort }; edge.output.Connect(edge); edge.input.Connect(edge); AddElement(edge); } } } public void ClearGraph() { // 清除所有元素 var elementsToRemove = graphElements.ToList(); foreach (var element in elementsToRemove) { RemoveElement(element); } } public override void BuildContextualMenu(ContextualMenuPopulateEvent evt) { base.BuildContextualMenu(evt); if (evt.target is GraphView) { var mousePosition = contentViewContainer.WorldToLocal(evt.mousePosition); evt.menu.AppendAction("添加章节节点", (_) => CreateChapterNodeAt(mousePosition)); } } public override List GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter) { var compatiblePorts = new List(); var startNode = startPort.node as ChapterNode; if (startNode == null) return compatiblePorts; ports.ForEach(port => { var targetNode = port.node as ChapterNode; if (targetNode == null) return; if (targetNode == startNode) return; if (startPort.direction == port.direction) return; if (startPort.node == port.node) return; compatiblePorts.Add(port); }); return compatiblePorts; } private GraphViewChange OnGraphViewChanged(GraphViewChange change) { // 先处理节点删除确认:用户取消则从移除列表中剔除 if (change.elementsToRemove != null) { var nodesToRemove = change.elementsToRemove.OfType().ToList(); foreach (var node in nodesToRemove) { if (!ConfirmRemoveNode(node)) { change.elementsToRemove.Remove(node); // 同时保留与该节点相连的边,避免边被单独删掉 var edgesToKeep = change.elementsToRemove.OfType() .Where(e => e.output?.node == node || e.input?.node == node) .ToList(); foreach (var edge in edgesToKeep) { change.elementsToRemove.Remove(edge); } } } } // 处理 Edge 删除 if (change.elementsToRemove != null) { foreach (var edge in change.elementsToRemove.OfType()) { HandleEdgeRemoved(edge); } foreach (var node in change.elementsToRemove.OfType()) { HandleNodeRemoved(node); } } // 处理 Edge 创建 if (change.edgesToCreate != null) { foreach (var edge in change.edgesToCreate) { HandleEdgeCreated(edge); } } // 处理节点移动 if (change.movedElements != null) { foreach (var element in change.movedElements) { if (element is ChapterNode node) { node.SyncPositionToSo(); } } } return change; } private void HandleEdgeCreated(Edge edge) { if (!(edge.output?.node is ChapterNode sourceNode)) return; if (!(edge.input?.node is ChapterNode targetNode)) return; var sourceSo = sourceNode.SceneSo; var targetSo = targetNode.SceneSo; if (sourceSo == null || targetSo == null) return; int exitIndex = edge.output.userData is int idx ? idx : -1; if (exitIndex < 0 || exitIndex >= sourceSo.exits.Count) return; Undo.RecordObject(sourceSo, "Connect Chapter"); sourceSo.exits[exitIndex].targetScene = targetSo; EditorUtility.SetDirty(sourceSo); } private void HandleEdgeRemoved(Edge edge) { if (!(edge.output?.node is ChapterNode sourceNode)) return; var sourceSo = sourceNode.SceneSo; if (sourceSo == null) return; int exitIndex = edge.output.userData is int idx ? idx : -1; if (exitIndex < 0 || exitIndex >= sourceSo.exits.Count) return; Undo.RecordObject(sourceSo, "Disconnect Chapter"); sourceSo.exits[exitIndex].targetScene = null; EditorUtility.SetDirty(sourceSo); } private bool ConfirmRemoveNode(ChapterNode node) { var so = node.SceneSo; if (so == null) return true; var referrers = new List(); foreach (var other in graphElements.OfType()) { if (other == node) continue; if (other.SceneSo == null || other.SceneSo.exits == null) continue; foreach (var exit in other.SceneSo.exits) { if (exit.targetScene == so) { referrers.Add(string.IsNullOrEmpty(other.SceneSo.title) ? other.SceneSo.name : other.SceneSo.title); break; } } } var name = string.IsNullOrEmpty(so.title) ? so.name : so.title; var msg = $"确定要删除章节节点 \"{name}\" 吗?"; if (referrers.Count > 0) { msg += $"\n\n以下节点仍引用该节点:\n• {string.Join("\n• ", referrers)}"; } return EditorUtility.DisplayDialog("删除确认", msg, "删除", "取消"); } private void HandleNodeRemoved(ChapterNode node) { var so = node.SceneSo; if (so == null) return; var path = AssetDatabase.GetAssetPath(so); if (!string.IsNullOrEmpty(path)) { AssetDatabase.DeleteAsset(path); } OnNodeDeleted?.Invoke(so); } public override void AddToSelection(ISelectable selectable) { base.AddToSelection(selectable); NotifySelectionChanged(); } public override void RemoveFromSelection(ISelectable selectable) { base.RemoveFromSelection(selectable); NotifySelectionChanged(); } public override void ClearSelection() { base.ClearSelection(); NotifySelectionChanged(); } private void NotifySelectionChanged() { var selectedNode = selection.OfType().FirstOrDefault(); OnNodeSelected?.Invoke(selectedNode?.SceneSo); } public void CreateChapterNodeAt(Vector2 position, string defaultFolder = null) { var folder = defaultFolder ?? EditorPrefs.GetString(DefaultFolderPrefKey, DefaultFolder); var path = EditorUtility.SaveFilePanelInProject("新建章节", "NewChapter", "asset", "选择新章节的保存位置", folder); if (string.IsNullOrEmpty(path)) return; var so = ScriptableObject.CreateInstance(); so.name = System.IO.Path.GetFileNameWithoutExtension(path); so.title = "新章节"; so.nodeColor = new Color(0.85f, 0.45f, 0.45f); so.graphPosition = position; so.exits = new System.Collections.Generic.List(); AssetDatabase.CreateAsset(so, path); EditorUtility.SetDirty(so); AssetDatabase.SaveAssets(); // 记住文件夹 var savedFolder = System.IO.Path.GetDirectoryName(path)?.Replace('\\', '/'); if (!string.IsNullOrEmpty(savedFolder)) { EditorPrefs.SetString(DefaultFolderPrefKey, savedFolder); } var node = new ChapterNode(so); AddElement(node); } public void AutoLayout() { var nodes = graphElements.OfType().ToList(); if (nodes.Count == 0) return; // 计算入度 var inDegree = nodes.ToDictionary(n => n, _ => 0); var edges = graphElements.OfType().ToList(); foreach (var edge in edges) { if (edge.input?.node is ChapterNode target && inDegree.ContainsKey(target)) { inDegree[target]++; } } // 分层 BFS var layer = new Dictionary(); var queue = new Queue(); // 入度为 0 的节点作为起点 foreach (var node in nodes) { if (inDegree[node] == 0) { layer[node] = 0; queue.Enqueue(node); } } // 如果没有入度为 0 的节点(纯循环),任选一个作为起点 if (queue.Count == 0) { layer[nodes[0]] = 0; queue.Enqueue(nodes[0]); } while (queue.Count > 0) { var current = queue.Dequeue(); var currentLayer = layer[current]; foreach (var edge in edges) { if (edge.output?.node == current && edge.input?.node is ChapterNode next) { if (!layer.ContainsKey(next)) { layer[next] = currentLayer + 1; queue.Enqueue(next); } else { layer[next] = Math.Max(layer[next], currentLayer + 1); } } } } // 为未分配层级的节点(孤立或不连通部分)分配层级 foreach (var node in nodes) { if (!layer.ContainsKey(node)) { layer[node] = 0; } } // 按层级分组并计算位置 var groups = layer.GroupBy(kvp => kvp.Value).OrderBy(g => g.Key).ToList(); Undo.RecordObjects(nodes.Select(n => n.SceneSo).ToArray(), "Auto Layout"); for (int i = 0; i < groups.Count; i++) { var group = groups[i]; var groupNodes = group.ToList(); float x = group.Key * HorizontalSpacing; float startY = -(groupNodes.Count - 1) * VerticalSpacing / 2f; for (int j = 0; j < groupNodes.Count; j++) { var node = groupNodes[j].Key; var y = startY + j * VerticalSpacing; var newPos = new Vector2(x, y); node.SetPosition(new Rect(newPos, Vector2.zero)); node.SceneSo.graphPosition = newPos; EditorUtility.SetDirty(node.SceneSo); } } } public void ValidateGraph() { var nodes = graphElements.OfType().ToList(); var issues = new List(); var soSet = new HashSet(nodes.Select(n => n.SceneSo)); // 孤立节点 foreach (var node in nodes) { var so = node.SceneSo; bool hasIncoming = graphElements.OfType().Any(e => e.input?.node == node); bool hasOutgoing = so.exits != null && so.exits.Any(e => e.targetScene != null); if (!hasIncoming && !hasOutgoing) { issues.Add($"孤立节点: {so.name} (没有入边也没有出边)"); } } // 循环引用 (DFS) var visited = new HashSet(); var visiting = new HashSet(); foreach (var so in soSet) { if (!visited.Contains(so)) { CheckCycle(so, visited, visiting, soSet, issues); } } // 空 exitName foreach (var node in nodes) { var so = node.SceneSo; if (so.exits == null) continue; for (int i = 0; i < so.exits.Count; i++) { if (string.IsNullOrEmpty(so.exits[i].exitName)) { issues.Add($"空 exitName: {so.name} 的第 {i + 1} 个出口"); } } } if (issues.Count == 0) { EditorUtility.DisplayDialog("验证结果", "图表验证通过,未发现问题。", "确定"); } else { var msg = string.Join("\n", issues.Take(20)); if (issues.Count > 20) { msg += $"\n... 还有 {issues.Count - 20} 个问题"; } EditorUtility.DisplayDialog($"验证发现 {issues.Count} 个问题", msg, "确定"); } } private void CheckCycle(TalkSceneSO current, HashSet visited, HashSet visiting, HashSet soSet, List issues) { visited.Add(current); visiting.Add(current); if (current.exits != null) { foreach (var exit in current.exits) { var next = exit.targetScene; if (next == null || !soSet.Contains(next)) continue; if (visiting.Contains(next)) { issues.Add($"循环引用: {current.name} -> {next.name}"); continue; } if (!visited.Contains(next)) { CheckCycle(next, visited, visiting, soSet, issues); } } } visiting.Remove(current); } } }