From b523155abfc098dc1e6cafb266a8acaf0db08985 Mon Sep 17 00:00:00 2001 From: Chad Wolfe Date: Thu, 14 Aug 2025 11:02:18 -0400 Subject: [PATCH 1/5] Small changes to make the system more modular --- Editor/CanvasView.cs | 6 +-- Editor/GraphAssetHandler.cs | 4 +- Editor/NodeReflection.cs | 15 +++++++- Editor/NodeView.cs | 7 ++++ .../BlueGraphEditor/NodeView Hovered.uss | 1 + .../BlueGraphEditor/NodeView Hovered.uss.meta | 10 +++++ Editor/Resources/BlueGraphEditor/NodeView.uss | 38 ++++++++++++++++--- Editor/Resources/BlueGraphEditorHover.meta | 8 ++++ Runtime/Attributes.cs | 7 +++- Runtime/Graph.cs | 2 +- Runtime/Node.cs | 20 ++++++++++ Runtime/Port.cs | 5 ++- 12 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 Editor/Resources/BlueGraphEditor/NodeView Hovered.uss create mode 100644 Editor/Resources/BlueGraphEditor/NodeView Hovered.uss.meta create mode 100644 Editor/Resources/BlueGraphEditorHover.meta diff --git a/Editor/CanvasView.cs b/Editor/CanvasView.cs index f51f74b..47f2fd5 100644 --- a/Editor/CanvasView.cs +++ b/Editor/CanvasView.cs @@ -21,7 +21,7 @@ public class CanvasView : GraphView public Graph Graph { get; private set; } - private readonly Label title; + private Label title; private readonly List commentViews = new List(); private readonly SearchWindow searchWindow; private readonly EdgeConnectorListener edgeConnectorListener; @@ -69,7 +69,7 @@ public CanvasView(GraphEditorWindow window) RegisterCallback(OnFirstResize); - title = new Label("BLUEGRAPH"); + title = new Label("BlueGraph"); title.AddToClassList("canvasViewTitle"); Add(title); @@ -278,7 +278,7 @@ public void Load(Graph graph) /// /// Create a new node from reflection data and insert into the Graph. /// - internal void AddNodeFromSearch(Node node, Vector2 screenPosition, PortView connectedPort = null, bool registerUndo = true) + public void AddNodeFromSearch(Node node, Vector2 screenPosition, PortView connectedPort = null, bool registerUndo = true) { // Calculate where to place this node on the graph var windowRoot = EditorWindow.rootVisualElement; diff --git a/Editor/GraphAssetHandler.cs b/Editor/GraphAssetHandler.cs index ae66e99..92d9162 100644 --- a/Editor/GraphAssetHandler.cs +++ b/Editor/GraphAssetHandler.cs @@ -24,7 +24,7 @@ public static bool OnOpenAsset(int instanceID, int line) /// /// Open the appropriate GraphEditor for the Graph asset /// - public static void OnOpenGraph(Graph graph) + public static GraphEditor OnOpenGraph(Graph graph) { var editor = UnityEditor.Editor.CreateEditor(graph) as GraphEditor; if (!editor) @@ -34,7 +34,9 @@ public static void OnOpenGraph(Graph graph) else { editor.CreateOrFocusEditorWindow(); + return editor; } + return null; } } } diff --git a/Editor/NodeReflection.cs b/Editor/NodeReflection.cs index a913260..22bd8ba 100644 --- a/Editor/NodeReflection.cs +++ b/Editor/NodeReflection.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -187,7 +188,7 @@ public NodeReflectionData(Type type, NodeAttribute nodeAttr) contextMethods = new Dictionary(); var attrs = type.GetCustomAttributes(true); - foreach (var attr in attrs) + foreach (var attr in attrs.Reverse()) { if (attr is TagsAttribute tagAttr) { @@ -206,6 +207,17 @@ public NodeReflectionData(Type type, NodeAttribute nodeAttr) HasControlElement = false }); } + else if (attr is InputAttribute input) + { + Ports.Add(new PortReflectionData() + { + Name = input.Name, + Type = input.Type, + Direction = PortDirection.Input, + Capacity = input.Multiple ? PortCapacity.Multiple : PortCapacity.Single, + HasControlElement = false + }); + } } // Load additional data from class fields @@ -215,6 +227,7 @@ public NodeReflectionData(Type type, NodeAttribute nodeAttr) LoadContextMethods(); } + public bool HasInputOfType(Type type) { foreach (var port in Ports) diff --git a/Editor/NodeView.cs b/Editor/NodeView.cs index bf4add6..9653af8 100644 --- a/Editor/NodeView.cs +++ b/Editor/NodeView.cs @@ -49,6 +49,12 @@ internal void Initialize(Node node, CanvasView canvas, EdgeConnectorListener con errorMessage = new Label { name = "error-label" }; errorContainer.Add(errorMessage); + if (errorContainer != null) + { + errorContainer.RegisterCallback( (MouseEnterEvent evt) => errorMessage.name = "error-label-hover"); + errorContainer.RegisterCallback( (MouseLeaveEvent evt) => errorMessage.name = "error-label"); + } + Insert(0, errorContainer); SetPosition(new Rect(node.Position, Vector2.one)); @@ -77,6 +83,7 @@ internal void Initialize(Node node, CanvasView canvas, EdgeConnectorListener con OnInitialize(); } + /// /// Executed after receiving a node target and initial configuration /// but before being added to the graph. diff --git a/Editor/Resources/BlueGraphEditor/NodeView Hovered.uss b/Editor/Resources/BlueGraphEditor/NodeView Hovered.uss new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/Editor/Resources/BlueGraphEditor/NodeView Hovered.uss @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Editor/Resources/BlueGraphEditor/NodeView Hovered.uss.meta b/Editor/Resources/BlueGraphEditor/NodeView Hovered.uss.meta new file mode 100644 index 0000000..8928c5e --- /dev/null +++ b/Editor/Resources/BlueGraphEditor/NodeView Hovered.uss.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: c728599441f2baa4eb1b629a0e69f29f +ScriptedImporter: + fileIDToRecycleName: + 11400000: stylesheet + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} diff --git a/Editor/Resources/BlueGraphEditor/NodeView.uss b/Editor/Resources/BlueGraphEditor/NodeView.uss index ccf5994..92de762 100644 --- a/Editor/Resources/BlueGraphEditor/NodeView.uss +++ b/Editor/Resources/BlueGraphEditor/NodeView.uss @@ -117,15 +117,43 @@ .nodeView #error-label { color: var(--bluegraph-node-error-color); -unity-text-align: upper-center; - font-size: 110%; + font-size: 16px; position: relative; - bottom: -50px; + bottom: 40%; + overflow: hidden; - min-width: 200px; - align-self: center; + min-width: 500px; + height: 16px; + + align-self: left; + white-space: normal; + -unity-text-align: upper-left + + transition: all 0.5s; +} +.nodeView #error-label-hover { + color: var(--bluegraph-node-error-color); + -unity-text-align: upper-center; + font-size: 16px; + + position: relative; + bottom: 40%; + + overflow: hidden; + + min-width: 500px; + + align-self: left; white-space: normal; + + height: 0px; + -unity-text-align: lower-left; + + overflow: visible; + + transition: height 0.5s; } .nodeView #error-icon { @@ -133,5 +161,5 @@ width: 20px; height: 20px; left: -16px; - top: -16px; + top: -24px; } diff --git a/Editor/Resources/BlueGraphEditorHover.meta b/Editor/Resources/BlueGraphEditorHover.meta new file mode 100644 index 0000000..c039537 --- /dev/null +++ b/Editor/Resources/BlueGraphEditorHover.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f60312d3c88e70d4394eb3d06b8322bd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Attributes.cs b/Runtime/Attributes.cs index 2f4bd70..d1b2dde 100644 --- a/Runtime/Attributes.cs +++ b/Runtime/Attributes.cs @@ -58,7 +58,7 @@ public TagsAttribute(params string[] tags) /// /// An input port exposed on a Node /// - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Class, AllowMultiple = true)] public class InputAttribute : Attribute { /// @@ -68,6 +68,8 @@ public class InputAttribute : Attribute /// public string Name { get; set; } + public Type Type; + /// /// Can this input accept multiple outputs at once. /// @@ -78,9 +80,10 @@ public class InputAttribute : Attribute /// public bool Editable { get; set; } = true; - public InputAttribute(string name = null) + public InputAttribute(string name = null, Type type = null) { Name = name; + this.Type = type; } } diff --git a/Runtime/Graph.cs b/Runtime/Graph.cs index 56a04ba..ce96bf8 100644 --- a/Runtime/Graph.cs +++ b/Runtime/Graph.cs @@ -127,7 +127,7 @@ private void OnEnable() /// /// Propagate OnValidate to all nodes. /// - private void OnValidate() + public virtual void OnValidate() { OnGraphValidate(); diff --git a/Runtime/Node.cs b/Runtime/Node.cs index 19fbc8f..d602f9d 100644 --- a/Runtime/Node.cs +++ b/Runtime/Node.cs @@ -11,6 +11,23 @@ public abstract class Node public event Action OnValidateEvent; public event Action OnErrorEvent; + [SerializeField] private string _graphID; + + /// + /// An ID for the node that is only ever set once. + /// + public string GraphID + { + get + { + if (id == null) + { + id = Guid.NewGuid().ToString(); + } + return id; + } + } + [SerializeField] private string id; public string ID @@ -133,6 +150,9 @@ public void Validate() OnValidate(); OnValidateEvent?.Invoke(); + + if (GraphID == "!") + return; } /// diff --git a/Runtime/Port.cs b/Runtime/Port.cs index 5ccc314..e32dbe7 100644 --- a/Runtime/Port.cs +++ b/Runtime/Port.cs @@ -114,9 +114,10 @@ public int ConnectionCount get { return connections.Count; } } - internal List Connections + public List Connections { get { return connections; } + set { connections = value; } } [SerializeField] private List connections = new List(); @@ -292,7 +293,7 @@ internal void Disconnect(Port port) /// internal void UpdateConnections() { - if (hasLoadedConnections) + if (hasLoadedConnections || Node == null) { return; } From dfe21953f628e2a78ee77af621fd33928aa2e3e1 Mon Sep 17 00:00:00 2001 From: Chad Wolfe Date: Thu, 14 Aug 2025 17:41:42 -0400 Subject: [PATCH 2/5] Added a port connection Cache for the Graph that is used to reconstruct connections when needed, graphs no longer break when switching between Edit/Play Modes --- Editor/CanvasView.cs | 5 +- Editor/GraphEditorWindow.cs | 52 +++- Runtime/Attributes.cs | 16 ++ Runtime/Graph.cs | 146 ++++++++++- Runtime/Port.cs | 22 +- Runtime/Utils.meta | 8 + Runtime/Utils/SerializedDictionary.cs | 287 +++++++++++++++++++++ Runtime/Utils/SerializedDictionary.cs.meta | 2 + Runtime/Utils/SerializedType.cs | 60 +++++ Runtime/Utils/SerializedType.cs.meta | 2 + 10 files changed, 594 insertions(+), 6 deletions(-) create mode 100644 Runtime/Utils.meta create mode 100644 Runtime/Utils/SerializedDictionary.cs create mode 100644 Runtime/Utils/SerializedDictionary.cs.meta create mode 100644 Runtime/Utils/SerializedType.cs create mode 100644 Runtime/Utils/SerializedType.cs.meta diff --git a/Editor/CanvasView.cs b/Editor/CanvasView.cs index 47f2fd5..240858f 100644 --- a/Editor/CanvasView.cs +++ b/Editor/CanvasView.cs @@ -224,6 +224,9 @@ public void AddSearchProvider(ISearchProvider provider) public void Load(Graph graph) { Graph = graph; + + graph.ReconstructPortConnections(); + serializedGraph = new SerializedObject(Graph); title.text = graph.Title; SetupZoom(graph.ZoomMinScale, graph.ZoomMaxScale); @@ -267,8 +270,6 @@ public void Load(Graph graph) node.Name = required.nodeName; node.Position = required.position; AddNodeFromSearch(node, node.Position, null, false); - - } } diff --git a/Editor/GraphEditorWindow.cs b/Editor/GraphEditorWindow.cs index 3a05023..83de9c1 100644 --- a/Editor/GraphEditorWindow.cs +++ b/Editor/GraphEditorWindow.cs @@ -1,4 +1,5 @@ -using UnityEditor; +using System.Threading.Tasks; +using UnityEditor; using UnityEngine; using UnityEngine.UIElements; @@ -13,12 +14,17 @@ public class GraphEditorWindow : EditorWindow public Graph Graph { get; protected set; } + private int cacheDelay = 10; + private int cacheTick = 0; + /// /// Load a graph asset in this window for editing /// public virtual void Load(Graph graph) { Graph = graph; + Graph.IsBeingEditted = true; + Graph.ReconstructPortConnections(); Canvas = new CanvasView(this); Canvas.Load(graph); @@ -29,6 +35,29 @@ public virtual void Load(Graph graph) Repaint(); } + private void OnDestroy() + { + OnClose(); + } + + protected virtual void OnFocus() + { + if (Graph != null) + Graph.IsBeingEditted = true; + } + + protected virtual void OnLostFocus() + { + if (Graph != null) + Graph.IsBeingEditted = false; + } + + /// + /// Override to add additional functional to the closing functionality of the Graph Editor. + /// + protected virtual void OnClose() + { } + protected virtual void Update() { // Canvas can be invalidated when the Unity Editor @@ -39,6 +68,20 @@ protected virtual void Update() return; } + cacheTick++; + + if(cacheTick%cacheDelay == 0) + { + if (Application.isPlaying) + { + Graph?.ReconstructPortConnections(); + } + else + { + Graph?.CachePortConnections(); + } + } + Canvas.Update(); } @@ -52,5 +95,12 @@ protected virtual void OnEnable() Load(Graph); } } + + private Task NextEditorFrame() + { + var tcs = new TaskCompletionSource(); + EditorApplication.delayCall += () => tcs.SetResult(true); + return tcs.Task; + } } } diff --git a/Runtime/Attributes.cs b/Runtime/Attributes.cs index d1b2dde..af4a532 100644 --- a/Runtime/Attributes.cs +++ b/Runtime/Attributes.cs @@ -232,4 +232,20 @@ public CustomNodeViewAttribute(Type nodeType) NodeType = nodeType; } } + + /// + /// Tells the Node Caching system to cache the Node to a specific type. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public class CacheToAttribute : Attribute + { + public Type Type; + public bool CacheAsBoth = true; + + public CacheToAttribute(Type type, bool cacheAsBoth = true) + { + Type = type; + CacheAsBoth = cacheAsBoth; + } + } } diff --git a/Runtime/Graph.cs b/Runtime/Graph.cs index ce96bf8..0a85780 100644 --- a/Runtime/Graph.cs +++ b/Runtime/Graph.cs @@ -1,6 +1,12 @@ -using System; +using BlueGraph.Utils; +using BlueGraph.Utils.Remedy.Framework; +using System; using System.Collections; using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEditor.Experimental.GraphView; +using UnityEditor.MemoryProfiler; using UnityEngine; namespace BlueGraph @@ -33,6 +39,8 @@ public virtual string Title get { return "BLUEGRAPH"; } } + public bool IsBeingEditted = false; + /// /// Retrieve the min zoom value scale used by CanvasView /// @@ -65,6 +73,25 @@ public virtual float ZoomMaxScale } } + [SerializeField] + private SerializableDictionary nodeByTypeCache; + /// + /// The nodes Cached in the Graph, by their type so they can be easily queried. + /// + protected SerializableDictionary NodesByTypeCache => nodeByTypeCache ??= new(); + /// + /// The cached connections between nodes from the Schematics Editor in a Dictionary that allows them to be reconstructed if they're lost. + /// + [SerializeField] + [HideInInspector] + internal SerializableDictionary portConnectionCache = new(); + /// + /// A Cache pairing Port IDs to their Ports that is concstructed at Runtime to improve Port Lookup time + /// + [SerializeField] + [HideInInspector] + private SerializableDictionary runtimePortCache = new(); + /// /// Retrieve all nodes on this graph /// @@ -98,6 +125,7 @@ public int AssetVersion [SerializeField, HideInInspector] private int assetVersion = 1; + /// /// Propagate OnDisable to all nodes. /// @@ -129,6 +157,9 @@ private void OnEnable() /// public virtual void OnValidate() { + if (!Application.isPlaying) + CacheNodesByType(); + OnGraphValidate(); foreach (var node in Nodes) @@ -255,6 +286,119 @@ public void RemoveNode(Node node) node.Graph = null; } + /// + /// Caches the Connections between ports so they can be recreated if they are lost at Runtime. + /// + public void CachePortConnections() + { + portConnectionCache.Clear(); + + foreach (var node in Nodes) + { + foreach(var kvp in node.Ports) + { + int connectionCount = kvp.Value.ConnectedPorts.Count(); + + portConnectionCache[kvp.Value.ID] = new string[connectionCount]; + + if (connectionCount == 0) continue; + + for (int i = 0; i < connectionCount; i++) + { + portConnectionCache[kvp.Value.ID][i] = kvp.Value.ConnectedPorts.ElementAt(i).ID; + } + } + } + } + + /// + /// Caches all the Nodes in the Graph By Type in the Dictionary + /// + public void CacheNodesByType() + { + NodesByTypeCache.Clear(); + ResetExtendedNodeCaches(); + + foreach (var node in Nodes) + { + var attr = node.GetType().GetCustomAttribute(false); + CacheNodeByType(node, attr); + } + } + + /// + /// Override this to handle the clearing of extended Node Caches + /// + protected virtual void ResetExtendedNodeCaches() + { } + + /// + /// Override this for cache handling. + /// + /// + protected virtual void CacheNodeByType(Node node, CacheToAttribute attr) + { + if (attr != null) + { + if (!NodesByTypeCache.ContainsKey(attr.Type)) + NodesByTypeCache.Add(attr.Type, new Node[0]); + NodesByTypeCache[attr.Type] = NodesByTypeCache[attr.Type].Append((Node)node).ToArray(); + } + if (attr == null || attr.CacheAsBoth) + { + var type = node.GetType(); + + if (!NodesByTypeCache.ContainsKey(type) || NodesByTypeCache[type] == null) + NodesByTypeCache[type] = new Node[0]; + NodesByTypeCache[type] = NodesByTypeCache[type].Append((Node)node).ToArray(); + } + } + + /// + /// Uses the previously Cached Node Port information to Reconstruct the Graph during runtime. + /// + public void ReconstructPortConnections() + { + foreach(var node in Nodes) + { + foreach(var kvp in node.Ports) + { + if (!portConnectionCache.ContainsKey(kvp.Value.ID)) continue; + + foreach (var connectedPortID in portConnectionCache[kvp.Value.ID]) + { + var portToConnect = FindPortByID(connectedPortID); + + if(portToConnect != null && !kvp.Value.ConnectedPorts.Any(port => port.ID == connectedPortID)) + kvp.Value.Connect(portToConnect, true); + } + } + } + } + + /// + /// Searches through all the ports of all the nodes in the graph to find a port with a matching ID. + /// + /// + /// + private Port FindPortByID(string portID) + { + if (runtimePortCache.ContainsKey(portID)) + return runtimePortCache[portID]; + else + { + foreach(var node in Nodes) + { + foreach(var kvp in node.Ports) + { + if (kvp.Value.ID == portID) + return kvp.Value; + } + } + } + return null; + } + /// /// Add a new edge between two Ports. /// diff --git a/Runtime/Port.cs b/Runtime/Port.cs index e32dbe7..3602203 100644 --- a/Runtime/Port.cs +++ b/Runtime/Port.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using UnityEngine; namespace BlueGraph @@ -122,6 +123,23 @@ public List Connections [SerializeField] private List connections = new List(); + [SerializeField] + private string id; + /// + /// The Port ID is stored in the Graph's ConnectionCache to rebuild broken connections. + /// + public string ID + { + get + { + if (string.IsNullOrEmpty(id)) + { + id = Guid.NewGuid().ToString(); + } + return id; + } + } + /// /// Enumerate all ports connected by edges to this port /// @@ -236,10 +254,10 @@ internal void DisconnectAll() /// /// Use Graph.AddEdge() over this. /// - internal void Connect(Port port) + internal void Connect(Port port, bool force = false) { // Skip if we're already connected - if (GetConnection(port) != null) + if (!force && GetConnection(port) != null) { return; } diff --git a/Runtime/Utils.meta b/Runtime/Utils.meta new file mode 100644 index 0000000..cefaf33 --- /dev/null +++ b/Runtime/Utils.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 76b2b26d0f4c2b846bf5a9d8b1db5d38 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Utils/SerializedDictionary.cs b/Runtime/Utils/SerializedDictionary.cs new file mode 100644 index 0000000..40fdbb2 --- /dev/null +++ b/Runtime/Utils/SerializedDictionary.cs @@ -0,0 +1,287 @@ +using JetBrains.Annotations; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace BlueGraph.Utils +{ + namespace Remedy.Framework + { + [Serializable] + public class SerializableDictionary : IDictionary, ISerializationCallbackReceiver + { + private Dictionary dictionary = new Dictionary(); + + [SerializeField] + private List items = new List(); + + private bool invalidFlag; + + public TValue this[TKey key] + { + get + { + if (dictionary.ContainsKey(key)) + return dictionary[key]; + else + { + //Debug.LogWarning("Key " + key + " doesn't exist!"); + return default(TValue); + } + } + + set + { + if (!dictionary.ContainsKey(key)) + dictionary.Add(key, value); + else + dictionary[key] = value; + } + } + + public ICollection Keys + { + get { return dictionary.Keys; } + } + + public ICollection Values + { + get { return dictionary.Values; } + } + + public void Add(TKey key, TValue value) + { + dictionary.Add(key, value); + } + + public bool ContainsKey(TKey key) + { + return dictionary.ContainsKey(key); + } + + public bool Remove(TKey key) + { + return dictionary.Remove(key); + } + + public bool TryGetValue(TKey key, out TValue value) + { + return dictionary.TryGetValue(key, out value); + } + + public void Clear() + { + dictionary.Clear(); + } + + public int Count + { + get { return dictionary.Count; } + } + + bool ICollection>.IsReadOnly + { + get { return (dictionary as ICollection>).IsReadOnly; } + } + + void ICollection>.Add(KeyValuePair item) + { + (dictionary as ICollection>).Add(item); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return (dictionary as ICollection>).Contains(item); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + (dictionary as ICollection>).CopyTo(array, arrayIndex); + } + + bool ICollection>.Remove(KeyValuePair item) + { + return (dictionary as ICollection>).Remove(item); + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return (dictionary as IEnumerable>).GetEnumerator(); + } + + public IEnumerator> GetEnumerator() + => dictionary.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public void OnBeforeSerialize() + { + if (invalidFlag) + { + return; + } + else + { + items.Clear(); + } + + foreach (var pair in dictionary) + { + items.Add(new DictionaryItem(pair.Key, pair.Value)); + } + } + + public void OnAfterDeserialize() + { + dictionary.Clear(); + + invalidFlag = false; + + for (var i = 0; i < items.Count; ++i) + { + if (items[i] != null) + { + if (items[i].key != null || !(dictionary.ContainsKey(items[i].key))) + { + dictionary.Add(items[i].key, items[i].value); + } + else + { + invalidFlag = true; + continue; + } + } + } + + if (!invalidFlag) + { + items.Clear(); + } + } + + public SerializableDictionary() + { + } + + /// + /// Clones the other Dictionary into this one. + /// + /// From. + public SerializableDictionary(SerializableDictionary from) + { + foreach (TKey key in from.Keys) + { + Add(key, from[key]); + } + } + + public TKey KeyAt(int index) + { + return items[index].key; + } + + public TValue ValueAt(int index) + { + return items[index].value; + } + + public override string ToString() + { + var returnValue = ""; + + var keyList = Keys.ToList(); + for (int i = 0; i < keyList.Count; i++) + { + var key = keyList[i]; + + var keyString = key is float ? (Math.Truncate(((float)(object)key) * 100) / 100).ToString() : key.ToString(); + var valueString = this[key] is float ? (Math.Truncate(((float)(object)this[key]) * 100) / 100).ToString() : this[key].ToString(); + var itemString = keyString + ":" + valueString; + + returnValue += itemString; + + if (i < keyList.Count - 1) + returnValue += ","; + } + + return returnValue; + } + + /// + /// Creates a new serializable dictionary from a string representation. + /// + /// The string representation of the dictionary. + /// A new serializable dictionary with the same key-value pairs as the string. + public static SerializableDictionary NewFromString(string dictionaryString) + { + var dictionary = new SerializableDictionary(); + + var items = dictionaryString.Split(','); + + foreach (var item in items) + { + var parts = item.Split(':'); + var keyString = parts[0]; + var valueString = parts[1]; + + var key = (TKey)Convert.ChangeType(keyString, typeof(TKey)); + var value = (TValue)Convert.ChangeType(valueString, typeof(TValue)); + + dictionary.Add(key, value); + } + + // Return the dictionary + return dictionary; + } + + /// + /// Sets the dictionary of this instance from a string representation. + /// + public void FromString(string dictionaryString) + { + dictionary = NewFromString(dictionaryString).dictionary; + } + + public static implicit operator Dictionary(SerializableDictionary serializableDictionary) + { + if (serializableDictionary == null) + return null; + + return new Dictionary(serializableDictionary.dictionary); + } + + public static implicit operator SerializableDictionary(Dictionary normalDictionary) + { + if (normalDictionary == null) + return null; + + var sDict = new SerializableDictionary(); + foreach (var kvp in normalDictionary) + { + sDict.Add(kvp.Key, kvp.Value); + } + return sDict; + } + + + + [Serializable] + public class DictionaryItem + { + [SerializeField] + public TKey key; + [SerializeField] + public TValue value; + + public DictionaryItem(TKey key, TValue value) + { + this.key = key; + this.value = value; + } + } + } + } +} \ No newline at end of file diff --git a/Runtime/Utils/SerializedDictionary.cs.meta b/Runtime/Utils/SerializedDictionary.cs.meta new file mode 100644 index 0000000..3640187 --- /dev/null +++ b/Runtime/Utils/SerializedDictionary.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ddf4afbc4df01974d94253dbf1dd3142 \ No newline at end of file diff --git a/Runtime/Utils/SerializedType.cs b/Runtime/Utils/SerializedType.cs new file mode 100644 index 0000000..0a9fc07 --- /dev/null +++ b/Runtime/Utils/SerializedType.cs @@ -0,0 +1,60 @@ +using System; +using UnityEngine; + +namespace BlueGraph.Utils +{ + [Serializable] + public class SerializableType + { + [SerializeField] private string typeName; + + public Type Type + { + get => string.IsNullOrEmpty(typeName) ? null : Type.GetType(typeName); + set => typeName = value?.AssemblyQualifiedName; + } + + public static implicit operator Type(SerializableType serializableType) + { + return serializableType?.Type; + } + + public static implicit operator SerializableType(Type type) + { + return new SerializableType { Type = type }; + } + + public override string ToString() + { + return Type?.FullName ?? "null"; + } + + public override bool Equals(object obj) + { + if (obj is SerializableType other) + { + return Type == other.Type; // Compare the actual Type objects + } + if (obj is Type type) + { + return Type == type; + } + return false; + } + + public override int GetHashCode() + { + return Type?.GetHashCode() ?? 0; + } + + public static bool operator ==(SerializableType a, SerializableType b) + { + if (ReferenceEquals(a, b)) return true; + if (a is null || b is null) return false; + return a.Type == b.Type; + } + + public static bool operator !=(SerializableType a, SerializableType b) => !(a == b); + } + +} \ No newline at end of file diff --git a/Runtime/Utils/SerializedType.cs.meta b/Runtime/Utils/SerializedType.cs.meta new file mode 100644 index 0000000..94c4ce7 --- /dev/null +++ b/Runtime/Utils/SerializedType.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 615a145a689a36b43a2304e2f751818e \ No newline at end of file From dad580ebe430dff3427e5c0185a0386495ec7562 Mon Sep 17 00:00:00 2001 From: Chad Wolfe Date: Thu, 14 Aug 2025 17:45:49 -0400 Subject: [PATCH 3/5] Add GetNodeArray() to Graph to get nodes previously cached by their Type --- Runtime/Graph.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Runtime/Graph.cs b/Runtime/Graph.cs index 0a85780..a00d2a5 100644 --- a/Runtime/Graph.cs +++ b/Runtime/Graph.cs @@ -222,7 +222,12 @@ public IEnumerable GetNodes() where T : Node } } } - + + public Node[] GetNodeArray() where TNode : Node + { + return NodesByTypeCache[typeof(TNode)]; + } + /// /// Add a new node to the Graph. /// From fdecde59ae02f0f674cf442797690c60961c8d3b Mon Sep 17 00:00:00 2001 From: Chad Wolfe Date: Thu, 14 Aug 2025 18:34:17 -0400 Subject: [PATCH 4/5] Changed name of GetNodesArray() to GetCachedNodes() for transparency --- Runtime/Graph.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Runtime/Graph.cs b/Runtime/Graph.cs index a00d2a5..845d38f 100644 --- a/Runtime/Graph.cs +++ b/Runtime/Graph.cs @@ -223,7 +223,10 @@ public IEnumerable GetNodes() where T : Node } } - public Node[] GetNodeArray() where TNode : Node + /// + /// Find all nodes on the Graph of, or inherited from, the given type, from the . + /// + public Node[] GetCachedNodes() where TNode : Node { return NodesByTypeCache[typeof(TNode)]; } From bcec3889ff8b1e675c94da61a43c97df4403f52b Mon Sep 17 00:00:00 2001 From: Chad Wolfe Date: Mon, 18 Aug 2025 11:31:33 -0400 Subject: [PATCH 5/5] Fix minor issues from PR feedback - Remove unnecessary magic '!' check in Node.Validate - Remove JetBrains namespace import and fix namespace in SerializableDictionary - Ensure class name matches filename for SerializableDictionary - Clarify usage of CacheToAttribute and node caching logic --- Runtime/Attributes.cs | 2 +- Runtime/Graph.cs | 4 +- Runtime/Node.cs | 3 - Runtime/Utils/SerializableDictionary.cs | 283 +++++++++++++++++ ...cs.meta => SerializableDictionary.cs.meta} | 0 Runtime/Utils/SerializedDictionary.cs | 287 ------------------ Runtime/Utils/SerializedType.cs | 1 - 7 files changed, 286 insertions(+), 294 deletions(-) create mode 100644 Runtime/Utils/SerializableDictionary.cs rename Runtime/Utils/{SerializedDictionary.cs.meta => SerializableDictionary.cs.meta} (100%) delete mode 100644 Runtime/Utils/SerializedDictionary.cs diff --git a/Runtime/Attributes.cs b/Runtime/Attributes.cs index af4a532..e68cee5 100644 --- a/Runtime/Attributes.cs +++ b/Runtime/Attributes.cs @@ -236,7 +236,7 @@ public CustomNodeViewAttribute(Type nodeType) /// /// Tells the Node Caching system to cache the Node to a specific type. /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] public class CacheToAttribute : Attribute { public Type Type; diff --git a/Runtime/Graph.cs b/Runtime/Graph.cs index 845d38f..cf05f77 100644 --- a/Runtime/Graph.cs +++ b/Runtime/Graph.cs @@ -1,5 +1,5 @@ -using BlueGraph.Utils; -using BlueGraph.Utils.Remedy.Framework; + +using BlueGraph.Utils; using System; using System.Collections; using System.Collections.Generic; diff --git a/Runtime/Node.cs b/Runtime/Node.cs index d602f9d..4366a52 100644 --- a/Runtime/Node.cs +++ b/Runtime/Node.cs @@ -150,9 +150,6 @@ public void Validate() OnValidate(); OnValidateEvent?.Invoke(); - - if (GraphID == "!") - return; } /// diff --git a/Runtime/Utils/SerializableDictionary.cs b/Runtime/Utils/SerializableDictionary.cs new file mode 100644 index 0000000..0c3bd27 --- /dev/null +++ b/Runtime/Utils/SerializableDictionary.cs @@ -0,0 +1,283 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace BlueGraph.Utils +{ + [Serializable] + public class SerializableDictionary : IDictionary, ISerializationCallbackReceiver + { + private Dictionary dictionary = new Dictionary(); + + [SerializeField] + private List items = new List(); + + private bool invalidFlag; + + public TValue this[TKey key] + { + get + { + if (dictionary.ContainsKey(key)) + return dictionary[key]; + else + { + //Debug.LogWarning("Key " + key + " doesn't exist!"); + return default(TValue); + } + } + + set + { + if (!dictionary.ContainsKey(key)) + dictionary.Add(key, value); + else + dictionary[key] = value; + } + } + + public ICollection Keys + { + get { return dictionary.Keys; } + } + + public ICollection Values + { + get { return dictionary.Values; } + } + + public void Add(TKey key, TValue value) + { + dictionary.Add(key, value); + } + + public bool ContainsKey(TKey key) + { + return dictionary.ContainsKey(key); + } + + public bool Remove(TKey key) + { + return dictionary.Remove(key); + } + + public bool TryGetValue(TKey key, out TValue value) + { + return dictionary.TryGetValue(key, out value); + } + + public void Clear() + { + dictionary.Clear(); + } + + public int Count + { + get { return dictionary.Count; } + } + + bool ICollection>.IsReadOnly + { + get { return (dictionary as ICollection>).IsReadOnly; } + } + + void ICollection>.Add(KeyValuePair item) + { + (dictionary as ICollection>).Add(item); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return (dictionary as ICollection>).Contains(item); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + (dictionary as ICollection>).CopyTo(array, arrayIndex); + } + + bool ICollection>.Remove(KeyValuePair item) + { + return (dictionary as ICollection>).Remove(item); + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return (dictionary as IEnumerable>).GetEnumerator(); + } + + public IEnumerator> GetEnumerator() + => dictionary.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public void OnBeforeSerialize() + { + if (invalidFlag) + { + return; + } + else + { + items.Clear(); + } + + foreach (var pair in dictionary) + { + items.Add(new DictionaryItem(pair.Key, pair.Value)); + } + } + + public void OnAfterDeserialize() + { + dictionary.Clear(); + + invalidFlag = false; + + for (var i = 0; i < items.Count; ++i) + { + if (items[i] != null) + { + if (items[i].key != null || !(dictionary.ContainsKey(items[i].key))) + { + dictionary.Add(items[i].key, items[i].value); + } + else + { + invalidFlag = true; + continue; + } + } + } + + if (!invalidFlag) + { + items.Clear(); + } + } + + public SerializableDictionary() + { + } + + /// + /// Clones the other Dictionary into this one. + /// + /// From. + public SerializableDictionary(SerializableDictionary from) + { + foreach (TKey key in from.Keys) + { + Add(key, from[key]); + } + } + + public TKey KeyAt(int index) + { + return items[index].key; + } + + public TValue ValueAt(int index) + { + return items[index].value; + } + + public override string ToString() + { + var returnValue = ""; + + var keyList = Keys.ToList(); + for (int i = 0; i < keyList.Count; i++) + { + var key = keyList[i]; + + var keyString = key is float ? (Math.Truncate(((float)(object)key) * 100) / 100).ToString() : key.ToString(); + var valueString = this[key] is float ? (Math.Truncate(((float)(object)this[key]) * 100) / 100).ToString() : this[key].ToString(); + var itemString = keyString + ":" + valueString; + + returnValue += itemString; + + if (i < keyList.Count - 1) + returnValue += ","; + } + + return returnValue; + } + + /// + /// Creates a new serializable dictionary from a string representation. + /// + /// The string representation of the dictionary. + /// A new serializable dictionary with the same key-value pairs as the string. + public static SerializableDictionary NewFromString(string dictionaryString) + { + var dictionary = new SerializableDictionary(); + + var items = dictionaryString.Split(','); + + foreach (var item in items) + { + var parts = item.Split(':'); + var keyString = parts[0]; + var valueString = parts[1]; + + var key = (TKey)Convert.ChangeType(keyString, typeof(TKey)); + var value = (TValue)Convert.ChangeType(valueString, typeof(TValue)); + + dictionary.Add(key, value); + } + + // Return the dictionary + return dictionary; + } + + /// + /// Sets the dictionary of this instance from a string representation. + /// + public void FromString(string dictionaryString) + { + dictionary = NewFromString(dictionaryString).dictionary; + } + + public static implicit operator Dictionary(SerializableDictionary serializableDictionary) + { + if (serializableDictionary == null) + return null; + + return new Dictionary(serializableDictionary.dictionary); + } + + public static implicit operator SerializableDictionary(Dictionary normalDictionary) + { + if (normalDictionary == null) + return null; + + var sDict = new SerializableDictionary(); + foreach (var kvp in normalDictionary) + { + sDict.Add(kvp.Key, kvp.Value); + } + return sDict; + } + + + + [Serializable] + public class DictionaryItem + { + [SerializeField] + public TKey key; + [SerializeField] + public TValue value; + + public DictionaryItem(TKey key, TValue value) + { + this.key = key; + this.value = value; + } + } + } +} \ No newline at end of file diff --git a/Runtime/Utils/SerializedDictionary.cs.meta b/Runtime/Utils/SerializableDictionary.cs.meta similarity index 100% rename from Runtime/Utils/SerializedDictionary.cs.meta rename to Runtime/Utils/SerializableDictionary.cs.meta diff --git a/Runtime/Utils/SerializedDictionary.cs b/Runtime/Utils/SerializedDictionary.cs deleted file mode 100644 index 40fdbb2..0000000 --- a/Runtime/Utils/SerializedDictionary.cs +++ /dev/null @@ -1,287 +0,0 @@ -using JetBrains.Annotations; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; - -namespace BlueGraph.Utils -{ - namespace Remedy.Framework - { - [Serializable] - public class SerializableDictionary : IDictionary, ISerializationCallbackReceiver - { - private Dictionary dictionary = new Dictionary(); - - [SerializeField] - private List items = new List(); - - private bool invalidFlag; - - public TValue this[TKey key] - { - get - { - if (dictionary.ContainsKey(key)) - return dictionary[key]; - else - { - //Debug.LogWarning("Key " + key + " doesn't exist!"); - return default(TValue); - } - } - - set - { - if (!dictionary.ContainsKey(key)) - dictionary.Add(key, value); - else - dictionary[key] = value; - } - } - - public ICollection Keys - { - get { return dictionary.Keys; } - } - - public ICollection Values - { - get { return dictionary.Values; } - } - - public void Add(TKey key, TValue value) - { - dictionary.Add(key, value); - } - - public bool ContainsKey(TKey key) - { - return dictionary.ContainsKey(key); - } - - public bool Remove(TKey key) - { - return dictionary.Remove(key); - } - - public bool TryGetValue(TKey key, out TValue value) - { - return dictionary.TryGetValue(key, out value); - } - - public void Clear() - { - dictionary.Clear(); - } - - public int Count - { - get { return dictionary.Count; } - } - - bool ICollection>.IsReadOnly - { - get { return (dictionary as ICollection>).IsReadOnly; } - } - - void ICollection>.Add(KeyValuePair item) - { - (dictionary as ICollection>).Add(item); - } - - bool ICollection>.Contains(KeyValuePair item) - { - return (dictionary as ICollection>).Contains(item); - } - - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) - { - (dictionary as ICollection>).CopyTo(array, arrayIndex); - } - - bool ICollection>.Remove(KeyValuePair item) - { - return (dictionary as ICollection>).Remove(item); - } - - IEnumerator> IEnumerable>.GetEnumerator() - { - return (dictionary as IEnumerable>).GetEnumerator(); - } - - public IEnumerator> GetEnumerator() - => dictionary.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public void OnBeforeSerialize() - { - if (invalidFlag) - { - return; - } - else - { - items.Clear(); - } - - foreach (var pair in dictionary) - { - items.Add(new DictionaryItem(pair.Key, pair.Value)); - } - } - - public void OnAfterDeserialize() - { - dictionary.Clear(); - - invalidFlag = false; - - for (var i = 0; i < items.Count; ++i) - { - if (items[i] != null) - { - if (items[i].key != null || !(dictionary.ContainsKey(items[i].key))) - { - dictionary.Add(items[i].key, items[i].value); - } - else - { - invalidFlag = true; - continue; - } - } - } - - if (!invalidFlag) - { - items.Clear(); - } - } - - public SerializableDictionary() - { - } - - /// - /// Clones the other Dictionary into this one. - /// - /// From. - public SerializableDictionary(SerializableDictionary from) - { - foreach (TKey key in from.Keys) - { - Add(key, from[key]); - } - } - - public TKey KeyAt(int index) - { - return items[index].key; - } - - public TValue ValueAt(int index) - { - return items[index].value; - } - - public override string ToString() - { - var returnValue = ""; - - var keyList = Keys.ToList(); - for (int i = 0; i < keyList.Count; i++) - { - var key = keyList[i]; - - var keyString = key is float ? (Math.Truncate(((float)(object)key) * 100) / 100).ToString() : key.ToString(); - var valueString = this[key] is float ? (Math.Truncate(((float)(object)this[key]) * 100) / 100).ToString() : this[key].ToString(); - var itemString = keyString + ":" + valueString; - - returnValue += itemString; - - if (i < keyList.Count - 1) - returnValue += ","; - } - - return returnValue; - } - - /// - /// Creates a new serializable dictionary from a string representation. - /// - /// The string representation of the dictionary. - /// A new serializable dictionary with the same key-value pairs as the string. - public static SerializableDictionary NewFromString(string dictionaryString) - { - var dictionary = new SerializableDictionary(); - - var items = dictionaryString.Split(','); - - foreach (var item in items) - { - var parts = item.Split(':'); - var keyString = parts[0]; - var valueString = parts[1]; - - var key = (TKey)Convert.ChangeType(keyString, typeof(TKey)); - var value = (TValue)Convert.ChangeType(valueString, typeof(TValue)); - - dictionary.Add(key, value); - } - - // Return the dictionary - return dictionary; - } - - /// - /// Sets the dictionary of this instance from a string representation. - /// - public void FromString(string dictionaryString) - { - dictionary = NewFromString(dictionaryString).dictionary; - } - - public static implicit operator Dictionary(SerializableDictionary serializableDictionary) - { - if (serializableDictionary == null) - return null; - - return new Dictionary(serializableDictionary.dictionary); - } - - public static implicit operator SerializableDictionary(Dictionary normalDictionary) - { - if (normalDictionary == null) - return null; - - var sDict = new SerializableDictionary(); - foreach (var kvp in normalDictionary) - { - sDict.Add(kvp.Key, kvp.Value); - } - return sDict; - } - - - - [Serializable] - public class DictionaryItem - { - [SerializeField] - public TKey key; - [SerializeField] - public TValue value; - - public DictionaryItem(TKey key, TValue value) - { - this.key = key; - this.value = value; - } - } - } - } -} \ No newline at end of file diff --git a/Runtime/Utils/SerializedType.cs b/Runtime/Utils/SerializedType.cs index 0a9fc07..ba27233 100644 --- a/Runtime/Utils/SerializedType.cs +++ b/Runtime/Utils/SerializedType.cs @@ -56,5 +56,4 @@ public override int GetHashCode() public static bool operator !=(SerializableType a, SerializableType b) => !(a == b); } - } \ No newline at end of file