Simple node editor

Here is the example code to create a node editor, in which you have draggable windows connected by a curve:

using UnityEngine;
using UnityEditor;

public class NodeEditor: EditorWindow {
	
	Rect window1;
	Rect window2;
	
	[MenuItem("Window/Node editor")]
	static void ShowEditor() {
		NodeEditor editor = EditorWindow.GetWindow<NodeEditor>();
		editor.Init();
	}
	
	public void Init() {
		window1 = new Rect(10, 10, 100, 100);	
		window2 = new Rect(210, 210, 100, 100);	
	}
	
	void OnGUI() {
		DrawNodeCurve(window1, window2); // Here the curve is drawn under the windows
		
		BeginWindows();
		window1 = GUI.Window(1, window1, DrawNodeWindow, "Window 1");	// Updates the Rect's when these are dragged
		window2 = GUI.Window(2, window2, DrawNodeWindow, "Window 2");
		EndWindows();
	}
	
	void DrawNodeWindow(int id) {
		GUI.DragWindow();
	}
	
	void DrawNodeCurve(Rect start, Rect end) {
		Vector3 startPos = new Vector3(start.x + start.width, start.y + start.height / 2, 0);
		Vector3 endPos = new Vector3(end.x, end.y + end.height / 2, 0);
		Vector3 startTan = startPos + Vector3.right * 50;
		Vector3 endTan = endPos + Vector3.left * 50;
		Color shadowCol = new Color(0, 0, 0, 0.06f);
		for (int i = 0; i < 3; i++)	// Draw a shadow
			Handles.DrawBezier(startPos, endPos, startTan, endTan, shadowCol, null, (i + 1) * 5);
		Handles.DrawBezier(startPos, endPos, startTan, endTan, Color.black, null, 1);
	}
}

Save it as “NodeEditor.cs” in your Assets/Editor folder.

UPDATE: An image of the editor extension:

1291253--59613--$NodeEditor.png

44 Likes

Ufff … not imagine how much I wanted this
thanks for posting :slight_smile:

I've modified this script a bit, hopefully it will be more useful.

You can now press a button to create a new node, then you can click a button to attach nodes together.

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;


public class NodeEditor : EditorWindow {

    List<Rect> windows = new List<Rect>();
    List<int> windowsToAttach = new List<int>();
    List<int> attachedWindows = new List<int>();

    [MenuItem("Window/Node editor")]
    static void ShowEditor() {
        NodeEditor editor = EditorWindow.GetWindow<NodeEditor>();
    }


    void OnGUI() {
        if (windowsToAttach.Count == 2) {
            attachedWindows.Add(windowsToAttach[0]);
            attachedWindows.Add(windowsToAttach[1]);
            windowsToAttach = new List<int>();
        }

        if (attachedWindows.Count >= 2) {
            for (int i = 0; i < attachedWindows.Count; i += 2) {
                DrawNodeCurve(windows[attachedWindows[i]], windows[attachedWindows[i + 1]]);
            }
        }

        BeginWindows();

        if (GUILayout.Button("Create Node")) {
            windows.Add(new Rect(10, 10, 100, 100));
        }

        for (int i = 0; i < windows.Count; i++) {
            windows[i] = GUI.Window(i, windows[i], DrawNodeWindow, "Window " + i);
        }

        EndWindows();
    }


    void DrawNodeWindow(int id) {
        if (GUILayout.Button("Attach")) {
            windowsToAttach.Add(id);
        }

        GUI.DragWindow();
    }


    void DrawNodeCurve(Rect start, Rect end) {
        Vector3 startPos = new Vector3(start.x + start.width, start.y + start.height / 2, 0);
        Vector3 endPos = new Vector3(end.x, end.y + end.height / 2, 0);
        Vector3 startTan = startPos + Vector3.right * 50;
        Vector3 endTan = endPos + Vector3.left * 50;
        Color shadowCol = new Color(0, 0, 0, 0.06f);

        for (int i = 0; i < 3; i++) {// Draw a shadow
            Handles.DrawBezier(startPos, endPos, startTan, endTan, shadowCol, null, (i + 1) * 5);
        }

        Handles.DrawBezier(startPos, endPos, startTan, endTan, Color.black, null, 1);
    }
}
9 Likes

Very nice, tatelax. Thanks for contributing :slight_smile:

This is really nice, I am going to use this instead of Twine for my branching story/dialogue trees. Thank you guys!

