Skip to content

Modding

Genery edited this page Aug 18, 2026 · 1 revision

Concepts

To achieve a decent degree of customizability of the Scene Graphs plugin, several new concepts had to be introduced. Of course there's the Edited Scene and the GraphEdit, but it was necessary to have an extra layer of abstraction to ensure scripts are able to achieve their intent independently of each other even as they work with the same data. This middle representation is what's called a Scene Graph View, which has almost no direct connection to the Controls that are used to represent it, no direct references to objects in the scene, and is able to be serialized and saved to disk so that it can be restored once the scene is reopened.

So, for clarity, here's an overview of the primary concepts that make up the Scene Graphs plugin. Understanding the differences between these will be necessary to modify the plugin and add the custom behavior you want.

Edited Scene

  • The edited scene. May or may not be saved to a file.
  • Accessible from a hook via editor.scene_root (shorthand for EditorInterface.get_edited_scene_root()).

Scene Graph View

  • View for short, it's an abstract representation of the scene graph, representing what the user wants to see in the scene graph
  • Script: scene_graph_view.gd (global class SceneGraphView)
  • Current view is accessible by editor.current_view (may be null during plugin initialization)
  • Contains hook options configured by the user
  • Contains view rules configured by the user
  • Contains scene-specific persistent data (such as scroll offset and zoom of the editor)
  • Contains an abstract representation of scene objects (such as nodes), their members (such as methods/signals/properties) and other persistent data that the user wants to see in the scene graph.
  • May contain object data such as the position of each object on the graph.
  • Does NOT store connections such as method-signal connections or node references, as connections reflect the state of the Edited Scene
  • A scene's view remains in memory when switching scene tabs.
  • Related capabilities:
    • handle_view_object_types to define a new object type; a classification of scene object that may be represented in the view.

Scene Graph View (Serialized)

  • A serializable copy of the data stored in a SceneGraphView
  • Runtime-only values (such as object instances) are replaced by primitives that can be saved to disk, and used to recover the referenced objects when the edited scene is reloaded.
  • A scene's view is serialized and written to disk when the scene is saved.
  • Related capabilities:

Scene Graph

Hooks

  • Hooks are scripts that get initialized alongside the Scene Graph plugin, and which can opt-in to certain capabilities, depending on what they intend to change.
  • Related capabilities:

Hooks

Hooks are scripts that get initialized alongside the Scene Graph plugin, and which can opt-in to certain capabilities, depending on what they intend to change. Hooks aren't nodes or resources, and they don't exist in the tree necessarily -- so you'll want your hooks to extend RefCounted. All hook scripts must have a single-parameter constructor -- this parameter is a reference to the SceneGraphEditor (global class, extends GraphEdit). Keep this reference in your hook, preferably in an editor property; you'll need to access it often. Example:

var editor : SceneGraphEditor;

func _init(editor : SceneGraphEditor):
	self.editor = editor;

You may also connect to the editor's various signals in this constructor.

Note about languages: This plugin does not utilize abstract classes or anything that would restrict hooks to be implemented in GDScript, so it's theoretically possible to implement a hook in any other programming language supported by Godot such as C#. However the syntax may be unwieldy, due to the need to access script properties and methods with no C# bindings. Required method names also need to be implemented in their original case (snake_case), which breaks C# conventions.

Enabling a hook

Custom hooks are enabled through the Project Settings. Once you have a hook script, navigate to Project > Project Settings > Scene Graph > Editor. In the "Hook Scripts" array, add an entry to your hook script. You may do this for any number of hooks you need.

While you're at it, also enable Dev Mode. This will add a Dev Tools dropdown in the Scene Graph toolbar with useful shortcuts for developing hooks, such as reloading the plugin. A lot of hook behavior is determined at plugin initialization, so if you change a hook's capabilities, you'll likely need to reload the plugin to see the changes.

Capabilities

All hooks are required to implement the following method: func get_scene_graph_capabilities() -> Array[String] The purpose of this method is to declare a list of "capabilities" that this hook intends to implement. You may think of capabilities as interfaces or traits in other programming languages.

