Editor Tools & Plugins
@tool makes a script run in the editor, so it can update visuals live as you edit properties.
35.1 @tool scripts
@tool makes a script run in the editor, so it can update visuals live as you edit properties.
Call update_configuration_warnings() when the condition changes. Be careful: @tool code that calls queue_free() or modifies the scene runs on your edited scene and can destroy work — commit often.
@tool
extends Node2D
@export var radius := 64.0:
set(v):
radius = v
queue_redraw()
@export var color := Color.CYAN:
set(v):
color = v
queue_redraw()
func _draw() -> void:
draw_circle(Vector2.ZERO, radius, color)
func _process(delta: float) -> void:
if Engine.is_editor_hint():
return # editor-only branch: skip gameplay code
rotation += delta
func _get_configuration_warnings() -> PackedStringArray:
var warnings := PackedStringArray()
if radius <= 0.0:
warnings.append("Radius must be positive.")
return warnings # shows a warning icon in the Scene dock35.2 Dynamic properties
_get_property_list(), _get(property), and _set(property, value) let you create fully virtual properties.
@tool
extends Node
@export var use_custom_speed := false:
set(v):
use_custom_speed = v
notify_property_list_changed() # refresh the Inspector
@export var custom_speed := 100.0
func _validate_property(property: Dictionary) -> void:
if property.name == "custom_speed" and not use_custom_speed:
property.usage = PROPERTY_USAGE_NO_EDITOR # hide unless enabled35.3 EditorScript (one-off automation)
@tool
extends EditorScript
func _run() -> void: # File → Run in the script editor (Ctrl+Shift+X)
var root := get_scene()
for node in root.find_children("*", "Sprite2D"):
node.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
print("Updated sprites in ", root.name)35.4 Editor plugins
Create via *Project Settings → Plugins → Create New Plugin*. It generates addons/my_plugin/plugin.cfg:
Other hooks: _has_main_screen() + _make_visible() for a main-screen tab, _handles(object) + _forward_canvas_gui_input() for viewport tools, add_control_to_bottom_panel(), add_import_plugin(), add_export_plugin().
[plugin]
name="My Plugin"
description="Adds a custom dock and node type."
author="You"
version="1.0"
script="plugin.gd"# addons/my_plugin/plugin.gd
@tool
extends EditorPlugin
var dock: Control
var inspector_plugin: EditorInspectorPlugin
func _enter_tree() -> void:
add_custom_type("Spawner", "Node2D", preload("spawner.gd"), preload("icon.svg"))
dock = preload("dock.tscn").instantiate()
add_control_to_dock(DOCK_SLOT_LEFT_UL, dock)
inspector_plugin = preload("health_inspector.gd").new()
add_inspector_plugin(inspector_plugin)
add_tool_menu_item("Clean Unused Assets", _clean_assets)
func _exit_tree() -> void:
remove_custom_type("Spawner")
remove_control_from_docks(dock)
dock.queue_free()
remove_inspector_plugin(inspector_plugin)
remove_tool_menu_item("Clean Unused Assets")
func _clean_assets() -> void:
print(EditorInterface.get_edited_scene_root())35.5 Custom inspectors
For full replacement editors, subclass EditorProperty, implement _update_property(), call emit_changed(get_edited_property(), new_value), and add it with add_property_editor(name, editor).
# health_inspector.gd
@tool
extends EditorInspectorPlugin
func _can_handle(object: Object) -> bool:
return object is HealthComponent
func _parse_property(object: Object, type: Variant.Type, name: String, hint_type: PropertyHint,
hint_string: String, usage_flags: int, wide: bool) -> bool:
if name == "max_health":
var button := Button.new()
button.text = "Set to 100"
button.pressed.connect(func(): object.max_health = 100; object.notify_property_list_changed())
add_custom_control(button)
return false # false = keep the default editor too35.6 Undo/redo in plugins
Share plugins through the Asset Library or Git submodules.
var ur := get_undo_redo() # EditorUndoRedoManager
ur.create_action("Move nodes")
ur.add_do_property(node, "position", new_pos)
ur.add_undo_property(node, "position", node.position)
ur.commit_action()