【Godot】Creating Custom Nodes and Editor Extensions (class_name, @icon, @export)

Created: 2026-02-08

Define custom node types with class_name, improve editor usability with @icon and @export, and extend the inspector with custom Resources and EditorInspectorPlugin.

Overview

Tested with: Godot 4.3+

As your Godot project grows, you'll find yourself repeatedly creating the same node combinations or wanting to organize inspector properties. By combining class_name for custom node type definitions with @export for property exposure, you can create reusable components that are easy to work with in the editor.

This article covers the basics of custom nodes, @export organization techniques, custom Resource creation, and inspector extensions.

Defining Custom Node Types with class_name

Let's start with the foundation of custom nodes: the class_name declaration. Declaring class_name in a script registers that class as a global type in the Godot editor. This improves code reusability and type safety by allowing the class to be searchable in the Add Node dialog and referenceable in type annotations.

For example, in an action game you could extract common "HP management" logic into a HealthComponent shared across enemies, players, and destructible objects. Here's the basic implementation:

# health_component.gd
class_name HealthComponent
extends Node

signal died
signal health_changed(new_health: int)

@export var max_health: int = 100
var current_health: int

func _ready():
    current_health = max_health

func take_damage(amount: int):
    current_health = max(current_health - amount, 0)
    health_changed.emit(current_health)
    if current_health <= 0:
        died.emit()

func heal(amount: int):
    current_health = min(current_health + amount, max_health)
    health_changed.emit(current_health)

What you can do after registration:

  • Search for and add HealthComponent in the "Add Node" dialog
  • Use type annotations like var health: HealthComponent
  • Check types with the is operator: if node is HealthComponent:

tips: class_name registers in the global namespace, so duplicate names within a project will cause errors. For large projects or addon development, consider using prefixes to avoid collisions.

Setting Custom Icons with @icon

Now that you've defined a custom node type, let's polish its appearance. Use @icon to customize the editor icon for your node. As your scene tree fills up with custom nodes, default icons make them hard to tell apart. A dedicated icon makes each node's role instantly recognizable.

@icon("res://icons/health_heart.svg")
class_name HealthComponent
extends Node
  • Reflected in both the scene tree and the Add Node dialog
  • SVG format is recommended (no degradation when scaling)
  • Icons display at 16x16px in the editor
@icon("res://icons/hitbox.svg")
class_name HitboxComponent
extends Area2D

@export var damage: int = 10

Exposing Properties with @export

With your custom node's appearance sorted out, let's make it configurable from the inspector. Variables annotated with @export appear in the inspector and become editable in the editor. Whether it's enemy movement speed or max HP, you can tweak gameplay-critical parameters without touching the script, making collaboration with designers much smoother.

Basic @export Annotations

class_name EnemyConfig
extends CharacterBody2D

# Basic types
@export var speed: float = 100.0
@export var enemy_name: String = "Goblin"
@export var is_boss: bool = false

# With range
@export_range(0, 100, 1) var health: int = 50
@export_range(0.0, 10.0, 0.1) var attack_interval: float = 2.0

# Resource references
@export var sprite_texture: Texture2D
@export var death_effect: PackedScene

# Enum
@export_enum("Patrol", "Chase", "Guard") var ai_type: String = "Patrol"

# Color
@export var tint_color: Color = Color.WHITE

# File path
@export_file("*.tscn") var next_scene: String

Arrays and Dictionaries

You can also export array-type properties, such as patrol route points or lists of drop items.

# Typed arrays
@export var patrol_points: Array[Vector2] = []
@export var drop_items: Array[PackedScene] = []

# Flag enum
@export_flags("Fire", "Water", "Earth", "Wind") var elements: int = 0

The result of @export_flags is a bitflag int. Use bitwise operations to check individual flags:

# How to check flags
const FIRE = 1
const WATER = 2
const EARTH = 4
const WIND = 8

func has_element(flag: int) -> bool:
    return elements & flag != 0

# Usage
if has_element(FIRE):
    print("Has fire element")

Organizing with @export_group / @export_subgroup

Now that you know the various export types, let's talk organization. When you have 10 or 20 exported properties, the inspector becomes a long, overwhelming list. Groups help you keep things tidy and easy to navigate.

The following example organizes a player character's movement, combat, and visual properties into clear groups:

class_name PlayerCharacter
extends CharacterBody2D

@export_group("Movement")
@export var move_speed: float = 200.0
@export var jump_force: float = 400.0
@export var gravity_scale: float = 1.0

@export_group("Combat")
@export var attack_power: int = 10
@export var defense: int = 5

@export_subgroup("Weapon")
@export var weapon_range: float = 50.0
@export var weapon_cooldown: float = 0.5

@export_subgroup("Special")
@export var special_attack_cost: int = 20
@export var special_damage_multiplier: float = 2.5

@export_group("Visual")
@export var trail_color: Color = Color.CYAN
@export var particle_effect: PackedScene

How it appears in the inspector:

  • @export_group creates collapsible section headers
  • @export_subgroup appears as subsections within a group
  • Keeps properties organized and reduces configuration errors

