From d3bb36fe0e8ff951d47c9e1922169d4582faed62 Mon Sep 17 00:00:00 2001 From: Thor Kramer Brigsted Date: Thu, 2 Nov 2017 14:54:03 +0100 Subject: [PATCH] Big update: Warning: Updating to this commit will break all node connections. Internal NodePorts now uses dicts instead of lists. This is faster and more manageable. Added instance ports. Added Node.Ports, Node.Outputs, Node.Inputs, Node.InstanceOutputs, Node.InstanceInputs Changed public GetInputByFieldName to GetInputValue and GetInputPort --- Example/Nodes/DisplayValue.cs | 2 +- Example/Nodes/MathNode.cs | 4 +- Example/Nodes/Vector.cs | 6 +- Scripts/Editor/NodeEditor.cs | 2 +- Scripts/Editor/NodeEditorGUI.cs | 21 ++-- Scripts/Editor/NodeEditorGUILayout.cs | 2 +- Scripts/Editor/NodeEditorUtilities.cs | 1 + Scripts/Node.cs | 163 ++++++++++++++++---------- Scripts/NodeDataCache.cs | 51 +++----- Scripts/NodePort.cs | 14 ++- 10 files changed, 150 insertions(+), 116 deletions(-) diff --git a/Example/Nodes/DisplayValue.cs b/Example/Nodes/DisplayValue.cs index 445517a..4e22d9c 100644 --- a/Example/Nodes/DisplayValue.cs +++ b/Example/Nodes/DisplayValue.cs @@ -3,7 +3,7 @@ [Input(ShowBackingValue.Never)] public object value; public override object GetValue(NodePort port) { - return GetInputByFieldName("value", value); + return GetInputValue("value", value); } } } diff --git a/Example/Nodes/MathNode.cs b/Example/Nodes/MathNode.cs index 564c4e8..bfa9630 100644 --- a/Example/Nodes/MathNode.cs +++ b/Example/Nodes/MathNode.cs @@ -15,8 +15,8 @@ public override object GetValue(NodePort port) { // Get new a and b values from input connections. Fallback to field values if input is not connected - float a = GetInputByFieldName("a", this.a); - float b = GetInputByFieldName("b", this.b); + float a = GetInputValue("a", this.a); + float b = GetInputValue("b", this.b); // After you've gotten your input values, you can perform your calculations and return a value if (port.fieldName == "result") diff --git a/Example/Nodes/Vector.cs b/Example/Nodes/Vector.cs index 804f087..3d8b0a8 100644 --- a/Example/Nodes/Vector.cs +++ b/Example/Nodes/Vector.cs @@ -6,9 +6,9 @@ namespace BasicNodes { [Output] public Vector3 vector; public override object GetValue(NodePort port) { - float x = GetInputByFieldName("x", this.x); - float y = GetInputByFieldName("y", this.y); - float z = GetInputByFieldName("z", this.z); + float x = GetInputValue("x", this.x); + float y = GetInputValue("y", this.y); + float z = GetInputValue("z", this.z); return new Vector3(x, y, z); } } diff --git a/Scripts/Editor/NodeEditor.cs b/Scripts/Editor/NodeEditor.cs index 6a5a5e4..1ede86b 100644 --- a/Scripts/Editor/NodeEditor.cs +++ b/Scripts/Editor/NodeEditor.cs @@ -28,7 +28,7 @@ public class NodeEditor { /// Draws standard field editors for all public fields protected virtual void OnBodyGUI() { - string[] excludes = { "m_Script", "graph", "position", "inputs", "outputs" }; + string[] excludes = { "m_Script", "graph", "position", "ports" }; portPositions = new Dictionary(); SerializedProperty iterator = serializedObject.GetIterator(); diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs index f28f84c..578eb3e 100644 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -117,9 +117,8 @@ public partial class NodeEditorWindow { foreach (Node node in graph.nodes) { //If a null node is found, return. This can happen if the nodes associated script is deleted. It is currently not possible in Unity to delete a null asset. if (node == null) continue; - for (int i = 0; i < node.OutputCount; i++) { - NodePort output = node.GetOutput(i); + foreach(NodePort output in node.Outputs) { //Needs cleanup. Null checks are ugly if (!portConnectionPoints.ContainsKey(output)) continue; Vector2 from = _portConnectionPoints[output].center; @@ -206,20 +205,18 @@ public partial class NodeEditorWindow { //Check if we are hovering any of this nodes ports //Check input ports - for (int i = 0; i < node.InputCount; i++) { - NodePort port = node.GetInput(i); + foreach(NodePort input in node.Inputs) { //Check if port rect is available - if (!portConnectionPoints.ContainsKey(port)) continue; - Rect r = GridToWindowRect(portConnectionPoints[port]); - if (r.Contains(mousePos)) hoveredPort = port; + if (!portConnectionPoints.ContainsKey(input)) continue; + Rect r = GridToWindowRect(portConnectionPoints[input]); + if (r.Contains(mousePos)) hoveredPort = input; } //Check all output ports - for (int i = 0; i < node.OutputCount; i++) { - NodePort port = node.GetOutput(i); + foreach(NodePort output in node.Outputs) { //Check if port rect is available - if (!portConnectionPoints.ContainsKey(port)) continue; - Rect r = GridToWindowRect(portConnectionPoints[port]); - if (r.Contains(mousePos)) hoveredPort = port; + if (!portConnectionPoints.ContainsKey(output)) continue; + Rect r = GridToWindowRect(portConnectionPoints[output]); + if (r.Contains(mousePos)) hoveredPort = output; } } diff --git a/Scripts/Editor/NodeEditorGUILayout.cs b/Scripts/Editor/NodeEditorGUILayout.cs index aea5a51..8ec6fbf 100644 --- a/Scripts/Editor/NodeEditorGUILayout.cs +++ b/Scripts/Editor/NodeEditorGUILayout.cs @@ -12,7 +12,7 @@ public static class NodeEditorGUILayout { public static void PropertyField(SerializedProperty property, bool includeChildren = true) { if (property == null) throw new NullReferenceException(); Node node = property.serializedObject.targetObject as Node; - NodePort port = node.GetPortByFieldName(property.name); + NodePort port = node.GetPort(property.name); float temp_labelWidth = EditorGUIUtility.labelWidth; diff --git a/Scripts/Editor/NodeEditorUtilities.cs b/Scripts/Editor/NodeEditorUtilities.cs index 51124d2..755f87f 100644 --- a/Scripts/Editor/NodeEditorUtilities.cs +++ b/Scripts/Editor/NodeEditorUtilities.cs @@ -35,6 +35,7 @@ public static class NodeEditorUtilities { /// Turns camelCaseString into Camel Case String public static string PrettifyCamelCase(this string camelCase) { + if (string.IsNullOrEmpty(camelCase)) return ""; string s = System.Text.RegularExpressions.Regex.Replace(camelCase, "([A-Z])", " $1", System.Text.RegularExpressions.RegexOptions.Compiled).Trim(); return char.ToUpper(s[0]) + s.Substring(1); } diff --git a/Scripts/Node.cs b/Scripts/Node.cs index 02f8c23..d3431a5 100644 --- a/Scripts/Node.cs +++ b/Scripts/Node.cs @@ -14,75 +14,101 @@ public abstract class Node : ScriptableObject { Always } + /// Iterate over all ports on this node. + public IEnumerable Ports { get { foreach (NodePort port in ports.Values) yield return port; } } + /// Iterate over all outputs on this node. + public IEnumerable Outputs { get { foreach (NodePort port in Ports) { if (port.direction == NodePort.IO.Output) yield return port; } } } + /// Iterate over all inputs on this node. + public IEnumerable Inputs { get { foreach (NodePort port in Ports) { if (port.direction == NodePort.IO.Input) yield return port; } } } + /// Iterate over all instance outputs on this node. + public IEnumerable InstanceOutputs { get { foreach (NodePort port in Ports) { if (port.direction == NodePort.IO.Input) yield return port; } } } + /// Iterate over all instance inputs on this node. + public IEnumerable InstanceInputs { get { foreach (NodePort port in Ports) { if (port.direction == NodePort.IO.Input) yield return port; } } } /// Parent [SerializeField] public NodeGraph graph; /// Position on the [SerializeField] public Vector2 position; /// Input s. It is recommended not to modify these at hand. Instead, see - [SerializeField] private List inputs = new List(); - /// Output s. It is recommended not to modify these at hand. Instead, see - [SerializeField] private List outputs = new List(); - /// Additional instance-specific inputs. - [SerializeField] public List instanceInputs = new List(); - /// Additional instance-specific outputs. - [SerializeField] public List instanceOutputs = new List(); - - public int InputCount { get { return inputs.Count; } } - public int OutputCount { get { return outputs.Count; } } + [SerializeField] private NodePortDictionary ports = new NodePortDictionary(); protected void OnEnable() { - NodeDataCache.UpdatePorts(this, inputs, outputs); + NodeDataCache.UpdatePorts(this, ports); Init(); } + /// Initialize node. Called on creation. + protected virtual void Init() { name = GetType().Name; } + /// Checks all connections for invalid references, and removes them. public void VerifyConnections() { - for (int i = 0; i < InputCount; i++) { - inputs[i].VerifyConnections(); - } - for (int i = 0; i < OutputCount; i++) { - outputs[i].VerifyConnections(); - } + foreach (NodePort port in Ports) port.VerifyConnections(); + } + + #region Instance Ports + /// Returns input port at index + public NodePort AddInstanceInput(Type type, string fieldName = null) { + return AddInstancePort(type, NodePort.IO.Input, fieldName); } /// Returns input port at index - public NodePort GetInput(int i) { - return inputs[i]; + public NodePort AddInstanceOutput(Type type, string fieldName = null) { + return AddInstancePort(type, NodePort.IO.Output, fieldName); } - /// Returns output port at index. - public NodePort GetOutput(int i) { - return outputs[i]; - } - - /// Returns input or output port which matches fieldName - public NodePort GetPortByFieldName(string fieldName) { - NodePort port = GetOutputByFieldName(fieldName); - if (port != null) return port; - else return GetInputByFieldName(fieldName); - } - - /// Returns output port which matches fieldName. Returns null if none found. - public NodePort GetOutputByFieldName(string fieldName) { - for (int i = 0; i < OutputCount; i++) { - if (outputs[i].fieldName == fieldName) return outputs[i]; + private NodePort AddInstancePort(Type type, NodePort.IO direction, string fieldName = null) { + if (fieldName == null) { + fieldName = "instanceInput_0"; + int i = 0; + while (HasPort(fieldName)) fieldName = "instanceInput_" + (++i); + } else if (HasPort(fieldName)) { + Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this); + return ports[fieldName]; } - return null; + NodePort port = new NodePort(fieldName, type, direction, this); + ports.Add(fieldName, port); + return port; } - /// Returns input port which matches fieldName. Returns null if none found. - public NodePort GetInputByFieldName(string fieldName) { - for (int i = 0; i < InputCount; i++) { - if (inputs[i].fieldName == fieldName) return inputs[i]; - } - return null; + public bool RemoveInstancePort(string fieldName) { + NodePort port = GetPort(fieldName); + if (port == null || port.IsStatic) return false; + ports.Remove(fieldName); + return true; + } + #endregion + + #region Ports + /// Returns output port which matches fieldName + public NodePort GetOutputPort(string fieldName) { + NodePort port = GetPort(fieldName); + if (port == null || port.direction != NodePort.IO.Output) return null; + else return port; } + /// Returns input port which matches fieldName + public NodePort GetInputPort(string fieldName) { + NodePort port = GetPort(fieldName); + if (port == null || port.direction != NodePort.IO.Input) return null; + else return port; + } + + /// Returns port which matches fieldName + public NodePort GetPort(string fieldName) { + if (ports.ContainsKey(fieldName)) return ports[fieldName]; + else return null; + } + + public bool HasPort(string fieldName) { + return ports.ContainsKey(fieldName); + } + #endregion + + #region Inputs/Outputs /// Return input value for a specified port. Returns fallback value if no ports are connected /// Field name of requested input port /// If no ports are connected, this value will be returned - public T GetInputByFieldName(string fieldName, T fallback = default(T)) { - NodePort port = GetInputByFieldName(fieldName); + public T GetInputValue(string fieldName, T fallback = default(T)) { + NodePort port = GetPort(fieldName); if (port != null && port.IsConnected) return port.GetInputValue(); else return fallback; } @@ -90,8 +116,8 @@ public abstract class Node : ScriptableObject { /// Return all input values for a specified port. Returns fallback value if no ports are connected /// Field name of requested input port /// If no ports are connected, this value will be returned - public T[] GetInputsByFieldName(string fieldName, params T[] fallback) { - NodePort port = GetInputByFieldName(fieldName); + public T[] GetInputValues(string fieldName, params T[] fallback) { + NodePort port = GetPort(fieldName); if (port != null && port.IsConnected) return port.GetInputValues(); else return fallback; } @@ -102,9 +128,7 @@ public abstract class Node : ScriptableObject { Debug.LogWarning("No GetValue(NodePort port) override defined for " + GetType()); return null; } - - /// Initialize node. Called on creation. - protected virtual void Init() { name = GetType().Name; } + #endregion /// Called whenever a connection is being made between two s /// Output Input @@ -112,32 +136,51 @@ public abstract class Node : ScriptableObject { /// Disconnect everything from this node public void ClearConnections() { - for (int i = 0; i < inputs.Count; i++) { - inputs[i].ClearConnections(); - } - for (int i = 0; i < outputs.Count; i++) { - outputs[i].ClearConnections(); - } + foreach (NodePort port in Ports) port.ClearConnections(); } public override int GetHashCode() { return JsonUtility.ToJson(this).GetHashCode(); } - /// Mark a serializable field as an input port. You can access this through + /// Mark a serializable field as an input port. You can access this through [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] public class InputAttribute : Attribute { public ShowBackingValue backingValue; - - /// Mark a serializable field as an input port. You can access this through + + /// Mark a serializable field as an input port. You can access this through /// Should we display the backing value for this port as an editor field? public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected) { this.backingValue = backingValue; } } - /// Mark a serializable field as an output port. You can access this through + /// Mark a serializable field as an output port. You can access this through [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] public class OutputAttribute : Attribute { - /// Mark a serializable field as an output port. You can access this through + /// Mark a serializable field as an output port. You can access this through public OutputAttribute() { } } + + [Serializable] private class NodePortDictionary : Dictionary, ISerializationCallbackReceiver { + [SerializeField] private List keys = new List(); + [SerializeField] private List values = new List(); + + public void OnBeforeSerialize() { + keys.Clear(); + values.Clear(); + foreach (KeyValuePair pair in this) { + keys.Add(pair.Key); + values.Add(pair.Value); + } + } + + 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.")); + + for (int i = 0; i < keys.Count; i++) + this.Add(keys[i], values[i]); + } + } } \ No newline at end of file diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs index 93a0bd5..b542be1 100644 --- a/Scripts/NodeDataCache.cs +++ b/Scripts/NodeDataCache.cs @@ -10,46 +10,33 @@ public static class NodeDataCache { private static PortDataCache portDataCache; private static bool Initialized { get { return portDataCache != null; } } - /// Checks for invalid and removes them. - /// Checks for missing ports and adds them. - /// Checks for invalid connections and removes them. - public static void UpdatePorts(Node node, List inputs, List outputs) { + /// Update static ports to reflect class fields. + public static void UpdatePorts(Node node, Dictionary ports) { if (!Initialized) BuildCache(); - List inputPorts = new List(); - List outputPorts = new List(); - + Dictionary staticPorts = new Dictionary(); System.Type nodeType = node.GetType(); - inputPorts = new List(); - outputPorts = new List(); + if (!portDataCache.ContainsKey(nodeType)) return; for (int i = 0; i < portDataCache[nodeType].Count; i++) { - if (portDataCache[nodeType][i].direction == NodePort.IO.Input) inputPorts.Add(new NodePort(portDataCache[nodeType][i], node)); - else outputPorts.Add(new NodePort(portDataCache[nodeType][i], node)); + staticPorts.Add(portDataCache[nodeType][i].fieldName, portDataCache[nodeType][i]); } - for (int i = inputs.Count - 1; i >= 0; i--) { - int index = inputPorts.FindIndex(x => inputs[i].fieldName == x.fieldName); - //If input nodeport does not exist, remove it - if (index == -1) inputs.RemoveAt(i); - //If input nodeport does exist, update it - else inputs[i].type = inputPorts[index].type; + // Cleanup port dict - Remove nonexisting static ports - update static port types + foreach (NodePort port in ports.Values) { + if (staticPorts.ContainsKey(port.fieldName)) { + NodePort staticPort = staticPorts[port.fieldName]; + if (port.IsDynamic || port.direction != staticPort.direction) ports.Remove(port.fieldName); + else port.type = staticPort.type; + } else { + ports.Remove(port.fieldName); + } } - for (int i = outputs.Count - 1; i >= 0; i--) { - int index = outputPorts.FindIndex(x => outputs[i].fieldName == x.fieldName); - //If output nodeport does not exist, remove it - if (index == -1) outputs.RemoveAt(i); - //If output nodeport does exist, update it - else outputs[i].type = outputPorts[index].type; - } - //Add - for (int i = 0; i < inputPorts.Count; i++) { - //If inputports contains a new port, add it - if (!inputs.Any(x => x.fieldName == inputPorts[i].fieldName)) inputs.Add(inputPorts[i]); - } - for (int i = 0; i < outputPorts.Count; i++) { - //If inputports contains a new port, add it - if (!outputs.Any(x => x.fieldName == outputPorts[i].fieldName)) outputs.Add(outputPorts[i]); + // Add missing ports + foreach (NodePort staticPort in staticPorts.Values) { + if (!ports.ContainsKey(staticPort.fieldName)) { + ports.Add(staticPort.fieldName, new NodePort(staticPort, node)); + } } } diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs index dfebe5c..f7f7aec 100644 --- a/Scripts/NodePort.cs +++ b/Scripts/NodePort.cs @@ -20,18 +20,21 @@ public class NodePort { public string fieldName { get { return _fieldName; } } public Node node { get { return _node; } } + public bool IsDynamic { get { return _dynamic; } } + public bool IsStatic { get { return !_dynamic; } } [SerializeField] private Node _node; [SerializeField] private string _fieldName; [SerializeField] public Type type; [SerializeField] private List connections = new List(); [SerializeField] private IO _direction; + [SerializeField] private bool _dynamic; /// Construct a static targetless nodeport. Used as a template. public NodePort(FieldInfo fieldInfo) { _fieldName = fieldInfo.Name; type = fieldInfo.FieldType; - + _dynamic = false; var attribs = fieldInfo.GetCustomAttributes(false); for (int i = 0; i < attribs.Length; i++) { if (attribs[i] is Node.InputAttribute) _direction = IO.Input; @@ -44,6 +47,7 @@ public class NodePort { _fieldName = nodePort._fieldName; type = nodePort.type; _direction = nodePort.direction; + _dynamic = nodePort._dynamic; _node = node; } @@ -53,6 +57,7 @@ public class NodePort { this.type = type; _direction = direction; _node = node; + _dynamic = true; } /// Checks all connections for invalid references, and removes them. @@ -60,7 +65,7 @@ public class NodePort { for (int i = connections.Count - 1; i >= 0; i--) { if (connections[i].node != null && !string.IsNullOrEmpty(connections[i].fieldName) && - connections[i].node.GetPortByFieldName(connections[i].fieldName) != null) + connections[i].node.GetPort(connections[i].fieldName) != null) continue; connections.RemoveAt(i); } @@ -69,6 +74,7 @@ public class NodePort { /// Return the output value of this node through its parent nodes GetValue override method. /// public object GetOutputValue() { + if (direction == IO.Input) return null; return node.GetValue(this); } @@ -167,7 +173,7 @@ public class NodePort { connections.RemoveAt(i); return null; } - NodePort port = connections[i].node.GetPortByFieldName(connections[i].fieldName); + NodePort port = connections[i].node.GetPort(connections[i].fieldName); if (port == null) { connections.RemoveAt(i); return null; @@ -218,7 +224,7 @@ public class NodePort { /// Returns the port that this points to private NodePort GetPort() { if (node == null || string.IsNullOrEmpty(fieldName)) return null; - return node.GetPortByFieldName(fieldName); + return node.GetPort(fieldName); } } } \ No newline at end of file