Example:

func get_scene_graph_capabilities() -> Array[String]:
	return ["configure_port_types","configure_hook_options","populate_graph_node_connections"];

This hook would then attempt to implement the "configure_port_types" capability, "configure_hook_options", and "populate_graph_node_connections". Each capability requires a certain selection of methods to be implemented in a hook, which will be validated and enforced at the time the plugin is initialized.

All valid capabilities (as defined by the plugin) and their required methods are listed as sections below.

Core Capabilities

configure_capabilities

This is a special kind of capability, which allows a hook to define new capabilities for other hooks to implement.

Required methods:

  • func configure_capabilities() -> void

    This method is called early in the plugin initialization process, before all other hooks are initialized. Call editor.hooks.register_capability(name : String, required_methods : Array[StringName]) to register a capability for other hooks to be able to implement. The first argument is the capability name that other hooks should use (snake_case), and the second is an array of method names any such hooks are required to implement.

    Later, you can access the list of hooks that were granted this capability through editor.hooks.capability_name or editor.hooks.get(&"capability_name") (replace capability_name with the name of your capability) to get an array of Objects; each of the objects being a hook with the capability.

Example built-in hooks: scene_objects.gd, connection_handles.gd

drag_and_drop

If your hook must implement drag and drop functionality, use the drag_and_drop capability. The editor will call your hook's can_drop_data and drop_data methods to let you handle drag and drop logic. DO NOT use set_drag_forwarding on the editor, as you'll be preventing all other hooks from utilizing drag and drop.

Required methods:

Example built-in hooks: scene_objects.gd

configure_port_types

If your hook adds new kinds of ports, implement the configure_port_types capability.

Required methods:

  • func configure_port_types() -> void

    Called during plugin initialization. From this method, call the following editor methods:

    • editor.register_port_type(name : StringName); to register a port with a name. This will assign an integer corresponding to the given port name -- this integer is what you'll be passing to GraphNode port-related methods.

      You can always get the integer assigned to a particular port name by calling editor.port_type(name : StringName) -- but the name must have been registered prior. Doing this assignment from StringName to int is necessary to avoid collisions between port type IDs from different hooks.

    • editor.add_valid_connection_type(from_type : int, to_type : int); (in Godot docs) to enable connections between your new port types. Remember to use editor.port_type() to obtain the integer representation of your named port types.

Example built-in hooks: methods_and_signals.gd, node_references.gd

override_connection_lines

If your hook intends to override the shape of connection lines, implement the override_connection_lines capability.

Required methods:

  • func get_connection_line(from_position: Vector2, to_position: Vector2) -> PackedVector2Array

    See Godot documentation on _get_connection_line in GraphEdit.

    This method is called any time native code wants to draw or determine the shape of a connection line. The only input parameters are the positions of the two corresponding ports (sometimes in graph coordinates, with and without zoom, other times in minimap coordinates -- it's not consistent). The return value is expected to be a PackedVector2Array of coordinates that make up the connection curve between the two points. If an empty array is returned, uses the default implementation (or another hook's).

    Warning: this method is called very often by native code, so it's very prone to crashing the editor if something goes wrong here. Please do your best to keep code here optimized.

    Tip: If you want to access the default implementation of the _get_connection_line method when it's not being overwritten by a script, there's a port of the implementation in the editor.get_default_connection_line(from_position: Vector2, to_position: Vector2) method in the SceneGraphEditor. You can call that directly, or copy the code to adjust it for your own needs.

Example built-in hooks: connection_handles.gd

populate_graph_nodes_from_view

If your hook intends to add a new kind of object type, and a GraphNode type to go with it, implement the populate_graph_nodes_from_view capability.

Required methods:

  • func populate_graph_nodes_from_view() -> void

    This method is called when the graph view has been updated, and is intended to add/remove GraphNodes in the graph based on the editor's current view.

Example built-in hooks: scene_objects.gd

