diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b96a660 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,23 @@ +## Contributing to xNode +💙Thank you for taking the time to contribute💙 + +If you haven't already, join our [Discord channel](https://discord.gg/qgPrHv4)! + +## Pull Requests +Try to keep your pull requests relevant, neat, and manageable. If you are adding multiple features, try splitting them into separate commits. +* Avoid including irellevant whitespace or formatting changes. +* Comment your code. +* Spell check your code / comments + +## New features +xNode aims to be simple and extendible, not trying to fix all of Unity's shortcomings. + +If your feature aims to cover something not related to editing nodes, it generally won't be accepted. If in doubt, ask on the Discord channel. + +## Coding conventions +Skim through the code and you'll get the hang of it quickly. +* Methods, Types and properties PascalCase +* Variables camelCase +* Public methods XML commented +* Open braces on same line as condition +* 4 spaces for indentation. diff --git a/README.md b/README.md index 5194a63..e9db741 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/Siccity/xNode/master/LICENSE.md) [![GitHub Wiki](https://img.shields.io/badge/wiki-available-brightgreen.svg)](https://github.com/Siccity/xNode/wiki) -[Go to Downloads](https://github.com/Siccity/xNode/releases) / [Go to Asset Store](http://u3d.as/108S) +[Downloads](https://github.com/Siccity/xNode/releases) / [Asset Store](http://u3d.as/108S) / [Documentation](https://github.com/Siccity/xNode/wiki) ### xNode Thinking of developing a node-based plugin? Then this is for you. You can download it as an archive and unpack to a new unity project, or connect it as git submodule. @@ -20,7 +20,6 @@ With a minimal footprint, it is ideal as a base for custom state machines, dialo * No runtime reflection (unless you need to edit/build node graphs at runtime. In this case, all reflection is cached.) * Does not rely on any 3rd party plugins * Custom node inspector code is very similar to regular custom inspector code -* For a full list of features, see [this page](https://github.com/Siccity/xNode/wiki/Full-features-list) ### Node example: ```csharp diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs index 946e43a..7b43574 100644 --- a/Scripts/Editor/NodeEditorAction.cs +++ b/Scripts/Editor/NodeEditorAction.cs @@ -140,8 +140,12 @@ namespace XNodeEditor { } else if (!IsHoveringNode) { // If click outside node, release field focus if (!isPanning) { + // I've got no idea which of these do what, so we'll just reset all of it. GUIUtility.hotControl = 0; GUIUtility.keyboardControl = 0; + EditorGUIUtility.editingTextField = false; + EditorGUIUtility.keyboardControl = 0; + EditorGUIUtility.hotControl = 0; } AssetDatabase.SaveAssets(); } @@ -166,8 +170,12 @@ namespace XNodeEditor { } break; case EventType.KeyDown: - if (e.keyCode == KeyCode.Delete) RemoveSelectedNodes(); - else if (e.keyCode == KeyCode.D && e.control) DublicateSelectedNodes(); + if (EditorGUIUtility.editingTextField) break; + else if (e.keyCode == KeyCode.F) Home(); + break; + case EventType.ValidateCommand: + if (e.commandName == "SoftDelete") RemoveSelectedNodes(); + else if (e.commandName == "Duplicate") DublicateSelectedNodes(); Repaint(); break; case EventType.Ignore: diff --git a/Scripts/Editor/NodeEditorBase.cs b/Scripts/Editor/NodeEditorBase.cs index 0868ce0..14ef5ce 100644 --- a/Scripts/Editor/NodeEditorBase.cs +++ b/Scripts/Editor/NodeEditorBase.cs @@ -10,7 +10,6 @@ namespace XNodeEditor.Internal { public class NodeEditorBase where A : Attribute, NodeEditorBase.INodeEditorAttrib where T : NodeEditorBase where K : ScriptableObject { /// Custom editors defined with [CustomNodeEditor] private static Dictionary editors; - private static Dictionary serializeds; public K target; public SerializedObject serializedObject; @@ -19,17 +18,10 @@ namespace XNodeEditor.Internal { Type type = target.GetType(); T editor = GetEditor(type); editor.target = target; - editor.serializedObject = GetSerialized(target); + editor.serializedObject = new SerializedObject(target); return editor; } - private static SerializedObject GetSerialized(K target) { - if (target == null) return null; - if (serializeds == null) serializeds = new Dictionary(); - if (!serializeds.ContainsKey(target)) serializeds.Add(target, new SerializedObject(target)); - return serializeds[target]; - } - private static T GetEditor(Type type) { if (type == null) return null; if (editors == null) CacheCustomEditors(); diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs index 2843151..4db162b 100644 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -364,8 +364,8 @@ namespace XNodeEditor { GUIContent content = new GUIContent(); content.text = type.PrettyName(); if (hoveredPort.IsStatic && hoveredPort.IsOutput) { - object obj = ObjectFromFieldName(hoveredPort.node, hoveredPort.fieldName); - if (obj != null) content.text += " = " + obj.ToString(); + object obj = hoveredPort.node.GetValue(hoveredPort); + content.text += " = " + (obj != null ? obj.ToString() : "null"); } Vector2 size = NodeEditorResources.styles.tooltip.CalcSize(content); Rect rect = new Rect(Event.current.mousePosition - (size), size); diff --git a/Scripts/Editor/NodeEditorGUILayout.cs b/Scripts/Editor/NodeEditorGUILayout.cs index 0a377f9..d173abf 100644 --- a/Scripts/Editor/NodeEditorGUILayout.cs +++ b/Scripts/Editor/NodeEditorGUILayout.cs @@ -70,13 +70,13 @@ namespace XNodeEditor { switch (showBacking) { case XNode.Node.ShowBackingValue.Unconnected: // Display a label if port is connected - if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName), NodeEditorResources.styles.outputPort, GUILayout.MinWidth(30)); + if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName), NodeEditorResources.OutputPort, GUILayout.MinWidth(30)); // Display an editable property field if port is not connected else EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30)); break; case XNode.Node.ShowBackingValue.Never: // Display a label - EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName), NodeEditorResources.styles.outputPort, GUILayout.MinWidth(30)); + EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName), NodeEditorResources.OutputPort, GUILayout.MinWidth(30)); break; case XNode.Node.ShowBackingValue.Always: // Display an editable property field diff --git a/Scripts/Editor/NodeEditorResources.cs b/Scripts/Editor/NodeEditorResources.cs index 9b55c8f..0a84e0a 100644 --- a/Scripts/Editor/NodeEditorResources.cs +++ b/Scripts/Editor/NodeEditorResources.cs @@ -1,4 +1,5 @@ -using UnityEngine; +using UnityEditor; +using UnityEngine; namespace XNodeEditor { public static class NodeEditorResources { @@ -15,9 +16,9 @@ namespace XNodeEditor { // Styles public static Styles styles { get { return _styles != null ? _styles : _styles = new Styles(); } } public static Styles _styles = null; - + public static GUIStyle OutputPort { get { return new GUIStyle(EditorStyles.label) { alignment = TextAnchor.UpperRight }; } } public class Styles { - public GUIStyle inputPort, outputPort, nodeHeader, nodeBody, tooltip, nodeHighlight; + public GUIStyle inputPort, nodeHeader, nodeBody, tooltip, nodeHighlight; public Styles() { GUIStyle baseStyle = new GUIStyle("Label"); @@ -27,10 +28,6 @@ namespace XNodeEditor { inputPort.alignment = TextAnchor.UpperLeft; inputPort.padding.left = 10; - outputPort = new GUIStyle(baseStyle); - outputPort.alignment = TextAnchor.UpperRight; - outputPort.padding.right = 10; - nodeHeader = new GUIStyle(); nodeHeader.alignment = TextAnchor.MiddleCenter; nodeHeader.fontStyle = FontStyle.Bold; diff --git a/Scripts/Editor/NodeEditorUtilities.cs b/Scripts/Editor/NodeEditorUtilities.cs index d0cbc7b..0d98420 100644 --- a/Scripts/Editor/NodeEditorUtilities.cs +++ b/Scripts/Editor/NodeEditorUtilities.cs @@ -1,12 +1,20 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; +using System.Text; +using UnityEditor; +using UnityEngine; +using Object = UnityEngine.Object; namespace XNodeEditor { /// A set of editor-only utilities and extensions for UnityNodeEditorBase public static class NodeEditorUtilities { + /// C#'s Script Icon [The one MonoBhevaiour Scripts have]. + private static Texture2D scriptIcon = (EditorGUIUtility.IconContent("cs Script Icon").image as Texture2D); + public static bool GetAttrib(Type classType, out T attribOut) where T : Attribute { object[] attribs = classType.GetCustomAttributes(typeof(T), false); return GetAttrib(attribs, out attribOut); @@ -85,5 +93,86 @@ namespace XNodeEditor { } } else return type.ToString(); } + + /// Creates a new C# Class. + [MenuItem("Assets/Create/xNode/Node C# Script", false, 89)] + private static void CreateNode() { + string[] guids = AssetDatabase.FindAssets("xNode_NodeTemplate.cs"); + if (guids.Length == 0) { + Debug.LogWarning("xNode_NodeTemplate.cs.txt not found in asset database"); + return; + } + string path = AssetDatabase.GUIDToAssetPath(guids[0]); + CreateFromTemplate( + "NewNode.cs", + path + ); + } + + /// Creates a new C# Class. + [MenuItem("Assets/Create/xNode/NodeGraph C# Script", false, 89)] + private static void CreateGraph() { + string[] guids = AssetDatabase.FindAssets("xNode_NodeGraphTemplate.cs"); + if (guids.Length == 0) { + Debug.LogWarning("xNode_NodeGraphTemplate.cs.txt not found in asset database"); + return; + } + string path = AssetDatabase.GUIDToAssetPath(guids[0]); + CreateFromTemplate( + "NewNodeGraph.cs", + path + ); + } + + public static void CreateFromTemplate(string initialName, string templatePath) { + ProjectWindowUtil.StartNameEditingIfProjectWindowExists( + 0, + ScriptableObject.CreateInstance(), + initialName, + scriptIcon, + templatePath + ); + } + + /// Inherits from EndNameAction, must override EndNameAction.Action + public class DoCreateCodeFile : UnityEditor.ProjectWindowCallback.EndNameEditAction { + public override void Action(int instanceId, string pathName, string resourceFile) { + Object o = CreateScript(pathName, resourceFile); + ProjectWindowUtil.ShowCreatedAsset(o); + } + } + + /// Creates Script from Template's path. + internal static UnityEngine.Object CreateScript(string pathName, string templatePath) { + string className = Path.GetFileNameWithoutExtension(pathName).Replace(" ", string.Empty); + string templateText = string.Empty; + + UTF8Encoding encoding = new UTF8Encoding(true, false); + + if (File.Exists(templatePath)) { + /// Read procedures. + StreamReader reader = new StreamReader(templatePath); + templateText = reader.ReadToEnd(); + reader.Close(); + + templateText = templateText.Replace("#SCRIPTNAME#", className); + templateText = templateText.Replace("#NOTRIM#", string.Empty); + /// You can replace as many tags you make on your templates, just repeat Replace function + /// e.g.: + /// templateText = templateText.Replace("#NEWTAG#", "MyText"); + + /// Write procedures. + + StreamWriter writer = new StreamWriter(Path.GetFullPath(pathName), false, encoding); + writer.Write(templateText); + writer.Close(); + + AssetDatabase.ImportAsset(pathName); + return AssetDatabase.LoadAssetAtPath(pathName, typeof(Object)); + } else { + Debug.LogError(string.Format("The template file was not found: {0}", templatePath)); + return null; + } + } } } \ No newline at end of file diff --git a/Scripts/Editor/Resources/ScriptTemplates.meta b/Scripts/Editor/Resources/ScriptTemplates.meta new file mode 100644 index 0000000..b2435e8 --- /dev/null +++ b/Scripts/Editor/Resources/ScriptTemplates.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 86b677955452bb5449f9f4dd47b6ddfe +folderAsset: yes +timeCreated: 1519049391 +licenseType: Free +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt b/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt new file mode 100644 index 0000000..e3d7c36 --- /dev/null +++ b/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt @@ -0,0 +1,9 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using XNode; + +[CreateAssetMenu] +public class #SCRIPTNAME# : NodeGraph { + #NOTRIM# +} \ No newline at end of file diff --git a/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt.meta b/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt.meta new file mode 100644 index 0000000..b55bd75 --- /dev/null +++ b/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 8165767f64da7d94e925f61a38da668c +timeCreated: 1519049802 +licenseType: Free +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt b/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt new file mode 100644 index 0000000..de791fc --- /dev/null +++ b/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt @@ -0,0 +1,18 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using XNode; + +public class #SCRIPTNAME# : Node { + + // Use this for initialization + protected override void Init() { + base.Init(); + #NOTRIM# + } + + // Return the correct value of an output port when requested + public override object GetValue(NodePort port) { + return null; // Replace this + } +} \ No newline at end of file diff --git a/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt.meta b/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt.meta new file mode 100644 index 0000000..455420a --- /dev/null +++ b/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 85f6f570600a1a44d8e734cb111a8b89 +timeCreated: 1519049802 +licenseType: Free +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Node.cs b/Scripts/Node.cs index 0e0449f..ddc43ea 100644 --- a/Scripts/Node.cs +++ b/Scripts/Node.cs @@ -202,6 +202,10 @@ namespace XNode { 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 [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] public class InputAttribute : Attribute { diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs index fe9f50d..b609066 100644 --- a/Scripts/NodePort.cs +++ b/Scripts/NodePort.cs @@ -9,8 +9,15 @@ namespace XNode { public enum IO { Input, Output } public int ConnectionCount { get { return connections.Count; } } - /// Return the first connection - public NodePort Connection { get { return connections.Count > 0 ? connections[0].Port : null; } } + /// Return the first non-null connection + public NodePort Connection { + get { + for (int i = 0; i < connections.Count; i++) { + if (connections[i] != null) return connections[i].Port; + } + return null; + } + } public IO direction { get { return _direction; } } public Node.ConnectionType connectionType { get { return _connectionType; } } @@ -54,8 +61,7 @@ namespace XNode { if (attribs[i] is Node.InputAttribute) { _direction = IO.Input; _connectionType = (attribs[i] as Node.InputAttribute).connectionType; - } - else if (attribs[i] is Node.OutputAttribute) { + } else if (attribs[i] is Node.OutputAttribute) { _direction = IO.Output; _connectionType = (attribs[i] as Node.OutputAttribute).connectionType; } @@ -79,7 +85,7 @@ namespace XNode { _direction = direction; _node = node; _dynamic = true; - _connectionType = connectionType; + _connectionType = connectionType; } /// Checks all connections for invalid references, and removes them. @@ -225,15 +231,17 @@ namespace XNode { connections.RemoveAt(i); } } - // Remove the other ports connection to this port - for (int i = 0; i < port.connections.Count; i++) { - if (port.connections[i].Port == this) { - port.connections.RemoveAt(i); + if (port != null) { + // Remove the other ports connection to this port + for (int i = 0; i < port.connections.Count; i++) { + if (port.connections[i].Port == this) { + port.connections.RemoveAt(i); + } } } // Trigger OnRemoveConnection node.OnRemoveConnection(this); - port.node.OnRemoveConnection(port); + if (port != null) port.node.OnRemoveConnection(port); } public void ClearConnections() {