Thanks for this. I have been wanting to learn about this. How can I destroy a node or attachment? Where is good place to start if i want to learn how to add scripts, variables, or some type of functionality to the nodes? If there a resource I can read?

Thanks!

can someone tell me
where can i learn more about program in this node?

Exactly what I was looking for! Thank you tatelax.

Question, how difficult would it be to add alt+mouse drag to scroll the area inside the editor window to add and view more windows?

Thanks

-Raiden

Hey this is awesome! Thanks guys.

Question: If I wanted to have each Window display like an Inspector for a certain class object, how would I do that (if it's even possible) ?

Suppose I have a Serializable class DialogNode.cs

public int id;
public string name;
public string phrase;

We all know how this would appear in the Inspector... would it be possible to get the same thing, only displaying in each of those Windows?

Thanks :smile:

You’d have to draw them yourself. You can get the variables and their types via Reflection. I’m working on something like that.

If you ever need panning the window, you can group controls using this method:

And move the group’s coordinates. The first code example can be modified this way:

using UnityEngine;
using UnityEditor;

public class NodeEditor: EditorWindow {			
	Rect window1;	
	Rect window2;
	float panX = 0;
	float panY = 0;	
		
	[MenuItem("Window/Node editor")]	
	static void ShowEditor() {		
		NodeEditor editor = EditorWindow.GetWindow<NodeEditor>();		
		editor.Init();		
	}		
	
	public void Init() {		
		window1 = new Rect(100, 100, 100, 100);   		
		window2 = new Rect(260, 260, 100, 100); 		
	}

	void OnGUI() {
		GUI.BeginGroup(new Rect(panX, panY, 100000, 100000));
		DrawNodeCurve(window1, window2); // Here the curve is drawn under the windows				
		
		BeginWindows();		
		window1 = GUI.Window(1, window1, DrawNodeWindow, "Window 1");   // Updates the Rect's when these are dragged		
		window2 = GUI.Window(2, window2, DrawNodeWindow, "Window 2");		
		EndWindows();

		GUI.EndGroup();		

		if (GUI.RepeatButton(new Rect(15, 5, 20, 20), "^")) {
			panY -= 1;
			Repaint();
		}
		
		if (GUI.RepeatButton(new Rect(5, 25, 20, 20), "<")) {
			panX -= 1;
			Repaint();
		}
		
		if (GUI.RepeatButton(new Rect(25, 25, 20, 20), ">")) {
			panX += 1;
			Repaint();
		}
		
		if (GUI.RepeatButton(new Rect(15, 45, 20, 20), "v")) {
			panY += 1;
			Repaint();
		}
	}
			
	void DrawNodeWindow(int id) {		
		GUI.DragWindow();		
	}

	void DrawNodeCurve(Rect start, Rect end) {
		Vector3 startPos = new Vector3(start.x + start.width, start.y + start.height / 2, 0);		
		Vector3 endPos = new Vector3(end.x, end.y + end.height / 2, 0);		
		Vector3 startTan = startPos + Vector3.right * 50;		
		Vector3 endTan = endPos + Vector3.left * 50;		
		Color shadowCol = new Color(0, 0, 0, 0.06f);		
		for (int i = 0; i < 3; i++) // Draw a shadow			
			Handles.DrawBezier(startPos, endPos, startTan, endTan, shadowCol, null, (i + 1) * 5);		
		Handles.DrawBezier(startPos, endPos, startTan, endTan, Color.black, null, 1);		
	}	
}
3 Likes

Hello,

I've added a couple things.. mostly the ability to "resize" the node window. Ideally the node stuff should probably be in a class, but it mostly works and is an example right? :)

One issue I tried to resolve was the rubber banding if you move your mouse to quickly while the handle is active and you're dragging to resize... oh, and the fact that once you reach the minimum size (_winMinY, _winMinX), their is a bit of snapping... if someone can help me fix that, it would be awesome (perhaps its' just a matter of getting the delta "acceleration" of the mouse movement.. hint, hint).

anyway, just wanted to give back a little as this example and others from Bunny83 drove me to this solution.. (note: the "free-to-use" corner image I made is attached here, just put it in the right folder and change the asset load path as you like (NodeEditor/Icons) )

enjoy!

//use code in followup

(edit).. also note, the image asset when imported should be configured with advanced, non-power-of-2 = none, alpha is transparency enabled, no mips, clamp on...

1678546--105171--ResizeHandle.png