populate_graph_node_connections

If your hook intends to add a new kind of connection type, implement the populate_graph_node_connections capability.

Required methods:

  • func populate_graph_node_connections() -> void

    This method is called when the graph view has been updated, after all populate_graph_nodes_from_view hooks have been called. This method is intended to add/remove connections in the graph to match the current state of the scene. Please take care not to remove connections from ports your hook doesn't handle to avoid conflicts.

Example built-in hooks: methods_and_signals.gd, node_references.gd

handle_view_object_types

If your hook intends to add a new kind of object type that gets stored in a view, implement the handle_view_object_types capability.

Required methods:

  • func get_supported_view_object_types() -> Array[String]

    Should return a list of object types (snake_case) that your hook adds.

  • func view_object_to_object_key(object_type : String, obj : Object) -> Variant

    Given an object and its type (of a supported type), this method should return a key to uniquely identify this object among other objects of its type. This key must not have any characters disallowed in Node names when stringified. This key must be reversible to obtain the object back. However, the key does not need to be persistent. The object's instance ID (from get_instance_id()) is a good option.

  • func object_key_to_view_object(object_type : String, key : Variant) -> Object

    Given an object type (of a supported type) and a key (resulting from the view_object_to_object_key method), this method should reverse the key conversion and return the original object. If using the object's instance ID as a key, use instance_from_id() to reverse it.

  • func object_key_serialize(object_type : String, key : Variant) -> Variant

    Given an object type (of a supported type) and a key (resulting from the view_object_to_object_key method), this method should return a serializable Variant to uniquely identify the represented object in the edited scene. This returned value must retain its meaning after reloading the scene or the editor, so if you're using runtime-only data such as the object's instance ID as the key, you must convert it to something more permanent (such as a NodePath). You may access the edited scene root via editor.scene_root.

  • func object_key_deserialize(object_type : String, serialized : Variant) -> Variant

    Given an object type (of a supported type) and a serialized representation (resulting from the object_key_deserialize method), this method should return the key of the object in the edited scene (same format as view_object_to_object_key). You may access the edited scene root via editor.scene_root.

Example built-in hooks: scene_objects.gd

view_serialization

If your hook intends to make changes to the serialized representation of scene graph views, implement the view_serialization capability. This is particularly useful if you ever store data in views that isn't in a persistent format (such as object instance IDs), to convert it to and from a persistent format when saving to disk.

Required methods:

  • func edit_serialized_view(serialized : Dictionary) -> void

    Called with a serialized representation of a view. The method is free to make any changes it wishes to the contents of this dictionary before saving to disk.

  • func edit_deserialized_view(view : SceneGraphView) -> void

    Called with a freshly deserialized view. The method is free to make any changes to the contents of the view.

Example built-in hooks: scene_objects.gd

configure_member_selector

If your hook intends to add a new member type, implement the configure_member_selector capability to extend the member selector and add new tabs for your members.

Required methods:

  • func get_member_selector_member_types() -> Array[String]

    This method should return a list of member types (snake_case) this hook wants to add tabs for in the member selector dialog.

  • func get_member_selector_tab_info(member_type : String) -> Dictionary

    Given a member type (of those returned by get_member_selector_member_types), this method should return a dictionary containing the following:

    • label (String): The name of the tab
    • icon (Texture2D): An icon for the tab. Feel free to use editor icons: EditorInterface.get_editor_theme().get_icon(&"ICON NAME HERE", &"EditorIcons")
    • is_input (bool, optional): Set to true if this considered an input member (shown when clicking the diamond-shaped port on the left side of a node)
    • is_output (bool, optional): Set to true if this considered an output member (shown when clicking the diamond-shaped port on the right side of a node)
  • func get_member_selector_member_list(object_type : String, obj : Object, member_type : String) -> Array[Dictionary]

    Given an object type (any), an object and a member type (of those returned by get_member_selector_member_types), this member should return a list of dictionaries, one for each member to show in the list, with the following:

    • member_type (String): the member type (same as parameter)
    • member_name (StringName): the member name
    • label (String): the label to show in the list for this entry
    • icon (Texture2D): the icon to show in the list for this entry

