Search Unity

  1. Unity 2020.1 has been released.
    Dismiss Notice
  2. Good news ✨ We have more Unite Now videos available for you to watch on-demand! Come check them out and ask our experts any questions!
    Dismiss Notice

Simple node editor

Discussion in 'Immediate Mode GUI (IMGUI)' started by unimechanic, Jul 5, 2013.

  1. Cognetic


    Jun 15, 2017
    I have encountered an issue when porting to ios recently that I have a fix for but i'd call it pretty unsatisfactory. I'm sure there is a cleaner fix I just don't have time to go through it.

    The Problem:
    The dynamic port system saves its object list with info such as:
    "Object refID="-750799744" type="System.Collections.Generic.List`1[[System.String, mscorlib, Version=, Culture=neutral, PublicKeyToken" etc

    This seems to be an issue because I work on windows and obviously have to compile the ios version in xcode on a mac. Its a problem because the info changes for the ios build(possibly because i have to target a different .NET framework) meaning the GetType method fails when reading data from a different compile and the xml doesn't load.

    The Solution
    My simple solution is to manually set the Type rather than using GetType to set it. So, in the XMLImportExport.cs specifically the ImportData method under the //OBJECTS section I insert a manual change for any relevant types.

    Code (CSharp):
    2.      Type type = typeof(List<string>);
    3.                     //this deal with Mac ios issues of assembly mismatch
    4.                   if (typeName.Contains("System.Collections.Generic.List`1[[System.Collections.Generic.List`1[[System.String, mscorlib, Version=, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"))
    5.                     {
    6.                         typeName = "System.Collections.Generic.List`1[System.Collections.Generic.List`1[System.String]]";
    7.                         type = typeof(List<List<string>>);
    8.                     }
    9.                     else if (typeName.Contains("System.Collections.Generic.List`1[[System.String, mscorlib,"))
    10.                     {
    11.                         typeName = "System.Collections.Generic.List`1[System.String]";
    12.                         type = typeof(List<string>);
    13.                     }
    14.                     else if (typeName.Contains("System.Collections.Generic.List`1[[System.Collections.Generic.List`1[[System.Int32, mscorlib, Version=, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"))
    15.                     {
    16.                         typeName = "System.Collections.Generic.List`1[System.Collections.Generic.List`1[System.Int32]]";
    17.                         type = typeof(List<List<Int32>>);
    19.                     }
    20.                     else if(typeName.Contains("System.Collections.Generic.List`1[[System.Int32, mscorlib,") )
    21.                     {
    22.                         typeName = "System.Collections.Generic.List`1[System.Int32]";
    23.                         type = typeof(List<Int32>);
    24.                     }
    25.                     else
    26.                     {
    27.                         type = Type.GetType(typeName, true);
    28.                     }
    Anyways, I'm sure there are better solutions or reasons I've missed for why the info is changing, you could also not save that info in the first place.

    I'm just posting this for an FYI and for any suggestions for a better way of approaching the issue.
    Seneral likes this.
  2. gangafinti


    Jun 13, 2013
    I was making a hotkey to focus a node but I noticed that I cannot just set the panning to the node position but I have to invert the node position to get the panning right and have the node in the center of the canvas. But I don't quite understand why? Why do I need to invert the position of the node? Did I just do a space transformation from ? space to ? space? Because the nodes are already in canvas space right? And panning is in canvas space also?

    Code (CSharp):
    1.         [HotkeyAttribute(KeyCode.F, EventModifiers.Control, EventType.KeyUp)]
    2.         private static void FocusNode(NodeEditorInputInfo inputInfo)
    3.         {
    4.             if (GUIUtility.keyboardControl > 0)
    5.                 return;
    6.             NodeEditorState state = inputInfo.editorState;
    7.             if (state.selectedNode != null)
    8.             {
    9.                 inputInfo.SetAsCurrentEnvironment();
    10.                 state.panOffset = new Vector2(-state.selectedNode.position.x, -state.selectedNode.position.y);
    11.                 inputInfo.inputEvent.Use();
    12.             }
    13.         }
    Seneral likes this.
  3. Seneral


    Jun 2, 2014
    Hm it does seem unintuitive I guess, and there's no strong reason for it to be that way anymore. It is basically setting the center of the canvas to the center of the window + panOffset -- NOT the center of the window to the center of the canvas + panOffset, which would be what you expected.
    But yeah, sometimes you need space transformations that can be VERY confusing, even to me - luckily pretty rarely.
    Hope it didn't cause you too much headache!
  4. undead_ooze


    Apr 24, 2018
    I have been using this for a project adapting the dialogue example, but I'm having an issue with saving and exporting/importing the canvas - I have to use a lot of game object references from a scene, and every time I open the canvas those references are lost. I figured exporting an xml and re importing it would work, but I get an error when I try exporting to xml. Anyone else having this issue or does anyone have any ideas on how to solve the issue with the scene references?
  5. Seneral


    Jun 2, 2014
    This is expected, since the canvas - by default - is an asset saved as a file, and these can never reference objects from scenes (only prefabs and such). If you saved the canvas in the scene instead (it basically living on a hidden object in the scene) it will retain the references, but only for that scene obviously. You can find the functionality next in the same dropdown and it will list all canvases saved in the current scene.
    If you need to have the canvas in multiple scenes, with different references each, but want the core structure synchronized, you might want to look into a different possibility: Find a way of tagging objects and adapt the nodes themselves to fetch the references when needed by name/tag.

    Note: I can't remember when I implemented this, quite a while ago, so don't know for sure how it behaves with multiple scenes loaded at once - probably can only access the canvases of one scene, best to test it out before!
    Note 2: You mentioned Importing/Exporting aswell, I assume the XML way? If that's the case, the XML format can't reference ANY external assets, be it in the scene or even another asset, it will attempt to serialize everything as XML and embed it which will only work for some objects, but certainly not game objects...
  6. undead_ooze


    Apr 24, 2018
    Thank you for your quick response! I had to deal with classes last week, so it took me a while to test this again.

    That was the first thing I thought, and I have tried this, but doesn't seem to work properly unless I'm doing something wrong. I only need it to be saved to individual scenes, which would work fine like this. However the save isn't working as expected - the object shows in there, the canvas saved is set as last session, and when l open the editor with a different canvas open the option to load canvas from scene is greyed out. Also if I open the node editor on a different scene the canvas is cleared out, and going back to the scene where it was saved does not allow me to load it.

    Another issue is that clicking on a selection will sometimes open it, but without actually being selected - it's hard to explain, but say, if there is a dropdown and I click it directly before clicking the node first and making sure it's selected the dropdown will open, but whatever selection I make won't actually stick.
  7. Seneral


    Jun 2, 2014
    Hm works for me as far as I tested. Only have 2018.2.16 installed right now.
    Try this in a new scene: Load any canvas (Calc example) and save it to the scene with a new name (this should also give a warning saying it is converted to a scene canvas from an asset).
    An object NodeEditor_SceneSaveHolder will appear in the scene (thought I made it hidden, but whatever, probably decided against it for debugging purposes). It should have two components with your newly saved canvas and a lastSession canvas. Now move the nodes around a bit, and save it in the scene under a new name. Now you should be able to see and load both canvases from the scene (not the lastSession obviously).
    After saving the scene and creating a new one, the window should indeed create a new canvas since the previously opened one cannot be found anymore (as it lives in the previously closed scene).
    Loading the scene again should allow you to load both test canvases again though, since they are stored in the NodeEditor_SceneSaveHolder object.

    If anything is different from what I described, please tell me exactly where and how. Maybe it's the unity version, haven't updated in a while, so will check that too.
  8. undead_ooze


    Apr 24, 2018
    I managed to get it working - seems the problem is that saving it once doesn't create the 2 components, it only creates the lastSession one, saving it again creates the second one. I also get no warning for the conversion to a scene canvas. In order to load it I have to open it through the lastSession component, and then Load canvas from scene using whatever save name I used - I haven't tested enough times to be certain of where it goes wrong if I don't follow these steps.

    Not sure if this is as intended, but once a canvas is loaded the components sometimes disappear, so technically I can't load a previous save in this case.

    I'm using 2018.3.3f1 right now. One thing that I noticed once I updated to this version is that the UI changed the colors of some elements - the save canvas to scene dialogue box is all light gray, which makes it hard to read the buttons.

    Thank you for your help so far!
  9. Seneral


    Jun 2, 2014
    Hm that's definitely unintended behaviour, probably from the new version. Haven't followed new versions for a while now so no clue. The UI color issue seems to happen sometimes due to either version incompabilities or import errors of the textures involved. When you say the dialogue box, you mean the one popping up on the canvas? As far as I remember that is using the same style as a node body, but these are fine?
  10. th3flyboy


    May 28, 2016
    I can confirm I encountered the same issue on my end as well with it being light grey.
  11. gangafinti


    Jun 13, 2013
    Hi, I was subscribing the node canvas to events and I noticed that when I add the event the subscribed method gets called multiple times. There is always one instance of the NodeCanvas right, because I logged the instance id of the object that has the method and I get back multile id's. How is this possible? When creating the working copy of the canvas, does the previous canvas get deleted?
  12. Seneral


    Jun 2, 2014
    The working copy is only created so that the canvas that is actively worked on is NOT the one in the AssetDatabase, incase something goes wrong. By default there should be no reason the same event for the canvas that is edited is repeated for the canvas that is saved - it is not even referenced.
    I can't check right now whether this happens due to the working copy or for some other reason, but since you mentioned you verified they are two different objects, that seems to be the only solution.
    Can you check if this happens with all events (or if not, for which it does)?
  13. gangafinti


    Jun 13, 2013
    I created a method OnEditorStartup() in NodeCanvas. I call this on the canvas cache when the editor gets opened. In this method I unsubscribe and the subscribe to all events in NodeEditorCalbacks(made them events so you cannot say Action<Node> = null). I subscribe the method OnNodeSelected() to NodeEditorCalbacks.OnSelectNode(). I know for sure this callback gets called only once. Then in the OnNodeSelected() method I debug the instance ID. And the first time I startup the editor it gets called once. But when I restart the editor it gets called twice with different ID's and this increments. This must mean there are multiple canvases right? I am now looking for the problem because I believe there are multiple canvases.

    // code in NodeEditorWindow

    Code (CSharp):
    1.         [MenuItem("Window/Story Editor")]
    2.         public static NodeEditorWindow OpenNodeEditor()
    3.         {
    4.             editorWindow = GetWindow<NodeEditorWindow>();
    5.             editorWindow.minSize = new Vector2(400, 200);
    7.             NodeEditor.ReInit(false);
    8.             editorWindow.canvasCache.nodeCanvas.OnEditorStartup();
    9.             Texture iconTexture = ResourceManager.LoadTexture(EditorGUIUtility.isProSkin ? "Textures/Icon_Dark.png" : "Textures/Icon_Light.png");
    10.             editorWindow.titleContent = new GUIContent("Story Editor", iconTexture);
    12.             return editorWindow;
    13.         }
    // code in story canvas

    Code (CSharp):
    1.         public override void OnEditorStartup()
    2.         {
    3.             Unsubscribe();
    4.             Subscribe();
    5.             EffectTypes.FetchEffectTypes();
    6.             SetupGUI();
    7.             base.OnEditorStartup();
    8.         }
    10.         protected void Subscribe()
    11.         {
    12.             NodeEditorCallbacks.OnDeleteNode += OnNodeDeleted;
    13.             NodeEditorCallbacks.OnAddNode += OnNodeCreated;
    14.             NodeEditorCallbacks.OnSelectNode += OnNodeSelected;
    15.         }
    17.         protected void Unsubscribe()
    18.         {
    19.             NodeEditorCallbacks.OnDeleteNode -= OnNodeDeleted;
    20.             NodeEditorCallbacks.OnAddNode -= OnNodeCreated;
    21.             NodeEditorCallbacks.OnSelectNode -= OnNodeSelected;
    22.         }
    Last edited: Mar 27, 2019
  14. gangafinti


    Jun 13, 2013
    AddClonedSO creates a new instance of a scriptable object but never deletes the old one am I correct? Could this be the problem? The objects still remain in memory right?
  15. Seneral


    Jun 2, 2014
    Just an idea, but have you verified that Subscribe/Unsubscribe are called exactly once each? With events (depending if events or UnityEvents) you should be able to verify the amount of listeners to be 1.
    Long time not worked with events of the like but when you open the editor for the second time, you remove the same methods again but they are from different instances of Canvas so it won't actually remove the previous listener. Not sure though. Try clearing it directly.
    The event system is not really made to handle multiple canvases at the same time so all subscribed methods will receive all events, even if it isn't on the same canvas.

    On the other hand, I dont know why the previously loaded canvas would still be generating events. After it is saved to lastSession it is closed and no code runs, then after the editor opens again it will work on a working copy of that lastSession but the ties should be broken completely.

    No, it's correct. The way the canvas are structured is that the canvas and all nodes and ports are SOs referencing each other. When creating a working copy first the canvas is copied, bit the referencs will still point to the old nodes. Afterwards, all nodes are cloned and recorded in pairs with their uncloned versions (AddClonedSO). Finally all references in the cloned canvas are replaced using these lists as lookup tables to replace all references to SOs with the newly cloned ones.

    That works fine until you add own SOs that should clone alongside the canvas. In that case, there are two methods you need to override to handle this. But even failing to do that should not cause any events to be duplicated.
  16. gangafinti


    Jun 13, 2013
    Well, I created a little test, and it appears that even if you delete a scriptable object it's method will still be called. So the canvas probably is another instance and the previous one is gone. But it's method will still be called. So I probably should unsub that canvas before saving and then sub the new canvas after saving.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    5. public class TestScripableObject : ScriptableObject {
    7.     private void Awake()
    8.     {
    9.         TestCallbackReciever.testEvent += OnTest;
    10.     }
    12.     private void OnTest()
    13.     {
    14.         Debug.Log(this.GetInstanceID());
    15.     }
    16. }
    19. using System.Collections;
    20. using System.Collections.Generic;
    21. using UnityEngine;
    23. public class Test : MonoBehaviour
    24. {
    25.     public void Start()
    26.     {
    27.         ScriptableObject so = ScriptableObject.CreateInstance(typeof(TestScripableObject));
    28.         TestCallbackReciever.OnIssueTestEvent();
    30.         ScriptableObject so2 = ScriptableObject.CreateInstance(typeof(TestScripableObject));
    31.         DestroyImmediate(so);
    32.         TestCallbackReciever.OnIssueTestEvent();
    33.     }
    34. }
    36. using System.Collections;
    37. using System.Collections.Generic;
    38. using UnityEngine;
    39. using System;
    41. public static class TestCallbackReciever
    42. {
    43.     public static event Action testEvent;
    45.     public static void OnIssueTestEvent()
    46.     {
    47.         Action temp = testEvent;
    48.         if (temp != null)
    49.             temp.Invoke();
    50.     }
    51. }
  17. Seneral


    Jun 2, 2014
    Ah yes that makes sense! Good luck going forward:)
  18. rin-chan


    Apr 1, 2017
    First thx for the greatest tool ever. I used it for AI, for customable skill, and for fun, it was awesome.

    But now, I'm trying to make this work in the game scene, I modified a little bit the NodeEditorWindow.cs
    but i dont know what to replace with the "Repaint", I trying with empty method, but the right click function disappeared
    I tried to trick the draw method and failed, then i say, forget the rightclick, i can add the "Insert" menu item beside the "file" menu, not elegant but acceptable.
    I tried to dive in more, until I see the click event part is made with "Attribute",
    Would u put a version which can be executed without unity?

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using System.IO;
    5. using NodeEditorFramework.Utilities;
    6. using System;
    8. namespace NodeEditorFramework.Standard
    9. {
    10.     public class NodeEditorStarter : MonoBehaviour
    11.     {
    12.         // Information about current instance
    13.         private static NodeEditorStarter _editor;
    14.         public static NodeEditorStarter editor { get { AssureEditor(); return _editor; } }
    15.         public static void AssureEditor() { if (_editor == null) _editor = FindObjectOfType<NodeEditorStarter>(); }
    17.         // Canvas cache
    18.         public NodeEditorUserCache canvasCache;
    19.         public NodeEditorInterface editorInterface;
    20.         // GUI
    23.         #region General
    25.         private void Start()
    26.         {
    27.             NodeEditor.ReInit(false);
    28.             Texture iconTexture = ResourceManager.LoadTexture(EditorGUIUtility.isProSkin ? "Textures/Icon_Dark.png" : "Textures/Icon_Light.png");
    29.         }
    32.         private void OnEnable()
    33.         {
    34.             _editor = this;
    35.             NormalReInit();
    37.             // Subscribe to events
    38.             NodeEditor.ClientRepaints -= Repaint;
    39.             NodeEditor.ClientRepaints += Repaint;
    40.             EditorLoadingControl.justLeftPlayMode -= NormalReInit;
    41.             EditorLoadingControl.justLeftPlayMode += NormalReInit;
    42.             EditorLoadingControl.justOpenedNewScene -= NormalReInit;
    43.             EditorLoadingControl.justOpenedNewScene += NormalReInit;
    44.             SceneView.onSceneGUIDelegate -= OnSceneGUI;
    45.             SceneView.onSceneGUIDelegate += OnSceneGUI;
    46.         }
    48.         private void Repaint()
    49.         {
    50.             try
    51.             {
    52.                 GUIUtility.ExitGUI();
    53.             }
    54.             catch
    55.             {
    57.             }
    58.             //throw new NotImplementedException();
    59.         }
    61.         private void OnDestroy()
    62.         {
    63.             // Unsubscribe from events
    64.             NodeEditor.ClientRepaints -= Repaint;
    65.             EditorLoadingControl.justLeftPlayMode -= NormalReInit;
    66.             EditorLoadingControl.justOpenedNewScene -= NormalReInit;
    67.             SceneView.onSceneGUIDelegate -= OnSceneGUI;
    69.             // Clear Cache
    70.             canvasCache.ClearCacheEvents();
    71.         }
    73.         private void OnLostFocus ()
    74.         { // Save any changes made while focussing this window
    75.             // Will also save before possible assembly reload, scene switch, etc. because these require focussing of a different window
    76.             canvasCache.SaveCache();
    77.         }
    79.         private void OnFocus ()
    80.         { // Make sure the canvas hasn't been corrupted externally
    81.             NormalReInit();
    82.         }
    84.         private void NormalReInit()
    85.         {
    86.             NodeEditor.ReInit(false);
    87.             AssureSetup();
    88.             if (canvasCache.nodeCanvas)
    89.                 canvasCache.nodeCanvas.Validate();
    90.         }
    92.         private void AssureSetup()
    93.         {
    94.             if (canvasCache == null)
    95.             { // Create cache
    96.                 canvasCache = new NodeEditorUserCache(Path.GetDirectoryName(AssetDatabase.GetAssetPath(MonoScript.FromMonoBehaviour(this))));
    97.             }
    98.             canvasCache.AssureCanvas();
    99.             if (editorInterface == null)
    100.             { // Setup editor interface
    101.                 editorInterface = new NodeEditorInterface();
    102.                 editorInterface.canvasCache = canvasCache;
    103.                 editorInterface.ShowNotificationAction = ShowNotification;
    104.             }
    105.         }
    107.         private void ShowNotification(GUIContent obj)
    108.         {
    109.             print("ShowNotification");
    111.             //throw new NotImplementedException();
    112.         }
    114.         #endregion
    116.         #region GUI
    118.         private void OnGUI()
    119.         {
    121.             // Initiation
    122.             NodeEditor.checkInit(true);
    123.             if (NodeEditor.InitiationError)
    124.             {
    125.                 GUILayout.Label("Node Editor Initiation failed! Check console for more information!");
    126.                 return;
    127.             }
    128.             AssureEditor ();
    129.             AssureSetup();
    131.             // ROOT: Start Overlay GUI for popups
    132.             OverlayGUI.StartOverlayGUI("NodeEditorWindow");
    134.             // Begin Node Editor GUI and set canvas rect
    135.             NodeEditorGUI.StartNodeGUI(true);
    136.             canvasCache.editorState.canvasRect = new Rect(0,0,3200,1800);
    138.             try
    139.             { // Perform drawing with error-handling
    140.                 NodeEditor.DrawCanvas(canvasCache.nodeCanvas, canvasCache.editorState);
    141.             }
    142.             catch (UnityException e)
    143.             { // On exceptions in drawing flush the canvas to avoid locking the UI
    144.                 canvasCache.NewNodeCanvas();
    145.                 NodeEditor.ReInit(true);
    146.                 Debug.LogError("Unloaded Canvas due to an exception during the drawing phase!");
    147.                 Debug.LogException(e);
    148.             }
    150.             // Draw Interface
    151.             editorInterface.DrawToolbarGUI(new Rect(0, 0, Screen.width, 0));
    152.             editorInterface.DrawModalPanel();
    154.             // End Node Editor GUI
    155.             NodeEditorGUI.EndNodeGUI();
    157.             // END ROOT: End Overlay GUI and draw popups
    158.             OverlayGUI.EndOverlayGUI();
    159.         }
    161.         private void OnSceneGUI(SceneView sceneview)
    162.         {
    163.             AssureSetup();
    164.             if (canvasCache.editorState != null && canvasCache.editorState.selectedNode != null)
    165.                 canvasCache.editorState.selectedNode.OnSceneGUI();
    166.             SceneView.lastActiveSceneView.Repaint();
    167.         }
    169.         #endregion
    170.     }
    171. }
  19. Seneral


    Jun 2, 2014
    I assume you want to have the interface at runtime? In that case, check out RTNodeEditor.cs and put it on an object, it is a runtime version. Of course, limitations apply, dropdowns don't work by default (since they are editor only), in order to save you need to export to XML (but you can load both XML and ScriptableObjects), but apart from that I think it all works.
    Tell me if you have any problems.
  20. ViCoX


    Nov 22, 2013
    Hi Seneral - nice work : )
    Is it possible to do "dynamic" port that's value type is decided when you drag connection to port?
    - J
  21. Seneral


    Jun 2, 2014
    Well, I guess you could make it an object type and cast at runtime. That might not be the best way though, you'd loose appropriate colouring for known types.

    If you want to fix that, here are two more options I can think of:

    Have object-type knob and once you detect a connection on it, create a dynamic knob (lookup docs + example, not much resources for that) of the required type, delete the connection to the object-type knob, and recreate the connection with the newly created one. Depending on your setup you might want to make the object-type knob dynamic aswell, so that you can delete it after a connection has been made, and restore it once the connection to the dynamic knob of specific type has been cleared. Basically switch out the object knob to a specific knob and back. Might require some digging in the NodeEditorInputControls to find the appropriate API calls.

    And a third option is to modify the framework to allow knobs to change their types. Might be easier since the type is saved as a string and there should be methods to re-read the type. Not entirely sure right now but might be your best option if you care about the coloring.
    Or just leave it as-is and take the first option.
  22. rin-chan


    Apr 1, 2017
    Thank for the notice, wasn't worth to try to modifier the source...
    It works as expected, enum drop down is editor only, I add a little script to use tools bar to replace it, the real problem is Object field, I made some Node which u can drag a GameObject in before, is there any smart way to simulate in runtime?

    Code (CSharp):
    1. public static int ToolBar<T>(int v,string[] override_name=null)where T:System.Enum
    2.         {
    3.             if(override_name==null)
    4.             {
    5.                 List<string> toolsnames = new List<string>();
    6.                 foreach (System.Enum i in Enum.GetValues(typeof(T)))
    7.                 {
    8.                     toolsnames.Add(Enum.GetName(typeof(T), i));
    9.                 }
    10.                 override_name = toolsnames.ToArray();
    11.             }
    13.             return GUILayout.Toolbar(v, override_name);
    14.         }
  23. Seneral


    Jun 2, 2014
    That toolbar code is nice! First I thought you added something to the top toolbar which reminds me, you would be able to make a quick and dirty dropdown.
    Currently, I only implemented a proper GenericMenu which is made for callback functions, not values. Ot should be easy to create a wrapper for that I suppose (but it wouldn't be able to highlight the currently selected value in its current state).
    And yes, all those field type inputs don't work as well, forgot about that. The thing is, in game there's no inspector either where you could even look at the GOs. So you're best bet might be either a search filter and list to choose from (maybe as a popup, or similar). Maybe there's already a solution out there or on the AS (as it is for ColorFields). If you only have a limited selection of GOs possible, you'd be able to make a popup (with the same method as the GenericMenu enum selector I told you about).
    Will try to postsome code later, am at uni right now
  24. rin-chan


    Apr 1, 2017
    In fact the object field is just a little part of the biggest problem
    for the moment i'm working on a game which the main idea is
    "Player can use node editor to make his own character's skill and behaviour then directly play with"
    since editing node is a part of the game, i'd like to have a better interface,
    so I plan to make a UGUI version, which nodes include UI components
    click event will be handled by UI, but the logic stays,
    in your opinion, how many and which part of the code i could keep
  25. Seneral


    Jun 2, 2014
    UI is one of the big parts of the project since it took a lot of effort to make it run at runtime. On whim I would say you can delete at least a third of the codebase, maybe?
    Not sure but UGUI is probably not able to run in the editor, but I don't see an easy way to keep both the editor GUI and the UGUI running in the same project without problems. You could create a copy with different namespace but youd need to manually force them (in code) to use separate Nodes (you'll need to change the NodeGUI to some create UI and callback system I assume, no sense in trying to use the same Node implementation). That would also force you to use XML to exchange canvases (and manually edit them to use the nodes of the other namespace respectively).
    In short, don't try, you'll need to switch to your runtime editor for normal editing aswell. Might want to start using XML for all aswell, and there won't be a nice last session save (you could do your own though). Might also relieve you a bit by being able to solely focus on runtime and throw alot of editor-only parts away (except for playmode switch).

    You'd also need to completely redo the input system. Not sure about how you could implement dragging/zooming with UGUI but it probably wont be easy (as for zooming, it's probably still easier than for IMGUI lol).
    For normal buttons you'd probably need callbacks I assume. The NodeGUI could probably be made a prefab instead of creating everything by code.

    These following classes classes are reliant on IMGUI and might need some rework:
    - ConnectionPort, ConnectionKnob and ValueConnectionKnob for knob and bezier drawing
    - Node for body and UI GUI, replace by prefab and callback system?
    - NodeGroup if you need it that is
    - NodeEditor for the drawing loop, need some new logic

    These classes are solely there for the IMGUI implementation and probably aren't needed in their current form:
    - NodeEditorGUI
    - NodeEditorInterface
    - all in Utilities/GUI/
    - NodeEditorWindow in Editor

    Anyway, SlimeQ on Github apparently managed to do all that with NGUI, so it doesn't seem to be impossible, although also far from easy.
    Please tell me if and how you manage to do this! :)
  26. Seneral


    Jun 2, 2014
    Finally got around to implement Undo using one of my other editor scripts, UndoPro.
    Since there are alot of asset developers which are already too far into development to update the framework, I'll write here what you'll have to look out for.

    First, here's the commit. As you can see the actual undo code is kept very small and easy to manage.
    Since UndoPro provides the option for a command-based undo system, I also went with a Reference-based undo. What this means is that undo only works because the nodes that are 'deleted' are actually still there, ready to be integrated into the canvas again. Any changes that are done (except for the Node GUI) is implemented using anonymous functions under the assumption that the reference to that node is still alive.

    What this means is that you have to make sure the ScriptableObjects are alive as long as the Undo records are expected to work. In this case: They have to survive script recompilation and playmode change.
    By default (before the last commit bringing Undo) this framework's cache system saved something called a working copy before any of those events, and upon loading created a working copy as well. This meant that the references are broken.
    So I made a small change to the cache system to save a so called SO memory dump for non-critical saves (saves that are not necessarily expected to survive a crash) and for all others, both a SO memory dump and a proper save of a working copy. Since a simple SO memory dump is actually faster than normal saves, this erases the lag you notice when unfocussing from the node editor window (which is my measure to save before script recompilation).
    The SO memory dump essentially makes sure all SOs ever used in this session (since the loading of the canvas or the last scene switch) are saved as an asset. That information is saved as a list inside NodeCanvas for now and is updated with new SOs whenever a node is created / deleted to make sure it includes all (else we would get broken undo actions).

    For your interest, the actions have to be serialized aswell to survive these events, but UndoPro takes care of that with SerializableAction, which is another of my scripts that can serialize any action, object or type for you (pretty cool if you ask me).

    Once that is taken care of, there are a few rules to abide to when actually recording undo actions with UndoPro. I've made a more detailed (hopefully enough) explanation here, but essentially you need to explicitly assign all data you want to save inside the anonymous functions to local variables right before you declare them. This makes sure they are saved together in such a way that SerializableAction can easily serialize them.

    Here's an example from the commit above:
    Code (csharp):
    1. NodeEditorState state = inputInfo.editorState;
    2. Node prevSelection = state.selectedNode, newSelection = state.focusedNode;
    3. UndoPro.UndoProManager.RecordOperation(
    4.     () => NodeEditorUndoActions.SetNodeSelection(state, newSelection),
    5.     () => NodeEditorUndoActions.SetNodeSelection(state, prevSelection),
    6.     "Node Selection", false, true);
    I recommend to put the actual work (how simple it might be) and the error-checking inside a static function. Makes it very easy to see which data is actually saved (state, newSelection, prevSelection). Note how I assigned them right before. using state.selectedNode directly would NOT work, since then the reference to that state would be kept and used to get it's selectedNode upon execution, you'd NOT store a snapshot of the current selection like is intended here.

    Btw, Node GUI is still handled by the Unity undo system, it works together without problems, so no additional efforts here.

    Quick Note on supported features:
    Every NEF feature except two should be supported:
    - NodeGroup (forgot, does anybody use them?)
    - Scene Canvas (forgot)
    - Runtime (obviously, although I assume you could get it to work at runtime without too much trouble)
    As for scene canvas, the difference would be to save the SO memory dump in the scene. If there's interest, I'll do that. Right now, undo in scene canvases would work, but past undo actions would break upon script recompilation or playmode state change.

    I hope that is clear:)
    Last edited: Jul 1, 2019
  27. CommunityUS


    Sep 2, 2011
    Has the state machine/behavior stuff had any further love?
  28. Seneral


    Jun 2, 2014
    No, unfortunately not, and I don't think I'll have to time to design and implement them properly anytime soon.
    There is the generic ConnectionPorts which could be used to create transitions, bit it might be better to roll your own specialised system.
    To avoid excessive trouble with serialisation/saving system, don't use classes where references are expected to be saved, but either structs (without expectations) or ScriptableObjects (and integrate them properly).
    Then the following is relevant:
    - Input(Creation) through Input system context creation (see docs) or use ConnectionPort
    - Input(Deletion) Don't thing there's an easy solution for now, may need to implement click detection on the transitions
    - Event integration if you need them, just add new ones
    - Undo, see above, if you follow the rules it's easy
    - Drawing (if you don't use ConnectionPort), likely put into NodeEditor.cs to find a good drawing order spot
    - Transition logic: Extend NodeEditorTraversal, it receives good events you can use. Will likely need an interface to your own custom code
    - Custom Canvas: Just for formality, extend Canvas and use your custom NodeEditorTraversal

    I think that's all. If you don't need to design a generic-good-for-all transition system that should be doable.
    CommunityUS likes this.
  29. arturmandas


    Sep 29, 2012
    Hi @Seneral . Thank you for creating this great asset. I am looking for a graph editor solution which works runtime - tried alternatives but they required manual prefab setup for making editor nodes work runtime. I was looking for examples to learn on, but couldn't find a sample project with runtime canvas. The webGL demo looks promising - is it possible to get this project somewhere?

    Also, does NEF support execution delaying/pausing?

    Best regards
  30. Seneral


    Jun 2, 2014
    I currently don't have access to the project files (not home for a few weeks) but it's really simple to recreate.
    Just assign the RTNodeEditor component (example implementation of a runtime Node Editor) to an object, setup the position to fullscreen, then load a bunch of examples in the editor (!) node editor and save them to the scene - now, when you hit play (or build) the runtime editor appears and you can load any pre-saved examples from the scene or XML-saved graphs from outside (in playmode, not build, you can of course also load asset files normally).

    Since NEF is completely IMGUI-based, there are no prefabs to set up. A developer has been successful in adapting it to use NGUI though, if that's what you want - might has it's advantages in some cases.

    There is currently no statemachine-like behaviour implemented, just a calculation-type behaviour.
    You can extend NodeCanvas and NodeCanvasTraversal to easily implement new behaviours though - they receive relevant events and you can do whatever you want there, including reacting to external events like timers, etc.
    Look at 'Default/Calculation Base/' and 'Default/Graph Example/' for reference.
  31. arturmandas


    Sep 29, 2012
  32. tossrock


    Mar 7, 2018
    Strange bug; I have a node that dynamically resizes based on some texture resources loaded, with an output knob per loaded resource. Very similar to the example
    . When laid out horizontally, the node's size can balloon out of control to many times the intended size. This seems to be caused by something in the
    . The very strange part is that it seems to be dependent on the Node's global position in the canvas, as well as the canvas's center relative to the viewport window. If the node's x position is less than it's total desired width (ie, if a node which should be 600 wide is at x = 590), it will start radically expanding in size when the viewport's position brings the edge of the node to less than (width - position) from the right side edge, ie within 10 pixels of the edge.

    Here's a video of the behavior:

    And the code for the node that's doing this:

    Code (CSharp):
    1. [Node(false, "Inputs/Textures")]
    2. public class TextureBankNode : Node
    3. {
    4.     public const string ID = "TextureBankNode";
    5.     public override string GetID => "TextureBankNode";
    6.     public override string Title => "Textures";
    7.     public override Vector2 MinSize => textures != null ? new Vector2(textures.Count * 70, 100) : new Vector2(200, 100);
    8.     public override bool AutoLayout => true;
    10.     public List<Texture2D> textures;
    11.     [System.NonSerialized]
    12.     private Dictionary<string, ValueConnectionKnob> texKnobs;
    15.     public void LoadTextures()
    16.     {
    17.         ValueConnectionKnobAttribute outKnobAttribs = new ValueConnectionKnobAttribute("Output", Direction.Out, typeof(Texture), NodeSide.Bottom);
    18.         textures = new List<Texture2D>(Resources.LoadAll<Texture2D>("PatternTextures"));
    19.         texKnobs = new Dictionary<string, ValueConnectionKnob>();
    20.         foreach (var tex in textures)
    21.         {
    22.             texKnobs[] = CreateValueConnectionKnob(outKnobAttribs);
    23.         }
    24.     }
    26.     public override void NodeGUI()
    27.     {
    29.         GUILayout.BeginHorizontal();
    30.         foreach (var tex in textures)
    31.         {
    32.             GUILayout.BeginVertical();
    33.             NodeUIElements.TexInfo(tex, width:64, height: 64);
    34.             texKnobs[].SetPosition();
    35.             GUILayout.EndVertical();
    36.         }
    37.         GUILayout.EndHorizontal();
    38.     }
    40.     public override bool Calculate()
    41.     {
    42.         if (textures == null || texKnobs == null)
    43.         {
    44.             try
    45.             {
    46.                 LoadTextures();
    47.             } catch (UnityException e)
    48.             {
    49.                 Debug.Log(e+":\n\n"+e.Message);
    50.             }
    51.         }
    52.         foreach (var tex in textures)
    53.         {
    54.             if (texKnobs.ContainsKey(
    55.                 texKnobs[].SetValue(tex);
    56.         }
    57.         return true;
    58.     }
    59. }
    and the simple NodeUI helper class:

    Code (CSharp):
    1. public static class NodeUIElements
    2. {
    3.     const int MAX_NAME_LENGTH = 12;
    4.     public static string TrimName(string name)
    5.     {
    6.         if (name.Length < MAX_NAME_LENGTH)
    7.         {
    8.             return name;
    9.         } else
    10.         {
    11.             return name.Substring(0, 8) + "...";
    12.         }
    13.     }
    15.     public static void TexInfo(Texture tex, float width=0, float height=0, bool showAttribs=true)
    16.     {
    17.         GUILayout.BeginVertical();
    18.         var layoutParams = new List<GUILayoutOption>();
    19.         if (width > 0)
    20.             layoutParams.Add(GUILayout.MaxWidth(width));
    21.         if (height > 0)
    22.             layoutParams.Add(GUILayout.MaxHeight(height));
    23.         GUILayout.Box(tex, layoutParams.ToArray());
    24.         GUILayout.Label("'" + TrimName( + "'");
    25.         GUILayout.Label("Size: " + tex.width + "x" + tex.height + "");
    26.         GUILayout.EndVertical();
    27.     }
    28. }
    Grateful for any help, and for this great framework!
    syscrusher likes this.
  33. Seneral


    Jun 2, 2014
    Ah happens when you don't test - AutoLayout was primarily introduced for vertical layout...
    Horizontal auto-layout is more difficult since it kind of cuts off (as far as I can remember). So it can't really use the last GUI element as vertical placement but only the knobs that are positioned vertically. I used GetGUIKnob instead of GetCanvasSpaceKnob though, which is why it was view depended. So an easy fix.
    Implemented in b59b96f
    Thanks for the clean report btw, much appreciated!

    Btw, in regards to the dynamic knobs you create, they are actually serialized in
    , so when serialization occurs, you are loosing the references to them in the dictionary and recreate them - resulting in duplicate set of knobs (for each serialization). As I can see you have two easy options:
    1. Use
    and it's order to identify the knobs and re-assign them to the textures when you find your dictionary empty.
    2. Delete all dynamic ports individually before recreating them (you'll loose all connections though)
    Obviously 1 or similar is preferred here
    Last edited: Oct 14, 2019
    tossrock and syscrusher like this.
  34. tossrock


    Mar 7, 2018
    XML serialization doesn't seem to work for me, even for very simple nodes.

    Code (CSharp):
    1.         private XmlElement SerializeObjectToXML(XmlElement parent, object obj)
    2.         {
    3.             // TODO: Need to handle asset references
    4.             // Because of runtime compability, always try to embed objects
    5.             // If that fails, try to find references to assets (e.g. for textures)
    6.             try
    7.             { // Try to embed object
    8.                 XmlSerializer serializer = new XmlSerializer(obj.GetType());
    9.                 XPathNavigator navigator = parent.CreateNavigator();
    10.                 using (XmlWriter writer = navigator.AppendChild())
    11.                     serializer.Serialize(writer, obj);
    12.                 return (XmlElement)parent.LastChild;
    13.             }
    14.             catch (Exception)
    15.             {
    16.                 Debug.Log("Could not serialize " + obj.ToString());
    17.                 return null;
    18.             }
    19.         }
    It fails in this block, on objects like the bool
    , or the float
    , or the string
    a + b
    . The exception that gets thrown is this:

    Code (CSharp):
    1. There was an error generating the XML document:
    3. WriteStartDocument cannot be called on writers created with ConformanceLevel.Fragment.
    5.   at System.Xml.Serialization.XmlSerializer.Serialize (System.Xml.XmlWriter xmlWriter, System.Object o, System.Xml.Serialization.XmlSerializerNamespaces namespaces, System.String encodingStyle, System.String id) [0x000f4] in <1d98d70bb7d8453b80c25aa561fdecd1>:0
    6.   at System.Xml.Serialization.XmlSerializer.Serialize (System.Xml.XmlWriter xmlWriter, System.Object o, System.Xml.Serialization.XmlSerializerNamespaces namespaces, System.String encodingStyle) [0x00000] in <1d98d70bb7d8453b80c25aa561fdecd1>:0
    7.   at System.Xml.Serialization.XmlSerializer.Serialize (System.Xml.XmlWriter xmlWriter, System.Object o, System.Xml.Serialization.XmlSerializerNamespaces namespaces) [0x00000] in <1d98d70bb7d8453b80c25aa561fdecd1>:0
    8.   at System.Xml.Serialization.XmlSerializer.Serialize (System.Xml.XmlWriter xmlWriter, System.Object o) [0x00000] in <1d98d70bb7d8453b80c25aa561fdecd1>:0
    9.   at NodeEditorFramework.IO.XMLImportExport.SerializeObjectToXML (System.Xml.XmlElement parent, System.Object obj) [0x0001c] in E:\code\canopy-unity\Assets\Plugins\Node_Editor\Default\IO Formats\XMLImportExport.cs:355
    There are answers online suggesting this can be worked around, eg this one, but I wanted to check if you were aware of this / had fixed it upstream before I go monkeying around with the source.
  35. Seneral


    Jun 2, 2014
    Yeah was made aware of it a while ago but came to the same conclusion as you, found no better fix... Here's the commit addressing that problem.
    tossrock likes this.
  36. Kenshen112


    Apr 2, 2013
  37. Baste


    Jan 24, 2013
    In general, anything that consistently makes Unity crash is a bug in Unity, and something you can send them a bug report about.
  38. Kenshen112


    Apr 2, 2013
    Fair enough, though do you see any reason why my code would cause the editor to break in any fashion? Because it only happens when my TNode is inheriting/implementing the Node class. I attempt to open the Node editor it shows a white window after which unity will crash.
    Last edited: Apr 23, 2020
  39. Baste


    Jan 24, 2013
    I don't know. Does Unity close, or does it just hard hang? If it's the second one, you might be triggering an infinite loop, in which case attaching a debugger should make it easy to find out what's going on.
  40. Kenshen112


    Apr 2, 2013
    It just shows the white editor window after clicking on Node editor, after that a box appears stating Unity has crashed. If I remove the inheritance to your node class then the window opens normally with no crash.

    So it doesn't hang it just closes the window then says unity has crashed then unity closes
  41. Seneral


    Jun 2, 2014
    Sorry, completely missed the notification.

    From the code it looks like TNode is implementing a constructor. Node is a ScriptableObject, which should not implement constructors (Unity speciality).
    Same goes for your calls to new TNode<T> () - that not only does not add the node to the graph as you probably expect, but is also invalid from a plain unity standpoint. Instead try to use Node.Create - note that this expects a node ID, not a type, so no generic type support here.

    Also I don't believe Unity even supports generic Scriptable Objects at all (last time I checked and don't think they'll change that anytime soon). So you might need a workaround for the generics entirely - perhaps just wrap that class in a node if you're adapting it, probably the easiest way.

    Then, if you end up having to deal with type selection after node creation or something nasty like that, you might be interested in an older example branch called Expression Nodes, that does stuff like that (although it's not kept up to date!)

    Btw, if you simply want to search in a node tree, you'd probably be better of rewriting that to fit the node structure. It uses concepts that would be terribly cumbersome to adapt to a node graph. Like the node.left/node.right - you'd have to realize this with a single-out connection in the node graph, and access it like this (from the top of my head):
    Code (csharp):
    1. [ConnectionKnob("Parent", Direction.In, ConnectionCount.Single)]
    2. public ConnectionKnob conParent;
    3. [ConnectionKnob("Left", Direction.Out, ConnectionCount.Single)]
    4. public ConnectionKnob conLeft;
    5. [ConnectionKnob("Right", Direction.Out, ConnectionCount.Single)]
    6. public ConnectionKnob conRight;
    7. // Later
    8. BinaryTreeNode left = conLeft.connected()? (BinaryTreeNode)conLeft.connections[0].body : null;
    9. // etc.
    Also as I said, the functions for creation or deletion don't apply anymore.
    Node.Create and Node.Delete have to be used.
    Last edited: Apr 25, 2020
  42. Kenshen112


    Apr 2, 2013
    That's an incredibly helpful answer thanks! I'll let you know what I get figured out with that
  43. tossrock


    Mar 7, 2018
    @Seneral I wonder, are you thinking at all about porting to the new UI Toolkit (previously known as UIElements) framework? It's intended to offer the same "In-editor + Runtime" capabilities as IMGUI does, and be more performant and customizable at the same time. Obviously, this would be a breaking change, so existing users might not want to migrate, but I think it could be a really powerful upgrade for those who want it.
    Last edited: May 11, 2020
  44. Seneral


    Jun 2, 2014
    No, I wont. Reason being simply that I haven't really been in touch with the latest developments in Unity for quite a while now, and I won't be spending too much time developing in Unity in the future (shift in focus). Although I do believe it would be a sensible move. In fact, a considerable amount of development time went into getting IMGUI to work right - mostly working around it's limitations, especially for zooming - so if the new UI does that better, that'd be great. However at the same time, before I'd go ahead and basically rewrite large parts of the framework (somebody did a NGUI port a few years ago, apparently that worked fine somehow), I'd probably write it more or less from scratch, following the same reasoning as vexe. In essence, make the XML IO the default goto save format. If you cut out the scriptable object serialization and GUI (and I'm assuming input as well), there's not really a whole lot of code left really (relatively speaking).
    So yeah, no real big changes planned to the framework from now on. The current LTS release will keep receiving fixes as new unity versions are released but that's about it (unless someone hops in of course).
    tossrock likes this.
  45. XynanXDB


    Jan 17, 2017
    Hi everyone, I'm creating my own node-based editor for AI Behavior Tree usage. I'm having problem on how to create stateful nodes that can be edited through scripts. The functionality I'm looking for is I can double click on a node, and it will bring me the script owned by the node. I want to have the ability to extend the individual node script or reuse the node script. How do I go about this problem?
  46. Seneral


    Jun 2, 2014
    If you mean the user of the node editor writing scripts to define the behaviour of individual nodes, that sounds quite difficult to be honest. If you were to use the normal CSharp scripting language you'd have to deal with lengthy recompiles, generate and manage the scripts in some temporary location, and of course provide an editor.
    For recompiles, you have to deal with the reload that comes with them anyway, still the user experience is provably going to be rather bad. You probably could use assembly definitions to only recompile the user's node scripts in the current canvas and not the whole project which would probably speed it up. As for editors, there are some inspector editors available, I don't remember under what license though. Else set you can use OpenAsset to open the script on double click, however that forces the user into a different environment, which is probably not optimal if your users are expected to modify these scripts frequently.

    Or, if you have a specific goal in mind for the nodes, create a small limited scripting language.

    Both very cumbersome to both implement and use, as far as I can tell. I would ask if that is really needed and avoid it if possible, however it is possible. Hope those ideas can help you
  47. XynanXDB


    Jan 17, 2017
    Thanks for your reply, I have settle to a more "ready to use" Graph implementation using GraphView instead of recreating a graph from scratch like how this thread does. However, due to the lack of documentation, the experience is like coding in fog :(
  48. Seneral


    Jun 2, 2014
    Oh I did get you're not basing off of NodeEditorFramework, although I'd wager it is probably just as ready to use as GraphView, if not more so. In fact, there are many non-built-in opensource graphing and node editor tools/frameworks out there, like xNode. I recommend choosing frameworks based on what you expect from them, not necessarily the built in one, that way you'll potentially save yourself a lot of trouble later on.

    Back to your goal though, I don't know what you're even trying to achieve, so I gave general technical advice. I assume whatever you're trying to achieve can probably be done in an easier way. Or at least I'd hope so, because as I tried to get across, unrelated to which node editor you use, it'll involve a ton of work.
  49. XynanXDB


    Jan 17, 2017
    I'm trying to make a node-based editor for behavior trees. Now, I have made a highly abstracted generic graph that can be overridden to make whatever graph to one's heart content. For the behavior tree I would imagine right clicking on a node to bring the C# script associated with it for editing or right click on the graph to create a new behavior and generate a new node with a C# script with it, kinda like how Unreal does with their behavior tree.
  50. Seneral


    Jun 2, 2014
    Well then what I wrote in my initial reply still stands, no matter what graph framework you use. Maybe there is a better way but that is what I think of your problem. Good luck!
    XynanXDB likes this.