1 Like

Updated:

  • fixed weird jumping of windows when resizing

  • fixed loosing mouse drag handles when cursor moves outside handle area

  • added a nice way to handle Bezier aliasing (and a free PNG attached)

  • dropped shadow stuff (easily added based on original code)

  • (fixed) issue: if mouseup occurs outside parent window boundaries, _handleActive remains true

  • (fixed) issue: seems like the parent window become modaless at some point.

  • (note, looks like the above two issues are related to not setting a hotcontrol on the handle area)

  • (minor) issue: when mouse is down outside of handle area in primary editor window and moved into a handle area (making it hot), there is a slight resize made. probably easily fixable, but that’s all I can add for now.

I really wish GUI.WindowFunction could be extended somehow so I could pass in oh… a generic.

using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System;
using System.Reflection;
using System.Text;

public class NodeEditorWindow  : EditorWindow
{
    static NodeEditorWindow  window;

    public Rect window1, window2, _handleArea;
    private bool _nodeOption, _options, _handleActive, _action;
    private Texture2D _resizeHandle, _aaLine;
    private GUIContent _icon;
    private float _winMinX, _winMinY;
    private int _mainwindowID;

    [MenuItem("Window/Node Editor")]
    static void Init()
    {
        window = (NodeEditorWindow )EditorWindow.GetWindow(typeof(NodeEditorWindow));
        window.title = "Node Editor";
        window.ShowNodes();
    }

    private void ShowNodes()
    {
        _winMinX = 100f;
        _winMinY = 100f;
        window1 = new Rect(30, 30, _winMinX, _winMinY);
        window2 = new Rect(210, 210, _winMinX, _winMinY);

        _resizeHandle = AssetDatabase.LoadAssetAtPath("Assets/NodeEditor/Icons/ResizeHandle.png", typeof(Texture2D)) as Texture2D;
        _aaLine = AssetDatabase.LoadAssetAtPath("Assets/NodeEditor/Icons/AA1x5.png", typeof(Texture2D)) as Texture2D;
        _icon = new GUIContent(_resizeHandle);
        _mainwindowID = GUIUtility.GetControlID(FocusType.Native); //grab primary editor window controlID
    }

    void OnGUI()
    {
        BeginWindows();
        window1 = GUI.Window(1, window1, DrawNodeWindow, "Window 1");   // Updates the Rect's when these are dragged
        window2 = GUI.Window(2, window2, DrawNodeWindow, "Window 2");
        EndWindows();

        DrawNodeCurve(window1, window2);

        GUILayout.BeginHorizontal(EditorStyles.toolbar);
        _options = GUILayout.Toggle(_options, "Toggle Me", EditorStyles.toolbarButton);
        GUILayout.FlexibleSpace();
        GUILayout.EndHorizontal();
       
        //if drag extends inner window bounds _handleActive remains true as event gets lost to parent window
        if ((Event.current.rawType == EventType.MouseUp) && (GUIUtility.hotControl != _mainwindowID))
        {
            GUIUtility.hotControl = 0;
        }
    }

    private void DrawNodeWindow(int id)
    {
        if (GUIUtility.hotControl == 0)  //mouseup event outside parent window?
        {
            _handleActive = false; //make sure handle is deactivated
        }
       
        float _cornerX = 0f;
        float _cornerY = 0f;
        switch (id) //case which window this is and nab size info
        {
            case 1:
                _cornerX = window1.width;
                _cornerY = window1.height;
                break;
            case 2:
                _cornerX = window2.width;
                _cornerY = window2.height;
                break;
        }

        //begin layout of contents
        GUILayout.BeginArea(new Rect(1, 16, _cornerX - 3, _cornerY - 1));
        GUILayout.BeginHorizontal(EditorStyles.toolbar);
        _nodeOption = GUILayout.Toggle(_nodeOption, "Node Toggle", EditorStyles.toolbarButton);
        GUILayout.FlexibleSpace();
        GUILayout.EndHorizontal();
        GUILayout.EndArea();

        GUILayout.BeginArea(new Rect(1, _cornerY - 16, _cornerX - 3, 14));
        GUILayout.BeginHorizontal(EditorStyles.toolbarTextField, GUILayout.ExpandWidth(true));
        GUILayout.FlexibleSpace();

        //grab corner area based on content reference
        _handleArea = GUILayoutUtility.GetRect(_icon, GUIStyle.none);
        GUI.DrawTexture(new Rect(_handleArea.xMin + 6, _handleArea.yMin - 3, 20, 20), _resizeHandle); //hacky placement
        _action = (Event.current.type == EventType.MouseDown) || (Event.current.type == EventType.MouseDrag);
        if (!_handleActive && _action)
        {
            if (_handleArea.Contains(Event.current.mousePosition, true))
            {
                _handleActive = true; //active when cursor is in contact area
                GUIUtility.hotControl = GUIUtility.GetControlID(FocusType.Native); //set handle hot
            }
        }

        EditorGUIUtility.AddCursorRect(_handleArea, MouseCursor.ResizeUpLeft);
        GUILayout.EndHorizontal();
        GUILayout.EndArea();

        //resize window
        if (_handleActive && (Event.current.type == EventType.MouseDrag))
        {
            ResizeNode(id, Event.current.delta.x, Event.current.delta.y);
            Repaint();
            Event.current.Use();
        }

        //enable drag for node
        if (!_handleActive)
        {
            GUI.DragWindow();
        }
    }