Example built-in hooks: scene_objects.gd, property_inspectors.gd

configure_hook_options

If your hook wants to define a set of options that can be configured per-view, implement the configure_hook_options capability.

Required methods:

  • func get_hook_options_id() -> String

    A unique ID for your hook, preferably snake case and with a namespace. e.g: "scene_graphs:methods_and_signals"

  • func create_hook_options() -> [OptionsType]

    This method should return a new object, preferably of a class unique to your hook. This class should contain exported properties for everything you want the user to be able to configure.

    You can later use editor.current_view.get_hook_options(self) to access the user configuration from your own hook.

    Tip: The class returned by this method may optionally implement a get_property_description(property : StringName) -> String method to provide tooltip text for each property.

  • func get_hook_options_label(options : [OptionsType]) -> String

    Given an object (of the type returned by create_hook_options), or null, this method should return a human-readable label to represent the name and configuration of the hook. When the options object is non-null, it contains the user-set values for each of the object's properties. When the options object is null, the method should return a neutral representation (a plain name for your hook) regardless of its configuration.

Optional methods:

  • func get_hook_description() -> String

    Sets text to show in the View Manager to describe what this hook does.

Example built-in hooks: methods_and_signals.gd, node_references.gd, property_inspectors.gd, connection_handles.gd

populate_popup_menu

If your hook wants to add popup menu options when right-clicking in the graph, use the populate_popup_menu capability.

Required methods:

  • func populate_popup_menu(at_position : Vector2, menu : PopupMenu, actions : Dictionary[int, Callable]) -> void

    Called when creating a popup menu. Parameters include:

    • at_position the position that the popup menu is being created. To get the GraphElement at this position, call editor.get_graph_element_at_position(at_position).
    • menu the PopupMenu being created. You can freely add items here. Give your menu items unique IDs so you can assign actions when pressed.
    • actions a dictionary of menu item IDs to callables. The method is intended to modify this dictionary by assigning a callable for each popup menu item ID it adds to the menu. Note that for submenus this is not necessary, as the submenu is created by the hook and callbacks can be handled by it.

Example built-in hooks: scene_objects.gd, connection_handles.gd, view_rules/nodes.gd

Scene Object GraphNode Capabilities

These capabilities are intended for adding functionality to the standard Scene Object GraphNode, be it new slots, ports, or anything else.

initialize_object_graph_node

Required methods:

  • func initialize_object_graph_node(graph_node : GraphNode) -> void

    Called when a new SceneObjectGraphNode is created. If your hook needs to store data for each graph node, consider adding a custom RefCounted-derived class to the graph node's metadata.

Example built-in hooks: methods_and_signals.gd, node_references.gd, property_inspectors.gd

create_object_graph_node_slots

If your hook wants to add new slots to Scene Object GraphNodes, use the create_object_graph_node_slots capability.

Required methods:

  • func create_object_graph_node_slots(graph_node : GraphNode) -> Array[Dictionary]

    Called when a SceneObjectGraphNode needs to refresh its contents, either after creation or when the view of the object it represents has changed. It is expected to return an array of dictionaries. Each dictionary represents a single slot (row) on the GraphNode, and contains the following properties:

    • control (Control): The Control node that will be displayed at the location of this slot
    • sort_key (int, optional): This value is used to sort slots before adding them to the GraphNode. Higher values appear lower in the GraphNode's list of slots. The sorting algorithm is stable, so if you use the same sort key for all slots your hook adds, they'll appear in the order they were appended to the list. Defaults to 0.
    • left_port (Dictionary, optional): If present, represents the port to show on the left side of this slot. Contains the following:
      • port_type (int): The port type for this port. Remember to use editor.port_type() to convert from a StringName.
      • port_color (Color): The color of this port.
      • member (Dictionary, optional): If present, declares that this port corresponds to a specific view object member. This can be used to match port indices to members and vice versa. Contents are:
        • member_type (String): The member type string.
        • member_name (StringName): The member name.
    • right_port (Dictionary, optional): If present, represents the port to show on the right side of this slot. See left_port for the format of the dictionary.
    • member (Dictionary, optional): If present, declares that this slot (neither the left or right ports -- the slot itself) corresponds to a specific view object member. This is useful if the slot doesn't contain any ports but nevertheless still represents a member. Same format as the member property in left_port and right_port.

