Godot 4.3: Introduction to GraphNode and GraphEdit


In this blog post, I will introduce you to GraphNode and GraphEdit.

Part of this blog post is inspired from this tutorial that I recommend. I will try to provide more details about things that should be known about GraphNodes, as of Godot 4.3. While sharing some similarities with the tutorial, this blog post focuses on the goal of managing interactions between nodes rather than managing the nodes themselves.

This blog post details how I approached GraphNodes and things I learned along the way while trying to make GraphNode and GraphEdit work the way I wanted. I won't cover certain parts, the goal today is to get things to work.

It should be noted, that as the APIs behind GraphNode are under heavy refactoring for a future Godot 4.x version. The information provided here can get outdated. However, I can attest that things indicated here have been attempted with Godot 4.3.

Some information may also be incorrect, but plenty of information are not well documented and based on trial and error. So your guess is as good as mine, if some information is incorrect, feel free to contact me or tell in the comments.

What are GraphNode and GraphEdit?

Let's start first with some terminology, as some of it can be confusing quite easily.

If you found this blog post, you certainly have a certain knowledge of what are graph nodes in general, otherwise I bring you to this Wikipedia page. That said, let's have a look at the different concepts behind graph nodes in Godot 4.3.

A GraphNode is a single element that contains ports. Using these ports, they can connect/link to other GraphNodes. They need to be part of a graph, by being placed directly as children of a GraphEdit (or in a GraphFrame within a GraphEdit).

There are two different kind of ports, the input ports (also called left ports) and the output ports (also called right ports). You can only connect input to output ports, and opposite. These ports are attached to slots, which is another concept I will describe after.

A slot is an element that can contain ports. In a GraphNode, a slot is automatically created for each child of the GraphNode inheriting Control (it can be anything!). These slots allows up to two ports to be located on each side of the control, one input (left) port and one output (right) port. These ports are vertically centered with the control, so if you resize the control, the ports will move.

Each port is known by a port_idx, which is their index on their side of the GraphNode. Input ports (left) go from 0 to get_input_port_count() - 1, while output ports (right) go from 0 to get_output_port_count() - 1.

If we have a slot 0 and a slot 1, with both having an input port (left) enabled, then the slot 0 will contain the port 0 and the slot 1 will contain the port 1. If we disable the port in the slot 0, then the slot 1 will now contain the port 0. If we had several more slots with ports enabled, then all our indices would be getting shifted the same way. It's very important to keep that in mind to be sure we're referring to the correct port.

Our first graph

Given we have an empty scene, to create our graph, we start by putting a GraphEdit component in our scene through the Godot editor. We can resize it and see it already provides us a quite complete editor for a graph with plenty of properties we can adjust!

We can then add a GraphNode as a children to our GraphEdit. Let's give it a title, by setting the Title property in the inspector! We can call it Hello world!. And now we run the project, we have our first graph node with a title! We can select it, drag it, all across our graph area.

But it would be more useful for it to have ports, right? Like I mentioned earlier, ports need to be located in a slot, so we have to first create our slot!

To create a slot, still in the Godot editor, we can simply add a Control element as a children of our GraphNode. Once it is done, we can see in the inspector of our GraphNode that there is a "Slots" property that contains one element. The slot has two checkboxes "Left Enabled" and "Right Enabled". If we check "Left Enabled", we have an input port. If we check "Right Enabled", we have an output port.

Let's enable both ports and duplicate our GraphNode so we have two nodes! If we run our project, we can drag from one port to the other, which creates a line. However it seems that they don't connect?

That's because we don't have the logic yet for them to connect. For that, let's get back at our GraphEdit and create a script for it! Our script should look as following.

class_name CustomGraphEdit extends GraphEdit

# our code ...

Still in our GraphEdit, let's go to the sidebar, in Node > Signals and register for the connection_request signal to add it to our script. We can then call connect_node each time a connection is requested.

func _on_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
    connect_node(from_node, from_port, to_node, to_port)