Creating Custom Resources

So far we've been exposing properties on nodes, but sometimes you want to separate the data itself from the script. For example, weapon data in an RPG bundles together a name, attack power, range, icon, and more. By defining this as a Resource, you can save it as a .tres file, share it across multiple nodes, and assign it via drag-and-drop in the editor.

# weapon_data.gd
class_name WeaponData
extends Resource

@export var weapon_name: String = ""
@export var damage: int = 10
@export var attack_speed: float = 1.0
@export var range: float = 50.0
@export var icon: Texture2D
@export_multiline var description: String = ""

How to create a resource file:

  1. Right-click in the FileSystem panel -> "New Resource"
  2. Search for and select WeaponData
  3. Set values in the inspector and save as a .tres file
# Usage example: weapon_holder.gd
class_name WeaponHolder
extends Node

@export var equipped_weapon: WeaponData

func get_damage() -> int:
    if equipped_weapon:
        return equipped_weapon.damage
    return 0

Benefits:

  • Separates data from logic
  • Drag-and-drop assignment in the editor
  • Multiple nodes can share the same resource

Shared resource caveat: When multiple nodes reference the same .tres file, changing a property on one affects all of them. If you need per-node values, use duplicate() to create a copy, or enable "Local to Scene" under the Resource section in the inspector.

func _ready():
    # Duplicate the shared resource to modify it independently
    equipped_weapon = equipped_weapon.duplicate()
    equipped_weapon.damage = 20  # Only affects this node

Extending the Property Editor with EditorInspectorPlugin

This section is a bit more advanced. You can add custom UI to the inspector when a specific custom node is selected. Ever wanted to see a damage calculation preview right in the inspector when selecting a WeaponHolder? This feature makes that possible.

The following code adds equipment info and a damage preview button to the top of the inspector when a WeaponHolder is selected:

# addons/my_tools/custom_inspector.gd
@tool
class_name WeaponDataInspector
extends EditorInspectorPlugin

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

func _parse_begin(object: Object):
    var holder = object as WeaponHolder
    if holder.equipped_weapon:
        var info = Label.new()
        info.text = "Equipped: %s (ATK: %d)" % [
            holder.equipped_weapon.weapon_name,
            holder.equipped_weapon.damage
        ]
        add_custom_control(info)

        var preview_button = Button.new()
        preview_button.text = "Preview Damage Calculation"
        preview_button.pressed.connect(func():
            print("Estimated damage: %d" % holder.get_damage())
        )
        add_custom_control(preview_button)
# addons/my_tools/plugin.gd
@tool
extends EditorPlugin

var inspector_plugin: WeaponDataInspector

func _enter_tree():
    inspector_plugin = WeaponDataInspector.new()
    add_inspector_plugin(inspector_plugin)

func _exit_tree():
    if inspector_plugin:
        remove_inspector_plugin(inspector_plugin)

Activating the plugin: An EditorInspectorPlugin requires a plugin.cfg file and must be enabled in project settings.

; addons/my_tools/plugin.cfg
[plugin]
name="My Tools"
description="Custom inspector for WeaponHolder"
author="Your Name"
version="1.0"
script="plugin.gd"
  1. Place plugin.cfg, plugin.gd, and custom_inspector.gd in the addons/my_tools/ folder
  2. Enable the plugin under "Project" -> "Project Settings" -> "Plugins" tab

Custom Nodes vs. Composition

Ever wondered whether to create a custom node or just combine existing nodes (composition)? Each approach has its strengths, and choosing between them depends on your project's scale and reusability needs.

AspectCustom Node (class_name)Composition (Child Node Setup)
ReusabilityEasy to reuse across the projectTends to be limited to specific scenes
DiscoverabilityAppears in the Add Node dialogRequires searching for scene files
Type safetyCheckable with is operator and type annotationsRequires verifying script existence
ComplexityBest for single-purpose functionalityBest for combining multiple nodes
ConfigurationDirect setup via @export in the inspectorIndividual setup per child node

Guidelines for choosing:

# Custom node: Reuse a single piece of functionality
@icon("res://icons/health.svg")
class_name HealthComponent
extends Node
# -> Shared across enemies, players, destructible objects, etc.

# Composition: Combine multiple nodes
# player.tscn
# ├── CharacterBody2D
# │   ├── HealthComponent
# │   ├── MovementComponent
# │   ├── Sprite2D
# │   └── CollisionShape2D
# -> Save as a scene and instantiate as needed

Summary

  • class_name defines a custom node type, making it available in the Add Node dialog and for type annotations
  • @icon customizes the editor icon for better visual identification
  • @export exposes properties in the inspector for code-free adjustments
  • @export_group / @export_subgroup organizes large numbers of properties
  • Custom Resources separate data from logic for better reusability
  • EditorInspectorPlugin displays custom UI when specific nodes are selected
  • Use custom nodes for reusable single-purpose functionality; use composition for complex multi-node setups

Further Reading