Example built-in hooks: methods_and_signals.gd, node_references.gd, property_inspectors.gd

claim_object_graph_node_member_slots

If multiple hooks add graph node slots or ports corresponding to the same members, this may result in undefined or undesired behavior. To fix this, hooks should use the claim_object_graph_node_member_slots capability: this allows each of the hooks to "bid" for each member that they want to create slots or ports for. The hooks can then decide whether or not to proceed or refrain from using the member depending on if they are the highest-bidding hook.

Required methods:

  • func get_object_graph_node_member_slot_bid(object_type : String, object : Object, member_type : String, member_name : StringName) -> float

    Given an object and member, and their respective types, this method is expected to return a "bid": a number above 0 if and only if this hook has any intention of adding slots or ports to GraphNodes corresponding to the given member.

    The hook can later (in create_object_graph_node_slots), call graph_node.claim_object_graph_node_member_slot(hook : Object, member_type : String, member_name : StringName) to check if they "won the bid" over the member. Note that if there's a tie for first, all those tied hooks are considered to have "won" for the purposes of this method.

Example built-in hooks: node_references.gd, property_inspectors.gd

draw_object_graph_node_port

If your hook intends to override the way ports are drawn on Scene Object GraphNodes, use the draw_object_graph_node_port capability.

Required methods:

  • func draw_object_graph_node_port(graph_node : GraphNode, slot_index: int, position: Vector2i, left: bool, color: Color) -> bool

    This method is called any time the graph node needs to draw a port. Use the draw_*() methods on the graph_node to do any drawing. Return false to prevent the default port drawing code from executing, or true if you still want the default port to be drawn.

Example built-in hooks: methods_and_signals.gd

Connection Handle Capabilities

These capabilities are intended for adding functionality to the standard connection handle GraphElement.

initialize_connection_handle

Required methods:

  • func initialize_connection_handle(handle : GraphElement) -> void

    Called when a new connection handle GraphElement is created. If your hook needs to store data for each handle, consider adding a custom RefCounted-derived class to the graph element's metadata.

Example built-in hooks: methods_and_signals.gd

draw_connection_handle

If your hook wants to change how connection handles look, use the draw_connection_handle capability.

Required methods:

  • func draw_connection_handle(handle : GraphElement, center : Vector2, connection_rotation: float, handle_size : float) -> bool

    This method is called when the connection handle needs to be drawn. Use the draw_*() methods on the handle to do any drawing. Return false to prevent the default handle drawing code from executing, or true if you still want the default handle to be drawn.

    Tip: There's two helper drawing functions in the connection handle script. You can call them from this method.

    • func draw_dot_handle(center : Vector2, rotation : float, size : float) -> void Draws a dot (this is the default).
    • func draw_arrow_handle(center : Vector2, rotation : float, size : float) -> void Draws an arrow (this is used for signal connections).

Example built-in hooks: methods_and_signals.gd

View Rule Capabilities

View Rules are a special type of hook whose purpose is to generate view data from the active scene. These are user-configurable and able to be layered on top of one another to create more complex rules to determine what to show in the graph. It's expected for each view rule type to have its own hook script, and for its only responsibility (or at least its primary one) to be implementing a single view rule capability.

All view rule hooks are required to implement these common methods:

  • func get_view_rule_id() -> String

    A unique ID for your view rule hook, preferably snake case and with a namespace. e.g: "scene_graphs:connected_methods_and_signals"

  • func get_view_rule_label(params : [ParamsType]) -> String

    Given a parameters object (of the type returned by create_view_rule_params, if implemented), or null, this method should return a human-readable label to represent the name and configuration of the view rule. When the params object is non-null, it contains the user-set values for each of the object's properties. When the params object is null, the method should return a neutral representation (a plain name for your view rule) regardless of its configuration.