    private void ResizeNode(int id, float deltaX, float deltaY)
    {
        switch (id)
        {
            case 1:
                if ((window1.width + deltaX) > _winMinX) { window1.xMax += deltaX; }
                if ((window1.height + deltaY) > _winMinY) { window1.yMax += deltaY; }
                break;
            case 2:
                if ((window2.width + deltaX) > _winMinX) { window2.xMax += deltaX; }
                if ((window2.height + deltaY) > _winMinY) { window2.yMax += deltaY; }
                break;
        }
    }

    void DrawNodeCurve(Rect start, Rect end)
    {
        Vector3 startPos = new Vector3(start.x + start.width, start.y + start.height / 2, 0);
        Vector3 endPos = new Vector3(end.x, end.y + end.height / 2, 0);
        Vector3 startTan = startPos + Vector3.right * 50;
        Vector3 endTan = endPos + Vector3.left * 50;
        Handles.DrawBezier(startPos, endPos, startTan, endTan, Color.black, _aaLine, 1.5f);
    }
}

:wink:

1719143--108445--AA1x5.png

2 Likes

thanks that is really cool !!

is it possible to change the color of the bezier curve after the line was clicked ?

I’m sure you can… I would probably just add support for that event state in OnGUI and modify DrawNodCurve to handle the color input you want based on that state.

How do I use this as an editor for a property on a monobehavior?
I can't figure out how to trigger this passing the property reference in.

I'm building a node based noise module editor.

HI, your script helped me a lot, however i’m facing a huge problem that i can not solve.
Here is my idea , in short ,part of my idea:
1.using a List to store the Window’s infomations such as Rect and parent node,child nodes and so on .
2.In OnGUI ,there is a " for " loop to draw each window by the List 's count and it’s information.
3.on user click add node , i always add new one to he List.
4.after click delete button i remove current focus window from the List

Now if i delete the node by the order i add them in. Everything works fine. However , I’m delete the node between the order I got Crash ( 1,2,3 i delete 2),which is an" Index out of bounds exception". anything wrong ?

some code to express:

void OnGUI()
{
    DrawNodes();
}

void DrawNodes()
{
    for(int i = 0;i < windowList.Count;i++)
    {
        ..........
         windowList[i].windowArea = GUI.Window(windowList[i].windowIndex,windowList[i].windowArea,OnDrawWindow,windowList[i].windowTitle);
    }

void OnDrawWindow(int id)
{
    //Draw some layouted content
    .........
    if(Event.current.type == EventType.KeyDown && Event.current.keycode == KeyCode.Delete)
    {
        windowList.RemoveAt(id);
        Repaint();
    }
}
}

Any help ? if i fixed this problem, that’s the time to post the code here!!!

perhaps it has something to do with your callback for ondrawwindow.. as you're asking it to draw the windows, then within, you're trying to remove it causing the error?

I would say moving the event handler to separate method to be called after or before drawnodes() might fix the issue.

Thank you tswalk , however your suggestion does’t work as well.
Maybe i need to find some documents about the OnGUI lifetime circle detail . if Repaint will break current frame and run another OnGUI that will fix it . because i Debug.Log() current windowList.Count .the length is correct,but it still called the one whick out of bounds .

Anybody needs a perfect version of node editor? I've added a few functions such as multiply root-child nodes, context menu with adding nodes, connecting root and child nodes. Btw I created it for my dialog system.