diff --git a/Example/ExampleNodeGraph.asset b/Example/ExampleNodeGraph.asset index 16e79ec..8e72bce 100644 Binary files a/Example/ExampleNodeGraph.asset and b/Example/ExampleNodeGraph.asset differ diff --git a/Example/ExampleNodeGraph.asset.meta b/Example/ExampleNodeGraph.asset.meta index 723ec56..6ec3d2f 100644 --- a/Example/ExampleNodeGraph.asset.meta +++ b/Example/ExampleNodeGraph.asset.meta @@ -1,6 +1,6 @@ fileFormatVersion: 2 -guid: 5398d565241ec2d489f41c368ca6cf24 -timeCreated: 1507498811 +guid: e8c47bc953732464a9bf3a76273d99ef +timeCreated: 1507916591 licenseType: Free NativeFormatImporter: mainObjectFileID: 11400000 diff --git a/Example/Nodes/BaseNode.cs b/Example/Nodes/BaseNode.cs deleted file mode 100644 index 26d4a8d..0000000 --- a/Example/Nodes/BaseNode.cs +++ /dev/null @@ -1,15 +0,0 @@ -using UnityEngine; - -[System.Serializable] -public class BaseNode : Node { - - [Input] public string input; - [Output] public string output; - public bool concat; - [TextArea] - public string SomeString; - [Header("New stuff")] - public Color col; - public AnimationCurve anim; - public Vector3 vec; -} diff --git a/Example/Nodes/ConstantValue.cs b/Example/Nodes/ConstantValue.cs new file mode 100644 index 0000000..b0b99ce --- /dev/null +++ b/Example/Nodes/ConstantValue.cs @@ -0,0 +1,13 @@ +[System.Serializable] +public class ConstantValue : ExampleNodeBase { + public float a; + [Output] public float value; + + protected override void Init() { + name = "Constant Value"; + } + + public override object GetValue(NodePort port) { + return a; + } +} diff --git a/Example/Nodes/BaseNode.cs.meta b/Example/Nodes/ConstantValue.cs.meta similarity index 76% rename from Example/Nodes/BaseNode.cs.meta rename to Example/Nodes/ConstantValue.cs.meta index e525d86..e836841 100644 --- a/Example/Nodes/BaseNode.cs.meta +++ b/Example/Nodes/ConstantValue.cs.meta @@ -1,6 +1,6 @@ fileFormatVersion: 2 -guid: 23665941e8cd89a48b1272ac5fd6510c -timeCreated: 1505462705 +guid: 707240ce8955a0240a7c0c4177d83bf5 +timeCreated: 1505937586 licenseType: Free MonoImporter: serializedVersion: 2 diff --git a/Example/Nodes/DisplayValue.cs b/Example/Nodes/DisplayValue.cs index 01a2b0f..d19dd21 100644 --- a/Example/Nodes/DisplayValue.cs +++ b/Example/Nodes/DisplayValue.cs @@ -1,5 +1,6 @@ using UnityEngine; -public class DisplayValue : Node { +public class DisplayValue : ExampleNodeBase { [Input] public float value; + } diff --git a/Example/Nodes/Editor/DisplayValueEditor.cs b/Example/Nodes/Editor/DisplayValueEditor.cs index b0f4643..0530c18 100644 --- a/Example/Nodes/Editor/DisplayValueEditor.cs +++ b/Example/Nodes/Editor/DisplayValueEditor.cs @@ -12,21 +12,7 @@ public class DisplayValueEditor : NodeEditor { } public float GetResult() { - float result = 0f; - NodePort port = target.GetInputByFieldName("value"); - if (port == null) return result; - int connectionCount = port.ConnectionCount; - for (int i = 0; i < connectionCount; i++) { - - NodePort connection = port.GetConnection(i); - if (connection == null) continue; - - object obj = connection.GetValue(); - if (obj == null) continue; - - if (connection.type == typeof(int)) result += (int)obj; - else if (connection.type == typeof(float)) result += (float)obj; - } - return result; + ExampleNodeBase t = target as ExampleNodeBase; + return t.GetInputFloat("value"); } } diff --git a/Example/Nodes/ExampleNodeBase.cs b/Example/Nodes/ExampleNodeBase.cs new file mode 100644 index 0000000..c47cff0 --- /dev/null +++ b/Example/Nodes/ExampleNodeBase.cs @@ -0,0 +1,22 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +public class ExampleNodeBase : Node { + + public float GetInputFloat(string fieldName) { + float result = 0f; + NodePort port = GetInputByFieldName(fieldName); + if (port == null) return result; + int connectionCount = port.ConnectionCount; + for (int i = 0; i < connectionCount; i++) { + NodePort connection = port.GetConnection(i); + if (connection == null) continue; + object obj = connection.GetValue(); + if (obj == null) continue; + if (connection.type == typeof(int)) result += (int)obj; + else if (connection.type == typeof(float)) result += (float)obj; + } + return result; + } +} diff --git a/Scripts/Editor/NodeEditorToolbar.cs.meta b/Example/Nodes/ExampleNodeBase.cs.meta similarity index 76% rename from Scripts/Editor/NodeEditorToolbar.cs.meta rename to Example/Nodes/ExampleNodeBase.cs.meta index cc85b52..1cc1c43 100644 --- a/Scripts/Editor/NodeEditorToolbar.cs.meta +++ b/Example/Nodes/ExampleNodeBase.cs.meta @@ -1,6 +1,6 @@ fileFormatVersion: 2 -guid: fa774a466fc664148b43879d282ea071 -timeCreated: 1505932458 +guid: 923bada49e668fd4a98b04fcb49999d7 +timeCreated: 1507917099 licenseType: Free MonoImporter: serializedVersion: 2 diff --git a/Example/Nodes/MathNode.cs b/Example/Nodes/MathNode.cs index 32aa322..475698d 100644 --- a/Example/Nodes/MathNode.cs +++ b/Example/Nodes/MathNode.cs @@ -1,9 +1,9 @@ using UnityEngine; [System.Serializable] -public class MathNode : Node { - public float a; - public float b; +public class MathNode : ExampleNodeBase { + [Input] public float c; + [Input] public float b; [Output] public float result; public enum MathType { Add, Subtract, Multiply, Divide} public MathType mathType = MathType.Add; @@ -13,6 +13,9 @@ public class MathNode : Node { } public override object GetValue(NodePort port) { + float a = GetInputFloat("c"); + float b = GetInputFloat("b"); + switch(port.fieldName) { case "result": switch(mathType) { @@ -25,8 +28,4 @@ public class MathNode : Node { } return 0f; } - - public override void OnCreateConnection(NodePort from, NodePort to) { - - } } diff --git a/Scripts/Editor/NodeEditor.cs b/Scripts/Editor/NodeEditor.cs index 901feb5..1bde7d3 100644 --- a/Scripts/Editor/NodeEditor.cs +++ b/Scripts/Editor/NodeEditor.cs @@ -63,14 +63,14 @@ public class NodeEditor { /// Draw node port GUI using automatic layouting. Returns port handle position. protected Vector2 DrawNodePortGUI(NodePort port) { GUIStyle style = port.direction == NodePort.IO.Input ? NodeEditorResources.styles.inputPort : NodeEditorResources.styles.outputPort; - Rect rect = GUILayoutUtility.GetRect(new GUIContent(port.name.PrettifyCamelCase()), style); + Rect rect = GUILayoutUtility.GetRect(new GUIContent(port.fieldName.PrettifyCamelCase()), style); return DrawNodePortGUI(rect, port); } /// Draw node port GUI in rect. Returns port handle position. protected Vector2 DrawNodePortGUI(Rect rect, NodePort port) { GUIStyle style = port.direction == NodePort.IO.Input ? NodeEditorResources.styles.inputPort : NodeEditorResources.styles.outputPort; - GUI.Label(rect, new GUIContent(port.name.PrettifyCamelCase()), style); + GUI.Label(rect, new GUIContent(port.fieldName.PrettifyCamelCase()), style); Vector2 handlePoint = rect.center; diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs index cf0c44e..4a7e617 100644 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -15,7 +15,6 @@ public partial class NodeEditorWindow { DrawConnections(); DrawDraggedConnection(); DrawNodes(); - DrawToolbar(); GUI.matrix = m; } @@ -128,7 +127,9 @@ public partial class NodeEditorWindow { if (!portConnectionPoints.ContainsKey(output)) continue; Vector2 from = _portConnectionPoints[output].center; for (int k = 0; k < output.ConnectionCount; k++) { + NodePort input = output.GetConnection(k); + if (input == null) return; //If a script has been updated and the port doesn't exist, it is removed and null is returned. If this happens, return. Vector2 to = _portConnectionPoints[input].center; DrawConnection(from, to, NodeEditorUtilities.GetTypeColor(output.type)); } diff --git a/Scripts/Editor/NodeEditorToolbar.cs b/Scripts/Editor/NodeEditorToolbar.cs deleted file mode 100644 index eb89b6f..0000000 --- a/Scripts/Editor/NodeEditorToolbar.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using UnityEditor; - -public partial class NodeEditorWindow { - - public void DrawToolbar() { - EditorGUILayout.BeginHorizontal("Toolbar"); - - if (DropdownButton("File", 50)) FileContextMenu(); - if (DropdownButton("Edit", 50)) EditContextMenu(); - if (DropdownButton("View", 50)) { } - if (DropdownButton("Settings", 70)) { } - if (DropdownButton("Tools", 50)) ToolsContextMenu(); - - //Draw hover info - if (Event.current.type == EventType.Layout || Event.current.type == EventType.Repaint) { - if (IsHoveringNode) { - GUILayout.Space(20); - string hoverInfo = hoveredNode.GetType().ToString(); - if (IsHoveringPort) hoverInfo += " > " + hoveredPort.name; - GUILayout.Label(hoverInfo); - } - } - - // Make the toolbar extend all throughout the window extension. - GUILayout.FlexibleSpace(); - - EditorGUILayout.EndHorizontal(); - } - - public void FileContextMenu() { - GenericMenu contextMenu = new GenericMenu(); - - contextMenu.AddItem(new GUIContent("Save"), false, Save); - contextMenu.AddItem(new GUIContent("Save As"), false, SaveAs); - - contextMenu.DropDown(new Rect(5f, 17f, 0f, 0f)); - } - - public void EditContextMenu() { - GenericMenu contextMenu = new GenericMenu(); - contextMenu.AddItem(new GUIContent("Clear"), false, () => graph.Clear()); - - contextMenu.DropDown(new Rect(5f, 17f, 0f, 0f)); - } - - public void ToolsContextMenu() { - GenericMenu contextMenu = new GenericMenu(); - contextMenu.AddItem(new GUIContent("Debug Custom Node Editors"), false, () => CacheCustomNodeEditors()); - - contextMenu.DropDown(new Rect(5f, 17f, 0f, 0f)); - } -} diff --git a/Scripts/Editor/NodeEditorUtilities.cs b/Scripts/Editor/NodeEditorUtilities.cs index 9d683bd..78ee295 100644 --- a/Scripts/Editor/NodeEditorUtilities.cs +++ b/Scripts/Editor/NodeEditorUtilities.cs @@ -41,6 +41,7 @@ public static class NodeEditorUtilities { /// Return color based on type public static Color GetTypeColor(Type type) { + if (type == null) return Color.gray; UnityEngine.Random.InitState(type.Name.GetHashCode()); return new Color(UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value); } diff --git a/Scripts/Node.cs b/Scripts/Node.cs index 49f62d0..60db807 100644 --- a/Scripts/Node.cs +++ b/Scripts/Node.cs @@ -19,17 +19,11 @@ public abstract class Node : ScriptableObject { public int InputCount { get { return inputs.Count; } } public int OutputCount { get { return outputs.Count; } } - protected Node() { - CachePorts(); //Cache the ports at creation time so we don't have to use reflection at runtime - } - protected void OnEnable() { - VerifyConnections(); - CachePorts(); + GetPorts(); Init(); } - /// Checks all connections for invalid references, and removes them. public void VerifyConnections() { for (int i = 0; i < InputCount; i++) { @@ -47,21 +41,19 @@ public abstract class Node : ScriptableObject { else return GetInputByFieldName(fieldName); } - /// Returns output port which matches 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]; } - Debug.LogWarning("No outputs with fieldName '" + fieldName+"'"); return null; } - /// Returns input port which matches fieldName + /// Returns input port which matches. 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]; } - Debug.LogWarning("No inputs with fieldName '" + fieldName+"'"); return null; } @@ -117,45 +109,7 @@ public abstract class Node : ScriptableObject { } } - /// Use reflection to find all fields with or , and write to and - private void CachePorts() { - List inputPorts = new List(); - List outputPorts = new List(); - - System.Reflection.FieldInfo[] fieldInfo = GetType().GetFields(); - for (int i = 0; i < fieldInfo.Length; i++) { - - //Get InputAttribute and OutputAttribute - object[] attribs = fieldInfo[i].GetCustomAttributes(false); - InputAttribute inputAttrib = null; - OutputAttribute outputAttrib = null; - for (int k = 0; k < attribs.Length; k++) { - if (attribs[k] is InputAttribute) inputAttrib = attribs[k] as InputAttribute; - else if (attribs[k] is OutputAttribute) outputAttrib = attribs[k] as OutputAttribute; - } - - if (inputAttrib != null && outputAttrib != null) Debug.LogError("Field " + fieldInfo + " cannot be both input and output."); - else if (inputAttrib != null) inputPorts.Add(new NodePort(fieldInfo[i], this)); - else if (outputAttrib != null) outputPorts.Add(new NodePort(fieldInfo[i], this)); - } - - //Remove - for (int i = inputs.Count-1; i >= 0; i--) { - //If input nodeport does not exist, remove it - if (!inputPorts.Any(x => inputs[i].fieldName == x.fieldName)) inputs.RemoveAt(i); - } - for (int i = outputs.Count - 1; i >= 0; i--) { - //If output nodeport does not exist, remove it - if (!outputPorts.Any(x => outputs[i].fieldName == x.fieldName)) outputs.RemoveAt(i); - } - //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]); - } + private void GetPorts() { + NodeDataCache.UpdatePorts(this, inputs, outputs); } } diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs new file mode 100644 index 0000000..f14fb9f --- /dev/null +++ b/Scripts/NodeDataCache.cs @@ -0,0 +1,115 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using System.Reflection; +using System.Linq; +using UnityEditor; + +/// Precaches reflection data in editor so we won't have to do it runtime +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) { + if (!Initialized) BuildCache(); + + List inputPorts = new List(); + List outputPorts = new List(); + + 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)); + } + + 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; + } + 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]); + } + } + + private static void BuildCache() { + portDataCache = new PortDataCache(); + System.Type baseType = typeof(Node); + Assembly assembly = Assembly.GetAssembly(baseType); + System.Type[] nodeTypes = assembly.GetTypes().Where(t => + !t.IsAbstract && + baseType.IsAssignableFrom(t) + ).ToArray(); + + for (int i = 0; i < nodeTypes.Length; i++) { + CachePorts(nodeTypes[i]); + } + } + + private static void CachePorts(System.Type nodeType) { + System.Reflection.FieldInfo[] fieldInfo = nodeType.GetFields(); + for (int i = 0; i < fieldInfo.Length; i++) { + + //Get InputAttribute and OutputAttribute + object[] attribs = fieldInfo[i].GetCustomAttributes(false); + Node.InputAttribute inputAttrib = attribs.FirstOrDefault(x => x is Node.InputAttribute) as Node.InputAttribute; + Node.OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute; + + if (inputAttrib == null && outputAttrib == null) continue; + + if (inputAttrib != null && outputAttrib != null) Debug.LogError("Field " + fieldInfo + " cannot be both input and output."); + else { + if (!portDataCache.ContainsKey(nodeType)) portDataCache.Add(nodeType, new List()); + portDataCache[nodeType].Add(new NodePort(fieldInfo[i])); + } + } + } + + [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.")); + + for (int i = 0; i < keys.Count; i++) + this.Add(keys[i], values[i]); + } + } +} diff --git a/Scripts/NodeDataCache.cs.meta b/Scripts/NodeDataCache.cs.meta new file mode 100644 index 0000000..34482f2 --- /dev/null +++ b/Scripts/NodeDataCache.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 64ea6af1e195d024d8df0ead1921e517 +timeCreated: 1507566823 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/NodeGraph.cs b/Scripts/NodeGraph.cs index 58baa23..4c1d798 100644 --- a/Scripts/NodeGraph.cs +++ b/Scripts/NodeGraph.cs @@ -1,5 +1,4 @@ -using System.Collections; -using System.Collections.Generic; +using System.Collections.Generic; using UnityEngine; using System; @@ -50,17 +49,17 @@ public abstract class NodeGraph : ScriptableObject, ISerializationCallbackReceiv } public void OnAfterDeserialize() { - for (int i = 0; i < nodes.Count; i++) { + /*for (int i = 0; i < nodes.Count; i++) { nodes[i].graph = this; - } - VerifyConnections(); + }*/ + //VerifyConnections(); } - /// Checks all connections for invalid references, and removes them. + /*/// Checks all connections for invalid references, and removes them. public void VerifyConnections() { for (int i = 0; i < nodes.Count; i++) { nodes[i].VerifyConnections(); } - } + }*/ } diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs index dfef17c..1980935 100644 --- a/Scripts/NodePort.cs +++ b/Scripts/NodePort.cs @@ -18,23 +18,18 @@ public class NodePort { public bool IsInput { get { return direction == IO.Input; } } public bool IsOutput { get { return direction == IO.Output; } } - public Node node { get; private set; } - [SerializeField] public string name; - public bool enabled { get { return _enabled; } set { _enabled = value; } } public string fieldName { get { return _fieldName; } } - [SerializeField] private List connections = new List(); + [SerializeField] public Node node; [SerializeField] private string _fieldName; [SerializeField] public Type type; - [SerializeField] private bool _enabled = true; + [SerializeField] private List connections = new List(); [SerializeField] private IO _direction; - public NodePort(FieldInfo fieldInfo, Node node) { + public NodePort(FieldInfo fieldInfo) { _fieldName = fieldInfo.Name; - name = _fieldName; type = fieldInfo.FieldType; - this.node = node; var attribs = fieldInfo.GetCustomAttributes(false); for (int i = 0; i < attribs.Length; i++) { @@ -43,6 +38,13 @@ public class NodePort { } } + public NodePort(NodePort nodePort, Node node) { + _fieldName = nodePort._fieldName; + type = nodePort.type; + this.node = node; + _direction = nodePort.direction; + } + /// Checks all connections for invalid references, and removes them. public void VerifyConnections() { for (int i = 0; i < connections.Count; i++) { @@ -75,7 +77,17 @@ public class NodePort { } public NodePort GetConnection(int i) { - return connections[i].Port; + //If the connection is broken for some reason, remove it. + if (connections[i].node == null || string.IsNullOrEmpty(connections[i].fieldName)) { + connections.RemoveAt(i); + return null; + } + NodePort port = connections[i].node.GetPortByFieldName(connections[i].fieldName); + if (port == null) { + connections.RemoveAt(i); + return null; + } + return port; } public bool IsConnectedTo(NodePort port) { @@ -99,15 +111,15 @@ public class NodePort { } public void ClearConnections() { - for (int i = 0; i < connections.Count; i++) { - Disconnect(connections[i].Port); + while(connections.Count > 0) { + Disconnect(connections[0].Port); } } [Serializable] public class PortConnection { - [SerializeField] public Node node; [SerializeField] public string fieldName; + [SerializeField] public Node node; public NodePort Port { get { return port != null ? port : port = GetPort(); } } [NonSerialized] private NodePort port; @@ -118,6 +130,7 @@ public class NodePort { } private NodePort GetPort() { + for (int i = 0; i < node.OutputCount; i++) { if (node.outputs[i].fieldName == fieldName) return node.outputs[i]; }