diff --git a/Scripts/Attributes/PortTypeOverrideAttribute.cs b/Scripts/Attributes/PortTypeOverrideAttribute.cs new file mode 100644 index 0000000..316ca2d --- /dev/null +++ b/Scripts/Attributes/PortTypeOverrideAttribute.cs @@ -0,0 +1,12 @@ +using System; +/// Overrides the ValueType of the Port, to have a ValueType different from the type of its serializable field +/// Especially useful in Dynamic Port Lists to create Value-Port Pairs with different type. +[AttributeUsage(AttributeTargets.Field)] +public class PortTypeOverrideAttribute : Attribute { + public Type type; + /// Overrides the ValueType of the Port + /// ValueType of the Port + public PortTypeOverrideAttribute(Type type) { + this.type = type; + } +} diff --git a/Scripts/Attributes/PortTypeOverrideAttribute.cs.meta b/Scripts/Attributes/PortTypeOverrideAttribute.cs.meta new file mode 100644 index 0000000..a587759 --- /dev/null +++ b/Scripts/Attributes/PortTypeOverrideAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1410c1437e863ab4fac7a7428aaca35b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs index 35b2e2a..8f3a574 100755 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using UnityEditor; @@ -12,8 +12,8 @@ namespace XNodeEditor { /// Contains GUI methods public partial class NodeEditorWindow { public NodeGraphEditor graphEditor; - private List selectionCache; - private List culledNodes; + private readonly HashSet selectionCache = new(); + private readonly HashSet culledNodes = new(); /// 19 if docked, 22 if not private int topPadding { get { return isDocked() ? 19 : 22; } } /// Executed after all other window GUI. Useful if Zoom is ruining your day. Automatically resets after being run. @@ -399,8 +399,13 @@ namespace XNodeEditor { private void DrawNodes() { Event e = Event.current; + if (e.type == EventType.Layout) { - selectionCache = new List(Selection.objects); + selectionCache.Clear(); + var objs = Selection.objects; + selectionCache.EnsureCapacity(objs.Length); + foreach (var obj in objs) + selectionCache.Add(obj); } System.Reflection.MethodInfo onValidate = null; @@ -432,7 +437,7 @@ namespace XNodeEditor { List removeEntries = new List(); - if (e.type == EventType.Layout) culledNodes = new List(); + if (e.type == EventType.Layout) culledNodes.Clear(); for (int n = 0; n < graph.nodes.Count; n++) { // Skip null nodes. The user could be in the process of renaming scripts, so removing them at this point is not advisable. if (graph.nodes[n] == null) continue; diff --git a/Scripts/Node.cs b/Scripts/Node.cs index 2cfd569..bad468d 100644 --- a/Scripts/Node.cs +++ b/Scripts/Node.cs @@ -399,6 +399,8 @@ namespace XNode { public void OnBeforeSerialize() { keys.Clear(); values.Clear(); + keys.Capacity = this.Count; + values.Capacity = this.Count; foreach (KeyValuePair pair in this) { keys.Add(pair.Key); values.Add(pair.Value); @@ -407,6 +409,9 @@ namespace XNode { public void OnAfterDeserialize() { this.Clear(); +#if UNITY_2021_3_OR_NEWER + this.EnsureCapacity(keys.Count); +#endif if (keys.Count != values.Count) throw new System.Exception("there are " + keys.Count + " keys and " + values.Count + " values after deserialization. Make sure that both key and value types are serializable."); diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs index f28de98..2283fd1 100644 --- a/Scripts/NodeDataCache.cs +++ b/Scripts/NodeDataCache.cs @@ -8,13 +8,24 @@ namespace XNode { public static class NodeDataCache { private static PortDataCache portDataCache; private static Dictionary> formerlySerializedAsCache; + private static Dictionary typeQualifiedNameCache; private static bool Initialized { get { return portDataCache != null; } } + public static string GetTypeQualifiedName(System.Type type) { + if(typeQualifiedNameCache == null) typeQualifiedNameCache = new Dictionary(); + + string name; + if (!typeQualifiedNameCache.TryGetValue(type, out name)) { + name = type.AssemblyQualifiedName; + typeQualifiedNameCache.Add(type, name); + } + return name; + } + /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. public static void UpdatePorts(Node node, Dictionary ports) { if (!Initialized) BuildCache(); - Dictionary staticPorts = new Dictionary(); Dictionary> removedPorts = new Dictionary>(); System.Type nodeType = node.GetType(); @@ -23,17 +34,15 @@ namespace XNode { List dynamicListPorts = new List(); - List typePortCache; - if (portDataCache.TryGetValue(nodeType, out typePortCache)) { - for (int i = 0; i < typePortCache.Count; i++) { - staticPorts.Add(typePortCache[i].fieldName, portDataCache[nodeType][i]); - } - } + Dictionary staticPorts; + if (!portDataCache.TryGetValue(nodeType, out staticPorts)) { + staticPorts = new Dictionary(); + } // Cleanup port dict - Remove nonexisting static ports - update static port types // AND update dynamic ports (albeit only those in lists) too, in order to enforce proper serialisation. // Loop through current node ports - foreach (NodePort port in ports.Values.ToList()) { + foreach (NodePort port in ports.Values.ToArray()) { // If port still exists, check it it has been changed NodePort staticPort; if (staticPorts.TryGetValue(port.fieldName, out staticPort)) { @@ -84,7 +93,7 @@ namespace XNode { foreach (NodePort listPort in dynamicListPorts) { // At this point we know that ports here are dynamic list ports // which have passed name/"backing port" checks, ergo we can proceed more safely. - string backingPortName = listPort.fieldName.Split(' ')[0]; + string backingPortName = listPort.fieldName.Substring(0, listPort.fieldName.IndexOf(' ')); NodePort backingPort = staticPorts[backingPortName]; // Update port constraints. Creating a new port instead will break the editor, mandating the need for setters. @@ -147,6 +156,7 @@ namespace XNode { // The following assemblies, and sub-assemblies (eg. UnityEngine.UI) are skipped case "UnityEditor": case "UnityEngine": + case "Unity": case "System": case "mscorlib": case "Microsoft": @@ -154,11 +164,11 @@ namespace XNode { default: try { - nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); - } + nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); + } catch (System.Exception ex) { - Debug.LogError("Catched exception when building " + assemblyName + " caches"); + Debug.LogError($"Catched exception when building {assemblyName} caches"); Debug.LogException(ex); } break; @@ -203,8 +213,9 @@ namespace XNode { if (inputAttrib != null && outputAttrib != null) Debug.LogError("Field " + fieldInfo[i].Name + " of type " + nodeType.FullName + " cannot be both input and output."); else { - if (!portDataCache.ContainsKey(nodeType)) portDataCache.Add(nodeType, new List()); - portDataCache[nodeType].Add(new NodePort(fieldInfo[i])); + if (!portDataCache.ContainsKey(nodeType)) portDataCache.Add(nodeType, new Dictionary()); + NodePort port = new NodePort(fieldInfo[i]); + portDataCache[nodeType].Add(port.fieldName, port); } if (formerlySerializedAsAttribute != null) { @@ -218,30 +229,6 @@ namespace XNode { } [System.Serializable] - private class PortDataCache : Dictionary>, ISerializationCallbackReceiver { - [SerializeField] private List keys = new List(); - [SerializeField] private List> values = new List>(); - - // save the dictionary to lists - public void OnBeforeSerialize() { - keys.Clear(); - values.Clear(); - foreach (var pair in this) { - keys.Add(pair.Key); - values.Add(pair.Value); - } - } - - // load dictionary from lists - public void OnAfterDeserialize() { - this.Clear(); - - if (keys.Count != values.Count) - throw new System.Exception(string.Format("there are {0} keys and {1} values after deserialization. Make sure that both key and value types are serializable.", keys.Count, values.Count)); - - for (int i = 0; i < keys.Count; i++) - this.Add(keys[i], values[i]); - } - } + private class PortDataCache : Dictionary> { } } } diff --git a/Scripts/NodeGraph.cs b/Scripts/NodeGraph.cs index d928f94..347bc8e 100644 --- a/Scripts/NodeGraph.cs +++ b/Scripts/NodeGraph.cs @@ -47,7 +47,7 @@ namespace XNode { public virtual void Clear() { if (Application.isPlaying) { for (int i = 0; i < nodes.Count; i++) { - Destroy(nodes[i]); + if (nodes[i] != null) Destroy(nodes[i]); } } nodes.Clear(); diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs index 8e0114f..ea5287f 100644 --- a/Scripts/NodePort.cs +++ b/Scripts/NodePort.cs @@ -47,8 +47,9 @@ namespace XNode { return valueType; } set { + if (valueType == value) return; valueType = value; - if (value != null) _typeQualifiedName = value.AssemblyQualifiedName; + if (value != null) _typeQualifiedName = NodeDataCache.GetTypeQualifiedName(value); } } private Type valueType; @@ -78,6 +79,10 @@ namespace XNode { _connectionType = (attribs[i] as Node.OutputAttribute).connectionType; _typeConstraint = (attribs[i] as Node.OutputAttribute).typeConstraint; } + // Override ValueType of the Port + if(attribs[i] is PortTypeOverrideAttribute) { + ValueType = (attribs[i] as PortTypeOverrideAttribute).type; + } } }