Search Unity

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