Create custom node types with tool scripts and editor gizmos
Godot allows you to create custom node types that extend the engine’s functionality. With the @tool annotation and editor gizmos, you can provide a seamless editing experience for your custom nodes.
Create a custom node by extending an existing node type:
class_name MyCustomNodeextends Node2D@export var radius: float = 50.0@export var color: Color = Color.WHITEfunc _draw(): draw_circle(Vector2.ZERO, radius, color)func _process(delta): # Your game logic here pass
The @tool annotation makes scripts run in the editor, enabling real-time visualization and editing:
@toolclass_name CustomShapeextends Node2D@export var points: PackedVector2Array = []: set(value): points = value queue_redraw() # Update in editor@export var line_color: Color = Color.WHITE: set(value): line_color = value queue_redraw()@export var line_width: float = 2.0: set(value): line_width = value queue_redraw()func _draw(): if points.size() < 2: return for i in range(points.size() - 1): draw_line(points[i], points[i + 1], line_color, line_width)func _ready(): if Engine.is_editor_hint(): # Editor-only initialization print("Running in editor") else: # Game-only initialization print("Running in game")
Always use Engine.is_editor_hint() to separate editor-only code from runtime code. Tool scripts run in both editor and game.
Use property setters to trigger updates when values change:
@toolextends Sprite2D@export var auto_scale: bool = false: set(value): auto_scale = value if value and Engine.is_editor_hint(): _update_scale()@export var target_size: Vector2 = Vector2(100, 100): set(value): target_size = value if auto_scale and Engine.is_editor_hint(): _update_scale()func _update_scale(): if texture == null: return var tex_size = texture.get_size() scale = target_size / tex_size
Allow selecting and editing sub-parts of your node:
func _subgizmos_intersect_ray(gizmo, camera, screen_pos): var node = gizmo.get_node_3d() # Return ID of clicked subgizmo, or -1 if none for i in range(node.control_points.size()): var point = node.control_points[i] var screen_point = camera.unproject_position(node.global_transform * point) if screen_point.distance_to(screen_pos) < 10: return i return -1func _subgizmos_intersect_frustum(gizmo, camera, frustum_planes): var node = gizmo.get_node_3d() var selected = PackedInt32Array() for i in range(node.control_points.size()): var point = node.global_transform * node.control_points[i] var inside = true for plane in frustum_planes: if plane.distance_to(point) > 0: inside = false break if inside: selected.append(i) return selectedfunc _get_subgizmo_transform(gizmo, subgizmo_id): var node = gizmo.get_node_3d() return Transform3D(Basis(), node.control_points[subgizmo_id])func _set_subgizmo_transform(gizmo, subgizmo_id, transform): var node = gizmo.get_node_3d() node.control_points[subgizmo_id] = transform.originfunc _commit_subgizmos(gizmo, ids, restores, cancel): var node = gizmo.get_node_3d() var undo_redo = get_undo_redo() if cancel: for i in range(ids.size()): node.control_points[ids[i]] = restores[i].origin return undo_redo.create_action("Move Control Points") for i in range(ids.size()): undo_redo.add_do_property(node, "control_points", node.control_points.duplicate()) undo_redo.add_undo_method(node, "set", "control_points", restores) undo_redo.commit_action()
@toolclass_name WaypointPathextends Path3D@export var waypoint_count: int = 3: set(value): waypoint_count = max(2, value) _update_waypoints()@export var spacing: float = 5.0: set(value): spacing = value _update_waypoints()@export var auto_generate: bool = false: set(value): auto_generate = value if value and Engine.is_editor_hint(): _update_waypoints()func _ready(): if Engine.is_editor_hint(): _update_waypoints()func _update_waypoints(): if not Engine.is_editor_hint(): return if curve == null: curve = Curve3D.new() curve.clear_points() for i in range(waypoint_count): var point = Vector3(i * spacing, 0, 0) curve.add_point(point)func regenerate(): _update_waypoints()
With corresponding gizmo:
@toolextends EditorNode3DGizmoPluginfunc _init(): create_material("path", Color.GREEN) create_handle_material("handles")func _get_gizmo_name(): return "WaypointPath"func _has_gizmo(node): return node is WaypointPathfunc _redraw(gizmo): gizmo.clear() var node = gizmo.get_node_3d() var curve = node.curve if curve == null or curve.point_count < 2: return # Draw path lines var lines = PackedVector3Array() for i in range(curve.point_count - 1): lines.append(curve.get_point_position(i)) lines.append(curve.get_point_position(i + 1)) gizmo.add_lines(lines, get_material("path", gizmo), false) # Add handles for waypoints var handles = PackedVector3Array() for i in range(curve.point_count): handles.append(curve.get_point_position(i)) gizmo.add_handles(handles, get_material("handles", gizmo), [])