@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:
@toolscripts — ordinary nodes with behavior in the editor. For example, a script that automatically places trees along a spline.- 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.
Storybook + Design Tokens: interactive component previews with live parameters in the editor. Tool scripts are “Storybook right inside the final product”.
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 aControl.MenuItem↔ Godotadd_tool_menu_item.
The concepts are equivalent, the names differ.