Additionally, all types of view rules can implement these optional methods:

  • func create_view_rule_params() -> [ParamsType]

    This method should return a new object, preferably of a class unique to your hook. This class should contain exported properties for everything you want the user to be able to configure.

    Tip: The class returned by this method may optionally implement a get_property_description(property : StringName) -> String method to provide tooltip text for each property.

  • func get_view_rule_description() -> String

    Sets text to show in the View Manager to describe what this view rule does.

There are currently two types of view rule capabilities: view_rule.object_source and view_rule.member_source.

view_rule.object_source

The purpose for object sources is to select objects from the edited scene to add them to the view (and later, these view objects get turned into graph nodes by populate_graph_nodes_from_view capabilities). This is an automated way of, for example, dragging nodes from the Scene tab into the Scene Graph to add them to the view. A object source view rule is thus able to programmatically determine what objects in the scene are of interest.

Required methods:

  • func generate_view_objects(params : [ParamsType]) -> Array

    This method should return an array containing a dictionary for each object that should be added to the view. The dictionaries' contents should be as such:

    • object_type (String): an object type string for the type of object you're adding.
    • object (Object): the represented object.

    This method also takes in a parameters object. This object is of the same type as returned by create_view_rule_params, containing all of the values configured by the user for the current view rule (or null if the method is not implemented).

Example built-in hooks: view_rules/nodes.gd

view_rule.member_source

The purpose for member sources is to select members from objects to add them to the view (and later, these members get turned into slots or ports by create_object_graph_node_slots capabilities). This is an automated way of, for example, pressing the Manage Members button on a Scene Object GraphNode and selecting members to add them to the view. A member source view rule is thus able to programmatically determine what members of objects in the scene are of interest.

Required methods:

  • func generate_view_object_members(object_type : String, obj : Object, params : [ParamsType]) -> Array

    Given an object and its type, this method should return an array containing a dictionary for each member of the object that should be added to the view. The dictionaries' contents should be as such:

    • member_type (String): an member type string for the type of member you're adding.
    • member_name (StringName): the represented member.

    The member is assumed to be intended to be added to the object passed into the method. You have the option, however, to specify that you want to add this member to a different object in the view, by including the following entries in the dictionary:

    • object_type (String, optional): an object type string for the type of object you intend to add the member for.
    • object (Object, optional): the object you intend to add the member for.

    If these are present, an object view will be created for the provided object, if not already present, and the member will be added to it.

    This method also takes in a parameters object. This object is of the same type as returned by create_view_rule_params, containing all of the values configured by the user for the current view rule (or null if the method is not implemented).

Example built-in hooks: view_rules/members_by_name.gd, view_rules/connected_methods_and_signals.gd, view_rules/node_reference_properties.gd

Types in built-in hooks

These are the type IDs used by built-in hooks. These may be useful if you're making a hook that directly interfaces with built-in behavior. Though, do note that all these IDs are freeform -- you can freely declare and use your own types if you're making something that isn't in this list.

Object Types

These are of type String.

  • "node" (for scene nodes)

Member Types

These are of type String.

  • "method" (for object methods, used by method-signal connections)
  • "signal" (for object signals, used by method-signal connections)
  • "property" (for object properties, used by both property inspectors and node reference connections)
  • "node_reference_out" (for node reference - generates the output port for node reference connections)

Port Types

Port types are ints, but to access them, you must use a StringName key to prevent collisions. Do not serialize the ints themselves, as port types can change for different hook configurations.

  • port_type(&"method") (for method ports, input)
  • port_type(&"signal") (for signal ports, output)
  • port_type(&"node_reference") (for node reference ports, input)
  • port_type(&"node_reference_out") (for node reference ports, output)