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