Creating Custom Nodes
Introduction
Building custom nodes that integrate seamlessly with Godot: Custom nodes are the most common type of GDExtension, extending Godot’s node system with specialized functionality. This guide covers everything from simple utility nodes to complex systems that integrate with the editor and provide rich Inspector interfaces.
Choosing the Right Base Class
Selecting the appropriate parent class: The base class you inherit from determines your node’s capabilities and where it appears in the editor’s node creation dialog.
Node Hierarchy Overview
// Base classes and their use cases:
// Object - Base for everything, minimal overhead
class DataProcessor : public Object {
// Use for: Utilities, managers, data structures
};
// RefCounted - Automatic memory management
class ConfigManager : public RefCounted {
// Use for: Shared objects, cached data, utilities
};
// Node - Basic scene tree functionality
class GameManager : public Node {
// Use for: Managers, controllers, invisible logic nodes
};
// Node2D - 2D positioning and transforms
class CustomSprite : public Node2D {
// Use for: 2D game objects, visual effects, UI overlays
};
// Node3D - 3D positioning and transforms
class CustomMesh : public Node3D {
// Use for: 3D game objects, spatial effects, 3D utilities
};
// Control - UI and layout system
class CustomButton : public Control {
// Use for: UI widgets, HUD elements, menus
};
// Resource - Saveable data
class GameSettings : public Resource {
// Use for: Configuration data, game assets, persistent data
};
Decision Matrix
| Base Class | Scene Tree | Transform | UI Layout | Auto-Save | Memory | Use Cases |
|---|---|---|---|---|---|---|
| Object | ❌ | ❌ | ❌ | ❌ | Manual | Data processors, utilities |
| RefCounted | ❌ | ❌ | ❌ | ❌ | Auto | Shared objects, managers |
| Node | ✅ | ❌ | ❌ | ❌ | Scene | Logic controllers, managers |
| Node2D | ✅ | 2D | ❌ | ❌ | Scene | 2D game objects, effects |
| Node3D | ✅ | 3D | ❌ | ❌ | Scene | 3D game objects, spatial |
| Control | ✅ | 2D | ✅ | ❌ | Scene | UI widgets, HUD elements |
| Resource | ❌ | ❌ | ❌ | ✅ | Auto | Assets, settings, data |
Creating a Basic Custom Node
Step 1: Header File Structure
// custom_timer.h
#ifndef CUSTOM_TIMER_H
#define CUSTOM_TIMER_H
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/core/class_db.hpp>
using namespace godot;
class CustomTimer : public Node {
GDCLASS(CustomTimer, Node)
private:
double time_left;
double wait_time;
bool autostart;
bool paused;
bool one_shot;
protected:
static void _bind_methods();
void _notification(int p_what);
public:
CustomTimer();
~CustomTimer();
// Core functionality
void start(double p_time = -1);
void stop();
void set_paused(bool p_paused);
bool is_stopped() const;
double get_time_left() const;
// Properties
void set_wait_time(double p_time);
double get_wait_time() const;
void set_autostart(bool p_enabled);
bool has_autostart() const;
void set_one_shot(bool p_enabled);
bool is_one_shot() const;
// Virtual overrides
void _ready() override;
void _process(double delta) override;
};
#endif // CUSTOM_TIMER_H
Step 2: Implementation File
// custom_timer.cpp
#include "custom_timer.h"
#include <godot_cpp/variant/utility_functions.hpp>
void CustomTimer::_bind_methods() {
// Bind methods for GDScript access
ClassDB::bind_method(D_METHOD("start", "time"), &CustomTimer::start, DEFVAL(-1));
ClassDB::bind_method(D_METHOD("stop"), &CustomTimer::stop);
ClassDB::bind_method(D_METHOD("set_paused", "paused"), &CustomTimer::set_paused);
ClassDB::bind_method(D_METHOD("is_stopped"), &CustomTimer::is_stopped);
ClassDB::bind_method(D_METHOD("get_time_left"), &CustomTimer::get_time_left);
// Bind properties with getters/setters
ClassDB::bind_method(D_METHOD("set_wait_time", "time"), &CustomTimer::set_wait_time);
ClassDB::bind_method(D_METHOD("get_wait_time"), &CustomTimer::get_wait_time);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "wait_time", PROPERTY_HINT_RANGE, "0,3600,0.01,or_greater"),
"set_wait_time", "get_wait_time");
ClassDB::bind_method(D_METHOD("set_autostart", "enabled"), &CustomTimer::set_autostart);
ClassDB::bind_method(D_METHOD("has_autostart"), &CustomTimer::has_autostart);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "autostart"),
"set_autostart", "has_autostart");
ClassDB::bind_method(D_METHOD("set_one_shot", "enabled"), &CustomTimer::set_one_shot);
ClassDB::bind_method(D_METHOD("is_one_shot"), &CustomTimer::is_one_shot);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "one_shot"),
"set_one_shot", "is_one_shot");
// Add property groups for organization
ADD_GROUP("Timer", "");
// Bind signals
ADD_SIGNAL(MethodInfo("timeout"));
ADD_SIGNAL(MethodInfo("started"));
ADD_SIGNAL(MethodInfo("stopped"));
}
CustomTimer::CustomTimer() {
time_left = 0.0;
wait_time = 1.0;
autostart = false;
paused = false;
one_shot = false;
// Don't process by default
set_process(false);
}
CustomTimer::~CustomTimer() {
// Cleanup if needed
}
void CustomTimer::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_READY:
if (autostart) {
start();
}
break;
}
}
void CustomTimer::_ready() {
// Called when node is ready
UtilityFunctions::print("CustomTimer ready");
}
void CustomTimer::_process(double delta) {
if (paused || time_left <= 0) {
return;
}
time_left -= delta;
if (time_left <= 0) {
time_left = 0;
set_process(false);
emit_signal("timeout");
if (!one_shot) {
// Restart for repeating timer
time_left = wait_time;
set_process(true);
}
}
}
void CustomTimer::start(double p_time) {
if (p_time > 0) {
wait_time = p_time;
}
time_left = wait_time;
paused = false;
set_process(true);
emit_signal("started");
}
void CustomTimer::stop() {
time_left = 0;
set_process(false);
emit_signal("stopped");
}
void CustomTimer::set_paused(bool p_paused) {
if (paused != p_paused) {
paused = p_paused;
set_process(!paused && time_left > 0);
}
}
bool CustomTimer::is_stopped() const {
return time_left <= 0;
}
double CustomTimer::get_time_left() const {
return MAX(0, time_left);
}
void CustomTimer::set_wait_time(double p_time) {
wait_time = MAX(0, p_time);
}
double CustomTimer::get_wait_time() const {
return wait_time;
}
void CustomTimer::set_autostart(bool p_enabled) {
autostart = p_enabled;
}
bool CustomTimer::has_autostart() const {
return autostart;
}
void CustomTimer::set_one_shot(bool p_enabled) {
one_shot = p_enabled;
}
bool CustomTimer::is_one_shot() const {
return one_shot;
}
Advanced Custom Node Features
Rich Inspector Properties
class AdvancedCustomNode : public Node2D {
GDCLASS(AdvancedCustomNode, Node2D)
private:
// Various property types
int health = 100;
float speed = 150.0f;
String character_name = "Hero";
Color tint_color = Color(1, 1, 1, 1);
Vector2 offset = Vector2(0, 0);
// Enum property
enum MovementType {
MOVEMENT_WALK,
MOVEMENT_RUN,
MOVEMENT_FLY
};
MovementType movement_type = MOVEMENT_WALK;
// Resource property
Ref<Texture2D> icon_texture;
// Array properties
PackedStringArray tags;
Array inventory_items;
// Node path property
NodePath target_path;
// File path property
String config_file_path = "res://config.cfg";
protected:
static void _bind_methods() {
// Basic properties
ClassDB::bind_method(D_METHOD("set_health", "health"), &AdvancedCustomNode::set_health);
ClassDB::bind_method(D_METHOD("get_health"), &AdvancedCustomNode::get_health);
ADD_PROPERTY(PropertyInfo(Variant::INT, "health", PROPERTY_HINT_RANGE, "0,999,1"),
"set_health", "get_health");
ClassDB::bind_method(D_METHOD("set_speed", "speed"), &AdvancedCustomNode::set_speed);
ClassDB::bind_method(D_METHOD("get_speed"), &AdvancedCustomNode::get_speed);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed", PROPERTY_HINT_RANGE, "0,1000,0.1"),
"set_speed", "get_speed");
// String with length limit
ClassDB::bind_method(D_METHOD("set_character_name", "name"), &AdvancedCustomNode::set_character_name);
ClassDB::bind_method(D_METHOD("get_character_name"), &AdvancedCustomNode::get_character_name);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "character_name", PROPERTY_HINT_LENGTH, "1,32"),
"set_character_name", "get_character_name");
// Color picker
ClassDB::bind_method(D_METHOD("set_tint_color", "color"), &AdvancedCustomNode::set_tint_color);
ClassDB::bind_method(D_METHOD("get_tint_color"), &AdvancedCustomNode::get_tint_color);
ADD_PROPERTY(PropertyInfo(Variant::COLOR, "tint_color"),
"set_tint_color", "get_tint_color");
// Vector with step
ClassDB::bind_method(D_METHOD("set_offset", "offset"), &AdvancedCustomNode::set_offset);
ClassDB::bind_method(D_METHOD("get_offset"), &AdvancedCustomNode::get_offset);
ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "offset", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT),
"set_offset", "get_offset");
// Enum dropdown
ClassDB::bind_method(D_METHOD("set_movement_type", "type"), &AdvancedCustomNode::set_movement_type);
ClassDB::bind_method(D_METHOD("get_movement_type"), &AdvancedCustomNode::get_movement_type);
ADD_PROPERTY(PropertyInfo(Variant::INT, "movement_type", PROPERTY_HINT_ENUM, "Walk,Run,Fly"),
"set_movement_type", "get_movement_type");
// Resource property
ClassDB::bind_method(D_METHOD("set_icon_texture", "texture"), &AdvancedCustomNode::set_icon_texture);
ClassDB::bind_method(D_METHOD("get_icon_texture"), &AdvancedCustomNode::get_icon_texture);
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "icon_texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"),
"set_icon_texture", "get_icon_texture");
// Array properties
ClassDB::bind_method(D_METHOD("set_tags", "tags"), &AdvancedCustomNode::set_tags);
ClassDB::bind_method(D_METHOD("get_tags"), &AdvancedCustomNode::get_tags);
ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "tags"),
"set_tags", "get_tags");
// Node path
ClassDB::bind_method(D_METHOD("set_target_path", "path"), &AdvancedCustomNode::set_target_path);
ClassDB::bind_method(D_METHOD("get_target_path"), &AdvancedCustomNode::get_target_path);
ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "target_path"),
"set_target_path", "get_target_path");
// File path
ClassDB::bind_method(D_METHOD("set_config_file_path", "path"), &AdvancedCustomNode::set_config_file_path);
ClassDB::bind_method(D_METHOD("get_config_file_path"), &AdvancedCustomNode::get_config_file_path);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "config_file_path", PROPERTY_HINT_FILE, "*.cfg,*.ini"),
"set_config_file_path", "get_config_file_path");
// Property groups
ADD_GROUP("Character", "character_");
ADD_GROUP("Movement", "movement_");
ADD_GROUP("Visual", "");
ADD_GROUP("Paths", "");
// Bind enum constants
BIND_ENUM_CONSTANT(MOVEMENT_WALK);
BIND_ENUM_CONSTANT(MOVEMENT_RUN);
BIND_ENUM_CONSTANT(MOVEMENT_FLY);
}
public:
// Property implementations
void set_health(int p_health) {
health = CLAMP(p_health, 0, 999);
notify_property_list_changed(); // Refresh inspector if needed
}
int get_health() const { return health; }
void set_speed(float p_speed) { speed = MAX(0, p_speed); }
float get_speed() const { return speed; }
void set_character_name(const String& p_name) {
character_name = p_name.substr(0, 32); // Enforce length limit
}
String get_character_name() const { return character_name; }
void set_tint_color(const Color& p_color) { tint_color = p_color; }
Color get_tint_color() const { return tint_color; }
void set_offset(const Vector2& p_offset) { offset = p_offset; }
Vector2 get_offset() const { return offset; }
void set_movement_type(int p_type) {
movement_type = static_cast<MovementType>(CLAMP(p_type, 0, 2));
}
int get_movement_type() const { return movement_type; }
void set_icon_texture(const Ref<Texture2D>& p_texture) { icon_texture = p_texture; }
Ref<Texture2D> get_icon_texture() const { return icon_texture; }
void set_tags(const PackedStringArray& p_tags) { tags = p_tags; }
PackedStringArray get_tags() const { return tags; }
void set_target_path(const NodePath& p_path) { target_path = p_path; }
NodePath get_target_path() const { return target_path; }
void set_config_file_path(const String& p_path) { config_file_path = p_path; }
String get_config_file_path() const { return config_file_path; }
};
VARIANT_ENUM_CAST(AdvancedCustomNode::MovementType);
Dynamic Properties
class DynamicPropertyNode : public Node {
GDCLASS(DynamicPropertyNode, Node)
private:
Dictionary dynamic_properties;
bool show_advanced = false;
String property_filter = "";
protected:
static void _bind_methods() {
ClassDB::bind_method(D_METHOD("set_show_advanced", "show"), &DynamicPropertyNode::set_show_advanced);
ClassDB::bind_method(D_METHOD("get_show_advanced"), &DynamicPropertyNode::get_show_advanced);
ClassDB::bind_method(D_METHOD("set_property_filter", "filter"), &DynamicPropertyNode::set_property_filter);
ClassDB::bind_method(D_METHOD("get_property_filter"), &DynamicPropertyNode::get_property_filter);
// This will appear in the inspector
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "show_advanced"),
"set_show_advanced", "get_show_advanced");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "property_filter", PROPERTY_HINT_PLACEHOLDER_TEXT, "Filter..."),
"set_property_filter", "get_property_filter");
}
// Override to provide dynamic property list
virtual void _get_property_list(List<PropertyInfo>* p_list) const override {
// Add dynamic properties based on current settings
if (show_advanced) {
p_list->push_back(PropertyInfo(Variant::FLOAT, "advanced_multiplier",
PROPERTY_HINT_RANGE, "0,10,0.1"));
p_list->push_back(PropertyInfo(Variant::VECTOR3, "advanced_offset"));
}
// Add filtered properties
if (!property_filter.is_empty()) {
// Add properties that match the filter
p_list->push_back(PropertyInfo(Variant::STRING, "filtered_property_1"));
p_list->push_back(PropertyInfo(Variant::STRING, "filtered_property_2"));
}
// Add separator
p_list->push_back(PropertyInfo(Variant::NIL, "", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_GROUP, "Dynamic Properties"));
// Add properties from dictionary
for (const KeyValue<Variant, Variant>& E : dynamic_properties) {
String prop_name = "dynamic_" + String(E.key);
Variant::Type type = E.value.get_type();
p_list->push_back(PropertyInfo(type, prop_name));
}
}
// Override to handle dynamic property getting
virtual bool _get(const StringName& p_name, Variant& r_ret) const override {
String name = p_name;
if (name == "advanced_multiplier") {
r_ret = dynamic_properties.get("advanced_multiplier", 1.0);
return true;
} else if (name == "advanced_offset") {
r_ret = dynamic_properties.get("advanced_offset", Vector3());
return true;
} else if (name.begins_with("filtered_property_")) {
r_ret = dynamic_properties.get(name, "");
return true;
} else if (name.begins_with("dynamic_")) {
String key = name.substr(8); // Remove "dynamic_" prefix
r_ret = dynamic_properties.get(key, Variant());
return true;
}
return false;
}
// Override to handle dynamic property setting
virtual bool _set(const StringName& p_name, const Variant& p_value) override {
String name = p_name;
if (name == "advanced_multiplier" || name == "advanced_offset" ||
name.begins_with("filtered_property_") || name.begins_with("dynamic_")) {
String key = name.begins_with("dynamic_") ? name.substr(8) : name;
dynamic_properties[key] = p_value;
return true;
}
return false;
}
public:
void set_show_advanced(bool p_show) {
if (show_advanced != p_show) {
show_advanced = p_show;
notify_property_list_changed();
}
}
bool get_show_advanced() const {
return show_advanced;
}
void set_property_filter(const String& p_filter) {
if (property_filter != p_filter) {
property_filter = p_filter;
notify_property_list_changed();
}
}
String get_property_filter() const {
return property_filter;
}
void add_dynamic_property(const String& name, const Variant& value) {
dynamic_properties[name] = value;
notify_property_list_changed();
}
void remove_dynamic_property(const String& name) {
dynamic_properties.erase(name);
notify_property_list_changed();
}
};
Tool Scripts and Editor Integration
Making Your Node Work in the Editor
class EditorIntegratedNode : public Node2D {
GDCLASS(EditorIntegratedNode, Node2D)
private:
Vector<Vector2> debug_points;
Color debug_color = Color(1, 0, 0, 0.5);
bool show_debug = true;
protected:
static void _bind_methods() {
ClassDB::bind_method(D_METHOD("set_show_debug", "show"), &EditorIntegratedNode::set_show_debug);
ClassDB::bind_method(D_METHOD("get_show_debug"), &EditorIntegratedNode::get_show_debug);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "show_debug"),
"set_show_debug", "get_show_debug");
ClassDB::bind_method(D_METHOD("set_debug_color", "color"), &EditorIntegratedNode::set_debug_color);
ClassDB::bind_method(D_METHOD("get_debug_color"), &EditorIntegratedNode::get_debug_color);
ADD_PROPERTY(PropertyInfo(Variant::COLOR, "debug_color"),
"set_debug_color", "get_debug_color");
ClassDB::bind_method(D_METHOD("add_debug_point", "point"), &EditorIntegratedNode::add_debug_point);
ClassDB::bind_method(D_METHOD("clear_debug_points"), &EditorIntegratedNode::clear_debug_points);
}
public:
void _ready() override {
// Different behavior in editor vs game
if (Engine::get_singleton()->is_editor_hint()) {
// Running in editor - setup editor-specific behavior
setup_editor_mode();
} else {
// Running in game - setup game behavior
setup_game_mode();
}
}
void _process(double delta) override {
if (Engine::get_singleton()->is_editor_hint()) {
// Editor processing
update_editor_visuals();
}
// Always update, but queue_redraw for editor
if (Engine::get_singleton()->is_editor_hint()) {
queue_redraw(); // Refresh editor display
}
}
void _draw() override {
if (!show_debug) return;
// Draw debug information
for (int i = 0; i < debug_points.size(); i++) {
draw_circle(debug_points[i], 5.0f, debug_color);
if (i > 0) {
draw_line(debug_points[i-1], debug_points[i], debug_color, 2.0f);
}
}
}
// Editor-specific functionality
void set_show_debug(bool p_show) {
show_debug = p_show;
queue_redraw();
}
bool get_show_debug() const {
return show_debug;
}
void set_debug_color(const Color& p_color) {
debug_color = p_color;
queue_redraw();
}
Color get_debug_color() const {
return debug_color;
}
void add_debug_point(const Vector2& point) {
debug_points.append(point);
queue_redraw();
}
void clear_debug_points() {
debug_points.clear();
queue_redraw();
}
private:
void setup_editor_mode() {
// Editor-only setup
set_process(true); // Enable processing in editor
// Add some sample debug points
debug_points.append(Vector2(0, 0));
debug_points.append(Vector2(50, 50));
debug_points.append(Vector2(100, 0));
}
void setup_game_mode() {
// Game-only setup
show_debug = false; // Hide debug visuals in game
debug_points.clear();
}
void update_editor_visuals() {
// Update editor-specific visuals
// This runs only in the editor
}
};
Custom Gizmos and Handles
class GizmoNode : public Node2D {
GDCLASS(GizmoNode, Node2D)
private:
float radius = 50.0f;
Vector2 handle_position = Vector2(50, 0);
protected:
static void _bind_methods() {
ClassDB::bind_method(D_METHOD("set_radius", "radius"), &GizmoNode::set_radius);
ClassDB::bind_method(D_METHOD("get_radius"), &GizmoNode::get_radius);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "radius", PROPERTY_HINT_RANGE, "1,500,1"),
"set_radius", "get_radius");
}
public:
void _draw() override {
if (Engine::get_singleton()->is_editor_hint()) {
// Draw circle outline
draw_arc(Vector2(), radius, 0, Math_TAU, 64, Color(1, 1, 1, 0.5), 2.0f);
// Draw handle
draw_circle(handle_position, 5.0f, Color(1, 0, 0));
}
}
void set_radius(float p_radius) {
radius = MAX(1.0f, p_radius);
handle_position = Vector2(radius, 0);
queue_redraw();
}
float get_radius() const {
return radius;
}
// Handle mouse interaction in editor
virtual bool _can_drop_data(const Vector2& position, const Variant& data) const override {
return Engine::get_singleton()->is_editor_hint();
}
virtual void _drop_data(const Vector2& position, const Variant& data) override {
if (Engine::get_singleton()->is_editor_hint()) {
// Handle dropped data in editor
handle_position = position;
radius = position.length();
queue_redraw();
}
}
};
Node Registration and Organization
Registering Your Custom Nodes
// In your module initialization file
void initialize_custom_nodes_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
// Register all your custom nodes
GDREGISTER_CLASS(CustomTimer);
GDREGISTER_CLASS(AdvancedCustomNode);
GDREGISTER_CLASS(DynamicPropertyNode);
GDREGISTER_CLASS(EditorIntegratedNode);
GDREGISTER_CLASS(GizmoNode);
// You can also register virtual base classes
// GDREGISTER_VIRTUAL_CLASS(BaseCustomNode);
}
void uninitialize_custom_nodes_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
// Cleanup if needed
}
Node Categories and Icons
// You can organize nodes in categories by naming conventions
class UI_CustomButton : public Control {
GDCLASS(UI_CustomButton, Control)
// Will appear under UI category in create dialog
};
class Game_InventoryManager : public Node {
GDCLASS(Game_InventoryManager, Node)
// Will appear under Game category
};
class Audio_SoundController : public Node {
GDCLASS(Audio_SoundController, Node)
// Will appear under Audio category
};
Best Practices and Common Patterns
1. Proper Initialization Order
class ProperlyInitializedNode : public Node2D {
GDCLASS(ProperlyInitializedNode, Node2D)
private:
Ref<Resource> required_resource;
Node* cached_child = nullptr;
public:
void _enter_tree() override {
// Called when node enters scene tree
// Good for: Initial setup that doesn't depend on children
}
void _ready() override {
// Called when node and all children are ready
// Good for: Finding child nodes, connecting signals
cached_child = get_node_or_null(NodePath("ChildNode"));
if (cached_child) {
// Connect to child signals
cached_child->connect("signal_name", Callable(this, "on_child_signal"));
}
// Load resources
required_resource = preload("res://assets/data.tres");
}
void _exit_tree() override {
// Called when node exits scene tree
// Good for: Cleanup, disconnecting signals
if (cached_child && cached_child->is_connected("signal_name", Callable(this, "on_child_signal"))) {
cached_child->disconnect("signal_name", Callable(this, "on_child_signal"));
}
}
};
2. Signal-Based Communication
class SignalBasedNode : public Node {
GDCLASS(SignalBasedNode, Node)
protected:
static void _bind_methods() {
// Define clear, descriptive signals
ADD_SIGNAL(MethodInfo("health_changed",
PropertyInfo(Variant::INT, "new_health"),
PropertyInfo(Variant::INT, "old_health")));
ADD_SIGNAL(MethodInfo("item_collected",
PropertyInfo(Variant::STRING, "item_name"),
PropertyInfo(Variant::INT, "quantity")));
ADD_SIGNAL(MethodInfo("state_changed",
PropertyInfo(Variant::STRING, "new_state"),
PropertyInfo(Variant::STRING, "previous_state")));
}
public:
void take_damage(int damage) {
int old_health = health;
health -= damage;
// Always emit specific, detailed signals
emit_signal("health_changed", health, old_health);
if (health <= 0) {
emit_signal("state_changed", "dead", "alive");
}
}
void collect_item(const String& item_name, int quantity = 1) {
// Process collection logic...
emit_signal("item_collected", item_name, quantity);
}
private:
int health = 100;
};
3. Performance Optimization
class OptimizedNode : public Node2D {
GDCLASS(OptimizedNode, Node2D)
private:
bool needs_update = true;
float update_interval = 0.1f;
float time_since_update = 0.0f;
public:
void _ready() override {
// Only enable processing when needed
set_process(false);
set_physics_process(false);
// Start processing only when required
start_processing();
}
void _process(double delta) override {
// Throttle updates
time_since_update += delta;
if (time_since_update < update_interval) {
return;
}
time_since_update = 0.0f;
// Only update when needed
if (needs_update) {
perform_update();
needs_update = false;
// Disable processing when done
if (!has_pending_updates()) {
set_process(false);
}
}
}
void mark_dirty() {
needs_update = true;
if (!is_processing()) {
set_process(true);
}
}
private:
void start_processing() {
set_process(true);
}
void perform_update() {
// Actual update logic
}
bool has_pending_updates() {
// Check if more updates are needed
return needs_update;
}
};
Conclusion
Creating custom nodes involves:
- Choosing the right base class based on functionality needs
- Proper method binding for GDScript integration
- Rich property systems with appropriate hints and validation
- Signal communication for loose coupling
- Editor integration for development workflow
- Performance optimization through selective processing
- Proper registration and organization
Well-designed custom nodes feel like native Godot classes, integrate seamlessly with the editor, and provide clear, useful functionality to game developers.