It's fairly easy to define a connection by just passing the arguments! We can try it and see how now our nodes connect.

Let's make it so that we can disconnect too, using the disconnection_request, by passing arguments the same way.

func _on_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
    disconnect_node(from_node, from_port, to_node, to_port)

We will also go to the GraphNode properties and check "Right Disconnects" in the inspector, so that when we click in the right port of a node that is connected, it disconnects. We can run our project and see how it works.

We only run in an issue, we can connect multiple times! So let's update our code a bit to prevent it from happening...

func _on_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
    for con in get_connection_list():
        if con.to_node == to_node and con.to_port == to_port:
            return
    connect_node(from_node, from_port, to_node, to_port)

Okay that's quite some code now, what are these arguments and what is get_connection_list?

According to Godot, at the time of writing this blog post:

Array[Dictionary] get_connection_list() const

Returns an Array containing the list of connections. A connection consists in a structure of the form { from_port: 0, from_node: "GraphNode name 0", to_port: 1, to_node: "GraphNode name 1" }.

Note: There are some examples you can find online that refer to a connection by using a structure with from instead of from_node and to instead of to_node. It's an old form that was deprecated.

Let's have a look at these properties returned by each element of get_connected_list:

  • from_port refers to the output port (right) where the connection starts from, and from_node is the name of associated node.
  • to_port refers to the input port (left) where the connection goes to, and to_node is the name of associated node.

What our previous code is doing is comparing if our graph (GraphEdit) contains any existing connection towards a node and do not connect to it if it is the case, allowing only one possible connection at a time.

Adding logic to GraphNodes

It would make sense to have our nodes aware they are being connected to!

To do so, let's first create a script that we will rely on in each GraphNode! Our script should look as following.

class_name CustomGraphNode extends GraphNode

var ports_connected = {
    "input": {},
    "output": {}
}

# some code ...

func is_port_connected(self_port_type: String, self_port: int) -> bool:
    for port_idx in ports_connected[self_port_type].keys():
        if ports_connected[self_port_type][port_idx]:
            return true
    return false

# ...

func on_connect(self_port_type: String, self_port: int, other_node: CustomGraphNode, other_port: int) -> void:
    print("I am connected to ", other_node, " on ", self_port_type, " port ", other_port)
    ports_connected[self_port_type][self_port] = true

func on_disconnect(self_port_type: String, self_port: int, other_node: CustomGraphNode, other_port: int) -> void:
    print("I am disconnected to ", other_node, " on ", self_port_type, " port ", other_port)
    ports_connected[self_port_type][self_port] = false

And then in our CustomGraphEdit we can adjust our code to call these functions...

func _on_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
    for con in get_connection_list():
        if con.to_node == to_node and con.to_port == to_port:
            return
    connect_node(from_node, from_port, to_node, to_port)

    var from_node_inst = find_child(from_node) as CustomGraphNode
    var to_node_inst = find_child(to_node) as CustomGraphNode

    from_node_inst.on_connect("output", from_port, to_node_inst, to_port)
    to_node_inst.on_connect("input", to_port, from_node_inst, from_port)

# ...

func _on_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
    disconnect_node(from_node, from_port, to_node, to_port)

    var from_node_inst = find_child(from_node) as CustomGraphNode
    var to_node_inst = find_child(to_node) as CustomGraphNode

    from_node_inst.on_disconnect("output", from_port, to_node_inst, to_port)
    to_node_inst.on_disconnect("input", to_port, from_node_inst, from_port)

We can connect and disconnect nodes and each nodes involved in the connection are notified of it.

This allows to easily make more complex systems. From it, we can do more complex operations by applying different logic for our nodes, traversing the nodes, etc.

This concludes this introduction to GraphNode and GraphEdit. I hope you enjoyed it and feel free to tell me if it has been useful.

Posted in GDScript, Godot, Godot 4, Tutorial on Oct 03, 2024.