├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── CONTRIBUTING.md ├── CONTRIBUTING.md.meta ├── LICENSE.md ├── LICENSE.md.meta ├── README.md ├── README.md.meta ├── Scripts.meta ├── Scripts ├── Attributes.meta ├── Attributes │ ├── NodeEnum.cs │ ├── NodeEnum.cs.meta │ ├── PortTypeOverrideAttribute.cs │ └── PortTypeOverrideAttribute.cs.meta ├── Editor.meta ├── Editor │ ├── AdvancedGenericMenu.cs │ ├── AdvancedGenericMenu.cs.meta │ ├── Drawers.meta │ ├── Drawers │ │ ├── NodeEnumDrawer.cs │ │ ├── NodeEnumDrawer.cs.meta │ │ ├── Odin.meta │ │ └── Odin │ │ │ ├── InNodeEditorAttributeProcessor.cs │ │ │ ├── InNodeEditorAttributeProcessor.cs.meta │ │ │ ├── InputAttributeDrawer.cs │ │ │ ├── InputAttributeDrawer.cs.meta │ │ │ ├── OutputAttributeDrawer.cs │ │ │ └── OutputAttributeDrawer.cs.meta │ ├── GraphAndNodeEditor.cs │ ├── GraphAndNodeEditor.cs.meta │ ├── GraphRenameFixAssetProcessor.cs │ ├── GraphRenameFixAssetProcessor.cs.meta │ ├── Internal.meta │ ├── Internal │ │ ├── RerouteReference.cs │ │ └── RerouteReference.cs.meta │ ├── NodeEditor.cs │ ├── NodeEditor.cs.meta │ ├── NodeEditorAction.cs │ ├── NodeEditorAction.cs.meta │ ├── NodeEditorAssetModProcessor.cs │ ├── NodeEditorAssetModProcessor.cs.meta │ ├── NodeEditorBase.cs │ ├── NodeEditorBase.cs.meta │ ├── NodeEditorGUI.cs │ ├── NodeEditorGUI.cs.meta │ ├── NodeEditorGUILayout.cs │ ├── NodeEditorGUILayout.cs.meta │ ├── NodeEditorPreferences.cs │ ├── NodeEditorPreferences.cs.meta │ ├── NodeEditorReflection.cs │ ├── NodeEditorReflection.cs.meta │ ├── NodeEditorResources.cs │ ├── NodeEditorResources.cs.meta │ ├── NodeEditorUtilities.cs │ ├── NodeEditorUtilities.cs.meta │ ├── NodeEditorWindow.cs │ ├── NodeEditorWindow.cs.meta │ ├── NodeGraphEditor.cs │ ├── NodeGraphEditor.cs.meta │ ├── NodeGraphImporter.cs │ ├── NodeGraphImporter.cs.meta │ ├── RenamePopup.cs │ ├── RenamePopup.cs.meta │ ├── Resources.meta │ ├── Resources │ │ ├── ScriptTemplates.meta │ │ ├── ScriptTemplates │ │ │ ├── xNode_NodeGraphTemplate.cs.txt │ │ │ ├── xNode_NodeGraphTemplate.cs.txt.meta │ │ │ ├── xNode_NodeTemplate.cs.txt │ │ │ └── xNode_NodeTemplate.cs.txt.meta │ │ ├── xnode_dot.png │ │ ├── xnode_dot.png.meta │ │ ├── xnode_dot_outer.png │ │ ├── xnode_dot_outer.png.meta │ │ ├── xnode_node.png │ │ ├── xnode_node.png.meta │ │ ├── xnode_node_highlight.png │ │ ├── xnode_node_highlight.png.meta │ │ ├── xnode_node_workfile.psd │ │ └── xnode_node_workfile.psd.meta │ ├── SceneGraphEditor.cs │ ├── SceneGraphEditor.cs.meta │ ├── XNodeEditor.asmdef │ └── XNodeEditor.asmdef.meta ├── Node.cs ├── Node.cs.meta ├── NodeDataCache.cs ├── NodeDataCache.cs.meta ├── NodeGraph.cs ├── NodeGraph.cs.meta ├── NodePort.cs ├── NodePort.cs.meta ├── SceneGraph.cs ├── SceneGraph.cs.meta ├── XNode.asmdef └── XNode.asmdef.meta ├── package.json └── package.json.meta /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cs] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = crlf 7 | insert_final_newline = false 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: thorbrigsted 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: thorbrigsted 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /[Ll]ibrary/ 2 | /[Tt]emp/ 3 | /[Oo]bj/ 4 | /[Bb]uild/ 5 | 6 | # Autogenerated VS/MD solution and project files 7 | *.csproj 8 | *.unityproj 9 | *.sln 10 | *.suo 11 | *.tmp 12 | *.user 13 | *.userprefs 14 | *.pidb 15 | *.booproj 16 | 17 | # Unity3D generated meta files 18 | *.pidb.meta 19 | 20 | # Unity3D Generated File On Crash Reports 21 | sysinfo.txt 22 | 23 | /Examples/ 24 | 25 | .git.meta 26 | .gitignore.meta 27 | .gitattributes.meta 28 | 29 | # OS X only: 30 | .DS_Store -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to xNode 2 | 💙Thank you for taking the time to contribute💙 3 | 4 | If you haven't already, join our [Discord channel](https://discord.gg/qgPrHv4)! 5 | 6 | ## Pull Requests 7 | Try to keep your pull requests relevant, neat, and manageable. If you are adding multiple features, split them into separate PRs. 8 | These are the main points to follow: 9 | 10 | 1) Use formatting which is consistent with the rest of xNode base (see below) 11 | 2) Keep _one feature_ per PR (see below) 12 | 3) xNode aims to be compatible with C# 4.x, do not use new language features 13 | 4) Avoid including irellevant whitespace or formatting changes 14 | 5) Comment your code 15 | 6) Spell check your code / comments 16 | 7) Use concrete types, not *var* 17 | 8) Use english language 18 | 19 | ## New features 20 | xNode aims to be simple and extendible, not trying to fix all of Unity's shortcomings. 21 | 22 | Approved changes might be rejected if bundled with rejected changes, so keep PRs as separate as possible. 23 | 24 | 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. 25 | 26 | ## Coding conventions 27 | Using consistent formatting is key to having a clean git history. Skim through the code and you'll get the hang of it quickly. 28 | * Methods, Types and properties PascalCase 29 | * Variables camelCase 30 | * Public methods XML commented. Params described if not obvious 31 | * Explicit usage of brackets when doing multiple math operations on the same line 32 | 33 | ## Formatting 34 | I use VSCode with the C# FixFormat extension and the following setting overrides: 35 | ```json 36 | "csharpfixformat.style.spaces.beforeParenthesis": false, 37 | "csharpfixformat.style.indent.regionIgnored": true 38 | ``` 39 | * Open braces on same line as condition 40 | * 4 spaces for indentation. 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bc1db8b29c76d44648c9c86c2dfade6d 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Thor Brigsted 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 77523c356ccf04f56b53e6527c6b12fd 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Discord](https://img.shields.io/discord/361769369404964864.svg)](https://discord.gg/qgPrHv4) 4 | [![GitHub issues](https://img.shields.io/github/issues/Siccity/xNode.svg)](https://github.com/Siccity/xNode/issues) 5 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/Siccity/xNode/master/LICENSE.md) 6 | [![GitHub Wiki](https://img.shields.io/badge/wiki-available-brightgreen.svg)](https://github.com/Siccity/xNode/wiki) 7 | [![openupm](https://img.shields.io/npm/v/com.github.siccity.xnode?label=openupm®istry_uri=https://package.openupm.com)](https://openupm.com/packages/com.github.siccity.xnode/) 8 | 9 | [Downloads](https://github.com/Siccity/xNode/releases) / [Asset Store](http://u3d.as/108S) / [Documentation](https://github.com/Siccity/xNode/wiki) 10 | 11 | Support xNode on [Ko-fi](https://ko-fi.com/Z8Z5DYWA) or [Patreon](https://www.patreon.com/thorbrigsted) 12 | 13 | For full Odin support, consider using [KAJed82's fork](https://github.com/KAJed82/xNode) 14 | 15 | ### xNode 16 | 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. 17 | 18 | xNode is super userfriendly, intuitive and will help you reap the benefits of node graphs in no time. 19 | With a minimal footprint, it is ideal as a base for custom state machines, dialogue systems, decision makers etc. 20 | 21 |

22 | 23 |

