using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Ichni.RhythmGame; using TMPro; using UniRx; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.InputSystem; using UnityEngine.UI; using UnityEngine.UI.Extensions; namespace Ichni.NodeScript { public class NodeManager : MonoBehaviour { public static NodeManager Instance; public List allNodes = new(); public GameObject nodeObjectPrefab; Canvas canvas; UILineRenderer dragLine; public float wireThickness = 5f; ConnectorSlot dragSource; bool isDraggingWire; // 中键拖拽面板 Vector2 _panelDragOrigin; bool _isPanning; // 单步调试 bool _debugMode; int _debugStep; public class WireConnection { public ConnectorSlot from, to; public UILineRenderer line; public bool selected; } public List connections = new(); public RectTransform canvasArea; public Camera refCamera; public Transform contextMenuRoot; public Transform NodeArea; public GameObject contextMenuItemPrefab; public GameObject uiInputPrefab; public GameElement startElement { get; private set; } // ---- 命名变量存储 ---- Dictionary _variables = new(); // ---- 脏元素收集,图执行完后统一 Refresh ---- HashSet _dirtyElements = new(); public void MarkDirty(GameElement e) { if (e != null) _dirtyElements.Add(e); } // ---- 生命周期调度表 ---- HashSet triggerTable = new(); HashSet runtimeTable = new(); public T GetVariable(string name) { if (_variables.TryGetValue(name, out var v) && v is T tv) return tv; return default; } public void SetVariable(string name, T value) { _variables[name] = value; } public bool TryGetVariable(string name, out T v) { if (_variables.TryGetValue(name, out var obj) && obj is T tv) { v = tv; return true; } v = default; return false; } void Awake() { if (Instance != null && Instance != this) { Destroy(this); return; } Instance = this; canvas = GetComponentInParent(); } void OnDestroy() { if (Instance == this) Instance = null; foreach (var w in connections) { if (w.line != null && w.line.gameObject != null) Destroy(w.line.gameObject); } connections.Clear(); if (dragLine != null && dragLine.gameObject != null) Destroy(dragLine.gameObject); } void Start() { if (refCamera == null) refCamera = canvas != null ? canvas.worldCamera : Camera.main; dragLine = CreateWire("DragWire", Color.white); dragLine.raycastTarget = false; dragLine.gameObject.SetActive(false); if (contextMenuRoot != null) contextMenuRoot.gameObject.SetActive(false); } /// 外部实例化 Manager 后调用,传入入口 GameElement 并自动创建 Start 节点 public void Init(GameElement element) { startElement = element; allNodes = FindObjectsByType(FindObjectsSortMode.None).ToList(); if (!allNodes.Any(n => n.nodeBase is NodeStart)) { var data = new NodeStart { NodeName = "Start", boundElement = element }; data.InitConnectors(); var go = nodeObjectPrefab != null ? Instantiate(nodeObjectPrefab, NodeArea) : new GameObject("Start", typeof(RectTransform)); var no = go.GetComponent(); if (no == null) no = go.AddComponent(); no.nodeBase = data; go.GetComponent().anchoredPosition = new Vector2(-400, 0); no.Init(); allNodes.Add(no); } } List selectedNodes = new(); (List<(Type type, Dictionary fields)> nodes, List<(int fromIdx, int toIdx, string outName, string inName)> wires) clipboard = (new(), new()); void Update() { if (Keyboard.current == null) return; var mousePos = Mouse.current.position.ReadValue(); // ---- 手动线悬停 / 点击检测 ---- WireConnection clickedWire = UpdateWireHover(); // ---- 空位点击 → 取消所有选中(线点击已在上一步处理) ---- if (Mouse.current.leftButton.wasPressedThisFrame && clickedWire == null) { var ped = new PointerEventData(EventSystem.current) { position = mousePos }; var hits = new List(); EventSystem.current.RaycastAll(ped, hits); bool hitUI = hits.Any(r => r.gameObject.GetComponent() != null || r.gameObject.GetComponent() != null); if (!hitUI) DeselectAll(); } // ---- 中键拖拽整个节点面板 ---- if (Mouse.current.middleButton.wasPressedThisFrame) { _panelDragOrigin = mousePos; _isPanning = true; } if (_isPanning && Mouse.current.middleButton.isPressed) { var delta = mousePos - _panelDragOrigin; _panelDragOrigin = mousePos; var nodeAreaRt = NodeArea as RectTransform; if (nodeAreaRt != null) nodeAreaRt.anchoredPosition += delta / (canvas != null ? canvas.scaleFactor : 1f); RefreshAllLines(); } if (Mouse.current.middleButton.wasReleasedThisFrame) _isPanning = false; // Esc → 退出调试 if (Keyboard.current.escapeKey.wasPressedThisFrame && _debugMode) DebugReset(); // Shift+Enter → 单步 if (Keyboard.current.leftShiftKey.isPressed && Keyboard.current.enterKey.wasPressedThisFrame) { if (!_debugMode) DebugInit(); DebugStep(); } // Ctrl+Enter → Run(保留) if (Keyboard.current.leftCtrlKey.isPressed && Keyboard.current.enterKey.wasPressedThisFrame) RunGraph(); // Ctrl+RightClick → Create Node if (Keyboard.current.leftCtrlKey.isPressed && Mouse.current.rightButton.wasPressedThisFrame) TryShowContextMenu(Mouse.current.position.ReadValue()); // Delete → Remove selected if (Keyboard.current.deleteKey.wasPressedThisFrame) DeleteSelected(); // Ctrl+C → Copy if (Keyboard.current.leftCtrlKey.isPressed && Keyboard.current.cKey.wasPressedThisFrame) CopySelected(); // Ctrl+V → Paste if (Keyboard.current.leftCtrlKey.isPressed && Keyboard.current.vKey.wasPressedThisFrame) PasteClipboard(); // F5 → 拓扑预览 if (Keyboard.current.f5Key.wasPressedThisFrame) PreviewOrder(); // // F1 → Save // if (Keyboard.current.f1Key.wasPressedThisFrame) // SaveToFile(); // // F2 → Load // if (Keyboard.current.f2Key.wasPressedThisFrame) // LoadFromFile(); } // ========== 选中 ========== void DeselectAll() { foreach (var n in selectedNodes) n.Selected = false; selectedNodes.Clear(); foreach (var w in connections) w.selected = false; } public void SelectNode(NodeObject node, PointerEventData e) { bool multi = e != null && (e.button == PointerEventData.InputButton.Left); bool shift = Keyboard.current != null && Keyboard.current.leftShiftKey.isPressed; if (!shift) { foreach (var n in selectedNodes) n.Selected = false; selectedNodes.Clear(); } if (selectedNodes.Contains(node)) { node.Selected = false; selectedNodes.Remove(node); } else { node.Selected = true; selectedNodes.Add(node); } } // ========== 删除 ========== void DeleteSelected() { // 1) 删选中线 for (int i = connections.Count - 1; i >= 0; i--) { var w = connections[i]; if (w.selected) RemoveConnection(w); } // 2) 删选中节点(含其关联线) foreach (var node in selectedNodes.ToList()) { for (int i = connections.Count - 1; i >= 0; i--) { if (connections[i].from?.ownerNode == node || connections[i].to?.ownerNode == node) RemoveConnection(connections[i]); } allNodes.Remove(node); Destroy(node.gameObject); } selectedNodes.Clear(); CleanupConnections(); } void RemoveConnection(WireConnection w) { // 断开 out→in 数据连接 if (w.from?.connectorOut is OutputAny outAny && w.to?.connectorIn is InputAny inAny) { outAny.DisconnectAny(inAny); inAny.DisconnectAny(); } else if (w.from?.connectorOut is OutputAny outAny2 && w.to?.connectorIn != null) { // OutputAny → Input: input 侧调 DisconnectAny var inType = w.to.connectorIn.GetType(); if (inType.IsGenericType && inType.GetGenericTypeDefinition() == typeof(Input<>)) inType.GetMethod("DisconnectAny")?.Invoke(w.to.connectorIn, null); } else if (w.from?.connectorOut != null && w.to?.connectorIn is InputAny inAny2) { inAny2.DisconnectAny(); } else if (w.from?.connectorOut != null && w.to?.connectorIn != null) { var outType = w.from.connectorOut.GetType(); var inType = w.to.connectorIn.GetType(); if (outType.IsGenericType && outType.GetGenericTypeDefinition() == typeof(Output<>)) { var discMethod = outType.GetMethod("Disconnect", new[] { inType }); discMethod?.Invoke(w.from.connectorOut, new[] { w.to.connectorIn }); } if (inType.IsGenericType && inType.GetGenericTypeDefinition() == typeof(Input<>)) { var discMethod = inType.GetMethod("Disconnect", new[] { outType }); discMethod?.Invoke(w.to.connectorIn, new[] { w.from.connectorOut }); } } // 销毁线视觉 if (w.line != null) { if (w.line.gameObject != null) Destroy(w.line.gameObject); } w.from = null; w.to = null; w.line = null; connections.Remove(w); } void CleanupConnections() { for (int i = connections.Count - 1; i >= 0; i--) { var w = connections[i]; bool dead = w.from == null || w.to == null || w.line == null || w.from.ownerNode == null || w.to.ownerNode == null || w.from.connectorOut == null || w.to.connectorIn == null; if (dead) { if (w.line != null && w.line.gameObject != null) Destroy(w.line.gameObject); connections.RemoveAt(i); } } } // ========== 保存 / 加载 ========== [Serializable] public class GraphData { public string startElementGuid; public List nodes = new(); public List wires = new(); } [Serializable] public class NodeData { public string typeName; public float posX, posY; public List fieldValues = new(); } [Serializable] public class WireData { public int fromNodeIdx, toNodeIdx; public string fromOutput, toInput; } [Serializable] public struct FieldPair { public string key; public string json; public FieldPair(string k, string j) { key = k; json = j; } } static string SavePath => Application.streamingAssetsPath + "/NodeScript/"; static string SaveFile => SavePath + "graph.json"; public static string GetSavePath(string name) => SavePath + name + ".json"; public GraphData SaveGraph() { var g = new GraphData(); g.startElementGuid = startElement?.elementGuid.ToString(); var idxMap = new Dictionary(); for (int i = 0; i < allNodes.Count; i++) { var n = allNodes[i]; idxMap[n] = i; var nd = new NodeData { typeName = n.nodeBase.GetType().FullName, posX = n.GetComponent().anchoredPosition.x, posY = n.GetComponent().anchoredPosition.y }; foreach (var f in n.nodeBase.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance)) { if (typeof(IInput).IsAssignableFrom(f.FieldType) || typeof(IOutput).IsAssignableFrom(f.FieldType)) continue; if (System.Attribute.IsDefined(f, typeof(NonSerializedAttribute))) continue; nd.fieldValues.Add(new FieldPair(f.Name, JsonUtility.ToJson(f.GetValue(n.nodeBase)))); } g.nodes.Add(nd); } foreach (var w in connections) { if (w.from?.ownerNode == null || w.to?.ownerNode == null) continue; g.wires.Add(new WireData { fromNodeIdx = idxMap.GetValueOrDefault(w.from.ownerNode, -1), toNodeIdx = idxMap.GetValueOrDefault(w.to.ownerNode, -1), fromOutput = w.from.connectorOut?.Name, toInput = w.to.connectorIn?.Name }); } return g; } public void SaveToFile(string name = null) { string path = string.IsNullOrEmpty(name) ? SaveFile : GetSavePath(name); System.IO.Directory.CreateDirectory(SavePath); var json = JsonUtility.ToJson(SaveGraph(), true); System.IO.File.WriteAllText(path, json); Debug.Log("[NodeManager] Saved to " + path); } public void LoadFromFile(string name = null) { string path = string.IsNullOrEmpty(name) ? SaveFile : GetSavePath(name); if (!System.IO.File.Exists(path)) { Debug.LogWarning("[NodeManager] No save file at " + path); return; } // 清空 foreach (var n in allNodes) Destroy(n.gameObject); allNodes.Clear(); foreach (var w in connections) { if (w.line != null) Destroy(w.line.gameObject); } connections.Clear(); selectedNodes.Clear(); var json = System.IO.File.ReadAllText(path); var g = JsonUtility.FromJson(json); // 重建节点 var newNodes = new List(); foreach (var nd in g.nodes) { var type = Type.GetType(nd.typeName) ?? AppDomain.CurrentDomain.GetAssemblies() .Select(a => a.GetType(nd.typeName)).FirstOrDefault(t => t != null); if (type == null) { Debug.LogWarning("[NodeManager] Type not found: " + nd.typeName); continue; } var data = (NodeBase)Activator.CreateInstance(type); data.NodeName = type.Name; data.InitConnectors(); // 恢复字段值 foreach (var fp in nd.fieldValues) { var f = type.GetField(fp.key, BindingFlags.Public | BindingFlags.Instance); if (f != null && (f.FieldType.IsValueType || f.FieldType == typeof(string))) try { f.SetValue(data, JsonUtility.FromJson(fp.json, f.FieldType)); } catch { } } var go = nodeObjectPrefab != null ? Instantiate(nodeObjectPrefab, transform) : new GameObject(data.NodeName, typeof(RectTransform)); var no = go.GetComponent(); if (no == null) no = go.AddComponent(); no.nodeBase = data; go.GetComponent().anchoredPosition = new Vector2(nd.posX, nd.posY); no.Init(); allNodes.Add(no); newNodes.Add(no); } // 重建连线 foreach (var wd in g.wires) { if (wd.fromNodeIdx < 0 || wd.toNodeIdx < 0) continue; if (wd.fromNodeIdx >= newNodes.Count || wd.toNodeIdx >= newNodes.Count) continue; var srcSlots = newNodes[wd.fromNodeIdx].GetComponentsInChildren(); var dstSlots = newNodes[wd.toNodeIdx].GetComponentsInChildren(); var src = srcSlots.FirstOrDefault(s => !s.isInput && s.connectorOut?.Name == wd.fromOutput); var dst = dstSlots.FirstOrDefault(s => s.isInput && s.connectorIn?.Name == wd.toInput); if (src != null && dst != null) TryConnect(src, dst); } // 绑定 Start 节点的 element var sel = EditorManager.instance?.operationManager?.currentSelectedElements; if (sel != null && sel.Count > 0) { foreach (var n in allNodes) { if (n.nodeBase is NodeStart start) { start.boundElement = sel[0]; startElement = sel[0]; } } } Debug.Log("[NodeManager] Loaded " + allNodes.Count + " nodes, " + connections.Count + " wires"); } // ========== 复制 ========== void CopySelected() { clipboard.nodes.Clear(); clipboard.wires.Clear(); var idxMap = new Dictionary(); for (int i = 0; i < selectedNodes.Count; i++) { var node = selectedNodes[i]; idxMap[node] = i; var fields = new Dictionary(); foreach (var f in node.nodeBase.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance)) { if (!typeof(IInput).IsAssignableFrom(f.FieldType) && !typeof(IOutput).IsAssignableFrom(f.FieldType)) fields[f.Name] = f.GetValue(node.nodeBase); } clipboard.nodes.Add((node.nodeBase.GetType(), fields)); } // 复制选中节点之间的连线 foreach (var w in connections) { if (w.selected || (w.from != null && w.to != null && selectedNodes.Contains(w.from.ownerNode) && selectedNodes.Contains(w.to.ownerNode))) { int fi = idxMap.GetValueOrDefault(w.from.ownerNode, -1); int ti = idxMap.GetValueOrDefault(w.to.ownerNode, -1); if (fi >= 0 && ti >= 0) clipboard.wires.Add((fi, ti, w.from.connectorOut?.Name, w.to.connectorIn?.Name)); } } } void PasteClipboard() { var pos = Mouse.current.position.ReadValue(); var newNodes = new List(); foreach (var (type, fields) in clipboard.nodes) { var node = CreateNodeInstance(type, pos); foreach (var kv in fields) { var f = type.GetField(kv.Key, BindingFlags.Public | BindingFlags.Instance); if (f != null && f.FieldType == kv.Value?.GetType()) f.SetValue(node.nodeBase, kv.Value); } newNodes.Add(node); pos += new Vector2(30, -30); } // 重连粘贴节点之间的线 foreach (var (fi, ti, outName, inName) in clipboard.wires) { if (fi >= newNodes.Count || ti >= newNodes.Count) continue; var srcSlots = newNodes[fi].GetComponentsInChildren(); var dstSlots = newNodes[ti].GetComponentsInChildren(); var src = srcSlots.FirstOrDefault(s => !s.isInput && s.connectorOut?.Name == outName); var dst = dstSlots.FirstOrDefault(s => s.isInput && s.connectorIn?.Name == inName); if (src != null && dst != null) TryConnect(src, dst); } } NodeObject CreateNodeInstance(Type nodeType, Vector2 screenPos, bool allowDuplicate = true) { // Start / Entry 全局唯一 if (!allowDuplicate && allNodes.Any(n => n.nodeBase.GetType() == nodeType)) { Debug.LogWarning("[NodeManager] " + nodeType.Name + " already exists"); return null; } var data = (NodeBase)Activator.CreateInstance(nodeType); data.NodeName = nodeType.Name; data.InitConnectors(); GameObject go = nodeObjectPrefab != null ? Instantiate(nodeObjectPrefab, NodeArea) : new GameObject(data.NodeName, typeof(RectTransform)); var n = go.GetComponent(); if (n == null) n = go.AddComponent(); n.nodeBase = data; RectTransform rt = go.GetComponent(); var parentRt = NodeArea as RectTransform; if (parentRt == null) parentRt = (RectTransform)transform; RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRt, screenPos, refCamera, out var lp); rt.anchoredPosition = lp; n.Init(); allNodes.Add(n); return n; } // ---- coord ---- Vector2 ToLocal(RectTransform rt) { Vector2 screen; if (canvas == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay) screen = rt.position; else screen = RectTransformUtility.WorldToScreenPoint(canvas.worldCamera, rt.position); RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.transform as RectTransform, screen, canvas.worldCamera, out Vector2 local); return local; } Vector2 ToLocal(Vector2 sp) { RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.transform as RectTransform, sp, canvas.worldCamera, out var l); return l; } // ---- wire drag ---- public void StartWireDrag(ConnectorSlot slot, PointerEventData e) { if (slot.isInput || slot.connectorOut == null) return; dragSource = slot; isDraggingWire = true; dragLine.color = slot.connectorOut.ConnectorColor; dragLine.gameObject.SetActive(true); dragLine.Points = new[] { ToLocal(slot.connectorRect), ToLocal(e.position) }; } public void UpdateWireDrag(PointerEventData e) { if (isDraggingWire) dragLine.Points = new[] { ToLocal(dragSource.connectorRect), ToLocal(e.position) }; } public void EndWireDrag(PointerEventData e) { if (!isDraggingWire) return; dragLine.gameObject.SetActive(false); var t = HitTestSlot(e, true); if (t != null && t != dragSource) TryConnect(dragSource, t); dragSource = null; isDraggingWire = false; } // ---- connect ---- void TryConnect(ConnectorSlot src, ConnectorSlot dst) { if (src.connectorOut == null || dst.connectorIn == null) return; if (src.ownerNode == dst.ownerNode) return; // 环检测:从 dst 出发沿已有连线 DFS,看能否回到 src if (WouldCreateCycle(src.ownerNode.nodeBase, dst.ownerNode.nodeBase)) { Debug.LogWarning($"[NodeManager] Connection rejected: would create cycle ({src.ownerNode.nodeBase.NodeName} -> {dst.ownerNode.nodeBase.NodeName})"); return; } // 类型检查:处理 InputAny / OutputAny var outType = src.connectorOut.DataType; var inType = dst.connectorIn.DataType; // InputAny 未锁定 → 允许连接,锁定类型 if (dst.connectorIn is InputAny inAny && inAny.DataType == null) { // OK: 类型将从 src 锁定 } // InputAny 已锁定 → 检查兼容 else if (dst.connectorIn is InputAny inAnyLocked && inAnyLocked.DataType != null) { if (!TypesCompatible(outType, inAnyLocked.DataType)) { Debug.LogWarning($"[NodeManager] Type mismatch: {outType?.Name} -> InputAny(locked={inAnyLocked.DataType.Name})"); return; } } // 标准泛型端口 → 兼容匹配 else if (!TypesCompatible(outType, inType)) { Debug.LogWarning($"[NodeManager] Type mismatch: {outType?.Name} != {inType?.Name}"); return; } // 实际连接 if (src.connectorOut is OutputAny outAny && dst.connectorIn is InputAny inAny2) { outAny.ConnectAny(inAny2); inAny2.ConnectAny(outAny); } else if (src.connectorOut is OutputAny outAny3) { var inT = dst.connectorIn.GetType(); if (inT.IsGenericType && inT.GetGenericTypeDefinition() == typeof(Input<>)) { // OutputAny → Input: 通过 ConnectAny 桥接 var connectAny = inT.GetMethod("ConnectAny", new[] { typeof(IOutput) }); connectAny?.Invoke(dst.connectorIn, new[] { (IOutput)outAny3 }); } else { outAny3.ConnectAny(dst.connectorIn as InputAny); (dst.connectorIn as InputAny)?.ConnectAny(outAny3); } } else if (dst.connectorIn is InputAny inAny4) { // Output → InputAny inAny4.ConnectAny(src.connectorOut); } else { // Standard Output → Input var outReal = src.connectorOut.GetType(); var inReal = dst.connectorIn.GetType(); var connectMethod = outReal.GetMethods(BindingFlags.Instance | BindingFlags.Public) .FirstOrDefault(m => m.Name == "Connect" && m.GetParameters().Length == 1); var connectInMethod = inReal.GetMethods(BindingFlags.Instance | BindingFlags.Public) .FirstOrDefault(m => m.Name == "Connect" && m.GetParameters().Length == 1); if (connectMethod == null || connectInMethod == null) { Debug.LogError($"[Wire] Reflection failed: {outReal.Name}.Connect({inReal.Name})"); return; } connectMethod.Invoke(src.connectorOut, new[] { dst.connectorIn }); connectInMethod.Invoke(dst.connectorIn, new[] { src.connectorOut }); } // 刷新连接点外观(InputAny 锁定后颜色会变) dst.RefreshAppearance(); var line = CreateWire("W_" + connections.Count, src.connectorOut.ConnectorColor); line.gameObject.SetActive(true); var pts = new[] { ToLocal(src.connectorRect), ToLocal(dst.connectorRect) }; line.Points = pts; FitRectToLine(line, pts); connections.Add(new WireConnection { from = src, to = dst, line = line }); // 连线后重算 L ComputeLValues(); } ConnectorSlot HitTestSlot(PointerEventData e, bool isInput) { var r = new List(); EventSystem.current.RaycastAll(e, r); return r.Select(x => x.gameObject.GetComponent()).FirstOrDefault(s => s != null && s.isInput == isInput && (isInput ? s.connectorIn != null : s.connectorOut != null)); } public void RefreshAllLines() { foreach (var w in connections) if (w.from != null && w.to != null) { var pts = new[] { ToLocal(w.from.connectorRect), ToLocal(w.to.connectorRect) }; w.line.Points = pts; FitRectToLine(w.line, pts); } } void FitRectToLine(UILineRenderer line, Vector2[] pts) { if (pts.Length < 2) return; var rt = line.GetComponent(); var min = pts[0]; var max = pts[0]; for (int i = 1; i < pts.Length; i++) { min = Vector2.Min(min, pts[i]); max = Vector2.Max(max, pts[i]); } float pad = wireThickness + 4f; Vector2 offset = min - new Vector2(pad, pad); // 将 Points 从 canvas 绝对坐标转为相对 RectTransform 的局部坐标 var localPts = new Vector2[pts.Length]; for (int i = 0; i < pts.Length; i++) localPts[i] = pts[i] - offset; line.Points = localPts; rt.anchoredPosition = offset; rt.sizeDelta = max - min + new Vector2(pad * 2f, pad * 2f); } UILineRenderer CreateWire(string n, Color c) { var g = new GameObject(n, typeof(RectTransform)); g.transform.SetParent(canvas.transform, false); var l = g.AddComponent(); l.color = c; l.LineThickness = wireThickness; l.RelativeSize = false; l.BezierMode = UILineRenderer.BezierType.Quick; l.Points = new Vector2[] { Vector2.zero, Vector2.zero }; l.raycastTarget = false; var h = g.AddComponent(); h.mgr = this; h.line = l; return l; } public void ToggleWireSelection(UILineRenderer line, bool shift) { var wc = connections.Find(w => w.line == line); if (wc == null) return; if (!shift) { foreach (var w in connections) w.selected = false; } wc.selected = !wc.selected; } WireConnection UpdateWireHover() { var canvasMouse = ToLocal(Mouse.current.position.ReadValue()); float hoverThreshold = wireThickness + 6f; WireConnection hovered = null; float bestDist = hoverThreshold; foreach (var w in connections) { if (w.line == null) continue; var pts = w.line.Points; if (pts == null || pts.Length < 2) continue; var rtPos = w.line.GetComponent().anchoredPosition; for (int i = 1; i < pts.Length; i++) { float d = DistToSegment(canvasMouse, rtPos + pts[i - 1], rtPos + pts[i]); if (d < bestDist) { bestDist = d; hovered = w; } } } foreach (var w in connections) { if (w.line == null) continue; w.line.LineThickness = w.selected ? wireThickness * 2f : (w == hovered) ? wireThickness + 3f : wireThickness; } if (Mouse.current.leftButton.wasPressedThisFrame && hovered != null) ToggleWireSelection(hovered.line, Keyboard.current.leftShiftKey.isPressed); return Mouse.current.leftButton.wasPressedThisFrame ? hovered : null; } static float DistToSegment(Vector2 p, Vector2 a, Vector2 b) { Vector2 ab = b - a; float lenSq = ab.sqrMagnitude; if (lenSq < 0.001f) return Vector2.Distance(p, a); float t = Mathf.Clamp01(Vector2.Dot(p - a, ab) / lenSq); return Vector2.Distance(p, a + t * ab); } // ---- 单步调试 ---- void DebugInit() { _debugMode = true; _debugStep = 0; _dirtyElements.Clear(); ComputeLValues(); foreach (var n in allNodes) n.nodeBase.Status = NodeStatus.Ready; triggerTable.Clear(); runtimeTable.Clear(); foreach (var n in allNodes) triggerTable.Add(n.nodeBase); UpdateAllStatusDisplay(); Debug.Log($"[NodeManager] Debug init — {allNodes.Count} nodes ready. Shift+Enter to step."); } void DebugStep() { if (!_debugMode) return; if (triggerTable.Count == 0 && runtimeTable.Count == 0) { Debug.Log($"[NodeManager] Debug done — all nodes completed in {_debugStep} steps."); DebugReset(); return; } // 记录本周期开始前 runtime 中的节点 var before = new HashSet(runtimeTable); _debugStep++; RunCycle(); // 详细日志 var sb = new System.Text.StringBuilder(); sb.AppendLine($"=== Step {_debugStep} ==="); foreach (var n in before) { var icon = n.Status == NodeStatus.Complete ? "✓" : n.Status == NodeStatus.Hang ? "⏳" : "▶"; var color = n.Status == NodeStatus.Complete ? "green" : n.Status == NodeStatus.Hang ? "yellow" : "white"; sb.AppendLine($" {icon} {n.NodeName} (L:{n.L}) → {n.Status}"); } sb.AppendLine($" triggers pending: {triggerTable.Count}, still running: {runtimeTable.Count}"); Debug.Log(sb.ToString()); UpdateAllStatusDisplay(); if (triggerTable.Count == 0 && runtimeTable.Count == 0) { Observable.NextFrame(FrameCountType.EndOfFrame) .Subscribe(_ => { foreach (var e in _dirtyElements) if (e is IBaseElement be) be.Refresh(); _dirtyElements.Clear(); }); } } void DebugReset() { _debugMode = false; _debugStep = 0; triggerTable.Clear(); runtimeTable.Clear(); foreach (var n in allNodes) n.nodeBase.Status = NodeStatus.Ready; UpdateAllStatusDisplay(); Debug.Log("[NodeManager] Debug reset."); } void PreviewOrder() { ComputeLValues(); var byLayer = allNodes.GroupBy(n => n.nodeBase.L) .OrderBy(g => g.Key) .ToList(); var sb = new System.Text.StringBuilder(); sb.AppendLine("═══ Topological Order (BFS layers) ═══"); foreach (var layer in byLayer) { var names = string.Join(", ", layer.Select(n => $"{n.nodeBase.NodeName}(L:{n.nodeBase.L})")); var fanOut = layer.Sum(n => connections.Count(w => w.from?.ownerNode == n)); sb.AppendLine($" Layer {layer.Key} ({layer.Count()} nodes, {fanOut} downstream wires): {names}"); } var isolated = allNodes.Where(n => n.nodeBase.L < 0).ToList(); if (isolated.Count > 0) { sb.AppendLine($" Unreachable ({isolated.Count} nodes): {string.Join(", ", isolated.Select(n => n.nodeBase.NodeName))}"); } var totalConnections = connections.Count; var cycles = totalConnections - (allNodes.Count - isolated.Count); // rough estimate sb.AppendLine($" Total: {allNodes.Count} nodes, {totalConnections} wires, {byLayer.Count} layers"); Debug.Log(sb.ToString()); UpdateAllStatusDisplay(); } void UpdateAllStatusDisplay() { foreach (var n in allNodes) n.UpdateStatusDisplay(); } // ---- run (lifecycle) ---- public void RunGraph() { _dirtyElements.Clear(); ComputeLValues(); triggerTable.Clear(); runtimeTable.Clear(); // 所有节点初始进入触发表 foreach (var n in allNodes) { n.nodeBase.Status = NodeStatus.Ready; triggerTable.Add(n.nodeBase); } Debug.Log("[NodeManager] === START (lifecycle) ==="); int maxIter = 10000; int iter = 0; while ((triggerTable.Count > 0 || runtimeTable.Count > 0) && iter++ < maxIter) RunCycle(); if (iter >= maxIter) Debug.LogWarning("[NodeManager] exceeded max iterations — possible infinite loop"); else Debug.Log($"[NodeManager] Run complete — {iter} cycles, {allNodes.Count(n => n.nodeBase.Status == NodeStatus.Complete)} nodes completed"); // 统一 Refresh 脏元素 UpdateAllStatusDisplay(); Observable.NextFrame(FrameCountType.EndOfFrame) .Subscribe(_ => { foreach (var e in _dirtyElements) if (e is IBaseElement be) be.Refresh(); _dirtyElements.Clear(); }); } void RunCycle() { // 触发表并入运行时表 foreach (var t in triggerTable) { if (t.Status == NodeStatus.Complete) continue; t.Status = NodeStatus.Ready; runtimeTable.Add(t); } triggerTable.Clear(); var toRemove = new List(); foreach (var node in runtimeTable) { if (node.Status == NodeStatus.Complete) { toRemove.Add(node); continue; } var result = node.Loop(); if (result.Triggers != null) { foreach (var t in result.Triggers) triggerTable.Add(t); } if (result.TriggerDownstream) { foreach (var w in connections) { if (w.from?.ownerNode?.nodeBase == node && w.to?.ownerNode?.nodeBase != null) { var downstream = w.to.ownerNode.nodeBase; if (downstream.Status != NodeStatus.Complete) triggerTable.Add(downstream); } } } if (result.RemoveFromRuntime) { node.Status = NodeStatus.Complete; toRemove.Add(node); } } foreach (var r in toRemove) runtimeTable.Remove(r); } /// BFS 计算所有节点到 Start 的最短距离 L,仅用于显示 public void ComputeLValues() { foreach (var n in allNodes) n.nodeBase.L = -1; var queue = new Queue(); foreach (var n in allNodes) { if (n.nodeBase is NodeStart || n.nodeBase is NodeEntry) { n.nodeBase.L = 0; queue.Enqueue(n.nodeBase); } } while (queue.Count > 0) { var cur = queue.Dequeue(); int nextL = cur.L + 1; foreach (var w in connections) { if (w.from?.ownerNode?.nodeBase == cur && w.to?.ownerNode?.nodeBase != null) { var downstream = w.to.ownerNode.nodeBase; if (downstream.L == -1) { downstream.L = nextL; queue.Enqueue(downstream); } } } } // 刷新所有节点标题显示 L 值 foreach (var n in allNodes) n.UpdateLDisplay(); } static bool IsNumeric(Type t) => t == typeof(float) || t == typeof(int); static bool TypesCompatible(Type from, Type to) { if (from == null || to == null) return true; // untyped → allow if (from == to) return true; if (IsNumeric(from) && IsNumeric(to)) return true; // int ↔ float return false; } /// 尝试添加 from→to 边是否会成环:从 to 出发 DFS,看能否回到 from bool WouldCreateCycle(NodeBase from, NodeBase to) { if (from == null || to == null) return true; var visited = new HashSet(); var stack = new Stack(); stack.Push(to); while (stack.Count > 0) { var cur = stack.Pop(); if (cur == from) return true; if (!visited.Add(cur)) continue; foreach (var w in connections) { if (w.from?.ownerNode?.nodeBase == cur && w.to?.ownerNode?.nodeBase != null) stack.Push(w.to.ownerNode.nodeBase); } } return false; } // ---- context menu ---- RectTransform _menuContent; string _searchFilter = ""; void EnsureScrollMenu() { if (contextMenuRoot == null) return; if (_menuContent != null) return; var rootRt = contextMenuRoot as RectTransform; rootRt.sizeDelta = new Vector2(350, 850); var scroll = contextMenuRoot.gameObject.GetComponent(); if (scroll == null) scroll = contextMenuRoot.gameObject.AddComponent(); var mask = contextMenuRoot.gameObject.GetComponent(); if (mask == null) mask = contextMenuRoot.gameObject.AddComponent(); var img = contextMenuRoot.gameObject.GetComponent(); if (img == null) { img = contextMenuRoot.gameObject.AddComponent(); img.color = new Color(0.1f, 0.1f, 0.1f, 0.95f); } // ---- 搜索栏(固定在顶部,不随滚动)---- if (uiInputPrefab != null) { var searchGo = Instantiate(uiInputPrefab, contextMenuRoot); searchGo.name = "Search"; var searchRt = searchGo.GetComponent(); searchRt.anchorMin = new Vector2(0, 1); searchRt.anchorMax = new Vector2(1, 1); searchRt.pivot = new Vector2(0.5f, 1); searchRt.anchoredPosition = new Vector2(0, -5); searchRt.sizeDelta = new Vector2(-10, 30); var searchInput = searchGo.GetComponentInChildren(); if (searchInput != null) { searchInput.onValueChanged.AddListener(v => { _searchFilter = v; BuildContextMenu(); }); } } // ---- 滚动区域 ---- var viewport = new GameObject("Viewport", typeof(RectTransform), typeof(Image), typeof(Mask)); viewport.transform.SetParent(contextMenuRoot, false); var vpRt = viewport.GetComponent(); vpRt.anchorMin = new Vector2(0, 0); vpRt.anchorMax = new Vector2(1, 1); vpRt.offsetMin = new Vector2(0, 0); vpRt.offsetMax = new Vector2(0, -40); // 给搜索栏让位 var content = new GameObject("Content", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter)); content.transform.SetParent(viewport.transform, false); var ctRt = content.GetComponent(); ctRt.anchorMin = new Vector2(0, 1); ctRt.anchorMax = new Vector2(1, 1); ctRt.pivot = new Vector2(0.5f, 1); ctRt.sizeDelta = new Vector2(0, 0); var vlg = content.GetComponent(); vlg.childForceExpandWidth = true; vlg.childForceExpandHeight = false; vlg.childControlWidth = true; vlg.childControlHeight = false; var csf = content.GetComponent(); csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize; scroll.viewport = vpRt; scroll.content = ctRt; scroll.horizontal = false; scroll.vertical = true; _menuContent = ctRt; } void TryShowContextMenu(Vector2 sp) { if (canvasArea == null || contextMenuRoot == null) return; if (!RectTransformUtility.RectangleContainsScreenPoint(canvasArea, sp, refCamera)) return; EnsureScrollMenu(); _searchFilter = ""; BuildContextMenu(); RectTransformUtility.ScreenPointToLocalPointInRectangle((RectTransform)contextMenuRoot.parent, sp, refCamera, out var lp); contextMenuRoot.gameObject.SetActive(true); (contextMenuRoot as RectTransform).anchoredPosition = lp; } void BuildContextMenu() { if (contextMenuRoot == null || _menuContent == null) return; foreach (Transform t in _menuContent) Destroy(t.gameObject); GetNodeTypes(); // ensure cached var filter = _searchFilter?.ToLower() ?? ""; foreach (var nt in _cachedSorted) { if (!string.IsNullOrEmpty(filter) && !nt.Value.ToLower().Contains(filter)) continue; var item = Instantiate(contextMenuItemPrefab, _menuContent); var l = item.GetComponentInChildren(); if (l != null) l.text = nt.Value; var b = item.GetComponentInChildren