~3 min read

@tool and EditorPlugin — extending the editor

Tool scripts, custom inspectors, EditorPlugin, adding nodes and docks to the Godot editor.

Godot is written entirely in its own language for the UI — this means that the editor itself can be extended with GDScript code. No C++ plugins, no rebuilds.

Two levels of extension:

  1. @tool scripts — ordinary nodes with behavior in the editor. For example, a script that automatically places trees along a spline.
  2. EditorPlugin — a real plugin: adds nodes, custom docks, inspectors, gizmos.

@tool — the script runs in the editor

A regular GDScript runs only in Play Mode. Add @tool on the first line — and the script will run right in the Scene View.

@tool
extends Node3D

@export var radius: float = 5.0:
    set(value):
        radius = value
        _rebuild_circle()  # redraw on change

@export var count: int = 8:
    set(value):
        count = value
        _rebuild_circle()

@export var rebuild: bool = false:
    set(value):
        if value: _rebuild_circle()
        rebuild = false

func _rebuild_circle() -> void:
    # Clear old children
    for child in get_children():
        child.queue_free()

    # Place spheres in a circle
    for i in count:
        var angle := TAU * i / count
        var sphere := MeshInstance3D.new()
        sphere.mesh = SphereMesh.new()
        sphere.position = Vector3(cos(angle), 0, sin(angle)) * radius
        add_child(sphere)
        sphere.owner = get_tree().edited_scene_root  # so children are saved into the .tscn

Change radius in the Inspector — the circle redraws immediately in the Scene View. Save the scene — the child spheres remain.

Web

Storybook + Design Tokens: interactive component previews with live parameters in the editor. Tool scripts are “Storybook right inside the final product”.

Unity
Tool scripts are the most common cause of a frozen editor

If you put a while true: or a heavy loop in a @tool script without an exit condition, the editor will freeze. Before saving, check: nothing infinite, no blocking operations. If everything breaks — launch Godot from the terminal with the --no-window flag to delete the script.

owner = edited_scene_root — mandatory

If you create children in a @tool script, you must set their owner:

var instance := preload("res://prefab.tscn").instantiate()
add_child(instance)
instance.owner = get_tree().edited_scene_root

Without owner, the children exist in the Scene Tree during editing, but are not saved into the .tscn. Saving and reopening the scene will make them disappear.

EditorPlugin — a real plugin

A full-fledged plugin is an EditorPlugin node that Godot loads when the editor starts. It can:

  • Add a new node type (add_custom_type).
  • Add a dock to the side panel (add_control_to_dock).
  • Add an item to the Project/Scene menu (add_tool_menu_item).
  • Register a gizmo for a node (add_node_3d_gizmo_plugin).
  • Intercept scene save/load.

Plugin structure

addons/
└── my_plugin/
    ├── plugin.cfg           ← manifest
    ├── plugin.gd            ← main EditorPlugin
    ├── my_node.gd           ← new node type
    └── icon.svg

plugin.cfg:

[plugin]

name="MyAwesomePlugin"
description="Adds X to the editor"
author="You"
version="1.0"
script="plugin.gd"

plugin.gd:

@tool
extends EditorPlugin

func _enter_tree() -> void:
    # Register a new node type "TerrainPainter"
    add_custom_type(
        "TerrainPainter",
        "Node3D",
        preload("res://addons/my_plugin/my_node.gd"),
        preload("res://addons/my_plugin/icon.svg")
    )

func _exit_tree() -> void:
    remove_custom_type("TerrainPainter")

After saving: Project → Project Settings → Plugins → enable yours. In the Add Node menu “TerrainPainter” will appear.

Dock panel

@tool
extends EditorPlugin

var dock: Control

func _enter_tree() -> void:
    dock = preload("res://addons/my_plugin/dock.tscn").instantiate()
    add_control_to_dock(EditorPlugin.DOCK_SLOT_LEFT_UR, dock)

func _exit_tree() -> void:
    remove_control_from_docks(dock)
    dock.queue_free()

dock.tscn is an ordinary scene with a Control node. It will appear in the editor as a real dock panel that can be dragged.

Inspector plugin — custom property edits

For non-standard property editors (for example, a color-palette picker instead of the usual Color):

extends EditorInspectorPlugin

func _can_handle(object: Object) -> bool:
    return object is MyCustomResource

func _parse_property(object, type, name, hint, hint_string, usage, wide):
    if name == "palette_color":
        add_property_editor(name, MyCustomPaletteEditor.new())
        return true
    return false

Gizmos — 3D visualization in the editor

Want to see your enemy’s aggro radius circle in the 3D viewport? Register a Node3DGizmoPlugin:

class_name EnemyGizmo extends EditorNode3DGizmoPlugin

func _has_gizmo(node):
    return node is Enemy  # your custom class

func _redraw(gizmo):
    var node = gizmo.get_node_3d() as Enemy
    gizmo.clear()
    # Draw a sphere wireframe of radius agro_radius
    var lines := PackedVector3Array()
    var segments := 32
    for i in segments:
        var a1 := TAU * i / segments
        var a2 := TAU * (i + 1) / segments
        lines.append(Vector3(cos(a1), 0, sin(a1)) * node.agro_radius)
        lines.append(Vector3(cos(a2), 0, sin(a2)) * node.agro_radius)
    gizmo.add_lines(lines, get_material("main", gizmo))

In the editor, an aggro circle will be drawn around every Enemy — an instant visual cue.

When to write a plugin vs use @tool

  • @tool — for a single scene: procedural placement, a generator, a preview.
  • EditorPlugin — for a reusable feature: custom nodes, non-standard inspectors, docks.

Most Asset Library plugins (Phantom Camera, Dialogic, Terrain3D) are EditorPlugins. They are activated via Project Settings → Plugins.

Comparison with Unity

Unity equivalent:

  • The [ExecuteAlways] MonoBehaviour attribute ↔ Godot @tool.
  • OnDrawGizmos() / OnDrawGizmosSelected() ↔ Godot EditorNode3DGizmoPlugin.
  • Custom Editor ([CustomEditor(typeof(...))]) ↔ Godot EditorInspectorPlugin.
  • EditorWindow ↔ Godot a dock with a Control.
  • MenuItem ↔ Godot add_tool_menu_item.

The concepts are equivalent, the names differ.