24 | 25 | ### Key features 26 | * Lightweight in runtime 27 | * Very little boilerplate code 28 | * Strong separation of editor and runtime code 29 | * No runtime reflection (unless you need to edit/build node graphs at runtime. In this case, all reflection is cached.) 30 | * Does not rely on any 3rd party plugins 31 | * Custom node inspector code is very similar to regular custom inspector code 32 | * Supported from Unity 5.3 and up 33 | 34 | ### Wiki 35 | * [Getting started](https://github.com/Siccity/xNode/wiki/Getting%20Started) - create your very first node node and graph 36 | * [Examples branch](https://github.com/Siccity/xNode/tree/examples) - look at other small projects 37 | 38 | ### Installation 39 |
Instructions 40 | 41 | ### Installing with Unity Package Manager 42 | ***Via Git URL*** 43 | *(Requires Unity version 2018.3.0b7 or above)* 44 | 45 | To install this project as a [Git dependency](https://docs.unity3d.com/Manual/upm-git.html) using the Unity Package Manager, 46 | add the following line to your project's `manifest.json`: 47 | 48 | ``` 49 | "com.github.siccity.xnode": "https://github.com/siccity/xNode.git" 50 | ``` 51 | 52 | You will need to have Git installed and available in your system's PATH. 53 | 54 | If you are using [Assembly Definitions](https://docs.unity3d.com/Manual/ScriptCompilationAssemblyDefinitionFiles.html) in your project, you will need to add `XNode` and/or `XNodeEditor` as Assembly Definition References. 55 | 56 | ***Via OpenUPM*** 57 | 58 | The package is available on the [openupm registry](https://openupm.com). It's recommended to install it via [openupm-cli](https://github.com/openupm/openupm-cli). 59 | 60 | ``` 61 | openupm add com.github.siccity.xnode 62 | ``` 63 | 64 | ### Installing with git 65 | ***Via Git Submodule*** 66 | 67 | To add xNode as a [submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) in your existing git project, 68 | run the following git command from your project root: 69 | 70 | ``` 71 | git submodule add git@github.com:Siccity/xNode.git Assets/Submodules/xNode 72 | ``` 73 | 74 | ### Installing 'the old way' 75 | If no source control or package manager is available to you, you can simply copy/paste the source files into your assets folder. 76 | 77 |
78 | 79 | ### Node example: 80 | ```csharp 81 | // public classes deriving from Node are registered as nodes for use within a graph 82 | public class MathNode : Node { 83 | // Adding [Input] or [Output] is all you need to do to register a field as a valid port on your node 84 | [Input] public float a; 85 | [Input] public float b; 86 | // The value of an output node field is not used for anything, but could be used for caching output results 87 | [Output] public float result; 88 | [Output] public float sum; 89 | 90 | // The value of 'mathType' will be displayed on the node in an editable format, similar to the inspector 91 | public MathType mathType = MathType.Add; 92 | public enum MathType { Add, Subtract, Multiply, Divide} 93 | 94 | // GetValue should be overridden to return a value for any specified output port 95 | public override object GetValue(NodePort port) { 96 | 97 | // Get new a and b values from input connections. Fallback to field values if input is not connected 98 | float a = GetInputValue("a", this.a); 99 | float b = GetInputValue("b", this.b); 100 | 101 | // After you've gotten your input values, you can perform your calculations and return a value 102 | if (port.fieldName == "result") 103 | switch(mathType) { 104 | case MathType.Add: default: return a + b; 105 | case MathType.Subtract: return a - b; 106 | case MathType.Multiply: return a * b; 107 | case MathType.Divide: return a / b; 108 | } 109 | else if (port.fieldName == "sum") return a + b; 110 | else return 0f; 111 | } 112 | } 113 | ``` 114 | 115 | ### Plugins 116 | Plugins are repositories that add functionality to xNode 117 | * [xNodeGroups](https://github.com/Siccity/xNodeGroups): adds resizable groups 118 | 119 | ### Community 120 | Join the [Discord](https://discord.gg/qgPrHv4 "Join Discord server") server to leave feedback or get support. 121 | Feel free to also leave suggestions/requests in the [issues](https://github.com/Siccity/xNode/issues "Go to Issues") page. 122 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 243efae3a6b7941ad8f8e54dcf38ce8c 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 657b15cb3ec32a24ca80faebf094d0f4 3 | folderAsset: yes 4 | timeCreated: 1505418321 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Scripts/Attributes.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5644dfc7eed151045af664a9d4fd1906 3 | folderAsset: yes 4 | timeCreated: 1541633926 5 | licenseType: Free 6 | DefaultImporter: 7 | externalObjects: {} 8 | userData: 9 | assetBundleName: 10 | assetBundleVariant: 11 | -------------------------------------------------------------------------------- /Scripts/Attributes/NodeEnum.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | /// Draw enums correctly within nodes. Without it, enums show up at the wrong positions. 4 | /// Enums with this attribute are not detected by EditorGui.ChangeCheck due to waiting before executing 5 | public class NodeEnumAttribute : PropertyAttribute { } -------------------------------------------------------------------------------- /Scripts/Attributes/NodeEnum.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 10a8338f6c985854697b35459181af0a 3 | timeCreated: 1541633942 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /Scripts/Attributes/PortTypeOverrideAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | /// Overrides the ValueType of the Port, to have a ValueType different from the type of its serializable field 3 | /// Especially useful in Dynamic Port Lists to create Value-Port Pairs with different type. 4 | [AttributeUsage(AttributeTargets.Field)] 5 | public class PortTypeOverrideAttribute : Attribute { 6 | public Type type; 7 | /// Overrides the ValueType of the Port 8 | /// ValueType of the Port 9 | public PortTypeOverrideAttribute(Type type) { 10 | this.type = type; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Scripts/Attributes/PortTypeOverrideAttribute.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1410c1437e863ab4fac7a7428aaca35b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 94d4fd78d9120634ebe0e8717610c412 3 | folderAsset: yes 4 | timeCreated: 1505418345 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Scripts/Editor/AdvancedGenericMenu.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_2019_1_OR_NEWER 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEditor.IMGUI.Controls; 5 | using UnityEngine; 6 | using static UnityEditor.GenericMenu; 7 | 8 | namespace XNodeEditor 9 | { 10 | public class AdvancedGenericMenu : AdvancedDropdown 11 | { 12 | public static float? DefaultMinWidth = 200f; 13 | public static float? DefaultMaxWidth = 300f; 14 | 15 | private class AdvancedGenericMenuItem : AdvancedDropdownItem 16 | { 17 | private MenuFunction func; 18 | 19 | private MenuFunction2 func2; 20 | private object userData; 21 | 22 | public AdvancedGenericMenuItem( string name ) : base( name ) 23 | { 24 | } 25 | 26 | public AdvancedGenericMenuItem( string name, bool enabled, Texture2D icon, MenuFunction func ) : base( name ) 27 | { 28 | Set( enabled, icon, func ); 29 | } 30 | 31 | public AdvancedGenericMenuItem( string name, bool enabled, Texture2D icon, MenuFunction2 func, object userData ) : base( name ) 32 | { 33 | Set( enabled, icon, func, userData ); 34 | } 35 | 36 | public void Set( bool enabled, Texture2D icon, MenuFunction func ) 37 | { 38 | this.enabled = enabled; 39 | this.icon = icon; 40 | this.func = func; 41 | } 42 | 43 | public void Set( bool enabled, Texture2D icon, MenuFunction2 func, object userData ) 44 | { 45 | this.enabled = enabled; 46 | this.icon = icon; 47 | this.func2 = func; 48 | this.userData = userData; 49 | } 50 | 51 | public void Run() 52 | { 53 | if ( func2 != null ) 54 | func2( userData ); 55 | else if ( func != null ) 56 | func(); 57 | } 58 | } 59 | 60 | private List items = new List(); 61 | 62 | private AdvancedGenericMenuItem FindOrCreateItem( string name, AdvancedGenericMenuItem currentRoot = null ) 63 | { 64 | if ( string.IsNullOrWhiteSpace( name ) ) 65 | return null; 66 | 67 | AdvancedGenericMenuItem item = null; 68 | 69 | string[] paths = name.Split( '/' ); 70 | if ( currentRoot == null ) 71 | { 72 | item = items.FirstOrDefault( x => x != null && x.name == paths[0] ); 73 | if ( item == null ) 74 | items.Add( item = new AdvancedGenericMenuItem( paths[0] ) ); 75 | } 76 | else 77 | { 78 | item = currentRoot.children.OfType().FirstOrDefault( x => x.name == paths[0] ); 79 | if ( item == null ) 80 | currentRoot.AddChild( item = new AdvancedGenericMenuItem( paths[0] ) ); 81 | } 82 | 83 | if ( paths.Length > 1 ) 84 | return FindOrCreateItem( string.Join( "/", paths, 1, paths.Length - 1 ), item ); 85 | 86 | return item; 87 | } 88 | 89 | private AdvancedGenericMenuItem FindParent( string name ) 90 | { 91 | string[] paths = name.Split( '/' ); 92 | return FindOrCreateItem( string.Join( "/", paths, 0, paths.Length - 1 ) ); 93 | } 94 | 95 | private string Name { get; set; } 96 | 97 | public AdvancedGenericMenu() : base( new AdvancedDropdownState() ) 98 | { 99 | Name = ""; 100 | } 101 | 102 | public AdvancedGenericMenu( string name, AdvancedDropdownState state ) : base( state ) 103 | { 104 | Name = name; 105 | } 106 | 107 | // 108 | // Summary: 109 | // Add a disabled item to the menu. 110 | // 111 | // Parameters: 112 | // content: 113 | // The GUIContent to display as a disabled menu item. 114 | public void AddDisabledItem( GUIContent content ) 115 | { 116 | //var parent = FindParent( content.text ); 117 | var item = FindOrCreateItem( content.text ); 118 | item.Set( false, null, null ); 119 | } 120 | 121 | // 122 | // Summary: 123 | // Add a disabled item to the menu. 124 | // 125 | // Parameters: 126 | // content: 127 | // The GUIContent to display as a disabled menu item. 128 | // 129 | // on: 130 | // Specifies whether to show that the item is currently activated (i.e. a tick next 131 | // to the item in the menu). 132 | public void AddDisabledItem( GUIContent content, bool on ) 133 | { 134 | } 135 | 136 | public void AddItem( string name, bool on, MenuFunction func ) 137 | { 138 | AddItem( new GUIContent( name ), on, func ); 139 | } 140 | 141 | public void AddItem( GUIContent content, bool on, MenuFunction func ) 142 | { 143 | //var parent = FindParent( content.text ); 144 | var item = FindOrCreateItem( content.text ); 145 | item.Set( true/*on*/, null, func ); 146 | } 147 | 148 | public void AddItem( string name, bool on, MenuFunction2 func, object userData ) 149 | { 150 | AddItem( new GUIContent( name ), on, func, userData ); 151 | } 152 | 153 | public void AddItem( GUIContent content, bool on, MenuFunction2 func, object userData ) 154 | { 155 | //var parent = FindParent( content.text ); 156 | var item = FindOrCreateItem( content.text ); 157 | item.Set( true/*on*/, null, func, userData ); 158 | } 159 | 160 | // 161 | // Summary: 162 | // Add a seperator item to the menu. 163 | // 164 | // Parameters: 165 | // path: 166 | // The path to the submenu, if adding a separator to a submenu. When adding a separator 167 | // to the top level of a menu, use an empty string as the path. 168 | public void AddSeparator( string path = null ) 169 | { 170 | var parent = string.IsNullOrWhiteSpace( path ) ? null : FindParent( path ); 171 | if ( parent == null ) 172 | items.Add( null ); 173 | else 174 | parent.AddSeparator(); 175 | } 176 | 177 | // 178 | // Summary: 179 | // Show the menu at the given screen rect. 180 | // 181 | // Parameters: 182 | // position: 183 | // The position at which to show the menu. 184 | public void DropDown( Rect position ) 185 | { 186 | position.width = Mathf.Clamp( position.width, DefaultMinWidth.HasValue ? DefaultMinWidth.Value : 1f, DefaultMaxWidth.HasValue ? DefaultMaxWidth.Value : Screen.width ); 187 | 188 | Show( position ); 189 | } 190 | 191 | protected override AdvancedDropdownItem BuildRoot() 192 | { 193 | var root = new AdvancedDropdownItem( Name ); 194 | 195 | foreach ( var m in items ) 196 | { 197 | if ( m == null ) 198 | root.AddSeparator(); 199 | else 200 | root.AddChild( m ); 201 | } 202 | 203 | return root; 204 | } 205 | 206 | protected override void ItemSelected( AdvancedDropdownItem item ) 207 | { 208 | if ( item is AdvancedGenericMenuItem gmItem ) 209 | gmItem.Run(); 210 | } 211 | } 212 | } 213 | #endif -------------------------------------------------------------------------------- /Scripts/Editor/AdvancedGenericMenu.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ddde711109af02e42bfe8eb006577081 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/Drawers.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7adf21edfb51f514fa991d7556ecd0ef 3 | folderAsset: yes 4 | timeCreated: 1541971984 5 | licenseType: Free 6 | DefaultImporter: 7 | externalObjects: {} 8 | userData: 9 | assetBundleName: 10 | assetBundleVariant: 11 | -------------------------------------------------------------------------------- /Scripts/Editor/Drawers/NodeEnumDrawer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using XNode; 7 | using XNodeEditor; 8 | 9 | namespace XNodeEditor { 10 | [CustomPropertyDrawer(typeof(NodeEnumAttribute))] 11 | public class NodeEnumDrawer : PropertyDrawer { 12 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { 13 | EditorGUI.BeginProperty(position, label, property); 14 | 15 | EnumPopup(position, property, label); 16 | 17 | EditorGUI.EndProperty(); 18 | } 19 | 20 | public static void EnumPopup(Rect position, SerializedProperty property, GUIContent label) { 21 | // Throw error on wrong type 22 | if (property.propertyType != SerializedPropertyType.Enum) { 23 | throw new ArgumentException("Parameter selected must be of type System.Enum"); 24 | } 25 | 26 | // Add label 27 | position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label); 28 | 29 | // Get current enum name 30 | string enumName = ""; 31 | if (property.enumValueIndex >= 0 && property.enumValueIndex < property.enumDisplayNames.Length) enumName = property.enumDisplayNames[property.enumValueIndex]; 32 | 33 | #if UNITY_2017_1_OR_NEWER 34 | // Display dropdown 35 | if (EditorGUI.DropdownButton(position, new GUIContent(enumName), FocusType.Passive)) { 36 | // Position is all wrong if we show the dropdown during the node draw phase. 37 | // Instead, add it to onLateGUI to display it later. 38 | NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property); 39 | } 40 | #else 41 | // Display dropdown 42 | if (GUI.Button(position, new GUIContent(enumName), "MiniPopup")) { 43 | // Position is all wrong if we show the dropdown during the node draw phase. 44 | // Instead, add it to onLateGUI to display it later. 45 | NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property); 46 | } 47 | #endif 48 | } 49 | 50 | public static void ShowContextMenuAtMouse(SerializedProperty property) { 51 | // Initialize menu 52 | GenericMenu menu = new GenericMenu(); 53 | 54 | // Add all enum display names to menu 55 | for (int i = 0; i < property.enumDisplayNames.Length; i++) { 56 | int index = i; 57 | menu.AddItem(new GUIContent(property.enumDisplayNames[i]), false, () => SetEnum(property, index)); 58 | } 59 | 60 | // Display at cursor position 61 | Rect r = new Rect(Event.current.mousePosition, new Vector2(0, 0)); 62 | menu.DropDown(r); 63 | } 64 | 65 | private static void SetEnum(SerializedProperty property, int index) { 66 | property.enumValueIndex = index; 67 | property.serializedObject.ApplyModifiedProperties(); 68 | property.serializedObject.Update(); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /Scripts/Editor/Drawers/NodeEnumDrawer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 83db81f92abadca439507e25d517cabe 3 | timeCreated: 1541633798 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /Scripts/Editor/Drawers/Odin.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 327994a52f523b641898a39ff7500a02 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Scripts/Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR && ODIN_INSPECTOR 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using Sirenix.OdinInspector.Editor; 6 | using UnityEngine; 7 | using XNode; 8 | 9 | namespace XNodeEditor { 10 | internal class OdinNodeInGraphAttributeProcessor : OdinAttributeProcessor where T : Node { 11 | public override bool CanProcessSelfAttributes(InspectorProperty property) { 12 | return false; 13 | } 14 | 15 | public override bool CanProcessChildMemberAttributes(InspectorProperty parentProperty, MemberInfo member) { 16 | if (!NodeEditor.inNodeEditor) 17 | return false; 18 | 19 | if (member.MemberType == MemberTypes.Field) { 20 | switch (member.Name) { 21 | case "graph": 22 | case "position": 23 | case "ports": 24 | return true; 25 | 26 | default: 27 | break; 28 | } 29 | } 30 | 31 | return false; 32 | } 33 | 34 | public override void ProcessChildMemberAttributes(InspectorProperty parentProperty, MemberInfo member, List attributes) { 35 | switch (member.Name) { 36 | case "graph": 37 | case "position": 38 | case "ports": 39 | attributes.Add(new HideInInspector()); 40 | break; 41 | 42 | default: 43 | break; 44 | } 45 | } 46 | } 47 | } 48 | #endif -------------------------------------------------------------------------------- /Scripts/Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3cf2561fbfea9a041ac81efbbb5b3e0d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR && ODIN_INSPECTOR 2 | using Sirenix.OdinInspector; 3 | using Sirenix.OdinInspector.Editor; 4 | using Sirenix.Utilities.Editor; 5 | using UnityEngine; 6 | using XNode; 7 | 8 | namespace XNodeEditor { 9 | public class InputAttributeDrawer : OdinAttributeDrawer { 10 | protected override bool CanDrawAttributeProperty(InspectorProperty property) { 11 | Node node = property.Tree.WeakTargets[0] as Node; 12 | return node != null; 13 | } 14 | 15 | protected override void DrawPropertyLayout(GUIContent label) { 16 | Node node = Property.Tree.WeakTargets[0] as Node; 17 | NodePort port = node.GetInputPort(Property.Name); 18 | 19 | if (!NodeEditor.inNodeEditor) { 20 | if (Attribute.backingValue == XNode.Node.ShowBackingValue.Always || Attribute.backingValue == XNode.Node.ShowBackingValue.Unconnected && !port.IsConnected) 21 | CallNextDrawer(label); 22 | return; 23 | } 24 | 25 | if (Property.Tree.WeakTargets.Count > 1) { 26 | SirenixEditorGUI.WarningMessageBox("Cannot draw ports with multiple nodes selected"); 27 | return; 28 | } 29 | 30 | if (port != null) { 31 | var portPropoerty = Property.Tree.GetUnityPropertyForPath(Property.UnityPropertyPath); 32 | if (portPropoerty == null) { 33 | SirenixEditorGUI.ErrorMessageBox("Port property missing at: " + Property.UnityPropertyPath); 34 | return; 35 | } else { 36 | var labelWidth = Property.GetAttribute(); 37 | if (labelWidth != null) 38 | GUIHelper.PushLabelWidth(labelWidth.Width); 39 | 40 | NodeEditorGUILayout.PropertyField(portPropoerty, label == null ? GUIContent.none : label, true, GUILayout.MinWidth(30)); 41 | 42 | if (labelWidth != null) 43 | GUIHelper.PopLabelWidth(); 44 | } 45 | } 46 | } 47 | } 48 | } 49 | #endif -------------------------------------------------------------------------------- /Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2fd590b2e9ea0bd49b6986a2ca9010ab 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR && ODIN_INSPECTOR 2 | using Sirenix.OdinInspector; 3 | using Sirenix.OdinInspector.Editor; 4 | using Sirenix.Utilities.Editor; 5 | using UnityEngine; 6 | using XNode; 7 | 8 | namespace XNodeEditor { 9 | public class OutputAttributeDrawer : OdinAttributeDrawer { 10 | protected override bool CanDrawAttributeProperty(InspectorProperty property) { 11 | Node node = property.Tree.WeakTargets[0] as Node; 12 | return node != null; 13 | } 14 | 15 | protected override void DrawPropertyLayout(GUIContent label) { 16 | Node node = Property.Tree.WeakTargets[0] as Node; 17 | NodePort port = node.GetOutputPort(Property.Name); 18 | 19 | if (!NodeEditor.inNodeEditor) { 20 | if (Attribute.backingValue == XNode.Node.ShowBackingValue.Always || Attribute.backingValue == XNode.Node.ShowBackingValue.Unconnected && !port.IsConnected) 21 | CallNextDrawer(label); 22 | return; 23 | } 24 | 25 | if (Property.Tree.WeakTargets.Count > 1) { 26 | SirenixEditorGUI.WarningMessageBox("Cannot draw ports with multiple nodes selected"); 27 | return; 28 | } 29 | 30 | if (port != null) { 31 | var portPropoerty = Property.Tree.GetUnityPropertyForPath(Property.UnityPropertyPath); 32 | if (portPropoerty == null) { 33 | SirenixEditorGUI.ErrorMessageBox("Port property missing at: " + Property.UnityPropertyPath); 34 | return; 35 | } else { 36 | var labelWidth = Property.GetAttribute(); 37 | if (labelWidth != null) 38 | GUIHelper.PushLabelWidth(labelWidth.Width); 39 | 40 | NodeEditorGUILayout.PropertyField(portPropoerty, label == null ? GUIContent.none : label, true, GUILayout.MinWidth(30)); 41 | 42 | if (labelWidth != null) 43 | GUIHelper.PopLabelWidth(); 44 | } 45 | } 46 | } 47 | } 48 | } 49 | #endif -------------------------------------------------------------------------------- /Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e7ebd8f2b42e2384aa109551dc46af88 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/GraphAndNodeEditor.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | #if ODIN_INSPECTOR 4 | using Sirenix.OdinInspector.Editor; 5 | using Sirenix.Utilities; 6 | using Sirenix.Utilities.Editor; 7 | #endif 8 | 9 | namespace XNodeEditor { 10 | /// Override graph inspector to show an 'Open Graph' button at the top 11 | [CustomEditor(typeof(XNode.NodeGraph), true)] 12 | #if ODIN_INSPECTOR 13 | public class GlobalGraphEditor : OdinEditor { 14 | public override void OnInspectorGUI() { 15 | if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { 16 | NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph); 17 | } 18 | base.OnInspectorGUI(); 19 | } 20 | } 21 | #else 22 | [CanEditMultipleObjects] 23 | public class GlobalGraphEditor : Editor { 24 | public override void OnInspectorGUI() { 25 | serializedObject.Update(); 26 | 27 | if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { 28 | NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph); 29 | } 30 | 31 | GUILayout.Space(EditorGUIUtility.singleLineHeight); 32 | GUILayout.Label("Raw data", "BoldLabel"); 33 | 34 | DrawDefaultInspector(); 35 | 36 | serializedObject.ApplyModifiedProperties(); 37 | } 38 | } 39 | #endif 40 | 41 | [CustomEditor(typeof(XNode.Node), true)] 42 | #if ODIN_INSPECTOR 43 | public class GlobalNodeEditor : OdinEditor { 44 | public override void OnInspectorGUI() { 45 | if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { 46 | SerializedProperty graphProp = serializedObject.FindProperty("graph"); 47 | NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph); 48 | w.Home(); // Focus selected node 49 | } 50 | base.OnInspectorGUI(); 51 | } 52 | } 53 | #else 54 | [CanEditMultipleObjects] 55 | public class GlobalNodeEditor : Editor { 56 | public override void OnInspectorGUI() { 57 | serializedObject.Update(); 58 | 59 | if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { 60 | SerializedProperty graphProp = serializedObject.FindProperty("graph"); 61 | NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph); 62 | w.Home(); // Focus selected node 63 | } 64 | 65 | GUILayout.Space(EditorGUIUtility.singleLineHeight); 66 | GUILayout.Label("Raw data", "BoldLabel"); 67 | 68 | // Now draw the node itself. 69 | DrawDefaultInspector(); 70 | 71 | serializedObject.ApplyModifiedProperties(); 72 | } 73 | } 74 | #endif 75 | } -------------------------------------------------------------------------------- /Scripts/Editor/GraphAndNodeEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bdd6e443125ccac4dad0665515759637 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/GraphRenameFixAssetProcessor.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using XNode; 3 | 4 | namespace XNodeEditor { 5 | /// 6 | /// This asset processor resolves an issue with the new v2 AssetDatabase system present on 2019.3 and later. When 7 | /// renaming a asset, it appears that sometimes the v2 AssetDatabase will swap which asset 8 | /// is the main asset (present at top level) between the and one of its 9 | /// sub-assets. As a workaround until Unity fixes this, this asset processor checks all renamed assets and if it 10 | /// finds a case where a has been made the main asset it will swap it back to being a sub-asset 11 | /// and rename the node to the default name for that node type. 12 | /// 13 | internal sealed class GraphRenameFixAssetProcessor : AssetPostprocessor { 14 | private static void OnPostprocessAllAssets( 15 | string[] importedAssets, 16 | string[] deletedAssets, 17 | string[] movedAssets, 18 | string[] movedFromAssetPaths) { 19 | for (int i = 0; i < movedAssets.Length; i++) { 20 | Node nodeAsset = AssetDatabase.LoadMainAssetAtPath(movedAssets[i]) as Node; 21 | 22 | // If the renamed asset is a node graph, but the v2 AssetDatabase has swapped a sub-asset node to be its 23 | // main asset, reset the node graph to be the main asset and rename the node asset back to its default 24 | // name. 25 | if (nodeAsset != null && AssetDatabase.IsMainAsset(nodeAsset)) { 26 | AssetDatabase.SetMainObject(nodeAsset.graph, movedAssets[i]); 27 | AssetDatabase.ImportAsset(movedAssets[i]); 28 | 29 | nodeAsset.name = NodeEditorUtilities.NodeDefaultName(nodeAsset.GetType()); 30 | EditorUtility.SetDirty(nodeAsset); 31 | } 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 65da1ff1c50a9984a9c95fd18799e8dd 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/Internal.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a6a1bbc054e282346a02e7bbddde3206 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Scripts/Editor/Internal/RerouteReference.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace XNodeEditor.Internal { 4 | public struct RerouteReference { 5 | public XNode.NodePort port; 6 | public int connectionIndex; 7 | public int pointIndex; 8 | 9 | public RerouteReference(XNode.NodePort port, int connectionIndex, int pointIndex) { 10 | this.port = port; 11 | this.connectionIndex = connectionIndex; 12 | this.pointIndex = pointIndex; 13 | } 14 | 15 | public void InsertPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex).Insert(pointIndex, pos); } 16 | public void SetPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex) [pointIndex] = pos; } 17 | public void RemovePoint() { port.GetReroutePoints(connectionIndex).RemoveAt(pointIndex); } 18 | public Vector2 GetPoint() { return port.GetReroutePoints(connectionIndex) [pointIndex]; } 19 | } 20 | } -------------------------------------------------------------------------------- /Scripts/Editor/Internal/RerouteReference.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 399f3c5fb717b2c458c3e9746f8959a3 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEngine; 6 | #if ODIN_INSPECTOR 7 | using Sirenix.OdinInspector.Editor; 8 | using Sirenix.Utilities; 9 | using Sirenix.Utilities.Editor; 10 | #endif 11 | #if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU 12 | using GenericMenu = XNodeEditor.AdvancedGenericMenu; 13 | #endif 14 | 15 | namespace XNodeEditor { 16 | /// Base class to derive custom Node editors from. Use this to create your own custom inspectors and editors for your nodes. 17 | [CustomNodeEditor(typeof(XNode.Node))] 18 | public class NodeEditor : XNodeEditor.Internal.NodeEditorBase { 19 | 20 | /// Fires every whenever a node was modified through the editor 21 | public static Action onUpdateNode; 22 | public readonly static Dictionary portPositions = new Dictionary(); 23 | 24 | #if ODIN_INSPECTOR 25 | protected internal static bool inNodeEditor = false; 26 | #endif 27 | 28 | public virtual void OnHeaderGUI() { 29 | GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); 30 | } 31 | 32 | /// Draws standard field editors for all public fields 33 | public virtual void OnBodyGUI() { 34 | #if ODIN_INSPECTOR 35 | inNodeEditor = true; 36 | #endif 37 | 38 | // Unity specifically requires this to save/update any serial object. 39 | // serializedObject.Update(); must go at the start of an inspector gui, and 40 | // serializedObject.ApplyModifiedProperties(); goes at the end. 41 | serializedObject.Update(); 42 | string[] excludes = { "m_Script", "graph", "position", "ports" }; 43 | 44 | #if ODIN_INSPECTOR 45 | try 46 | { 47 | #if ODIN_INSPECTOR_3 48 | objectTree.BeginDraw( true ); 49 | #else 50 | InspectorUtilities.BeginDrawPropertyTree(objectTree, true); 51 | #endif 52 | } 53 | catch ( ArgumentNullException ) 54 | { 55 | #if ODIN_INSPECTOR_3 56 | objectTree.EndDraw(); 57 | #else 58 | InspectorUtilities.EndDrawPropertyTree(objectTree); 59 | #endif 60 | NodeEditor.DestroyEditor(this.target); 61 | return; 62 | } 63 | 64 | GUIHelper.PushLabelWidth( 84 ); 65 | objectTree.Draw( true ); 66 | #if ODIN_INSPECTOR_3 67 | objectTree.EndDraw(); 68 | #else 69 | InspectorUtilities.EndDrawPropertyTree(objectTree); 70 | #endif 71 | GUIHelper.PopLabelWidth(); 72 | #else 73 | 74 | // Iterate through serialized properties and draw them like the Inspector (But with ports) 75 | SerializedProperty iterator = serializedObject.GetIterator(); 76 | bool enterChildren = true; 77 | while (iterator.NextVisible(enterChildren)) { 78 | enterChildren = false; 79 | if (excludes.Contains(iterator.name)) continue; 80 | NodeEditorGUILayout.PropertyField(iterator, true); 81 | } 82 | #endif 83 | 84 | // Iterate through dynamic ports and draw them in the order in which they are serialized 85 | foreach (XNode.NodePort dynamicPort in target.DynamicPorts) { 86 | if (NodeEditorGUILayout.IsDynamicPortListPort(dynamicPort)) continue; 87 | NodeEditorGUILayout.PortField(dynamicPort); 88 | } 89 | 90 | serializedObject.ApplyModifiedProperties(); 91 | 92 | #if ODIN_INSPECTOR 93 | // Call repaint so that the graph window elements respond properly to layout changes coming from Odin 94 | if (GUIHelper.RepaintRequested) { 95 | GUIHelper.ClearRepaintRequest(); 96 | window.Repaint(); 97 | } 98 | #endif 99 | 100 | #if ODIN_INSPECTOR 101 | inNodeEditor = false; 102 | #endif 103 | } 104 | 105 | public virtual int GetWidth() { 106 | Type type = target.GetType(); 107 | int width; 108 | if (type.TryGetAttributeWidth(out width)) return width; 109 | else return 208; 110 | } 111 | 112 | /// Returns color for target node 113 | public virtual Color GetTint() { 114 | // Try get color from [NodeTint] attribute 115 | Type type = target.GetType(); 116 | Color color; 117 | if (type.TryGetAttributeTint(out color)) return color; 118 | // Return default color (grey) 119 | else return NodeEditorPreferences.GetSettings().tintColor; 120 | } 121 | 122 | public virtual GUIStyle GetBodyStyle() { 123 | return NodeEditorResources.styles.nodeBody; 124 | } 125 | 126 | public virtual GUIStyle GetBodyHighlightStyle() { 127 | return NodeEditorResources.styles.nodeHighlight; 128 | } 129 | 130 | /// Override to display custom node header tooltips 131 | public virtual string GetHeaderTooltip() { 132 | return null; 133 | } 134 | 135 | /// Add items for the context menu when right-clicking this node. Override to add custom menu items. 136 | public virtual void AddContextMenuItems(GenericMenu menu) { 137 | bool canRemove = true; 138 | // Actions if only one node is selected 139 | if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { 140 | XNode.Node node = Selection.activeObject as XNode.Node; 141 | menu.AddItem(new GUIContent("Move To Top"), false, () => NodeEditorWindow.current.MoveNodeToTop(node)); 142 | menu.AddItem(new GUIContent("Rename"), false, NodeEditorWindow.current.RenameSelectedNode); 143 | 144 | canRemove = NodeGraphEditor.GetEditor(node.graph, NodeEditorWindow.current).CanRemove(node); 145 | } 146 | 147 | // Add actions to any number of selected nodes 148 | menu.AddItem(new GUIContent("Copy"), false, NodeEditorWindow.current.CopySelectedNodes); 149 | menu.AddItem(new GUIContent("Duplicate"), false, NodeEditorWindow.current.DuplicateSelectedNodes); 150 | 151 | if (canRemove) menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes); 152 | else menu.AddItem(new GUIContent("Remove"), false, null); 153 | 154 | // Custom sctions if only one node is selected 155 | if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { 156 | XNode.Node node = Selection.activeObject as XNode.Node; 157 | menu.AddCustomContextMenuItems(node); 158 | } 159 | } 160 | 161 | /// Rename the node asset. This will trigger a reimport of the node. 162 | public void Rename(string newName) { 163 | if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType()); 164 | target.name = newName; 165 | OnRename(); 166 | AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); 167 | } 168 | 169 | /// Called after this node's name has changed. 170 | public virtual void OnRename() { } 171 | 172 | [AttributeUsage(AttributeTargets.Class)] 173 | public class CustomNodeEditorAttribute : Attribute, 174 | XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib { 175 | private Type inspectedType; 176 | /// Tells a NodeEditor which Node type it is an editor for 177 | /// Type that this editor can edit 178 | public CustomNodeEditorAttribute(Type inspectedType) { 179 | this.inspectedType = inspectedType; 180 | } 181 | 182 | public Type GetInspectedType() { 183 | return inspectedType; 184 | } 185 | } 186 | } 187 | } -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 712c3fc5d9eeb4c45b1e23918df6018f 3 | timeCreated: 1505462176 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorAction.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: aa7d4286bf0ad2e4086252f2893d2cf5 3 | timeCreated: 1505426655 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorAssetModProcessor.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | using System.IO; 4 | 5 | namespace XNodeEditor { 6 | /// Deals with modified assets 7 | class NodeEditorAssetModProcessor : UnityEditor.AssetModificationProcessor { 8 | 9 | /// Automatically delete Node sub-assets before deleting their script. 10 | /// This is important to do, because you can't delete null sub assets. 11 | /// For another workaround, see: https://gitlab.com/RotaryHeart-UnityShare/subassetmissingscriptdelete 12 | private static AssetDeleteResult OnWillDeleteAsset (string path, RemoveAssetOptions options) { 13 | // Skip processing anything without the .cs extension 14 | if (Path.GetExtension(path) != ".cs") return AssetDeleteResult.DidNotDelete; 15 | 16 | // Get the object that is requested for deletion 17 | UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath (path); 18 | 19 | // If we aren't deleting a script, return 20 | if (!(obj is UnityEditor.MonoScript)) return AssetDeleteResult.DidNotDelete; 21 | 22 | // Check script type. Return if deleting a non-node script 23 | UnityEditor.MonoScript script = obj as UnityEditor.MonoScript; 24 | System.Type scriptType = script.GetClass (); 25 | if (scriptType == null || (scriptType != typeof (XNode.Node) && !scriptType.IsSubclassOf (typeof (XNode.Node)))) return AssetDeleteResult.DidNotDelete; 26 | 27 | // Find all ScriptableObjects using this script 28 | string[] guids = AssetDatabase.FindAssets ("t:" + scriptType); 29 | for (int i = 0; i < guids.Length; i++) { 30 | string assetpath = AssetDatabase.GUIDToAssetPath (guids[i]); 31 | Object[] objs = AssetDatabase.LoadAllAssetRepresentationsAtPath (assetpath); 32 | for (int k = 0; k < objs.Length; k++) { 33 | XNode.Node node = objs[k] as XNode.Node; 34 | if (node.GetType () == scriptType) { 35 | if (node != null && node.graph != null) { 36 | // Delete the node and notify the user 37 | Debug.LogWarning (node.name + " of " + node.graph + " depended on deleted script and has been removed automatically.", node.graph); 38 | node.graph.RemoveNode (node); 39 | } 40 | } 41 | } 42 | } 43 | // We didn't actually delete the script. Tell the internal system to carry on with normal deletion procedure 44 | return AssetDeleteResult.DidNotDelete; 45 | } 46 | 47 | /// Automatically re-add loose node assets to the Graph node list 48 | [InitializeOnLoadMethod] 49 | private static void OnReloadEditor () { 50 | // Find all NodeGraph assets 51 | string[] guids = AssetDatabase.FindAssets ("t:" + typeof (XNode.NodeGraph)); 52 | for (int i = 0; i < guids.Length; i++) { 53 | string assetpath = AssetDatabase.GUIDToAssetPath (guids[i]); 54 | XNode.NodeGraph graph = AssetDatabase.LoadAssetAtPath (assetpath, typeof (XNode.NodeGraph)) as XNode.NodeGraph; 55 | graph.nodes.RemoveAll(x => x == null); //Remove null items 56 | Object[] objs = AssetDatabase.LoadAllAssetRepresentationsAtPath (assetpath); 57 | // Ensure that all sub node assets are present in the graph node list 58 | for (int u = 0; u < objs.Length; u++) { 59 | // Ignore null sub assets 60 | if (objs[u] == null) continue; 61 | if (!graph.nodes.Contains (objs[u] as XNode.Node)) graph.nodes.Add(objs[u] as XNode.Node); 62 | } 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorAssetModProcessor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e515e86efe8160243a68b7c06d730c9c 3 | timeCreated: 1507982232 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using UnityEditor; 6 | using UnityEngine; 7 | #if ODIN_INSPECTOR 8 | using Sirenix.OdinInspector.Editor; 9 | #endif 10 | 11 | namespace XNodeEditor.Internal { 12 | /// Handles caching of custom editor classes and their target types. Accessible with GetEditor(Type type) 13 | /// Editor Type. Should be the type of the deriving script itself (eg. NodeEditor) 14 | /// Attribute Type. The attribute used to connect with the runtime type (eg. CustomNodeEditorAttribute) 15 | /// Runtime Type. The ScriptableObject this can be an editor for (eg. Node) 16 | public abstract class NodeEditorBase where A : Attribute, NodeEditorBase.INodeEditorAttrib where T : NodeEditorBase where K : ScriptableObject { 17 | /// Custom editors defined with [CustomNodeEditor] 18 | private static Dictionary editorTypes; 19 | private static Dictionary editors = new Dictionary(); 20 | public NodeEditorWindow window; 21 | public K target; 22 | public SerializedObject serializedObject; 23 | #if ODIN_INSPECTOR 24 | private PropertyTree _objectTree; 25 | public PropertyTree objectTree { 26 | get { 27 | if (this._objectTree == null){ 28 | try { 29 | bool wasInEditor = NodeEditor.inNodeEditor; 30 | NodeEditor.inNodeEditor = true; 31 | this._objectTree = PropertyTree.Create(this.serializedObject); 32 | NodeEditor.inNodeEditor = wasInEditor; 33 | } catch (ArgumentException ex) { 34 | Debug.Log(ex); 35 | } 36 | } 37 | return this._objectTree; 38 | } 39 | } 40 | #endif 41 | 42 | public static T GetEditor(K target, NodeEditorWindow window) { 43 | if (target == null) return null; 44 | T editor; 45 | if (!editors.TryGetValue(target, out editor)) { 46 | Type type = target.GetType(); 47 | Type editorType = GetEditorType(type); 48 | editor = Activator.CreateInstance(editorType) as T; 49 | editor.target = target; 50 | editor.serializedObject = new SerializedObject(target); 51 | editor.window = window; 52 | editor.OnCreate(); 53 | editors.Add(target, editor); 54 | } 55 | if (editor.target == null) editor.target = target; 56 | if (editor.window != window) editor.window = window; 57 | if (editor.serializedObject == null) editor.serializedObject = new SerializedObject(target); 58 | return editor; 59 | } 60 | 61 | public static void DestroyEditor( K target ) 62 | { 63 | if ( target == null ) return; 64 | T editor; 65 | if ( editors.TryGetValue( target, out editor ) ) 66 | { 67 | editors.Remove( target ); 68 | } 69 | } 70 | 71 | private static Type GetEditorType(Type type) { 72 | if (type == null) return null; 73 | if (editorTypes == null) CacheCustomEditors(); 74 | Type result; 75 | if (editorTypes.TryGetValue(type, out result)) return result; 76 | //If type isn't found, try base type 77 | return GetEditorType(type.BaseType); 78 | } 79 | 80 | private static void CacheCustomEditors() { 81 | editorTypes = new Dictionary(); 82 | 83 | //Get all classes deriving from NodeEditor via reflection 84 | Type[] nodeEditors = typeof(T).GetDerivedTypes(); 85 | for (int i = 0; i < nodeEditors.Length; i++) { 86 | if (nodeEditors[i].IsAbstract) continue; 87 | var attribs = nodeEditors[i].GetCustomAttributes(typeof(A), false); 88 | if (attribs == null || attribs.Length == 0) continue; 89 | A attrib = attribs[0] as A; 90 | editorTypes.Add(attrib.GetInspectedType(), nodeEditors[i]); 91 | } 92 | } 93 | 94 | /// Called on creation, after references have been set 95 | public virtual void OnCreate() { } 96 | 97 | public interface INodeEditorAttrib { 98 | Type GetInspectedType(); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorBase.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e85122ded59aceb4eb4b1bd9d9202642 3 | timeCreated: 1511353946 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorGUI.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 756276bfe9a0c2f4da3930ba1964f58d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorGUILayout.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1d6c2d118d1c77948a23f2f4a34d1f64 3 | timeCreated: 1507966608 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorPreferences.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using UnityEngine.Serialization; 6 | 7 | namespace XNodeEditor { 8 | public enum NoodlePath { Curvy, Straight, Angled, ShaderLab } 9 | public enum NoodleStroke { Full, Dashed } 10 | 11 | public static class NodeEditorPreferences { 12 | 13 | /// The last editor we checked. This should be the one we modify 14 | private static XNodeEditor.NodeGraphEditor lastEditor; 15 | /// The last key we checked. This should be the one we modify 16 | private static string lastKey = "xNode.Settings"; 17 | 18 | private static Dictionary typeColors = new Dictionary(); 19 | private static Dictionary settings = new Dictionary(); 20 | 21 | [System.Serializable] 22 | public class Settings : ISerializationCallbackReceiver { 23 | [SerializeField] private Color32 _gridLineColor = new Color(.23f, .23f, .23f); 24 | public Color32 gridLineColor { get { return _gridLineColor; } set { _gridLineColor = value; _gridTexture = null; _crossTexture = null; } } 25 | 26 | [SerializeField] private Color32 _gridBgColor = new Color(.19f, .19f, .19f); 27 | public Color32 gridBgColor { get { return _gridBgColor; } set { _gridBgColor = value; _gridTexture = null; } } 28 | 29 | [Obsolete("Use maxZoom instead")] 30 | public float zoomOutLimit { get { return maxZoom; } set { maxZoom = value; } } 31 | 32 | [UnityEngine.Serialization.FormerlySerializedAs("zoomOutLimit")] 33 | public float maxZoom = 5f; 34 | public float minZoom = 1f; 35 | public Color32 tintColor = new Color32(90, 97, 105, 255); 36 | public Color32 highlightColor = new Color32(255, 255, 255, 255); 37 | public bool gridSnap = true; 38 | public bool autoSave = true; 39 | public bool openOnCreate = true; 40 | public bool dragToCreate = true; 41 | public bool createFilter = true; 42 | public bool zoomToMouse = true; 43 | public bool portTooltips = true; 44 | [SerializeField] private string typeColorsData = ""; 45 | [NonSerialized] public Dictionary typeColors = new Dictionary(); 46 | [FormerlySerializedAs("noodleType")] public NoodlePath noodlePath = NoodlePath.Curvy; 47 | public float noodleThickness = 2f; 48 | 49 | public NoodleStroke noodleStroke = NoodleStroke.Full; 50 | 51 | private Texture2D _gridTexture; 52 | public Texture2D gridTexture { 53 | get { 54 | if (_gridTexture == null) _gridTexture = NodeEditorResources.GenerateGridTexture(gridLineColor, gridBgColor); 55 | return _gridTexture; 56 | } 57 | } 58 | private Texture2D _crossTexture; 59 | public Texture2D crossTexture { 60 | get { 61 | if (_crossTexture == null) _crossTexture = NodeEditorResources.GenerateCrossTexture(gridLineColor); 62 | return _crossTexture; 63 | } 64 | } 65 | 66 | public void OnAfterDeserialize() { 67 | // Deserialize typeColorsData 68 | typeColors = new Dictionary(); 69 | string[] data = typeColorsData.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 70 | for (int i = 0; i < data.Length; i += 2) { 71 | Color col; 72 | if (ColorUtility.TryParseHtmlString("#" + data[i + 1], out col)) { 73 | typeColors.Add(data[i], col); 74 | } 75 | } 76 | } 77 | 78 | public void OnBeforeSerialize() { 79 | // Serialize typeColors 80 | typeColorsData = ""; 81 | foreach (var item in typeColors) { 82 | typeColorsData += item.Key + "," + ColorUtility.ToHtmlStringRGB(item.Value) + ","; 83 | } 84 | } 85 | } 86 | 87 | /// Get settings of current active editor 88 | public static Settings GetSettings() { 89 | if (XNodeEditor.NodeEditorWindow.current == null) return new Settings(); 90 | 91 | if (lastEditor != XNodeEditor.NodeEditorWindow.current.graphEditor) { 92 | object[] attribs = XNodeEditor.NodeEditorWindow.current.graphEditor.GetType().GetCustomAttributes(typeof(XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute), true); 93 | if (attribs.Length == 1) { 94 | XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute attrib = attribs[0] as XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute; 95 | lastEditor = XNodeEditor.NodeEditorWindow.current.graphEditor; 96 | lastKey = attrib.editorPrefsKey; 97 | } else return null; 98 | } 99 | if (!settings.ContainsKey(lastKey)) VerifyLoaded(); 100 | return settings[lastKey]; 101 | } 102 | 103 | #if UNITY_2019_1_OR_NEWER 104 | [SettingsProvider] 105 | public static SettingsProvider CreateXNodeSettingsProvider() { 106 | SettingsProvider provider = new SettingsProvider("Preferences/Node Editor", SettingsScope.User) { 107 | guiHandler = (searchContext) => { XNodeEditor.NodeEditorPreferences.PreferencesGUI(); }, 108 | keywords = new HashSet(new [] { "xNode", "node", "editor", "graph", "connections", "noodles", "ports" }) 109 | }; 110 | return provider; 111 | } 112 | #endif 113 | 114 | #if !UNITY_2019_1_OR_NEWER 115 | [PreferenceItem("Node Editor")] 116 | #endif 117 | private static void PreferencesGUI() { 118 | VerifyLoaded(); 119 | Settings settings = NodeEditorPreferences.settings[lastKey]; 120 | 121 | if (GUILayout.Button(new GUIContent("Documentation", "https://github.com/Siccity/xNode/wiki"), GUILayout.Width(100))) Application.OpenURL("https://github.com/Siccity/xNode/wiki"); 122 | EditorGUILayout.Space(); 123 | 124 | NodeSettingsGUI(lastKey, settings); 125 | GridSettingsGUI(lastKey, settings); 126 | SystemSettingsGUI(lastKey, settings); 127 | TypeColorsGUI(lastKey, settings); 128 | if (GUILayout.Button(new GUIContent("Set Default", "Reset all values to default"), GUILayout.Width(120))) { 129 | ResetPrefs(); 130 | } 131 | } 132 | 133 | private static void GridSettingsGUI(string key, Settings settings) { 134 | //Label 135 | EditorGUILayout.LabelField("Grid", EditorStyles.boldLabel); 136 | settings.gridSnap = EditorGUILayout.Toggle(new GUIContent("Snap", "Hold CTRL in editor to invert"), settings.gridSnap); 137 | settings.zoomToMouse = EditorGUILayout.Toggle(new GUIContent("Zoom to Mouse", "Zooms towards mouse position"), settings.zoomToMouse); 138 | EditorGUILayout.LabelField("Zoom"); 139 | EditorGUI.indentLevel++; 140 | settings.maxZoom = EditorGUILayout.FloatField(new GUIContent("Max", "Upper limit to zoom"), settings.maxZoom); 141 | settings.minZoom = EditorGUILayout.FloatField(new GUIContent("Min", "Lower limit to zoom"), settings.minZoom); 142 | EditorGUI.indentLevel--; 143 | settings.gridLineColor = EditorGUILayout.ColorField("Color", settings.gridLineColor); 144 | settings.gridBgColor = EditorGUILayout.ColorField(" ", settings.gridBgColor); 145 | if (GUI.changed) { 146 | SavePrefs(key, settings); 147 | 148 | NodeEditorWindow.RepaintAll(); 149 | } 150 | EditorGUILayout.Space(); 151 | } 152 | 153 | private static void SystemSettingsGUI(string key, Settings settings) { 154 | //Label 155 | EditorGUILayout.LabelField("System", EditorStyles.boldLabel); 156 | settings.autoSave = EditorGUILayout.Toggle(new GUIContent("Autosave", "Disable for better editor performance"), settings.autoSave); 157 | settings.openOnCreate = EditorGUILayout.Toggle(new GUIContent("Open Editor on Create", "Disable to prevent openening the editor when creating a new graph"), settings.openOnCreate); 158 | if (GUI.changed) SavePrefs(key, settings); 159 | EditorGUILayout.Space(); 160 | } 161 | 162 | private static void NodeSettingsGUI(string key, Settings settings) { 163 | //Label 164 | EditorGUILayout.LabelField("Node", EditorStyles.boldLabel); 165 | settings.tintColor = EditorGUILayout.ColorField("Tint", settings.tintColor); 166 | settings.highlightColor = EditorGUILayout.ColorField("Selection", settings.highlightColor); 167 | settings.noodlePath = (NoodlePath) EditorGUILayout.EnumPopup("Noodle path", (Enum) settings.noodlePath); 168 | settings.noodleThickness = EditorGUILayout.FloatField(new GUIContent("Noodle thickness", "Noodle Thickness of the node connections"), settings.noodleThickness); 169 | settings.noodleStroke = (NoodleStroke) EditorGUILayout.EnumPopup("Noodle stroke", (Enum) settings.noodleStroke); 170 | settings.portTooltips = EditorGUILayout.Toggle("Port Tooltips", settings.portTooltips); 171 | settings.dragToCreate = EditorGUILayout.Toggle(new GUIContent("Drag to Create", "Drag a port connection anywhere on the grid to create and connect a node"), settings.dragToCreate); 172 | settings.createFilter = EditorGUILayout.Toggle(new GUIContent("Create Filter", "Only show nodes that are compatible with the selected port"), settings.createFilter); 173 | 174 | //END 175 | if (GUI.changed) { 176 | SavePrefs(key, settings); 177 | NodeEditorWindow.RepaintAll(); 178 | } 179 | EditorGUILayout.Space(); 180 | } 181 | 182 | private static void TypeColorsGUI(string key, Settings settings) { 183 | //Label 184 | EditorGUILayout.LabelField("Types", EditorStyles.boldLabel); 185 | 186 | //Clone keys so we can enumerate the dictionary and make changes. 187 | var typeColorKeys = new List(typeColors.Keys); 188 | 189 | //Display type colors. Save them if they are edited by the user 190 | foreach (var type in typeColorKeys) { 191 | string typeColorKey = NodeEditorUtilities.PrettyName(type); 192 | Color col = typeColors[type]; 193 | EditorGUI.BeginChangeCheck(); 194 | EditorGUILayout.BeginHorizontal(); 195 | col = EditorGUILayout.ColorField(typeColorKey, col); 196 | EditorGUILayout.EndHorizontal(); 197 | if (EditorGUI.EndChangeCheck()) { 198 | typeColors[type] = col; 199 | if (settings.typeColors.ContainsKey(typeColorKey)) settings.typeColors[typeColorKey] = col; 200 | else settings.typeColors.Add(typeColorKey, col); 201 | SavePrefs(key, settings); 202 | NodeEditorWindow.RepaintAll(); 203 | } 204 | } 205 | } 206 | 207 | /// Load prefs if they exist. Create if they don't 208 | private static Settings LoadPrefs() { 209 | // Create settings if it doesn't exist 210 | if (!EditorPrefs.HasKey(lastKey)) { 211 | if (lastEditor != null) EditorPrefs.SetString(lastKey, JsonUtility.ToJson(lastEditor.GetDefaultPreferences())); 212 | else EditorPrefs.SetString(lastKey, JsonUtility.ToJson(new Settings())); 213 | } 214 | return JsonUtility.FromJson(EditorPrefs.GetString(lastKey)); 215 | } 216 | 217 | /// Delete all prefs 218 | public static void ResetPrefs() { 219 | if (EditorPrefs.HasKey(lastKey)) EditorPrefs.DeleteKey(lastKey); 220 | if (settings.ContainsKey(lastKey)) settings.Remove(lastKey); 221 | typeColors = new Dictionary(); 222 | VerifyLoaded(); 223 | NodeEditorWindow.RepaintAll(); 224 | } 225 | 226 | /// Save preferences in EditorPrefs 227 | private static void SavePrefs(string key, Settings settings) { 228 | EditorPrefs.SetString(key, JsonUtility.ToJson(settings)); 229 | } 230 | 231 | /// Check if we have loaded settings for given key. If not, load them 232 | private static void VerifyLoaded() { 233 | if (!settings.ContainsKey(lastKey)) settings.Add(lastKey, LoadPrefs()); 234 | } 235 | 236 | /// Return color based on type 237 | public static Color GetTypeColor(System.Type type) { 238 | VerifyLoaded(); 239 | if (type == null) return Color.gray; 240 | Color col; 241 | if (!typeColors.TryGetValue(type, out col)) { 242 | string typeName = type.PrettyName(); 243 | if (settings[lastKey].typeColors.ContainsKey(typeName)) typeColors.Add(type, settings[lastKey].typeColors[typeName]); 244 | else { 245 | #if UNITY_5_4_OR_NEWER 246 | UnityEngine.Random.State oldState = UnityEngine.Random.state; 247 | UnityEngine.Random.InitState(typeName.GetHashCode()); 248 | #else 249 | int oldSeed = UnityEngine.Random.seed; 250 | UnityEngine.Random.seed = typeName.GetHashCode(); 251 | #endif 252 | col = new Color(UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value); 253 | typeColors.Add(type, col); 254 | #if UNITY_5_4_OR_NEWER 255 | UnityEngine.Random.state = oldState; 256 | #else 257 | UnityEngine.Random.seed = oldSeed; 258 | #endif 259 | } 260 | } 261 | return col; 262 | } 263 | } 264 | } -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorPreferences.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6b1f47e387a6f714c9f2ff82a6888c85 3 | timeCreated: 1507920216 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorReflection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using UnityEditor; 7 | using UnityEngine; 8 | #if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU 9 | using GenericMenu = XNodeEditor.AdvancedGenericMenu; 10 | #endif 11 | 12 | namespace XNodeEditor { 13 | /// Contains reflection-related extensions built for xNode 14 | public static class NodeEditorReflection { 15 | [NonSerialized] private static Dictionary nodeTint; 16 | [NonSerialized] private static Dictionary nodeWidth; 17 | /// All available node types 18 | public static Type[] nodeTypes { get { return _nodeTypes != null ? _nodeTypes : _nodeTypes = GetNodeTypes(); } } 19 | 20 | [NonSerialized] private static Type[] _nodeTypes = null; 21 | 22 | /// Return a delegate used to determine whether window is docked or not. It is faster to cache this delegate than run the reflection required each time. 23 | public static Func GetIsDockedDelegate(this EditorWindow window) { 24 | BindingFlags fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; 25 | MethodInfo isDockedMethod = typeof(EditorWindow).GetProperty("docked", fullBinding).GetGetMethod(true); 26 | return (Func) Delegate.CreateDelegate(typeof(Func), window, isDockedMethod); 27 | } 28 | 29 | public static Type[] GetNodeTypes() { 30 | //Get all classes deriving from Node via reflection 31 | return GetDerivedTypes(typeof(XNode.Node)); 32 | } 33 | 34 | /// Custom node tint colors defined with [NodeColor(r, g, b)] 35 | public static bool TryGetAttributeTint(this Type nodeType, out Color tint) { 36 | if (nodeTint == null) { 37 | CacheAttributes(ref nodeTint, x => x.color); 38 | } 39 | return nodeTint.TryGetValue(nodeType, out tint); 40 | } 41 | 42 | /// Get custom node widths defined with [NodeWidth(width)] 43 | public static bool TryGetAttributeWidth(this Type nodeType, out int width) { 44 | if (nodeWidth == null) { 45 | CacheAttributes(ref nodeWidth, x => x.width); 46 | } 47 | return nodeWidth.TryGetValue(nodeType, out width); 48 | } 49 | 50 | private static void CacheAttributes(ref Dictionary dict, Func getter) where A : Attribute { 51 | dict = new Dictionary(); 52 | for (int i = 0; i < nodeTypes.Length; i++) { 53 | object[] attribs = nodeTypes[i].GetCustomAttributes(typeof(A), true); 54 | if (attribs == null || attribs.Length == 0) continue; 55 | A attrib = attribs[0] as A; 56 | dict.Add(nodeTypes[i], getter(attrib)); 57 | } 58 | } 59 | 60 | /// Get FieldInfo of a field, including those that are private and/or inherited 61 | public static FieldInfo GetFieldInfo(this Type type, string fieldName) { 62 | // If we can't find field in the first run, it's probably a private field in a base class. 63 | FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 64 | // Search base classes for private fields only. Public fields are found above 65 | while (field == null && (type = type.BaseType) != typeof(XNode.Node)) field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); 66 | return field; 67 | } 68 | 69 | /// Get all classes deriving from baseType via reflection 70 | public static Type[] GetDerivedTypes(this Type baseType) { 71 | List types = new List(); 72 | System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); 73 | foreach (Assembly assembly in assemblies) { 74 | try { 75 | types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); 76 | } catch (ReflectionTypeLoadException) { } 77 | } 78 | return types.ToArray(); 79 | } 80 | 81 | /// Find methods marked with the [ContextMenu] attribute and add them to the context menu 82 | public static void AddCustomContextMenuItems(this GenericMenu contextMenu, object obj) { 83 | KeyValuePair[] items = GetContextMenuMethods(obj); 84 | if (items.Length != 0) { 85 | contextMenu.AddSeparator(""); 86 | List invalidatedEntries = new List(); 87 | foreach (KeyValuePair checkValidate in items) { 88 | if (checkValidate.Key.validate && !(bool) checkValidate.Value.Invoke(obj, null)) { 89 | invalidatedEntries.Add(checkValidate.Key.menuItem); 90 | } 91 | } 92 | for (int i = 0; i < items.Length; i++) { 93 | KeyValuePair kvp = items[i]; 94 | if (invalidatedEntries.Contains(kvp.Key.menuItem)) { 95 | contextMenu.AddDisabledItem(new GUIContent(kvp.Key.menuItem)); 96 | } else { 97 | contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null)); 98 | } 99 | } 100 | } 101 | } 102 | 103 | /// Call OnValidate on target 104 | public static void TriggerOnValidate(this UnityEngine.Object target) { 105 | System.Reflection.MethodInfo onValidate = null; 106 | if (target != null) { 107 | onValidate = target.GetType().GetMethod("OnValidate", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 108 | if (onValidate != null) onValidate.Invoke(target, null); 109 | } 110 | } 111 | 112 | public static KeyValuePair[] GetContextMenuMethods(object obj) { 113 | Type type = obj.GetType(); 114 | MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); 115 | List> kvp = new List>(); 116 | for (int i = 0; i < methods.Length; i++) { 117 | ContextMenu[] attribs = methods[i].GetCustomAttributes(typeof(ContextMenu), true).Select(x => x as ContextMenu).ToArray(); 118 | if (attribs == null || attribs.Length == 0) continue; 119 | if (methods[i].GetParameters().Length != 0) { 120 | Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " has parameters and cannot be used for context menu commands."); 121 | continue; 122 | } 123 | if (methods[i].IsStatic) { 124 | Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " is static and cannot be used for context menu commands."); 125 | continue; 126 | } 127 | 128 | for (int k = 0; k < attribs.Length; k++) { 129 | kvp.Add(new KeyValuePair(attribs[k], methods[i])); 130 | } 131 | } 132 | #if UNITY_5_5_OR_NEWER 133 | //Sort menu items 134 | kvp.Sort((x, y) => x.Key.priority.CompareTo(y.Key.priority)); 135 | #endif 136 | return kvp.ToArray(); 137 | } 138 | 139 | /// Very crude. Uses a lot of reflection. 140 | public static void OpenPreferences() { 141 | try { 142 | #if UNITY_2018_3_OR_NEWER 143 | SettingsService.OpenUserPreferences("Preferences/Node Editor"); 144 | #else 145 | //Open preferences window 146 | Assembly assembly = Assembly.GetAssembly(typeof(UnityEditor.EditorWindow)); 147 | Type type = assembly.GetType("UnityEditor.PreferencesWindow"); 148 | type.GetMethod("ShowPreferencesWindow", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, null); 149 | 150 | //Get the window 151 | EditorWindow window = EditorWindow.GetWindow(type); 152 | 153 | //Make sure custom sections are added (because waiting for it to happen automatically is too slow) 154 | FieldInfo refreshField = type.GetField("m_RefreshCustomPreferences", BindingFlags.NonPublic | BindingFlags.Instance); 155 | if ((bool) refreshField.GetValue(window)) { 156 | type.GetMethod("AddCustomSections", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(window, null); 157 | refreshField.SetValue(window, false); 158 | } 159 | 160 | //Get sections 161 | FieldInfo sectionsField = type.GetField("m_Sections", BindingFlags.Instance | BindingFlags.NonPublic); 162 | IList sections = sectionsField.GetValue(window) as IList; 163 | 164 | //Iterate through sections and check contents 165 | Type sectionType = sectionsField.FieldType.GetGenericArguments() [0]; 166 | FieldInfo sectionContentField = sectionType.GetField("content", BindingFlags.Instance | BindingFlags.Public); 167 | for (int i = 0; i < sections.Count; i++) { 168 | GUIContent sectionContent = sectionContentField.GetValue(sections[i]) as GUIContent; 169 | if (sectionContent.text == "Node Editor") { 170 | //Found contents - Set index 171 | FieldInfo sectionIndexField = type.GetField("m_SelectedSectionIndex", BindingFlags.Instance | BindingFlags.NonPublic); 172 | sectionIndexField.SetValue(window, i); 173 | return; 174 | } 175 | } 176 | #endif 177 | } catch (Exception e) { 178 | Debug.LogError(e); 179 | Debug.LogWarning("Unity has changed around internally. Can't open properties through reflection. Please contact xNode developer and supply unity version number."); 180 | } 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorReflection.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c78a0fa4a13abcd408ebe73006b7b1bb 3 | timeCreated: 1505419458 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorResources.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace XNodeEditor { 5 | public static class NodeEditorResources { 6 | // Textures 7 | public static Texture2D dot { get { return _dot != null ? _dot : _dot = Resources.Load("xnode_dot"); } } 8 | private static Texture2D _dot; 9 | public static Texture2D dotOuter { get { return _dotOuter != null ? _dotOuter : _dotOuter = Resources.Load("xnode_dot_outer"); } } 10 | private static Texture2D _dotOuter; 11 | public static Texture2D nodeBody { get { return _nodeBody != null ? _nodeBody : _nodeBody = Resources.Load("xnode_node"); } } 12 | private static Texture2D _nodeBody; 13 | public static Texture2D nodeHighlight { get { return _nodeHighlight != null ? _nodeHighlight : _nodeHighlight = Resources.Load("xnode_node_highlight"); } } 14 | private static Texture2D _nodeHighlight; 15 | 16 | // Styles 17 | public static Styles styles { get { return _styles != null ? _styles : _styles = new Styles(); } } 18 | public static Styles _styles = null; 19 | public static GUIStyle OutputPort { get { return new GUIStyle(EditorStyles.label) { alignment = TextAnchor.UpperRight }; } } 20 | public class Styles { 21 | public GUIStyle inputPort, outputPort, nodeHeader, nodeBody, tooltip, nodeHighlight; 22 | 23 | public Styles() { 24 | GUIStyle baseStyle = new GUIStyle("Label"); 25 | baseStyle.fixedHeight = 18; 26 | 27 | inputPort = new GUIStyle(baseStyle); 28 | inputPort.alignment = TextAnchor.UpperLeft; 29 | inputPort.padding.left = 0; 30 | inputPort.active.background = dot; 31 | inputPort.normal.background = dotOuter; 32 | 33 | outputPort = new GUIStyle(baseStyle); 34 | outputPort.alignment = TextAnchor.UpperRight; 35 | outputPort.padding.right = 0; 36 | outputPort.active.background = dot; 37 | outputPort.normal.background = dotOuter; 38 | 39 | nodeHeader = new GUIStyle(); 40 | nodeHeader.alignment = TextAnchor.MiddleCenter; 41 | nodeHeader.fontStyle = FontStyle.Bold; 42 | nodeHeader.normal.textColor = Color.white; 43 | 44 | nodeBody = new GUIStyle(); 45 | nodeBody.normal.background = NodeEditorResources.nodeBody; 46 | nodeBody.border = new RectOffset(32, 32, 32, 32); 47 | nodeBody.padding = new RectOffset(16, 16, 4, 16); 48 | 49 | nodeHighlight = new GUIStyle(); 50 | nodeHighlight.normal.background = NodeEditorResources.nodeHighlight; 51 | nodeHighlight.border = new RectOffset(32, 32, 32, 32); 52 | 53 | tooltip = new GUIStyle("helpBox"); 54 | tooltip.alignment = TextAnchor.MiddleCenter; 55 | } 56 | } 57 | 58 | public static Texture2D GenerateGridTexture(Color line, Color bg) { 59 | Texture2D tex = new Texture2D(64, 64); 60 | Color[] cols = new Color[64 * 64]; 61 | for (int y = 0; y < 64; y++) { 62 | for (int x = 0; x < 64; x++) { 63 | Color col = bg; 64 | if (y % 16 == 0 || x % 16 == 0) col = Color.Lerp(line, bg, 0.65f); 65 | if (y == 63 || x == 63) col = Color.Lerp(line, bg, 0.35f); 66 | cols[(y * 64) + x] = col; 67 | } 68 | } 69 | tex.SetPixels(cols); 70 | tex.wrapMode = TextureWrapMode.Repeat; 71 | tex.filterMode = FilterMode.Bilinear; 72 | tex.name = "Grid"; 73 | tex.Apply(); 74 | return tex; 75 | } 76 | 77 | public static Texture2D GenerateCrossTexture(Color line) { 78 | Texture2D tex = new Texture2D(64, 64); 79 | Color[] cols = new Color[64 * 64]; 80 | for (int y = 0; y < 64; y++) { 81 | for (int x = 0; x < 64; x++) { 82 | Color col = line; 83 | if (y != 31 && x != 31) col.a = 0; 84 | cols[(y * 64) + x] = col; 85 | } 86 | } 87 | tex.SetPixels(cols); 88 | tex.wrapMode = TextureWrapMode.Clamp; 89 | tex.filterMode = FilterMode.Bilinear; 90 | tex.name = "Grid"; 91 | tex.Apply(); 92 | return tex; 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorResources.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 69f55d341299026489b29443c3dd13d1 3 | timeCreated: 1505418919 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using UnityEditor; 8 | using UnityEngine; 9 | using Object = UnityEngine.Object; 10 | 11 | namespace XNodeEditor { 12 | /// A set of editor-only utilities and extensions for xNode 13 | public static class NodeEditorUtilities { 14 | 15 | /// C#'s Script Icon [The one MonoBhevaiour Scripts have]. 16 | private static Texture2D scriptIcon = (EditorGUIUtility.IconContent("cs Script Icon").image as Texture2D); 17 | 18 | /// Saves Attribute from Type+Field for faster lookup. Resets on recompiles. 19 | private static Dictionary>> typeAttributes = new Dictionary>>(); 20 | 21 | /// Saves ordered PropertyAttribute from Type+Field for faster lookup. Resets on recompiles. 22 | private static Dictionary>> typeOrderedPropertyAttributes = new Dictionary>>(); 23 | 24 | public static bool GetAttrib(Type classType, out T attribOut) where T : Attribute { 25 | object[] attribs = classType.GetCustomAttributes(typeof(T), false); 26 | return GetAttrib(attribs, out attribOut); 27 | } 28 | 29 | public static bool GetAttrib(object[] attribs, out T attribOut) where T : Attribute { 30 | for (int i = 0; i < attribs.Length; i++) { 31 | if (attribs[i] is T) { 32 | attribOut = attribs[i] as T; 33 | return true; 34 | } 35 | } 36 | attribOut = null; 37 | return false; 38 | } 39 | 40 | public static bool GetAttrib(Type classType, string fieldName, out T attribOut) where T : Attribute { 41 | // If we can't find field in the first run, it's probably a private field in a base class. 42 | FieldInfo field = classType.GetFieldInfo(fieldName); 43 | // This shouldn't happen. Ever. 44 | if (field == null) { 45 | Debug.LogWarning("Field " + fieldName + " couldnt be found"); 46 | attribOut = null; 47 | return false; 48 | } 49 | object[] attribs = field.GetCustomAttributes(typeof(T), true); 50 | return GetAttrib(attribs, out attribOut); 51 | } 52 | 53 | public static bool HasAttrib(object[] attribs) where T : Attribute { 54 | for (int i = 0; i < attribs.Length; i++) { 55 | if (attribs[i].GetType() == typeof(T)) { 56 | return true; 57 | } 58 | } 59 | return false; 60 | } 61 | 62 | public static bool GetCachedAttrib(Type classType, string fieldName, out T attribOut) where T : Attribute { 63 | Dictionary> typeFields; 64 | if (!typeAttributes.TryGetValue(classType, out typeFields)) { 65 | typeFields = new Dictionary>(); 66 | typeAttributes.Add(classType, typeFields); 67 | } 68 | 69 | Dictionary typeTypes; 70 | if (!typeFields.TryGetValue(fieldName, out typeTypes)) { 71 | typeTypes = new Dictionary(); 72 | typeFields.Add(fieldName, typeTypes); 73 | } 74 | 75 | Attribute attr; 76 | if (!typeTypes.TryGetValue(typeof(T), out attr)) { 77 | if (GetAttrib(classType, fieldName, out attribOut)) { 78 | typeTypes.Add(typeof(T), attribOut); 79 | return true; 80 | } else typeTypes.Add(typeof(T), null); 81 | } 82 | 83 | if (attr == null) { 84 | attribOut = null; 85 | return false; 86 | } 87 | 88 | attribOut = attr as T; 89 | return true; 90 | } 91 | 92 | public static List GetCachedPropertyAttribs(Type classType, string fieldName) { 93 | Dictionary> typeFields; 94 | if (!typeOrderedPropertyAttributes.TryGetValue(classType, out typeFields)) { 95 | typeFields = new Dictionary>(); 96 | typeOrderedPropertyAttributes.Add(classType, typeFields); 97 | } 98 | 99 | List typeAttributes; 100 | if (!typeFields.TryGetValue(fieldName, out typeAttributes)) { 101 | FieldInfo field = classType.GetFieldInfo(fieldName); 102 | object[] attribs = field.GetCustomAttributes(typeof(PropertyAttribute), true); 103 | typeAttributes = attribs.Cast().Reverse().ToList(); //Unity draws them in reverse 104 | typeFields.Add(fieldName, typeAttributes); 105 | } 106 | 107 | return typeAttributes; 108 | } 109 | 110 | public static bool IsMac() { 111 | #if UNITY_2017_1_OR_NEWER 112 | return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX; 113 | #else 114 | return SystemInfo.operatingSystem.StartsWith("Mac"); 115 | #endif 116 | } 117 | 118 | /// Returns true if this can be casted to 119 | public static bool IsCastableTo(this Type from, Type to) { 120 | if (to.IsAssignableFrom(from)) return true; 121 | var methods = from.GetMethods(BindingFlags.Public | BindingFlags.Static) 122 | .Where( 123 | m => m.ReturnType == to && 124 | (m.Name == "op_Implicit" || 125 | m.Name == "op_Explicit") 126 | ); 127 | return methods.Count() > 0; 128 | } 129 | 130 | /// 131 | /// Looking for ports with value Type compatible with a given type. 132 | /// 133 | /// Node to search 134 | /// Type to find compatiblities 135 | /// 136 | /// True if NodeType has some port with value type compatible 137 | public static bool HasCompatiblePortType(Type nodeType, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) { 138 | Type findType = typeof(XNode.Node.InputAttribute); 139 | if (direction == XNode.NodePort.IO.Output) 140 | findType = typeof(XNode.Node.OutputAttribute); 141 | 142 | //Get All fields from node type and we go filter only field with portAttribute. 143 | //This way is possible to know the values of the all ports and if have some with compatible value tue 144 | foreach (FieldInfo f in XNode.NodeDataCache.GetNodeFields(nodeType)) { 145 | var portAttribute = f.GetCustomAttributes(findType, false).FirstOrDefault(); 146 | if (portAttribute != null) { 147 | if (IsCastableTo(f.FieldType, compatibleType)) { 148 | return true; 149 | } 150 | } 151 | } 152 | 153 | return false; 154 | } 155 | 156 | /// 157 | /// Filter only node types that contains some port value type compatible with an given type 158 | /// 159 | /// List with all nodes type to filter 160 | /// Compatible Type to Filter 161 | /// Return Only Node Types with ports compatible, or an empty list 162 | public static List GetCompatibleNodesTypes(Type[] nodeTypes, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) { 163 | //Result List 164 | List filteredTypes = new List(); 165 | 166 | //Return empty list 167 | if (nodeTypes == null) { return filteredTypes; } 168 | if (compatibleType == null) { return filteredTypes; } 169 | 170 | //Find compatiblity 171 | foreach (Type findType in nodeTypes) { 172 | if (HasCompatiblePortType(findType, compatibleType, direction)) { 173 | filteredTypes.Add(findType); 174 | } 175 | } 176 | 177 | return filteredTypes; 178 | } 179 | 180 | 181 | /// Return a prettiefied type name. 182 | public static string PrettyName(this Type type) { 183 | if (type == null) return "null"; 184 | if (type == typeof(System.Object)) return "object"; 185 | if (type == typeof(float)) return "float"; 186 | else if (type == typeof(int)) return "int"; 187 | else if (type == typeof(long)) return "long"; 188 | else if (type == typeof(double)) return "double"; 189 | else if (type == typeof(string)) return "string"; 190 | else if (type == typeof(bool)) return "bool"; 191 | else if (type.IsGenericType) { 192 | string s = ""; 193 | Type genericType = type.GetGenericTypeDefinition(); 194 | if (genericType == typeof(List<>)) s = "List"; 195 | else s = type.GetGenericTypeDefinition().ToString(); 196 | 197 | Type[] types = type.GetGenericArguments(); 198 | string[] stypes = new string[types.Length]; 199 | for (int i = 0; i < types.Length; i++) { 200 | stypes[i] = types[i].PrettyName(); 201 | } 202 | return s + "<" + string.Join(", ", stypes) + ">"; 203 | } else if (type.IsArray) { 204 | string rank = ""; 205 | for (int i = 1; i < type.GetArrayRank(); i++) { 206 | rank += ","; 207 | } 208 | Type elementType = type.GetElementType(); 209 | if (!elementType.IsArray) return elementType.PrettyName() + "[" + rank + "]"; 210 | else { 211 | string s = elementType.PrettyName(); 212 | int i = s.IndexOf('['); 213 | return s.Substring(0, i) + "[" + rank + "]" + s.Substring(i); 214 | } 215 | } else return type.ToString(); 216 | } 217 | 218 | /// Returns the default name for the node type. 219 | public static string NodeDefaultName(Type type) { 220 | string typeName = type.Name; 221 | // Automatically remove redundant 'Node' postfix 222 | if (typeName.EndsWith("Node")) typeName = typeName.Substring(0, typeName.LastIndexOf("Node")); 223 | typeName = UnityEditor.ObjectNames.NicifyVariableName(typeName); 224 | return typeName; 225 | } 226 | 227 | /// Returns the default creation path for the node type. 228 | public static string NodeDefaultPath(Type type) { 229 | string typePath = type.ToString().Replace('.', '/'); 230 | // Automatically remove redundant 'Node' postfix 231 | if (typePath.EndsWith("Node")) typePath = typePath.Substring(0, typePath.LastIndexOf("Node")); 232 | typePath = UnityEditor.ObjectNames.NicifyVariableName(typePath); 233 | return typePath; 234 | } 235 | 236 | /// Creates a new C# Class. 237 | [MenuItem("Assets/Create/xNode/Node C# Script", false, 89)] 238 | private static void CreateNode() { 239 | string[] guids = AssetDatabase.FindAssets("xNode_NodeTemplate.cs"); 240 | if (guids.Length == 0) { 241 | Debug.LogWarning("xNode_NodeTemplate.cs.txt not found in asset database"); 242 | return; 243 | } 244 | string path = AssetDatabase.GUIDToAssetPath(guids[0]); 245 | CreateFromTemplate( 246 | "NewNode.cs", 247 | path 248 | ); 249 | } 250 | 251 | /// Creates a new C# Class. 252 | [MenuItem("Assets/Create/xNode/NodeGraph C# Script", false, 89)] 253 | private static void CreateGraph() { 254 | string[] guids = AssetDatabase.FindAssets("xNode_NodeGraphTemplate.cs"); 255 | if (guids.Length == 0) { 256 | Debug.LogWarning("xNode_NodeGraphTemplate.cs.txt not found in asset database"); 257 | return; 258 | } 259 | string path = AssetDatabase.GUIDToAssetPath(guids[0]); 260 | CreateFromTemplate( 261 | "NewNodeGraph.cs", 262 | path 263 | ); 264 | } 265 | 266 | public static void CreateFromTemplate(string initialName, string templatePath) { 267 | ProjectWindowUtil.StartNameEditingIfProjectWindowExists( 268 | 0, 269 | ScriptableObject.CreateInstance(), 270 | initialName, 271 | scriptIcon, 272 | templatePath 273 | ); 274 | } 275 | 276 | /// Inherits from EndNameAction, must override EndNameAction.Action 277 | public class DoCreateCodeFile : UnityEditor.ProjectWindowCallback.EndNameEditAction { 278 | public override void Action(int instanceId, string pathName, string resourceFile) { 279 | Object o = CreateScript(pathName, resourceFile); 280 | ProjectWindowUtil.ShowCreatedAsset(o); 281 | } 282 | } 283 | 284 | /// Creates Script from Template's path. 285 | internal static UnityEngine.Object CreateScript(string pathName, string templatePath) { 286 | string className = Path.GetFileNameWithoutExtension(pathName).Replace(" ", string.Empty); 287 | string templateText = string.Empty; 288 | 289 | UTF8Encoding encoding = new UTF8Encoding(true, false); 290 | 291 | if (File.Exists(templatePath)) { 292 | /// Read procedures. 293 | StreamReader reader = new StreamReader(templatePath); 294 | templateText = reader.ReadToEnd(); 295 | reader.Close(); 296 | 297 | templateText = templateText.Replace("#SCRIPTNAME#", className); 298 | templateText = templateText.Replace("#NOTRIM#", string.Empty); 299 | /// You can replace as many tags you make on your templates, just repeat Replace function 300 | /// e.g.: 301 | /// templateText = templateText.Replace("#NEWTAG#", "MyText"); 302 | 303 | /// Write procedures. 304 | 305 | StreamWriter writer = new StreamWriter(Path.GetFullPath(pathName), false, encoding); 306 | writer.Write(templateText); 307 | writer.Close(); 308 | 309 | AssetDatabase.ImportAsset(pathName); 310 | return AssetDatabase.LoadAssetAtPath(pathName, typeof(Object)); 311 | } else { 312 | Debug.LogError(string.Format("The template file was not found: {0}", templatePath)); 313 | return null; 314 | } 315 | } 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorUtilities.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 120960fe5b50aba418a8e8ad3c4c4bc8 3 | timeCreated: 1506073499 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorWindow.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEditor; 3 | using UnityEditor.Callbacks; 4 | using UnityEngine; 5 | using System; 6 | using Object = UnityEngine.Object; 7 | 8 | namespace XNodeEditor { 9 | [InitializeOnLoad] 10 | public partial class NodeEditorWindow : EditorWindow { 11 | public static NodeEditorWindow current; 12 | 13 | /// Stores node positions for all nodePorts. 14 | public Dictionary portConnectionPoints { get { return _portConnectionPoints; } } 15 | private Dictionary _portConnectionPoints = new Dictionary(); 16 | [SerializeField] private NodePortReference[] _references = new NodePortReference[0]; 17 | [SerializeField] private Rect[] _rects = new Rect[0]; 18 | 19 | private Func isDocked { 20 | get { 21 | if (_isDocked == null) _isDocked = this.GetIsDockedDelegate(); 22 | return _isDocked; 23 | } 24 | } 25 | private Func _isDocked; 26 | 27 | [System.Serializable] private class NodePortReference { 28 | [SerializeField] private XNode.Node _node; 29 | [SerializeField] private string _name; 30 | 31 | public NodePortReference(XNode.NodePort nodePort) { 32 | _node = nodePort.node; 33 | _name = nodePort.fieldName; 34 | } 35 | 36 | public XNode.NodePort GetNodePort() { 37 | if (_node == null) { 38 | return null; 39 | } 40 | return _node.GetPort(_name); 41 | } 42 | } 43 | 44 | private void OnDisable() { 45 | // Cache portConnectionPoints before serialization starts 46 | int count = portConnectionPoints.Count; 47 | _references = new NodePortReference[count]; 48 | _rects = new Rect[count]; 49 | int index = 0; 50 | foreach (var portConnectionPoint in portConnectionPoints) { 51 | _references[index] = new NodePortReference(portConnectionPoint.Key); 52 | _rects[index] = portConnectionPoint.Value; 53 | index++; 54 | } 55 | } 56 | 57 | private void OnEnable() { 58 | // Reload portConnectionPoints if there are any 59 | int length = _references.Length; 60 | if (length == _rects.Length) { 61 | for (int i = 0; i < length; i++) { 62 | XNode.NodePort nodePort = _references[i].GetNodePort(); 63 | if (nodePort != null) 64 | _portConnectionPoints.Add(nodePort, _rects[i]); 65 | } 66 | } 67 | } 68 | 69 | public Dictionary nodeSizes { get { return _nodeSizes; } } 70 | private Dictionary _nodeSizes = new Dictionary(); 71 | public XNode.NodeGraph graph; 72 | public Vector2 panOffset { get { return _panOffset; } set { _panOffset = value; Repaint(); } } 73 | private Vector2 _panOffset; 74 | public float zoom { get { return _zoom; } set { _zoom = Mathf.Clamp(value, NodeEditorPreferences.GetSettings().minZoom, NodeEditorPreferences.GetSettings().maxZoom); Repaint(); } } 75 | private float _zoom = 1; 76 | 77 | void OnFocus() { 78 | current = this; 79 | ValidateGraphEditor(); 80 | if (graphEditor != null) { 81 | graphEditor.OnWindowFocus(); 82 | if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); 83 | } 84 | 85 | dragThreshold = Math.Max(1f, Screen.width / 1000f); 86 | } 87 | 88 | void OnLostFocus() { 89 | if (graphEditor != null) graphEditor.OnWindowFocusLost(); 90 | } 91 | 92 | [InitializeOnLoadMethod] 93 | private static void OnLoad() { 94 | Selection.selectionChanged -= OnSelectionChanged; 95 | Selection.selectionChanged += OnSelectionChanged; 96 | } 97 | 98 | /// Handle Selection Change events 99 | private static void OnSelectionChanged() { 100 | XNode.NodeGraph nodeGraph = Selection.activeObject as XNode.NodeGraph; 101 | if (nodeGraph && !AssetDatabase.Contains(nodeGraph)) { 102 | if (NodeEditorPreferences.GetSettings().openOnCreate) Open(nodeGraph); 103 | } 104 | } 105 | 106 | /// Make sure the graph editor is assigned and to the right object 107 | private void ValidateGraphEditor() { 108 | NodeGraphEditor graphEditor = NodeGraphEditor.GetEditor(graph, this); 109 | if (this.graphEditor != graphEditor && graphEditor != null) { 110 | this.graphEditor = graphEditor; 111 | graphEditor.OnOpen(); 112 | } 113 | } 114 | 115 | /// Create editor window 116 | public static NodeEditorWindow Init() { 117 | NodeEditorWindow w = CreateInstance(); 118 | w.titleContent = new GUIContent("xNode"); 119 | w.wantsMouseMove = true; 120 | w.Show(); 121 | return w; 122 | } 123 | 124 | public void Save() { 125 | if (AssetDatabase.Contains(graph)) { 126 | EditorUtility.SetDirty(graph); 127 | if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); 128 | } else SaveAs(); 129 | } 130 | 131 | public void SaveAs() { 132 | string path = EditorUtility.SaveFilePanelInProject("Save NodeGraph", "NewNodeGraph", "asset", ""); 133 | if (string.IsNullOrEmpty(path)) return; 134 | else { 135 | XNode.NodeGraph existingGraph = AssetDatabase.LoadAssetAtPath(path); 136 | if (existingGraph != null) AssetDatabase.DeleteAsset(path); 137 | AssetDatabase.CreateAsset(graph, path); 138 | EditorUtility.SetDirty(graph); 139 | if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); 140 | } 141 | } 142 | 143 | private void DraggableWindow(int windowID) { 144 | GUI.DragWindow(); 145 | } 146 | 147 | public Vector2 WindowToGridPosition(Vector2 windowPosition) { 148 | return (windowPosition - (position.size * 0.5f) - (panOffset / zoom)) * zoom; 149 | } 150 | 151 | public Vector2 GridToWindowPosition(Vector2 gridPosition) { 152 | return (position.size * 0.5f) + (panOffset / zoom) + (gridPosition / zoom); 153 | } 154 | 155 | public Rect GridToWindowRectNoClipped(Rect gridRect) { 156 | gridRect.position = GridToWindowPositionNoClipped(gridRect.position); 157 | return gridRect; 158 | } 159 | 160 | public Rect GridToWindowRect(Rect gridRect) { 161 | gridRect.position = GridToWindowPosition(gridRect.position); 162 | gridRect.size /= zoom; 163 | return gridRect; 164 | } 165 | 166 | public Vector2 GridToWindowPositionNoClipped(Vector2 gridPosition) { 167 | Vector2 center = position.size * 0.5f; 168 | // UI Sharpness complete fix - Round final offset not panOffset 169 | float xOffset = Mathf.Round(center.x * zoom + (panOffset.x + gridPosition.x)); 170 | float yOffset = Mathf.Round(center.y * zoom + (panOffset.y + gridPosition.y)); 171 | return new Vector2(xOffset, yOffset); 172 | } 173 | 174 | public void SelectNode(XNode.Node node, bool add) { 175 | if (add) { 176 | List selection = new List(Selection.objects); 177 | selection.Add(node); 178 | Selection.objects = selection.ToArray(); 179 | } else Selection.objects = new Object[] { node }; 180 | } 181 | 182 | public void DeselectNode(XNode.Node node) { 183 | List selection = new List(Selection.objects); 184 | selection.Remove(node); 185 | Selection.objects = selection.ToArray(); 186 | } 187 | 188 | [OnOpenAsset(0)] 189 | public static bool OnOpen(int instanceID, int line) { 190 | XNode.NodeGraph nodeGraph = EditorUtility.InstanceIDToObject(instanceID) as XNode.NodeGraph; 191 | if (nodeGraph != null) { 192 | Open(nodeGraph); 193 | return true; 194 | } 195 | return false; 196 | } 197 | 198 | /// Open the provided graph in the NodeEditor 199 | public static NodeEditorWindow Open(XNode.NodeGraph graph) { 200 | if (!graph) return null; 201 | 202 | NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow; 203 | w.wantsMouseMove = true; 204 | w.graph = graph; 205 | return w; 206 | } 207 | 208 | /// Repaint all open NodeEditorWindows. 209 | public static void RepaintAll() { 210 | NodeEditorWindow[] windows = Resources.FindObjectsOfTypeAll(); 211 | for (int i = 0; i < windows.Length; i++) { 212 | windows[i].Repaint(); 213 | } 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5ce2bf59ec7a25c4ba691cad7819bf38 3 | timeCreated: 1505418450 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeGraphEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using UnityEditor; 4 | using UnityEngine; 5 | #if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU 6 | using GenericMenu = XNodeEditor.AdvancedGenericMenu; 7 | #endif 8 | 9 | namespace XNodeEditor { 10 | /// Base class to derive custom Node Graph editors from. Use this to override how graphs are drawn in the editor. 11 | [CustomNodeGraphEditor(typeof(XNode.NodeGraph))] 12 | public class NodeGraphEditor : XNodeEditor.Internal.NodeEditorBase { 13 | [Obsolete("Use window.position instead")] 14 | public Rect position { get { return window.position; } set { window.position = value; } } 15 | /// Are we currently renaming a node? 16 | protected bool isRenaming; 17 | 18 | public virtual void OnGUI() { } 19 | 20 | /// Called when opened by NodeEditorWindow 21 | public virtual void OnOpen() { } 22 | 23 | /// Called when NodeEditorWindow gains focus 24 | public virtual void OnWindowFocus() { } 25 | 26 | /// Called when NodeEditorWindow loses focus 27 | public virtual void OnWindowFocusLost() { } 28 | 29 | public virtual Texture2D GetGridTexture() { 30 | return NodeEditorPreferences.GetSettings().gridTexture; 31 | } 32 | 33 | public virtual Texture2D GetSecondaryGridTexture() { 34 | return NodeEditorPreferences.GetSettings().crossTexture; 35 | } 36 | 37 | /// Return default settings for this graph type. This is the settings the user will load if no previous settings have been saved. 38 | public virtual NodeEditorPreferences.Settings GetDefaultPreferences() { 39 | return new NodeEditorPreferences.Settings(); 40 | } 41 | 42 | /// Returns context node menu path. Null or empty strings for hidden nodes. 43 | public virtual string GetNodeMenuName(Type type) { 44 | //Check if type has the CreateNodeMenuAttribute 45 | XNode.Node.CreateNodeMenuAttribute attrib; 46 | if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path 47 | return attrib.menuName; 48 | else // Return generated path 49 | return NodeEditorUtilities.NodeDefaultPath(type); 50 | } 51 | 52 | /// The order by which the menu items are displayed. 53 | public virtual int GetNodeMenuOrder(Type type) { 54 | //Check if type has the CreateNodeMenuAttribute 55 | XNode.Node.CreateNodeMenuAttribute attrib; 56 | if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path 57 | return attrib.order; 58 | else 59 | return 0; 60 | } 61 | 62 | /// 63 | /// Called before connecting two ports in the graph view to see if the output port is compatible with the input port 64 | /// 65 | public virtual bool CanConnect(XNode.NodePort output, XNode.NodePort input) { 66 | return output.CanConnectTo(input); 67 | } 68 | 69 | /// 70 | /// Add items for the context menu when right-clicking this node. 71 | /// Override to add custom menu items. 72 | /// 73 | /// 74 | /// Use it to filter only nodes with ports value type, compatible with this type 75 | /// Direction of the compatiblity 76 | public virtual void AddContextMenuItems(GenericMenu menu, Type compatibleType = null, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) { 77 | Vector2 pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition); 78 | 79 | Type[] nodeTypes; 80 | 81 | if (compatibleType != null && NodeEditorPreferences.GetSettings().createFilter) { 82 | nodeTypes = NodeEditorUtilities.GetCompatibleNodesTypes(NodeEditorReflection.nodeTypes, compatibleType, direction).OrderBy(GetNodeMenuOrder).ToArray(); 83 | } else { 84 | nodeTypes = NodeEditorReflection.nodeTypes.OrderBy(GetNodeMenuOrder).ToArray(); 85 | } 86 | 87 | for (int i = 0; i < nodeTypes.Length; i++) { 88 | Type type = nodeTypes[i]; 89 | 90 | //Get node context menu path 91 | string path = GetNodeMenuName(type); 92 | if (string.IsNullOrEmpty(path)) continue; 93 | 94 | // Check if user is allowed to add more of given node type 95 | XNode.Node.DisallowMultipleNodesAttribute disallowAttrib; 96 | bool disallowed = false; 97 | if (NodeEditorUtilities.GetAttrib(type, out disallowAttrib)) { 98 | int typeCount = target.nodes.Count(x => x.GetType() == type); 99 | if (typeCount >= disallowAttrib.max) disallowed = true; 100 | } 101 | 102 | // Add node entry to context menu 103 | if (disallowed) menu.AddItem(new GUIContent(path), false, null); 104 | else menu.AddItem(new GUIContent(path), false, () => { 105 | XNode.Node node = CreateNode(type, pos); 106 | if (node != null) NodeEditorWindow.current.AutoConnect(node); // handle null nodes to avoid nullref exceptions 107 | }); 108 | } 109 | menu.AddSeparator(""); 110 | if (NodeEditorWindow.copyBuffer != null && NodeEditorWindow.copyBuffer.Length > 0) menu.AddItem(new GUIContent("Paste"), false, () => NodeEditorWindow.current.PasteNodes(pos)); 111 | else menu.AddDisabledItem(new GUIContent("Paste")); 112 | menu.AddItem(new GUIContent("Preferences"), false, () => NodeEditorReflection.OpenPreferences()); 113 | menu.AddCustomContextMenuItems(target); 114 | } 115 | 116 | /// Returned gradient is used to color noodles 117 | /// The output this noodle comes from. Never null. 118 | /// The output this noodle comes from. Can be null if we are dragging the noodle. 119 | public virtual Gradient GetNoodleGradient(XNode.NodePort output, XNode.NodePort input) { 120 | Gradient grad = new Gradient(); 121 | 122 | // If dragging the noodle, draw solid, slightly transparent 123 | if (input == null) { 124 | Color a = GetTypeColor(output.ValueType); 125 | grad.SetKeys( 126 | new GradientColorKey[] { new GradientColorKey(a, 0f) }, 127 | new GradientAlphaKey[] { new GradientAlphaKey(0.6f, 0f) } 128 | ); 129 | } 130 | // If normal, draw gradient fading from one input color to the other 131 | else { 132 | Color a = GetTypeColor(output.ValueType); 133 | Color b = GetTypeColor(input.ValueType); 134 | // If any port is hovered, tint white 135 | if (window.hoveredPort == output || window.hoveredPort == input) { 136 | a = Color.Lerp(a, Color.white, 0.8f); 137 | b = Color.Lerp(b, Color.white, 0.8f); 138 | } 139 | grad.SetKeys( 140 | new GradientColorKey[] { new GradientColorKey(a, 0f), new GradientColorKey(b, 1f) }, 141 | new GradientAlphaKey[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) } 142 | ); 143 | } 144 | return grad; 145 | } 146 | 147 | /// Returned float is used for noodle thickness 148 | /// The output this noodle comes from. Never null. 149 | /// The output this noodle comes from. Can be null if we are dragging the noodle. 150 | public virtual float GetNoodleThickness(XNode.NodePort output, XNode.NodePort input) { 151 | return NodeEditorPreferences.GetSettings().noodleThickness; 152 | } 153 | 154 | public virtual NoodlePath GetNoodlePath(XNode.NodePort output, XNode.NodePort input) { 155 | return NodeEditorPreferences.GetSettings().noodlePath; 156 | } 157 | 158 | public virtual NoodleStroke GetNoodleStroke(XNode.NodePort output, XNode.NodePort input) { 159 | return NodeEditorPreferences.GetSettings().noodleStroke; 160 | } 161 | 162 | /// Returned color is used to color ports 163 | public virtual Color GetPortColor(XNode.NodePort port) { 164 | return GetTypeColor(port.ValueType); 165 | } 166 | 167 | /// 168 | /// The returned Style is used to configure the paddings and icon texture of the ports. 169 | /// Use these properties to customize your port style. 170 | /// 171 | /// The properties used is: 172 | /// [Left and Right], [Background] = border texture, 173 | /// and [Background] = dot texture; 174 | /// 175 | /// the owner of the style 176 | /// 177 | public virtual GUIStyle GetPortStyle(XNode.NodePort port) { 178 | if (port.direction == XNode.NodePort.IO.Input) 179 | return NodeEditorResources.styles.inputPort; 180 | 181 | return NodeEditorResources.styles.outputPort; 182 | } 183 | 184 | /// The returned color is used to color the background of the door. 185 | /// Usually used for outer edge effect 186 | public virtual Color GetPortBackgroundColor(XNode.NodePort port) { 187 | return Color.gray; 188 | } 189 | 190 | /// Returns generated color for a type. This color is editable in preferences 191 | public virtual Color GetTypeColor(Type type) { 192 | return NodeEditorPreferences.GetTypeColor(type); 193 | } 194 | 195 | /// Override to display custom tooltips 196 | public virtual string GetPortTooltip(XNode.NodePort port) { 197 | Type portType = port.ValueType; 198 | string tooltip = ""; 199 | tooltip = portType.PrettyName(); 200 | if (port.IsOutput) { 201 | object obj = port.node.GetValue(port); 202 | tooltip += " = " + (obj != null ? obj.ToString() : "null"); 203 | } 204 | return tooltip; 205 | } 206 | 207 | /// Deal with objects dropped into the graph through DragAndDrop 208 | public virtual void OnDropObjects(UnityEngine.Object[] objects) { 209 | if (GetType() != typeof(NodeGraphEditor)) Debug.Log("No OnDropObjects override defined for " + GetType()); 210 | } 211 | 212 | /// Create a node and save it in the graph asset 213 | public virtual XNode.Node CreateNode(Type type, Vector2 position) { 214 | Undo.RecordObject(target, "Create Node"); 215 | XNode.Node node = target.AddNode(type); 216 | if (node == null) return null; // handle null nodes to avoid nullref exceptions 217 | Undo.RegisterCreatedObjectUndo(node, "Create Node"); 218 | node.position = position; 219 | if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type); 220 | if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) AssetDatabase.AddObjectToAsset(node, target); 221 | if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); 222 | NodeEditorWindow.RepaintAll(); 223 | return node; 224 | } 225 | 226 | /// Creates a copy of the original node in the graph 227 | public virtual XNode.Node CopyNode(XNode.Node original) { 228 | Undo.RecordObject(target, "Duplicate Node"); 229 | XNode.Node node = target.CopyNode(original); 230 | Undo.RegisterCreatedObjectUndo(node, "Duplicate Node"); 231 | node.name = original.name; 232 | if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) AssetDatabase.AddObjectToAsset(node, target); 233 | if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); 234 | return node; 235 | } 236 | 237 | /// Return false for nodes that can't be removed 238 | public virtual bool CanRemove(XNode.Node node) { 239 | // Check graph attributes to see if this node is required 240 | Type graphType = target.GetType(); 241 | XNode.NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll( 242 | graphType.GetCustomAttributes(typeof(XNode.NodeGraph.RequireNodeAttribute), true), x => x as XNode.NodeGraph.RequireNodeAttribute); 243 | if (attribs.Any(x => x.Requires(node.GetType()))) { 244 | if (target.nodes.Count(x => x.GetType() == node.GetType()) <= 1) { 245 | return false; 246 | } 247 | } 248 | return true; 249 | } 250 | 251 | /// Safely remove a node and all its connections. 252 | public virtual void RemoveNode(XNode.Node node) { 253 | if (!CanRemove(node)) return; 254 | 255 | // Remove the node 256 | Undo.RecordObject(node, "Delete Node"); 257 | Undo.RecordObject(target, "Delete Node"); 258 | foreach (var port in node.Ports) 259 | foreach (var conn in port.GetConnections()) 260 | Undo.RecordObject(conn.node, "Delete Node"); 261 | target.RemoveNode(node); 262 | Undo.DestroyObjectImmediate(node); 263 | if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); 264 | } 265 | 266 | [AttributeUsage(AttributeTargets.Class)] 267 | public class CustomNodeGraphEditorAttribute : Attribute, 268 | XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib { 269 | private Type inspectedType; 270 | public string editorPrefsKey; 271 | /// Tells a NodeGraphEditor which Graph type it is an editor for 272 | /// Type that this editor can edit 273 | /// Define unique key for unique layout settings instance 274 | public CustomNodeGraphEditorAttribute(Type inspectedType, string editorPrefsKey = "xNode.Settings") { 275 | this.inspectedType = inspectedType; 276 | this.editorPrefsKey = editorPrefsKey; 277 | } 278 | 279 | public Type GetInspectedType() { 280 | return inspectedType; 281 | } 282 | } 283 | } 284 | } -------------------------------------------------------------------------------- /Scripts/Editor/NodeGraphEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ddcbb5432255d3247a0718b15a9c193c 3 | timeCreated: 1505462176 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeGraphImporter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEditor.Experimental.AssetImporters; 6 | using UnityEngine; 7 | using XNode; 8 | 9 | namespace XNodeEditor { 10 | /// Deals with modified assets 11 | class NodeGraphImporter : AssetPostprocessor { 12 | private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { 13 | foreach (string path in importedAssets) { 14 | // Skip processing anything without the .asset extension 15 | if (Path.GetExtension(path) != ".asset") continue; 16 | 17 | // Get the object that is requested for deletion 18 | NodeGraph graph = AssetDatabase.LoadAssetAtPath(path); 19 | if (graph == null) continue; 20 | 21 | // Get attributes 22 | Type graphType = graph.GetType(); 23 | NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll( 24 | graphType.GetCustomAttributes(typeof(NodeGraph.RequireNodeAttribute), true), x => x as NodeGraph.RequireNodeAttribute); 25 | 26 | Vector2 position = Vector2.zero; 27 | foreach (NodeGraph.RequireNodeAttribute attrib in attribs) { 28 | if (attrib.type0 != null) AddRequired(graph, attrib.type0, ref position); 29 | if (attrib.type1 != null) AddRequired(graph, attrib.type1, ref position); 30 | if (attrib.type2 != null) AddRequired(graph, attrib.type2, ref position); 31 | } 32 | } 33 | } 34 | 35 | private static void AddRequired(NodeGraph graph, Type type, ref Vector2 position) { 36 | if (!graph.nodes.Any(x => x.GetType() == type)) { 37 | XNode.Node node = graph.AddNode(type); 38 | node.position = position; 39 | position.x += 200; 40 | if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type); 41 | if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(graph))) AssetDatabase.AddObjectToAsset(node, graph); 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /Scripts/Editor/NodeGraphImporter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7a816f2790bf3da48a2d6d0035ebc9a0 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/RenamePopup.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace XNodeEditor { 5 | /// Utility for renaming assets 6 | public class RenamePopup : EditorWindow { 7 | private const string inputControlName = "nameInput"; 8 | 9 | public static RenamePopup current { get; private set; } 10 | public Object target; 11 | public string input; 12 | 13 | private bool firstFrame = true; 14 | 15 | /// Show a rename popup for an asset at mouse position. Will trigger reimport of the asset on apply. 16 | public static RenamePopup Show(Object target, float width = 200) { 17 | RenamePopup window = EditorWindow.GetWindow(true, "Rename " + target.name, true); 18 | if (current != null) current.Close(); 19 | current = window; 20 | window.target = target; 21 | window.input = target.name; 22 | window.minSize = new Vector2(100, 44); 23 | window.position = new Rect(0, 0, width, 44); 24 | window.UpdatePositionToMouse(); 25 | return window; 26 | } 27 | 28 | private void UpdatePositionToMouse() { 29 | if (Event.current == null) return; 30 | Vector3 mousePoint = GUIUtility.GUIToScreenPoint(Event.current.mousePosition); 31 | Rect pos = position; 32 | pos.x = mousePoint.x - position.width * 0.5f; 33 | pos.y = mousePoint.y - 10; 34 | position = pos; 35 | } 36 | 37 | private void OnLostFocus() { 38 | // Make the popup close on lose focus 39 | Close(); 40 | } 41 | 42 | private void OnGUI() { 43 | if (firstFrame) { 44 | UpdatePositionToMouse(); 45 | firstFrame = false; 46 | } 47 | GUI.SetNextControlName(inputControlName); 48 | input = EditorGUILayout.TextField(input); 49 | EditorGUI.FocusTextInControl(inputControlName); 50 | Event e = Event.current; 51 | // If input is empty, revert name to default instead 52 | if (input == null || input.Trim() == "") { 53 | if (GUILayout.Button("Revert to default") || (e.isKey && e.keyCode == KeyCode.Return)) { 54 | target.name = NodeEditorUtilities.NodeDefaultName(target.GetType()); 55 | NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename(); 56 | if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) { 57 | AssetDatabase.SetMainObject((target as XNode.Node).graph, AssetDatabase.GetAssetPath(target)); 58 | AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); 59 | } 60 | Close(); 61 | target.TriggerOnValidate(); 62 | } 63 | } 64 | // Rename asset to input text 65 | else { 66 | if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) { 67 | target.name = input; 68 | NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename(); 69 | if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) { 70 | AssetDatabase.SetMainObject((target as XNode.Node).graph, AssetDatabase.GetAssetPath(target)); 71 | AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); 72 | } 73 | Close(); 74 | target.TriggerOnValidate(); 75 | } 76 | } 77 | 78 | if (e.isKey && e.keyCode == KeyCode.Escape) { 79 | Close(); 80 | } 81 | } 82 | 83 | private void OnDestroy() { 84 | EditorGUIUtility.editingTextField = false; 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /Scripts/Editor/RenamePopup.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4ef3ddc25518318469bce838980c64be 3 | timeCreated: 1552067957 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 964fc201163fe884ca6a20094b6f3b49 3 | folderAsset: yes 4 | timeCreated: 1506110871 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources/ScriptTemplates.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 86b677955452bb5449f9f4dd47b6ddfe 3 | folderAsset: yes 4 | timeCreated: 1519049391 5 | licenseType: Free 6 | DefaultImporter: 7 | externalObjects: {} 8 | userData: 9 | assetBundleName: 10 | assetBundleVariant: 11 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using XNode; 5 | 6 | [CreateAssetMenu] 7 | public class #SCRIPTNAME# : NodeGraph { 8 | #NOTRIM# 9 | } -------------------------------------------------------------------------------- /Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8165767f64da7d94e925f61a38da668c 3 | timeCreated: 1519049802 4 | licenseType: Free 5 | TextScriptImporter: 6 | externalObjects: {} 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using XNode; 5 | 6 | public class #SCRIPTNAME# : Node { 7 | 8 | // Use this for initialization 9 | protected override void Init() { 10 | base.Init(); 11 | #NOTRIM# 12 | } 13 | 14 | // Return the correct value of an output port when requested 15 | public override object GetValue(NodePort port) { 16 | return null; // Replace this 17 | } 18 | } -------------------------------------------------------------------------------- /Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 85f6f570600a1a44d8e734cb111a8b89 3 | timeCreated: 1519049802 4 | licenseType: Free 5 | TextScriptImporter: 6 | externalObjects: {} 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Siccity/xNode/d6effd70f5574369e3415c423ef3e621ea309564/Scripts/Editor/Resources/xnode_dot.png -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_dot.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 75a1fe0b102226a418486ed823c9a7fb 3 | timeCreated: 1506110357 4 | licenseType: Free 5 | TextureImporter: 6 | fileIDToRecycleName: {} 7 | serializedVersion: 4 8 | mipmaps: 9 | mipMapMode: 0 10 | enableMipMap: 0 11 | sRGBTexture: 0 12 | linearTexture: 0 13 | fadeOut: 0 14 | borderMipMap: 0 15 | mipMapsPreserveCoverage: 0 16 | alphaTestReferenceValue: 0.5 17 | mipMapFadeDistanceStart: 1 18 | mipMapFadeDistanceEnd: 3 19 | bumpmap: 20 | convertToNormalMap: 0 21 | externalNormalMap: 0 22 | heightScale: 0.25 23 | normalMapFilter: 0 24 | isReadable: 0 25 | grayScaleToAlpha: 0 26 | generateCubemap: 6 27 | cubemapConvolution: 0 28 | seamlessCubemap: 0 29 | textureFormat: 1 30 | maxTextureSize: 2048 31 | textureSettings: 32 | serializedVersion: 2 33 | filterMode: -1 34 | aniso: 1 35 | mipBias: -1 36 | wrapU: 1 37 | wrapV: -1 38 | wrapW: -1 39 | nPOTScale: 0 40 | lightmap: 0 41 | compressionQuality: 50 42 | spriteMode: 0 43 | spriteExtrude: 1 44 | spriteMeshType: 1 45 | alignment: 0 46 | spritePivot: {x: 0.5, y: 0.5} 47 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 48 | spritePixelsToUnits: 100 49 | alphaUsage: 1 50 | alphaIsTransparency: 1 51 | spriteTessellationDetail: -1 52 | textureType: 2 53 | textureShape: 1 54 | maxTextureSizeSet: 0 55 | compressionQualitySet: 0 56 | textureFormatSet: 0 57 | platformSettings: 58 | - buildTarget: DefaultTexturePlatform 59 | maxTextureSize: 2048 60 | textureFormat: -1 61 | textureCompression: 1 62 | compressionQuality: 50 63 | crunchedCompression: 0 64 | allowsAlphaSplitting: 0 65 | overridden: 0 66 | - buildTarget: Standalone 67 | maxTextureSize: 2048 68 | textureFormat: -1 69 | textureCompression: 1 70 | compressionQuality: 50 71 | crunchedCompression: 0 72 | allowsAlphaSplitting: 0 73 | overridden: 0 74 | - buildTarget: Android 75 | maxTextureSize: 2048 76 | textureFormat: -1 77 | textureCompression: 1 78 | compressionQuality: 50 79 | crunchedCompression: 0 80 | allowsAlphaSplitting: 0 81 | overridden: 0 82 | - buildTarget: WebGL 83 | maxTextureSize: 2048 84 | textureFormat: -1 85 | textureCompression: 1 86 | compressionQuality: 50 87 | crunchedCompression: 0 88 | allowsAlphaSplitting: 0 89 | overridden: 0 90 | spriteSheet: 91 | serializedVersion: 2 92 | sprites: [] 93 | outline: [] 94 | physicsShape: [] 95 | spritePackingTag: 96 | userData: 97 | assetBundleName: 98 | assetBundleVariant: 99 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_dot_outer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Siccity/xNode/d6effd70f5574369e3415c423ef3e621ea309564/Scripts/Editor/Resources/xnode_dot_outer.png -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_dot_outer.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 434ca8b4bdfa5574abb0002bbc9b65ad 3 | timeCreated: 1506110357 4 | licenseType: Free 5 | TextureImporter: 6 | fileIDToRecycleName: {} 7 | serializedVersion: 4 8 | mipmaps: 9 | mipMapMode: 0 10 | enableMipMap: 0 11 | sRGBTexture: 0 12 | linearTexture: 0 13 | fadeOut: 0 14 | borderMipMap: 0 15 | mipMapsPreserveCoverage: 0 16 | alphaTestReferenceValue: 0.5 17 | mipMapFadeDistanceStart: 1 18 | mipMapFadeDistanceEnd: 3 19 | bumpmap: 20 | convertToNormalMap: 0 21 | externalNormalMap: 0 22 | heightScale: 0.25 23 | normalMapFilter: 0 24 | isReadable: 0 25 | grayScaleToAlpha: 0 26 | generateCubemap: 6 27 | cubemapConvolution: 0 28 | seamlessCubemap: 0 29 | textureFormat: 1 30 | maxTextureSize: 2048 31 | textureSettings: 32 | serializedVersion: 2 33 | filterMode: -1 34 | aniso: 1 35 | mipBias: -1 36 | wrapU: 1 37 | wrapV: -1 38 | wrapW: -1 39 | nPOTScale: 0 40 | lightmap: 0 41 | compressionQuality: 50 42 | spriteMode: 0 43 | spriteExtrude: 1 44 | spriteMeshType: 1 45 | alignment: 0 46 | spritePivot: {x: 0.5, y: 0.5} 47 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 48 | spritePixelsToUnits: 100 49 | alphaUsage: 1 50 | alphaIsTransparency: 1 51 | spriteTessellationDetail: -1 52 | textureType: 2 53 | textureShape: 1 54 | maxTextureSizeSet: 0 55 | compressionQualitySet: 0 56 | textureFormatSet: 0 57 | platformSettings: 58 | - buildTarget: DefaultTexturePlatform 59 | maxTextureSize: 2048 60 | textureFormat: -1 61 | textureCompression: 1 62 | compressionQuality: 50 63 | crunchedCompression: 0 64 | allowsAlphaSplitting: 0 65 | overridden: 0 66 | - buildTarget: Standalone 67 | maxTextureSize: 2048 68 | textureFormat: -1 69 | textureCompression: 1 70 | compressionQuality: 50 71 | crunchedCompression: 0 72 | allowsAlphaSplitting: 0 73 | overridden: 0 74 | - buildTarget: Android 75 | maxTextureSize: 2048 76 | textureFormat: -1 77 | textureCompression: 1 78 | compressionQuality: 50 79 | crunchedCompression: 0 80 | allowsAlphaSplitting: 0 81 | overridden: 0 82 | - buildTarget: WebGL 83 | maxTextureSize: 2048 84 | textureFormat: -1 85 | textureCompression: 1 86 | compressionQuality: 50 87 | crunchedCompression: 0 88 | allowsAlphaSplitting: 0 89 | overridden: 0 90 | spriteSheet: 91 | serializedVersion: 2 92 | sprites: [] 93 | outline: [] 94 | physicsShape: [] 95 | spritePackingTag: 96 | userData: 97 | assetBundleName: 98 | assetBundleVariant: 99 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Siccity/xNode/d6effd70f5574369e3415c423ef3e621ea309564/Scripts/Editor/Resources/xnode_node.png -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_node.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2fea1dcb24935ef4ca514d534eb6aa3d 3 | timeCreated: 1507454532 4 | licenseType: Free 5 | TextureImporter: 6 | fileIDToRecycleName: {} 7 | serializedVersion: 4 8 | mipmaps: 9 | mipMapMode: 0 10 | enableMipMap: 0 11 | sRGBTexture: 0 12 | linearTexture: 0 13 | fadeOut: 0 14 | borderMipMap: 0 15 | mipMapsPreserveCoverage: 0 16 | alphaTestReferenceValue: 0.5 17 | mipMapFadeDistanceStart: 1 18 | mipMapFadeDistanceEnd: 3 19 | bumpmap: 20 | convertToNormalMap: 0 21 | externalNormalMap: 0 22 | heightScale: 0.25 23 | normalMapFilter: 0 24 | isReadable: 0 25 | grayScaleToAlpha: 0 26 | generateCubemap: 6 27 | cubemapConvolution: 0 28 | seamlessCubemap: 0 29 | textureFormat: 1 30 | maxTextureSize: 2048 31 | textureSettings: 32 | serializedVersion: 2 33 | filterMode: -1 34 | aniso: 1 35 | mipBias: -1 36 | wrapU: 1 37 | wrapV: 1 38 | wrapW: 1 39 | nPOTScale: 0 40 | lightmap: 0 41 | compressionQuality: 50 42 | spriteMode: 0 43 | spriteExtrude: 1 44 | spriteMeshType: 1 45 | alignment: 0 46 | spritePivot: {x: 0.5, y: 0.5} 47 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 48 | spritePixelsToUnits: 100 49 | alphaUsage: 1 50 | alphaIsTransparency: 1 51 | spriteTessellationDetail: -1 52 | textureType: 2 53 | textureShape: 1 54 | maxTextureSizeSet: 0 55 | compressionQualitySet: 0 56 | textureFormatSet: 0 57 | platformSettings: 58 | - buildTarget: DefaultTexturePlatform 59 | maxTextureSize: 2048 60 | textureFormat: -1 61 | textureCompression: 1 62 | compressionQuality: 50 63 | crunchedCompression: 0 64 | allowsAlphaSplitting: 0 65 | overridden: 0 66 | - buildTarget: Standalone 67 | maxTextureSize: 2048 68 | textureFormat: -1 69 | textureCompression: 1 70 | compressionQuality: 50 71 | crunchedCompression: 0 72 | allowsAlphaSplitting: 0 73 | overridden: 0 74 | - buildTarget: Android 75 | maxTextureSize: 2048 76 | textureFormat: -1 77 | textureCompression: 1 78 | compressionQuality: 50 79 | crunchedCompression: 0 80 | allowsAlphaSplitting: 0 81 | overridden: 0 82 | - buildTarget: WebGL 83 | maxTextureSize: 2048 84 | textureFormat: -1 85 | textureCompression: 1 86 | compressionQuality: 50 87 | crunchedCompression: 0 88 | allowsAlphaSplitting: 0 89 | overridden: 0 90 | spriteSheet: 91 | serializedVersion: 2 92 | sprites: [] 93 | outline: [] 94 | physicsShape: [] 95 | spritePackingTag: 96 | userData: 97 | assetBundleName: 98 | assetBundleVariant: 99 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_node_highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Siccity/xNode/d6effd70f5574369e3415c423ef3e621ea309564/Scripts/Editor/Resources/xnode_node_highlight.png -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_node_highlight.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2ab2b92d7e1771b47bba0a46a6f0f6d5 3 | timeCreated: 1516610730 4 | licenseType: Free 5 | TextureImporter: 6 | fileIDToRecycleName: {} 7 | externalObjects: {} 8 | serializedVersion: 4 9 | mipmaps: 10 | mipMapMode: 0 11 | enableMipMap: 0 12 | sRGBTexture: 0 13 | linearTexture: 0 14 | fadeOut: 0 15 | borderMipMap: 0 16 | mipMapsPreserveCoverage: 0 17 | alphaTestReferenceValue: 0.5 18 | mipMapFadeDistanceStart: 1 19 | mipMapFadeDistanceEnd: 3 20 | bumpmap: 21 | convertToNormalMap: 0 22 | externalNormalMap: 0 23 | heightScale: 0.25 24 | normalMapFilter: 0 25 | isReadable: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: -1 35 | aniso: 1 36 | mipBias: -1 37 | wrapU: 1 38 | wrapV: 1 39 | wrapW: -1 40 | nPOTScale: 0 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 49 | spritePixelsToUnits: 100 50 | alphaUsage: 1 51 | alphaIsTransparency: 1 52 | spriteTessellationDetail: -1 53 | textureType: 2 54 | textureShape: 1 55 | maxTextureSizeSet: 0 56 | compressionQualitySet: 0 57 | textureFormatSet: 0 58 | platformSettings: 59 | - buildTarget: DefaultTexturePlatform 60 | maxTextureSize: 2048 61 | resizeAlgorithm: 0 62 | textureFormat: -1 63 | textureCompression: 1 64 | compressionQuality: 50 65 | crunchedCompression: 0 66 | allowsAlphaSplitting: 0 67 | overridden: 0 68 | androidETC2FallbackOverride: 0 69 | - buildTarget: Standalone 70 | maxTextureSize: 2048 71 | resizeAlgorithm: 0 72 | textureFormat: -1 73 | textureCompression: 1 74 | compressionQuality: 50 75 | crunchedCompression: 0 76 | allowsAlphaSplitting: 0 77 | overridden: 0 78 | androidETC2FallbackOverride: 0 79 | spriteSheet: 80 | serializedVersion: 2 81 | sprites: [] 82 | outline: [] 83 | physicsShape: [] 84 | spritePackingTag: 85 | userData: 86 | assetBundleName: 87 | assetBundleVariant: 88 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_node_workfile.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Siccity/xNode/d6effd70f5574369e3415c423ef3e621ea309564/Scripts/Editor/Resources/xnode_node_workfile.psd -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_node_workfile.psd.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2267efa6e1e349348ae0b28fb659a6e2 3 | timeCreated: 1507454532 4 | licenseType: Free 5 | TextureImporter: 6 | fileIDToRecycleName: {} 7 | serializedVersion: 4 8 | mipmaps: 9 | mipMapMode: 0 10 | enableMipMap: 0 11 | sRGBTexture: 0 12 | linearTexture: 0 13 | fadeOut: 0 14 | borderMipMap: 0 15 | mipMapsPreserveCoverage: 0 16 | alphaTestReferenceValue: 0.5 17 | mipMapFadeDistanceStart: 1 18 | mipMapFadeDistanceEnd: 3 19 | bumpmap: 20 | convertToNormalMap: 0 21 | externalNormalMap: 0 22 | heightScale: 0.25 23 | normalMapFilter: 0 24 | isReadable: 0 25 | grayScaleToAlpha: 0 26 | generateCubemap: 6 27 | cubemapConvolution: 0 28 | seamlessCubemap: 0 29 | textureFormat: 1 30 | maxTextureSize: 2048 31 | textureSettings: 32 | serializedVersion: 2 33 | filterMode: -1 34 | aniso: 1 35 | mipBias: -1 36 | wrapU: 1 37 | wrapV: -1 38 | wrapW: -1 39 | nPOTScale: 0 40 | lightmap: 0 41 | compressionQuality: 50 42 | spriteMode: 0 43 | spriteExtrude: 1 44 | spriteMeshType: 1 45 | alignment: 0 46 | spritePivot: {x: 0.5, y: 0.5} 47 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 48 | spritePixelsToUnits: 100 49 | alphaUsage: 1 50 | alphaIsTransparency: 1 51 | spriteTessellationDetail: -1 52 | textureType: 2 53 | textureShape: 1 54 | maxTextureSizeSet: 0 55 | compressionQualitySet: 0 56 | textureFormatSet: 0 57 | platformSettings: 58 | - buildTarget: DefaultTexturePlatform 59 | maxTextureSize: 2048 60 | textureFormat: -1 61 | textureCompression: 1 62 | compressionQuality: 50 63 | crunchedCompression: 0 64 | allowsAlphaSplitting: 0 65 | overridden: 0 66 | - buildTarget: Standalone 67 | maxTextureSize: 2048 68 | textureFormat: -1 69 | textureCompression: 1 70 | compressionQuality: 50 71 | crunchedCompression: 0 72 | allowsAlphaSplitting: 0 73 | overridden: 0 74 | - buildTarget: Android 75 | maxTextureSize: 2048 76 | textureFormat: -1 77 | textureCompression: 1 78 | compressionQuality: 50 79 | crunchedCompression: 0 80 | allowsAlphaSplitting: 0 81 | overridden: 0 82 | - buildTarget: WebGL 83 | maxTextureSize: 2048 84 | textureFormat: -1 85 | textureCompression: 1 86 | compressionQuality: 50 87 | crunchedCompression: 0 88 | allowsAlphaSplitting: 0 89 | overridden: 0 90 | spriteSheet: 91 | serializedVersion: 2 92 | sprites: [] 93 | outline: [] 94 | physicsShape: [] 95 | spritePackingTag: 96 | userData: 97 | assetBundleName: 98 | assetBundleVariant: 99 | -------------------------------------------------------------------------------- /Scripts/Editor/SceneGraphEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using XNode; 7 | 8 | namespace XNodeEditor { 9 | [CustomEditor(typeof(SceneGraph), true)] 10 | public class SceneGraphEditor : Editor { 11 | private SceneGraph sceneGraph; 12 | private bool removeSafely; 13 | private Type graphType; 14 | 15 | public override void OnInspectorGUI() { 16 | if (sceneGraph.graph == null) { 17 | if (GUILayout.Button("New graph", GUILayout.Height(40))) { 18 | if (graphType == null) { 19 | Type[] graphTypes = NodeEditorReflection.GetDerivedTypes(typeof(NodeGraph)); 20 | GenericMenu menu = new GenericMenu(); 21 | for (int i = 0; i < graphTypes.Length; i++) { 22 | Type graphType = graphTypes[i]; 23 | menu.AddItem(new GUIContent(graphType.Name), false, () => CreateGraph(graphType)); 24 | } 25 | menu.ShowAsContext(); 26 | } else { 27 | CreateGraph(graphType); 28 | } 29 | } 30 | } else { 31 | if (GUILayout.Button("Open graph", GUILayout.Height(40))) { 32 | NodeEditorWindow.Open(sceneGraph.graph); 33 | } 34 | if (removeSafely) { 35 | GUILayout.BeginHorizontal(); 36 | GUILayout.Label("Really remove graph?"); 37 | GUI.color = new Color(1, 0.8f, 0.8f); 38 | if (GUILayout.Button("Remove")) { 39 | removeSafely = false; 40 | Undo.RecordObject(sceneGraph, "Removed graph"); 41 | sceneGraph.graph = null; 42 | } 43 | GUI.color = Color.white; 44 | if (GUILayout.Button("Cancel")) { 45 | removeSafely = false; 46 | } 47 | GUILayout.EndHorizontal(); 48 | } else { 49 | GUI.color = new Color(1, 0.8f, 0.8f); 50 | if (GUILayout.Button("Remove graph")) { 51 | removeSafely = true; 52 | } 53 | GUI.color = Color.white; 54 | } 55 | } 56 | DrawDefaultInspector(); 57 | } 58 | 59 | private void OnEnable() { 60 | sceneGraph = target as SceneGraph; 61 | Type sceneGraphType = sceneGraph.GetType(); 62 | if (sceneGraphType == typeof(SceneGraph)) { 63 | graphType = null; 64 | } else { 65 | Type baseType = sceneGraphType.BaseType; 66 | if (baseType.IsGenericType) { 67 | graphType = sceneGraphType = baseType.GetGenericArguments() [0]; 68 | } 69 | } 70 | } 71 | 72 | public void CreateGraph(Type type) { 73 | Undo.RecordObject(sceneGraph, "Create graph"); 74 | sceneGraph.graph = ScriptableObject.CreateInstance(type) as NodeGraph; 75 | sceneGraph.graph.name = sceneGraph.name + "-graph"; 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /Scripts/Editor/SceneGraphEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: aea725adabc311f44b5ea8161360a915 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/XNodeEditor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "XNodeEditor", 3 | "references": [ 4 | "XNode" 5 | ], 6 | "optionalUnityReferences": [], 7 | "includePlatforms": [ 8 | "Editor" 9 | ], 10 | "excludePlatforms": [], 11 | "allowUnsafeCode": false, 12 | "overrideReferences": false, 13 | "precompiledReferences": [], 14 | "autoReferenced": true, 15 | "defineConstraints": [], 16 | "versionDefines": [] 17 | } -------------------------------------------------------------------------------- /Scripts/Editor/XNodeEditor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 002c1bbed08fa44d282ef34fd5edb138 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Scripts/Node.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f26231e5ab9368746948d0ea49e8178a 3 | timeCreated: 1505419984 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/NodeDataCache.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Reflection; 4 | using UnityEngine; 5 | 6 | namespace XNode { 7 | /// Precaches reflection data in editor so we won't have to do it runtime 8 | public static class NodeDataCache { 9 | private static PortDataCache portDataCache; 10 | private static Dictionary> formerlySerializedAsCache; 11 | private static Dictionary typeQualifiedNameCache; 12 | private static bool Initialized { get { return portDataCache != null; } } 13 | 14 | public static string GetTypeQualifiedName(System.Type type) { 15 | if(typeQualifiedNameCache == null) typeQualifiedNameCache = new Dictionary(); 16 | 17 | string name; 18 | if (!typeQualifiedNameCache.TryGetValue(type, out name)) { 19 | name = type.AssemblyQualifiedName; 20 | typeQualifiedNameCache.Add(type, name); 21 | } 22 | return name; 23 | } 24 | 25 | /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. 26 | public static void UpdatePorts(Node node, Dictionary ports) { 27 | if (!Initialized) BuildCache(); 28 | 29 | Dictionary> removedPorts = new Dictionary>(); 30 | System.Type nodeType = node.GetType(); 31 | 32 | Dictionary formerlySerializedAs = null; 33 | if (formerlySerializedAsCache != null) formerlySerializedAsCache.TryGetValue(nodeType, out formerlySerializedAs); 34 | 35 | List dynamicListPorts = new List(); 36 | 37 | Dictionary staticPorts; 38 | if (!portDataCache.TryGetValue(nodeType, out staticPorts)) { 39 | staticPorts = new Dictionary(); 40 | } 41 | 42 | // Cleanup port dict - Remove nonexisting static ports - update static port types 43 | // AND update dynamic ports (albeit only those in lists) too, in order to enforce proper serialisation. 44 | // Loop through current node ports 45 | foreach (NodePort port in ports.Values.ToArray()) { 46 | // If port still exists, check it it has been changed 47 | NodePort staticPort; 48 | if (staticPorts.TryGetValue(port.fieldName, out staticPort)) { 49 | // If port exists but with wrong settings, remove it. Re-add it later. 50 | if (port.IsDynamic || port.direction != staticPort.direction || port.connectionType != staticPort.connectionType || port.typeConstraint != staticPort.typeConstraint) { 51 | // If port is not dynamic and direction hasn't changed, add it to the list so we can try reconnecting the ports connections. 52 | if (!port.IsDynamic && port.direction == staticPort.direction) removedPorts.Add(port.fieldName, port.GetConnections()); 53 | port.ClearConnections(); 54 | ports.Remove(port.fieldName); 55 | } else port.ValueType = staticPort.ValueType; 56 | } 57 | // If port doesn't exist anymore, remove it 58 | else if (port.IsStatic) { 59 | //See if the field is tagged with FormerlySerializedAs, if so add the port with its new field name to removedPorts 60 | // so it can be reconnected in missing ports stage. 61 | string newName = null; 62 | if (formerlySerializedAs != null && formerlySerializedAs.TryGetValue(port.fieldName, out newName)) removedPorts.Add(newName, port.GetConnections()); 63 | 64 | port.ClearConnections(); 65 | ports.Remove(port.fieldName); 66 | } 67 | // If the port is dynamic and is managed by a dynamic port list, flag it for reference updates 68 | else if (IsDynamicListPort(port)) { 69 | dynamicListPorts.Add(port); 70 | } 71 | } 72 | // Add missing ports 73 | foreach (NodePort staticPort in staticPorts.Values) { 74 | if (!ports.ContainsKey(staticPort.fieldName)) { 75 | NodePort port = new NodePort(staticPort, node); 76 | //If we just removed the port, try re-adding the connections 77 | List reconnectConnections; 78 | if (removedPorts.TryGetValue(staticPort.fieldName, out reconnectConnections)) { 79 | for (int i = 0; i < reconnectConnections.Count; i++) { 80 | NodePort connection = reconnectConnections[i]; 81 | if (connection == null) continue; 82 | // CAVEAT: Ports connected under special conditions defined in graphEditor.CanConnect overrides will not auto-connect. 83 | // To fix this, this code would need to be moved to an editor script and call graphEditor.CanConnect instead of port.CanConnectTo. 84 | // This is only a problem in the rare edge case where user is using non-standard CanConnect overrides and changes port type of an already connected port 85 | if (port.CanConnectTo(connection)) port.Connect(connection); 86 | } 87 | } 88 | ports.Add(staticPort.fieldName, port); 89 | } 90 | } 91 | 92 | // Finally, make sure dynamic list port settings correspond to the settings of their "backing port" 93 | foreach (NodePort listPort in dynamicListPorts) { 94 | // At this point we know that ports here are dynamic list ports 95 | // which have passed name/"backing port" checks, ergo we can proceed more safely. 96 | string backingPortName = listPort.fieldName.Substring(0, listPort.fieldName.IndexOf(' ')); 97 | NodePort backingPort = staticPorts[backingPortName]; 98 | 99 | // Update port constraints. Creating a new port instead will break the editor, mandating the need for setters. 100 | listPort.ValueType = GetBackingValueType(backingPort.ValueType); 101 | listPort.direction = backingPort.direction; 102 | listPort.connectionType = backingPort.connectionType; 103 | listPort.typeConstraint = backingPort.typeConstraint; 104 | } 105 | } 106 | 107 | /// 108 | /// Extracts the underlying types from arrays and lists, the only collections for dynamic port lists 109 | /// currently supported. If the given type is not applicable (i.e. if the dynamic list port was not 110 | /// defined as an array or a list), returns the given type itself. 111 | /// 112 | private static System.Type GetBackingValueType(System.Type portValType) { 113 | if (portValType.HasElementType) { 114 | return portValType.GetElementType(); 115 | } 116 | if (portValType.IsGenericType && portValType.GetGenericTypeDefinition() == typeof(List<>)) { 117 | return portValType.GetGenericArguments()[0]; 118 | } 119 | return portValType; 120 | } 121 | 122 | /// Returns true if the given port is in a dynamic port list. 123 | private static bool IsDynamicListPort(NodePort port) { 124 | // Ports flagged as "dynamicPortList = true" end up having a "backing port" and a name with an index, but we have 125 | // no guarantee that a dynamic port called "output 0" is an element in a list backed by a static "output" port. 126 | // Thus, we need to check for attributes... (but at least we don't need to look at all fields this time) 127 | string[] fieldNameParts = port.fieldName.Split(' '); 128 | if (fieldNameParts.Length != 2) return false; 129 | 130 | FieldInfo backingPortInfo = port.node.GetType().GetField(fieldNameParts[0]); 131 | if (backingPortInfo == null) return false; 132 | 133 | object[] attribs = backingPortInfo.GetCustomAttributes(true); 134 | return attribs.Any(x => { 135 | Node.InputAttribute inputAttribute = x as Node.InputAttribute; 136 | Node.OutputAttribute outputAttribute = x as Node.OutputAttribute; 137 | return inputAttribute != null && inputAttribute.dynamicPortList || 138 | outputAttribute != null && outputAttribute.dynamicPortList; 139 | }); 140 | } 141 | 142 | /// Cache node types 143 | private static void BuildCache() { 144 | portDataCache = new PortDataCache(); 145 | System.Type baseType = typeof(Node); 146 | List nodeTypes = new List(); 147 | System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); 148 | 149 | // Loop through assemblies and add node types to list 150 | foreach (Assembly assembly in assemblies) { 151 | // Skip certain dlls to improve performance 152 | string assemblyName = assembly.GetName().Name; 153 | int index = assemblyName.IndexOf('.'); 154 | if (index != -1) assemblyName = assemblyName.Substring(0, index); 155 | switch (assemblyName) { 156 | // The following assemblies, and sub-assemblies (eg. UnityEngine.UI) are skipped 157 | case "UnityEditor": 158 | case "UnityEngine": 159 | case "Unity": 160 | case "System": 161 | case "mscorlib": 162 | case "Microsoft": 163 | continue; 164 | default: 165 | nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); 166 | break; 167 | } 168 | } 169 | 170 | for (int i = 0; i < nodeTypes.Count; i++) { 171 | CachePorts(nodeTypes[i]); 172 | } 173 | } 174 | 175 | public static List GetNodeFields(System.Type nodeType) { 176 | List fieldInfo = new List(nodeType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)); 177 | 178 | // GetFields doesnt return inherited private fields, so walk through base types and pick those up 179 | System.Type tempType = nodeType; 180 | while ((tempType = tempType.BaseType) != typeof(XNode.Node)) { 181 | FieldInfo[] parentFields = tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); 182 | for (int i = 0; i < parentFields.Length; i++) { 183 | // Ensure that we do not already have a member with this type and name 184 | FieldInfo parentField = parentFields[i]; 185 | if (fieldInfo.TrueForAll(x => x.Name != parentField.Name)) { 186 | fieldInfo.Add(parentField); 187 | } 188 | } 189 | } 190 | return fieldInfo; 191 | } 192 | 193 | private static void CachePorts(System.Type nodeType) { 194 | List fieldInfo = GetNodeFields(nodeType); 195 | 196 | for (int i = 0; i < fieldInfo.Count; i++) { 197 | 198 | //Get InputAttribute and OutputAttribute 199 | object[] attribs = fieldInfo[i].GetCustomAttributes(true); 200 | Node.InputAttribute inputAttrib = attribs.FirstOrDefault(x => x is Node.InputAttribute) as Node.InputAttribute; 201 | Node.OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute; 202 | UnityEngine.Serialization.FormerlySerializedAsAttribute formerlySerializedAsAttribute = attribs.FirstOrDefault(x => x is UnityEngine.Serialization.FormerlySerializedAsAttribute) as UnityEngine.Serialization.FormerlySerializedAsAttribute; 203 | 204 | if (inputAttrib == null && outputAttrib == null) continue; 205 | 206 | if (inputAttrib != null && outputAttrib != null) Debug.LogError("Field " + fieldInfo[i].Name + " of type " + nodeType.FullName + " cannot be both input and output."); 207 | else { 208 | if (!portDataCache.ContainsKey(nodeType)) portDataCache.Add(nodeType, new Dictionary()); 209 | NodePort port = new NodePort(fieldInfo[i]); 210 | portDataCache[nodeType].Add(port.fieldName, port); 211 | } 212 | 213 | if (formerlySerializedAsAttribute != null) { 214 | if (formerlySerializedAsCache == null) formerlySerializedAsCache = new Dictionary>(); 215 | if (!formerlySerializedAsCache.ContainsKey(nodeType)) formerlySerializedAsCache.Add(nodeType, new Dictionary()); 216 | 217 | if (formerlySerializedAsCache[nodeType].ContainsKey(formerlySerializedAsAttribute.oldName)) Debug.LogError("Another FormerlySerializedAs with value '" + formerlySerializedAsAttribute.oldName + "' already exist on this node."); 218 | else formerlySerializedAsCache[nodeType].Add(formerlySerializedAsAttribute.oldName, fieldInfo[i].Name); 219 | } 220 | } 221 | } 222 | 223 | [System.Serializable] 224 | private class PortDataCache : Dictionary> { } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /Scripts/NodeDataCache.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 64ea6af1e195d024d8df0ead1921e517 3 | timeCreated: 1507566823 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/NodeGraph.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace XNode { 6 | /// Base class for all node graphs 7 | [Serializable] 8 | public abstract class NodeGraph : ScriptableObject { 9 | 10 | /// All nodes in the graph. 11 | /// See: 12 | [SerializeField] public List nodes = new List(); 13 | 14 | /// Add a node to the graph by type (convenience method - will call the System.Type version) 15 | public T AddNode() where T : Node { 16 | return AddNode(typeof(T)) as T; 17 | } 18 | 19 | /// Add a node to the graph by type 20 | public virtual Node AddNode(Type type) { 21 | Node.graphHotfix = this; 22 | Node node = ScriptableObject.CreateInstance(type) as Node; 23 | node.graph = this; 24 | nodes.Add(node); 25 | return node; 26 | } 27 | 28 | /// Creates a copy of the original node in the graph 29 | public virtual Node CopyNode(Node original) { 30 | Node.graphHotfix = this; 31 | Node node = ScriptableObject.Instantiate(original); 32 | node.graph = this; 33 | node.ClearConnections(); 34 | nodes.Add(node); 35 | return node; 36 | } 37 | 38 | /// Safely remove a node and all its connections 39 | /// The node to remove 40 | public virtual void RemoveNode(Node node) { 41 | node.ClearConnections(); 42 | nodes.Remove(node); 43 | if (Application.isPlaying) Destroy(node); 44 | } 45 | 46 | /// Remove all nodes and connections from the graph 47 | public virtual void Clear() { 48 | if (Application.isPlaying) { 49 | for (int i = 0; i < nodes.Count; i++) { 50 | if (nodes[i] != null) Destroy(nodes[i]); 51 | } 52 | } 53 | nodes.Clear(); 54 | } 55 | 56 | /// Create a new deep copy of this graph 57 | public virtual XNode.NodeGraph Copy() { 58 | // Instantiate a new nodegraph instance 59 | NodeGraph graph = Instantiate(this); 60 | // Instantiate all nodes inside the graph 61 | for (int i = 0; i < nodes.Count; i++) { 62 | if (nodes[i] == null) continue; 63 | Node.graphHotfix = graph; 64 | Node node = Instantiate(nodes[i]) as Node; 65 | node.graph = graph; 66 | graph.nodes[i] = node; 67 | } 68 | 69 | // Redirect all connections 70 | for (int i = 0; i < graph.nodes.Count; i++) { 71 | if (graph.nodes[i] == null) continue; 72 | foreach (NodePort port in graph.nodes[i].Ports) { 73 | port.Redirect(nodes, graph.nodes); 74 | } 75 | } 76 | 77 | return graph; 78 | } 79 | 80 | protected virtual void OnDestroy() { 81 | // Remove all nodes prior to graph destruction 82 | Clear(); 83 | } 84 | 85 | #region Attributes 86 | /// Automatically ensures the existance of a certain node type, and prevents it from being deleted. 87 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] 88 | public class RequireNodeAttribute : Attribute { 89 | public Type type0; 90 | public Type type1; 91 | public Type type2; 92 | 93 | /// Automatically ensures the existance of a certain node type, and prevents it from being deleted 94 | public RequireNodeAttribute(Type type) { 95 | this.type0 = type; 96 | this.type1 = null; 97 | this.type2 = null; 98 | } 99 | 100 | /// Automatically ensures the existance of a certain node type, and prevents it from being deleted 101 | public RequireNodeAttribute(Type type, Type type2) { 102 | this.type0 = type; 103 | this.type1 = type2; 104 | this.type2 = null; 105 | } 106 | 107 | /// Automatically ensures the existance of a certain node type, and prevents it from being deleted 108 | public RequireNodeAttribute(Type type, Type type2, Type type3) { 109 | this.type0 = type; 110 | this.type1 = type2; 111 | this.type2 = type3; 112 | } 113 | 114 | public bool Requires(Type type) { 115 | if (type == null) return false; 116 | if (type == type0) return true; 117 | else if (type == type1) return true; 118 | else if (type == type2) return true; 119 | return false; 120 | } 121 | } 122 | #endregion 123 | } 124 | } -------------------------------------------------------------------------------- /Scripts/NodeGraph.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 093f68ef2455d544fa2d14b80c811322 3 | timeCreated: 1505461376 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/NodePort.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using UnityEngine; 5 | 6 | namespace XNode { 7 | [Serializable] 8 | public class NodePort { 9 | public enum IO { Input, Output } 10 | 11 | public int ConnectionCount { get { return connections.Count; } } 12 | /// Return the first non-null connection 13 | public NodePort Connection { 14 | get { 15 | for (int i = 0; i < connections.Count; i++) { 16 | if (connections[i] != null) return connections[i].Port; 17 | } 18 | return null; 19 | } 20 | } 21 | 22 | public IO direction { 23 | get { return _direction; } 24 | internal set { _direction = value; } 25 | } 26 | public Node.ConnectionType connectionType { 27 | get { return _connectionType; } 28 | internal set { _connectionType = value; } 29 | } 30 | public Node.TypeConstraint typeConstraint { 31 | get { return _typeConstraint; } 32 | internal set { _typeConstraint = value; } 33 | } 34 | 35 | /// Is this port connected to anytihng? 36 | public bool IsConnected { get { return connections.Count != 0; } } 37 | public bool IsInput { get { return direction == IO.Input; } } 38 | public bool IsOutput { get { return direction == IO.Output; } } 39 | 40 | public string fieldName { get { return _fieldName; } } 41 | public Node node { get { return _node; } } 42 | public bool IsDynamic { get { return _dynamic; } } 43 | public bool IsStatic { get { return !_dynamic; } } 44 | public Type ValueType { 45 | get { 46 | if (valueType == null && !string.IsNullOrEmpty(_typeQualifiedName)) valueType = Type.GetType(_typeQualifiedName, false); 47 | return valueType; 48 | } 49 | set { 50 | if (valueType == value) return; 51 | valueType = value; 52 | if (value != null) _typeQualifiedName = NodeDataCache.GetTypeQualifiedName(value); 53 | } 54 | } 55 | private Type valueType; 56 | 57 | [SerializeField] private string _fieldName; 58 | [SerializeField] private Node _node; 59 | [SerializeField] private string _typeQualifiedName; 60 | [SerializeField] private List connections = new List(); 61 | [SerializeField] private IO _direction; 62 | [SerializeField] private Node.ConnectionType _connectionType; 63 | [SerializeField] private Node.TypeConstraint _typeConstraint; 64 | [SerializeField] private bool _dynamic; 65 | 66 | /// Construct a static targetless nodeport. Used as a template. 67 | public NodePort(FieldInfo fieldInfo) { 68 | _fieldName = fieldInfo.Name; 69 | ValueType = fieldInfo.FieldType; 70 | _dynamic = false; 71 | var attribs = fieldInfo.GetCustomAttributes(false); 72 | for (int i = 0; i < attribs.Length; i++) { 73 | if (attribs[i] is Node.InputAttribute) { 74 | _direction = IO.Input; 75 | _connectionType = (attribs[i] as Node.InputAttribute).connectionType; 76 | _typeConstraint = (attribs[i] as Node.InputAttribute).typeConstraint; 77 | } else if (attribs[i] is Node.OutputAttribute) { 78 | _direction = IO.Output; 79 | _connectionType = (attribs[i] as Node.OutputAttribute).connectionType; 80 | _typeConstraint = (attribs[i] as Node.OutputAttribute).typeConstraint; 81 | } 82 | // Override ValueType of the Port 83 | if(attribs[i] is PortTypeOverrideAttribute) { 84 | ValueType = (attribs[i] as PortTypeOverrideAttribute).type; 85 | } 86 | } 87 | } 88 | 89 | /// Copy a nodePort but assign it to another node. 90 | public NodePort(NodePort nodePort, Node node) { 91 | _fieldName = nodePort._fieldName; 92 | ValueType = nodePort.valueType; 93 | _direction = nodePort.direction; 94 | _dynamic = nodePort._dynamic; 95 | _connectionType = nodePort._connectionType; 96 | _typeConstraint = nodePort._typeConstraint; 97 | _node = node; 98 | } 99 | 100 | /// Construct a dynamic port. Dynamic ports are not forgotten on reimport, and is ideal for runtime-created ports. 101 | public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node.TypeConstraint typeConstraint, Node node) { 102 | _fieldName = fieldName; 103 | this.ValueType = type; 104 | _direction = direction; 105 | _node = node; 106 | _dynamic = true; 107 | _connectionType = connectionType; 108 | _typeConstraint = typeConstraint; 109 | } 110 | 111 | /// Checks all connections for invalid references, and removes them. 112 | public void VerifyConnections() { 113 | for (int i = connections.Count - 1; i >= 0; i--) { 114 | if (connections[i].node != null && 115 | !string.IsNullOrEmpty(connections[i].fieldName) && 116 | connections[i].node.GetPort(connections[i].fieldName) != null) 117 | continue; 118 | connections.RemoveAt(i); 119 | } 120 | } 121 | 122 | /// Return the output value of this node through its parent nodes GetValue override method. 123 | /// 124 | public object GetOutputValue() { 125 | if (direction == IO.Input) return null; 126 | return node.GetValue(this); 127 | } 128 | 129 | /// Return the output value of the first connected port. Returns null if none found or invalid. 130 | /// 131 | public object GetInputValue() { 132 | NodePort connectedPort = Connection; 133 | if (connectedPort == null) return null; 134 | return connectedPort.GetOutputValue(); 135 | } 136 | 137 | /// Return the output values of all connected ports. 138 | /// 139 | public object[] GetInputValues() { 140 | object[] objs = new object[ConnectionCount]; 141 | for (int i = 0; i < ConnectionCount; i++) { 142 | NodePort connectedPort = connections[i].Port; 143 | if (connectedPort == null) { // if we happen to find a null port, remove it and look again 144 | connections.RemoveAt(i); 145 | i--; 146 | continue; 147 | } 148 | objs[i] = connectedPort.GetOutputValue(); 149 | } 150 | return objs; 151 | } 152 | 153 | /// Return the output value of the first connected port. Returns null if none found or invalid. 154 | /// 155 | public T GetInputValue() { 156 | object obj = GetInputValue(); 157 | return obj is T ? (T) obj : default(T); 158 | } 159 | 160 | /// Return the output values of all connected ports. 161 | /// 162 | public T[] GetInputValues() { 163 | object[] objs = GetInputValues(); 164 | T[] ts = new T[objs.Length]; 165 | for (int i = 0; i < objs.Length; i++) { 166 | if (objs[i] is T) ts[i] = (T) objs[i]; 167 | } 168 | return ts; 169 | } 170 | 171 | /// Return true if port is connected and has a valid input. 172 | /// 173 | public bool TryGetInputValue(out T value) { 174 | object obj = GetInputValue(); 175 | if (obj is T) { 176 | value = (T) obj; 177 | return true; 178 | } else { 179 | value = default(T); 180 | return false; 181 | } 182 | } 183 | 184 | /// Return the sum of all inputs. 185 | /// 186 | public float GetInputSum(float fallback) { 187 | object[] objs = GetInputValues(); 188 | if (objs.Length == 0) return fallback; 189 | float result = 0; 190 | for (int i = 0; i < objs.Length; i++) { 191 | if (objs[i] is float) result += (float) objs[i]; 192 | } 193 | return result; 194 | } 195 | 196 | /// Return the sum of all inputs. 197 | /// 198 | public int GetInputSum(int fallback) { 199 | object[] objs = GetInputValues(); 200 | if (objs.Length == 0) return fallback; 201 | int result = 0; 202 | for (int i = 0; i < objs.Length; i++) { 203 | if (objs[i] is int) result += (int) objs[i]; 204 | } 205 | return result; 206 | } 207 | 208 | /// Connect this to another 209 | /// The to connect to 210 | public void Connect(NodePort port) { 211 | if (connections == null) connections = new List(); 212 | if (port == null) { Debug.LogWarning("Cannot connect to null port"); return; } 213 | if (port == this) { Debug.LogWarning("Cannot connect port to self."); return; } 214 | if (IsConnectedTo(port)) { Debug.LogWarning("Port already connected. "); return; } 215 | if (direction == port.direction) { Debug.LogWarning("Cannot connect two " + (direction == IO.Input ? "input" : "output") + " connections"); return; } 216 | #if UNITY_EDITOR 217 | UnityEditor.Undo.RecordObject(node, "Connect Port"); 218 | UnityEditor.Undo.RecordObject(port.node, "Connect Port"); 219 | #endif 220 | if (port.connectionType == Node.ConnectionType.Override && port.ConnectionCount != 0) { port.ClearConnections(); } 221 | if (connectionType == Node.ConnectionType.Override && ConnectionCount != 0) { ClearConnections(); } 222 | connections.Add(new PortConnection(port)); 223 | if (port.connections == null) port.connections = new List(); 224 | if (!port.IsConnectedTo(this)) port.connections.Add(new PortConnection(this)); 225 | node.OnCreateConnection(this, port); 226 | port.node.OnCreateConnection(this, port); 227 | } 228 | 229 | public List GetConnections() { 230 | List result = new List(); 231 | for (int i = 0; i < connections.Count; i++) { 232 | NodePort port = GetConnection(i); 233 | if (port != null) result.Add(port); 234 | } 235 | return result; 236 | } 237 | 238 | public NodePort GetConnection(int i) { 239 | //If the connection is broken for some reason, remove it. 240 | if (connections[i].node == null || string.IsNullOrEmpty(connections[i].fieldName)) { 241 | connections.RemoveAt(i); 242 | return null; 243 | } 244 | NodePort port = connections[i].node.GetPort(connections[i].fieldName); 245 | if (port == null) { 246 | connections.RemoveAt(i); 247 | return null; 248 | } 249 | return port; 250 | } 251 | 252 | /// Get index of the connection connecting this and specified ports 253 | public int GetConnectionIndex(NodePort port) { 254 | for (int i = 0; i < ConnectionCount; i++) { 255 | if (connections[i].Port == port) return i; 256 | } 257 | return -1; 258 | } 259 | 260 | public bool IsConnectedTo(NodePort port) { 261 | for (int i = 0; i < connections.Count; i++) { 262 | if (connections[i].Port == port) return true; 263 | } 264 | return false; 265 | } 266 | 267 | /// Returns true if this port can connect to specified port 268 | public bool CanConnectTo(NodePort port) { 269 | // Figure out which is input and which is output 270 | NodePort input = null, output = null; 271 | if (IsInput) input = this; 272 | else output = this; 273 | if (port.IsInput) input = port; 274 | else output = port; 275 | // If there isn't one of each, they can't connect 276 | if (input == null || output == null) return false; 277 | // Check input type constraints 278 | if (input.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false; 279 | if (input.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false; 280 | if (input.typeConstraint == XNode.Node.TypeConstraint.InheritedInverse && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; 281 | if (input.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; 282 | // Check output type constraints 283 | if (output.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false; 284 | if (output.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false; 285 | if (output.typeConstraint == XNode.Node.TypeConstraint.InheritedInverse && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; 286 | if (output.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; 287 | // Success 288 | return true; 289 | } 290 | 291 | /// Disconnect this port from another port 292 | public void Disconnect(NodePort port) { 293 | // Remove this ports connection to the other 294 | for (int i = connections.Count - 1; i >= 0; i--) { 295 | if (connections[i].Port == port) { 296 | connections.RemoveAt(i); 297 | } 298 | } 299 | if (port != null) { 300 | // Remove the other ports connection to this port 301 | for (int i = 0; i < port.connections.Count; i++) { 302 | if (port.connections[i].Port == this) { 303 | port.connections.RemoveAt(i); 304 | // Trigger OnRemoveConnection from this side port 305 | port.node.OnRemoveConnection(port); 306 | } 307 | } 308 | } 309 | // Trigger OnRemoveConnection 310 | node.OnRemoveConnection(this); 311 | } 312 | 313 | /// Disconnect this port from another port 314 | public void Disconnect(int i) { 315 | // Remove the other ports connection to this port 316 | NodePort otherPort = connections[i].Port; 317 | if (otherPort != null) { 318 | otherPort.connections.RemoveAll(it => { return it.Port == this; }); 319 | } 320 | // Remove this ports connection to the other 321 | connections.RemoveAt(i); 322 | 323 | // Trigger OnRemoveConnection 324 | node.OnRemoveConnection(this); 325 | if (otherPort != null) otherPort.node.OnRemoveConnection(otherPort); 326 | } 327 | 328 | public void ClearConnections() { 329 | while (connections.Count > 0) { 330 | Disconnect(connections[0].Port); 331 | } 332 | } 333 | 334 | /// Get reroute points for a given connection. This is used for organization 335 | public List GetReroutePoints(int index) { 336 | return connections[index].reroutePoints; 337 | } 338 | 339 | /// Swap connections with another node 340 | public void SwapConnections(NodePort targetPort) { 341 | int aConnectionCount = connections.Count; 342 | int bConnectionCount = targetPort.connections.Count; 343 | 344 | List portConnections = new List(); 345 | List targetPortConnections = new List(); 346 | 347 | // Cache port connections 348 | for (int i = 0; i < aConnectionCount; i++) 349 | portConnections.Add(connections[i].Port); 350 | 351 | // Cache target port connections 352 | for (int i = 0; i < bConnectionCount; i++) 353 | targetPortConnections.Add(targetPort.connections[i].Port); 354 | 355 | ClearConnections(); 356 | targetPort.ClearConnections(); 357 | 358 | // Add port connections to targetPort 359 | for (int i = 0; i < portConnections.Count; i++) 360 | targetPort.Connect(portConnections[i]); 361 | 362 | // Add target port connections to this one 363 | for (int i = 0; i < targetPortConnections.Count; i++) 364 | Connect(targetPortConnections[i]); 365 | 366 | } 367 | 368 | /// Copy all connections pointing to a node and add them to this one 369 | public void AddConnections(NodePort targetPort) { 370 | int connectionCount = targetPort.ConnectionCount; 371 | for (int i = 0; i < connectionCount; i++) { 372 | PortConnection connection = targetPort.connections[i]; 373 | NodePort otherPort = connection.Port; 374 | Connect(otherPort); 375 | } 376 | } 377 | 378 | /// Move all connections pointing to this node, to another node 379 | public void MoveConnections(NodePort targetPort) { 380 | int connectionCount = connections.Count; 381 | 382 | // Add connections to target port 383 | for (int i = 0; i < connectionCount; i++) { 384 | PortConnection connection = targetPort.connections[i]; 385 | NodePort otherPort = connection.Port; 386 | Connect(otherPort); 387 | } 388 | ClearConnections(); 389 | } 390 | 391 | /// Swap connected nodes from the old list with nodes from the new list 392 | public void Redirect(List oldNodes, List newNodes) { 393 | foreach (PortConnection connection in connections) { 394 | int index = oldNodes.IndexOf(connection.node); 395 | if (index >= 0) connection.node = newNodes[index]; 396 | } 397 | } 398 | 399 | [Serializable] 400 | private class PortConnection { 401 | [SerializeField] public string fieldName; 402 | [SerializeField] public Node node; 403 | public NodePort Port { get { return port != null ? port : port = GetPort(); } } 404 | 405 | [NonSerialized] private NodePort port; 406 | /// Extra connection path points for organization 407 | [SerializeField] public List reroutePoints = new List(); 408 | 409 | public PortConnection(NodePort port) { 410 | this.port = port; 411 | node = port.node; 412 | fieldName = port.fieldName; 413 | } 414 | 415 | /// Returns the port that this points to 416 | private NodePort GetPort() { 417 | if (node == null || string.IsNullOrEmpty(fieldName)) return null; 418 | return node.GetPort(fieldName); 419 | } 420 | } 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /Scripts/NodePort.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7dd2f76ac25c6f44c9426dff3e7491a3 3 | timeCreated: 1505734054 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/SceneGraph.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using XNode; 5 | 6 | namespace XNode { 7 | /// Lets you instantiate a node graph in the scene. This allows you to reference in-scene objects. 8 | public class SceneGraph : MonoBehaviour { 9 | public NodeGraph graph; 10 | } 11 | 12 | /// Derive from this class to create a SceneGraph with a specific graph type. 13 | /// 14 | /// 15 | /// public class MySceneGraph : SceneGraph { 16 | /// 17 | /// } 18 | /// 19 | /// 20 | public class SceneGraph : SceneGraph where T : NodeGraph { 21 | public new T graph { get { return base.graph as T; } set { base.graph = value; } } 22 | } 23 | } -------------------------------------------------------------------------------- /Scripts/SceneGraph.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7915171fc13472a40a0162003052d2db 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/XNode.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "XNode", 3 | "references": [], 4 | "optionalUnityReferences": [], 5 | "includePlatforms": [], 6 | "excludePlatforms": [], 7 | "allowUnsafeCode": false, 8 | "overrideReferences": false, 9 | "precompiledReferences": [], 10 | "autoReferenced": true, 11 | "defineConstraints": [], 12 | "versionDefines": [] 13 | } 14 | -------------------------------------------------------------------------------- /Scripts/XNode.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b8e24fd1eb19b4226afebb2810e3c19b 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.github.siccity.xnode", 3 | "description": "xNode provides a set of APIs and an editor interface for creating and editing custom node graphs.", 4 | "version": "1.8.0", 5 | "unity": "2018.1", 6 | "displayName": "xNode" 7 | } 8 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e9869d68f06b74538a01e9b8e406159e 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------