Object, Resource, and Node Classes
Overview
What are Objects, Resources, and Nodes? These are the three fundamental building blocks of Godot. Think of Object as the foundation that provides basic features every game object needs - like the ability to receive signals, store properties, and call methods. Resource extends Object for data that should be saved and loaded (like textures, sounds, or custom save files). Node extends Object for things that exist in your game world and participate in the game loop (like players, enemies, or UI elements).
Why this hierarchy matters: Understanding this hierarchy is crucial because it determines what your C++ classes can do. Inherit from Object for basic functionality, Resource for saveable data, or Node for game entities that need to process each frame.
The Godot object hierarchy forms the foundation of all engine functionality. Every class that can interact with the engine must inherit from Object, which provides essential services like reference counting, signal emission, property management, and method binding.
Class Hierarchy
---
config:
theme: 'base'
themeVariables:
darkMode: true
background: '#262B33'
primaryColor: '#2b4268ff'
primaryTextColor: '#C1C4CA'
primaryBorderColor: '#3a3f47ff'
mainBkg: '#262B33'
secondBkg: '#425f5fff'
textColor: '#C1C4CA'
tertiaryBkg: '#4d4962ff'
classText: '#C1C4CA'
lineColor: '#978c72ff'
labelBoxBkgColor: '#425f5fff'
labelBoxBorderColor: '#8c9c81ff'
labelTextColor: '#C1C4CA'
mainContrastColor: '#FFFFFF'
noteColor: '#7a7253ff'
noteBkgColor: '#3a3f47ff'
noteTextColor: '#C1C4CA'
noteBorderColor: '#6a6f77ff'
---
classDiagram
class Object {
<<base>>
+StringName class_name
+ObjectID instance_id
+HashMap~StringName, Variant~ metadata
+connect(signal, callable)
+disconnect(signal, callable)
+emit_signal(signal, ...)
+set(property, value)
+get(property)
+call(method, ...)
+notification(what)
#_bind_methods()
}
class RefCounted {
<<reference counting>>
-SafeRefCount ref_count
+reference()
+unreference()
+get_reference_count()
}
class Resource {
<<data container>>
+String resource_path
+String resource_name
+bool resource_local_to_scene
+setup_local_to_scene()
+duplicate(subresources)
+save(path)
+load(path)
}
class Node {
<<scene tree>>
+StringName name
+Node* parent
+Vector~Node*~ children
+add_child(node)
+remove_child(node)
+get_tree()
+_ready()
+_process(delta)
+_physics_process(delta)
+_input(event)
}
Object <|-- RefCounted
RefCounted <|-- Resource
Object <|-- Node
style Object fill:#2b4268ff,stroke:#779DC9ff,stroke-width:2px,color:#FFFFFF
style RefCounted fill:#425f5fff,stroke:#8c9c81ff,stroke-width:2px,color:#FFFFFF
style Resource fill:#4d4962ff,stroke:#8983a5ff,stroke-width:2px,color:#FFFFFF
style Node fill:#7a6253ff,stroke:#c7ac9bff,stroke-width:2px,color:#FFFFFF
godot::Object - The Foundation
The base of everything: Object is like the “DNA” that all Godot classes share. It provides the fundamental capabilities that make something recognizable to the Godot engine - the ability to have properties that show up in the inspector, emit and receive signals for communication, and be called from GDScript. When you inherit from Object, your C++ class becomes a “first-class citizen” in the Godot ecosystem.
Core Functionality
Object is the base class for all Godot classes, providing fundamental services:
// include/godot_cpp/classes/object.hpp
class Object {
private:
// Internal state maintained by Object
ObjectID _instance_id; // Unique identifier
GDExtensionObjectPtr _owner; // Engine-side object pointer
StringName _class_name; // Runtime type information
// Bookkeeping structures
HashMap<StringName, Variant> metadata; // User metadata
List<Connection> signal_connections; // Active signal connections
HashMap<StringName, PropertyInfo> properties; // Registered properties
public:
// Core object interface
void set(const StringName &p_name, const Variant &p_value);
Variant get(const StringName &p_name) const;
// Dynamic method invocation
Variant call(const StringName &p_method, const Variant **p_args, int p_argcount);
Variant callv(const StringName &p_method, const Array &p_args);
// Signal system
Error connect(const StringName &p_signal, const Callable &p_callable,
uint32_t p_flags = 0);
void disconnect(const StringName &p_signal, const Callable &p_callable);
void emit_signal(const StringName &p_signal, const Variant **p_args, int p_argcount);
// Type system
bool is_class(const StringName &p_class) const;
StringName get_class() const;
// Notification system
void notification(int p_what, bool p_reversed = false);
virtual void _notification(int p_what) {}
};
Object Lifecycle in GDExtension
class MyObject : public Object {
GDCLASS(MyObject, Object)
protected:
static void _bind_methods() {
// Called once during class registration
// Sets up all bindings for the class
ClassDB::bind_method(D_METHOD("my_method"), &MyObject::my_method);
ADD_SIGNAL(MethodInfo("my_signal",
PropertyInfo(Variant::STRING, "message")));
ADD_PROPERTY(PropertyInfo(Variant::INT, "my_property"),
"set_my_property", "get_my_property");
}
public:
MyObject() {
// Constructor - object not yet fully initialized
// Don't access engine services here
}
void _init() {
// Called after engine-side initialization
// Safe to use engine services
}
virtual void _notification(int p_what) {
switch (p_what) {
case NOTIFICATION_POSTINITIALIZE:
// Object fully constructed
break;
case NOTIFICATION_PREDELETE:
// About to be deleted
cleanup();
break;
}
}
~MyObject() {
// Destructor - object already disconnected from engine
}
};
Object State Management
Object maintains extensive state for engine integration:
// State tracking example
class StatefulObject : public Object {
GDCLASS(StatefulObject, Object)
private:
// Object automatically tracks:
// 1. All property values
// 2. Signal connections
// 3. Metadata entries
// 4. Script instance (if any)
// 5. Editor-specific data
protected:
static void _bind_methods() {
// Properties are automatically tracked
ClassDB::bind_method(D_METHOD("set_health", "value"), &StatefulObject::set_health);
ClassDB::bind_method(D_METHOD("get_health"), &StatefulObject::get_health);
ADD_PROPERTY(PropertyInfo(Variant::INT, "health", PROPERTY_HINT_RANGE, "0,100"),
"set_health", "get_health");
// Signals are registered in object state
ADD_SIGNAL(MethodInfo("health_changed",
PropertyInfo(Variant::INT, "new_health")));
// Groups for organizational purposes
ADD_GROUP("Stats", "stat_");
ADD_PROPERTY(PropertyInfo(Variant::INT, "stat_strength"),
"set_strength", "get_strength");
}
int health = 100;
int strength = 10;
public:
void set_health(int p_health) {
if (health != p_health) {
health = p_health;
emit_signal("health_changed", health);
// Property change notification for editor
notify_property_list_changed();
}
}
int get_health() const { return health; }
};
godot::Resource - Data Management
Data that persists: Resources are for anything you want to save to disk and load later - think of them as “files” that Godot understands. This includes obvious things like images and sounds, but also custom data like player stats, level configurations, or game settings. Resources automatically handle reference counting (so they’re only deleted when nothing is using them) and can be shared between different parts of your game.
Resource Architecture
Resources are reference-counted data containers designed for sharing and saving:
class MyResource : public Resource {
GDCLASS(MyResource, Resource)
private:
String data;
Ref<Texture2D> texture;
Array items;
protected:
static void _bind_methods() {
ClassDB::bind_method(D_METHOD("set_data", "data"), &MyResource::set_data);
ClassDB::bind_method(D_METHOD("get_data"), &MyResource::get_data);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "data"), "set_data", "get_data");
ClassDB::bind_method(D_METHOD("set_texture", "texture"), &MyResource::set_texture);
ClassDB::bind_method(D_METHOD("get_texture"), &MyResource::get_texture);
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture",
PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"),
"set_texture", "get_texture");
}
public:
// Resources are automatically serializable
void set_data(const String &p_data) {
data = p_data;
emit_changed(); // Notify users of change
}
String get_data() const { return data; }
void set_texture(const Ref<Texture2D> &p_texture) {
texture = p_texture;
emit_changed();
}
Ref<Texture2D> get_texture() const { return texture; }
// Custom duplication logic
virtual Ref<Resource> duplicate(bool p_subresources = false) const override {
Ref<MyResource> copy;
copy.instantiate();
copy->data = data;
if (p_subresources && texture.is_valid()) {
copy->texture = texture->duplicate();
} else {
copy->texture = texture;
}
return copy;
}
};
Resource Usage Patterns
// Creating and using resources
void ResourceExample::demonstrate_resources() {
// Create new resource
Ref<MyResource> res;
res.instantiate();
res->set_data("Hello");
// Save to disk
ResourceSaver::save(res, "res://my_resource.tres");
// Load from disk
Ref<MyResource> loaded = ResourceLoader::load("res://my_resource.tres");
// Resources are shared by default
node1->set_resource(loaded);
node2->set_resource(loaded); // Same instance
// Make unique copy
Ref<MyResource> unique_copy = loaded->duplicate(true);
node3->set_resource(unique_copy); // Different instance
// Local to scene resources
res->set_local_to_scene(true); // Each scene gets its own copy
}
Resource Loading and Caching
class ResourceManager : public Object {
GDCLASS(ResourceManager, Object)
private:
HashMap<String, Ref<Resource>> cache;
public:
Ref<Resource> load_cached(const String &p_path) {
// Check cache first
if (cache.has(p_path)) {
return cache[p_path];
}
// Load and cache
Ref<Resource> res = ResourceLoader::load(p_path);
if (res.is_valid()) {
cache[p_path] = res;
// Connect to changed signal for cache invalidation
res->connect("changed", Callable(this, "on_resource_changed").bind(p_path));
}
return res;
}
void on_resource_changed(const String &p_path) {
print_line("Resource changed: " + p_path);
// Could reload or notify dependents
}
};
godot::Node - Scene Tree Building Block
Living game entities: Nodes are the “actors” in your game - things that exist in the game world and do stuff each frame. Every Node exists in a tree structure (parent-child relationships) and can participate in the game loop by overriding methods like _process() and _ready(). When you want to create game objects that move, respond to input, or update over time, you inherit from Node or one of its specialized subclasses.
Node Architecture
Nodes form the scene tree and provide the game loop integration:
class MyNode : public Node {
GDCLASS(MyNode, Node)
private:
bool initialized = false;
float time_accumulated = 0.0f;
NodePath target_path;
Node *cached_target = nullptr;
protected:
static void _bind_methods() {
// Bind lifecycle methods
ClassDB::bind_method(D_METHOD("initialize"), &MyNode::initialize);
// Properties with NodePath
ClassDB::bind_method(D_METHOD("set_target_path", "path"), &MyNode::set_target_path);
ClassDB::bind_method(D_METHOD("get_target_path"), &MyNode::get_target_path);
ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "target_path"),
"set_target_path", "get_target_path");
// Custom signals
ADD_SIGNAL(MethodInfo("target_reached"));
}
public:
virtual void _ready() override {
// Node is in tree and ready
initialize();
// Resolve node paths
if (!target_path.is_empty()) {
cached_target = get_node<Node>(target_path);
}
// Configure processing
set_process(true);
set_physics_process(true);
set_process_input(true);
}
virtual void _enter_tree() override {
// Node added to tree but children might not be ready
print_line("Entered tree: " + get_path());
}
virtual void _exit_tree() override {
// Node removed from tree
cached_target = nullptr; // Clear cached references
}
virtual void _process(double delta) override {
time_accumulated += delta;
if (cached_target) {
Vector3 target_pos = Object::cast_to<Node3D>(cached_target)->get_global_position();
Vector3 my_pos = Object::cast_to<Node3D>(this)->get_global_position();
if (my_pos.distance_to(target_pos) < 1.0) {
emit_signal("target_reached");
}
}
}
};
Node Tree Operations
class TreeOperations : public Node {
GDCLASS(TreeOperations, Node)
public:
void demonstrate_tree_ops() {
// Creating nodes
Node *child = memnew(Node);
child->set_name("Child");
add_child(child);
// Node ownership (for scene saving)
child->set_owner(this);
// Finding nodes
Node *found = get_node_or_null(NodePath("Child"));
Node *recursive = find_child("**/DeepChild", true, false);
// Getting typed nodes
Node3D *spatial = get_node<Node3D>("Child");
// Tree traversal
TypedArray<Node> children = get_children();
for (int i = 0; i < children.size(); i++) {
Node *c = Object::cast_to<Node>(children[i]);
print_line("Child: " + c->get_name());
}
// Scene tree access
SceneTree *tree = get_tree();
if (tree) {
// Get nodes in group
TypedArray<Node> enemies = tree->get_nodes_in_group("enemies");
// Scene operations
tree->change_scene_to_file("res://next_level.tscn");
}
// Deferred operations (safe during callbacks)
child->queue_free(); // Delete at end of frame
call_deferred("add_child", another_child);
}
};
Node Groups and Metadata
class GroupedNode : public Node {
GDCLASS(GroupedNode, Node)
public:
virtual void _ready() override {
// Add to groups for easy lookup
add_to_group("enemies");
add_to_group("damageable");
// Store metadata
set_meta("health", 100);
set_meta("team", "red");
// Check group membership
if (is_in_group("enemies")) {
get_tree()->call_group("players", "alert_enemy_spawned", this);
}
}
void take_damage(int amount) {
int health = get_meta("health", 100);
health -= amount;
set_meta("health", health);
if (health <= 0) {
remove_from_group("damageable");
add_to_group("dead");
queue_free();
}
}
};
State Management and Bookkeeping
How Godot tracks your objects: Behind the scenes, Godot maintains extensive information about every object - what properties it has, what signals it can emit, which methods can be called, and how it relates to other objects. This “bookkeeping” is what makes features like the inspector, signal connections, and GDScript interaction work automatically.
How Object Maintains State
The Object class maintains extensive internal state through several mechanisms:
// Internal Object state management (conceptual)
class Object {
private:
// 1. Property Storage
struct PropertyStorage {
HashMap<StringName, Variant> properties;
HashMap<StringName, PropertyInfo> property_info;
Vector<PropertyInfo> property_list_cache;
bool property_list_changed = false;
} property_data;
// 2. Signal Connections
struct SignalData {
struct Connection {
Callable callable;
uint32_t flags;
Vector<Variant> binds;
};
HashMap<StringName, List<Connection>> connections;
HashMap<StringName, MethodInfo> signal_info;
} signal_data;
// 3. Metadata
HashMap<StringName, Variant> metadata;
// 4. Script Instance
ScriptInstance *script_instance = nullptr;
// 5. Message Queue
MessageQueue *message_queue; // For deferred calls
// 6. Instance Binding
void *instance_binding[MAX_LANGUAGE_BINDINGS];
public:
// State is automatically managed during:
void set(const StringName &p_property, const Variant &p_value) {
// 1. Update property storage
property_data.properties[p_property] = p_value;
// 2. Call setter if exists
if (has_method("set_" + p_property)) {
call("set_" + p_property, p_value);
}
// 3. Emit property changed
emit_signal(CoreStringNames::property_list_changed);
// 4. Notify editor if applicable
if (Engine::get_singleton()->is_editor_hint()) {
EditorInterface::mark_property_dirty(this, p_property);
}
}
};
Bookkeeping for Bindings
class BindingBookkeeping : public Node {
GDCLASS(BindingBookkeeping, Node)
protected:
static void _bind_methods() {
// Every bound method is tracked
MethodInfo method_info;
method_info.name = "process_data";
method_info.return_val = PropertyInfo(Variant::BOOL, "success");
method_info.arguments.push_back(PropertyInfo(Variant::DICTIONARY, "data"));
ClassDB::bind_method(D_METHOD("process_data", "data"),
&BindingBookkeeping::process_data);
// Method is now:
// 1. Registered in ClassDB
// 2. Available for call() and callv()
// 3. Exposed to GDScript
// 4. Visible in documentation
// 5. Accessible via RPC if configured
}
public:
bool process_data(const Dictionary &data) {
// This method can be called from:
// - GDScript: node.process_data({"key": "value"})
// - C++: call("process_data", data)
// - Editor: Via inspector actions
// - Network: If RPC configured
return true;
}
};
Signal System
Communication between objects: Signals are Godot’s way of letting objects talk to each other without tight coupling. Think of them like a radio broadcast - one object emits a signal (“the player died”) and any number of other objects can listen for that signal and react accordingly (update the UI, play a sound, restart the level). This keeps your code modular and easy to maintain.
Signal Implementation Details
class SignalEmitter : public Node {
GDCLASS(SignalEmitter, Node)
protected:
static void _bind_methods() {
// Define signal with parameters
ADD_SIGNAL(MethodInfo("health_changed",
PropertyInfo(Variant::INT, "old_health"),
PropertyInfo(Variant::INT, "new_health")));
// Signal with return value (for gathering results)
MethodInfo damage_signal("damage_dealt");
damage_signal.return_val = PropertyInfo(Variant::BOOL, "handled");
damage_signal.arguments.push_back(PropertyInfo(Variant::INT, "amount"));
ADD_SIGNAL(damage_signal);
}
int health = 100;
public:
void take_damage(int amount) {
int old_health = health;
health = Math::max(0, health - amount);
// Emit signal with arguments
emit_signal("health_changed", old_health, health);
// Emit and check if handled
bool handled = emit_signal("damage_dealt", amount);
if (!handled) {
print_line("Damage not handled by any connection");
}
}
void connect_signals() {
// Connect with different flags
connect("health_changed",
Callable(this, "on_health_changed"),
CONNECT_DEFERRED); // Call at end of frame
connect("damage_dealt",
Callable(this, "on_damage").bind(true), // Bind extra argument
CONNECT_ONE_SHOT); // Disconnect after first emission
}
void on_health_changed(int old_h, int new_h) {
print_line(vformat("Health: %d -> %d", old_h, new_h));
}
};
Property System
Making data visible and editable: Properties are how you expose your C++ variables to the Godot editor and GDScript. When you properly register a property, it automatically appears in the inspector where designers can tweak values, can be modified from GDScript, and can be saved with scenes. The property system handles type conversion and validation automatically.
Advanced Property Features
class AdvancedProperties : public Resource {
GDCLASS(AdvancedProperties, Resource)
private:
int level = 1;
float experience = 0.0f;
String player_name = "Player";
Ref<Resource> equipped_item;
Array inventory;
protected:
static void _bind_methods() {
// Property with range hint
ClassDB::bind_method(D_METHOD("set_level", "level"), &AdvancedProperties::set_level);
ClassDB::bind_method(D_METHOD("get_level"), &AdvancedProperties::get_level);
ADD_PROPERTY(PropertyInfo(Variant::INT, "level",
PROPERTY_HINT_RANGE, "1,100,1"),
"set_level", "get_level");
// Property with expression hint
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "experience",
PROPERTY_HINT_EXP_RANGE, "0,10000,0.1"),
"set_experience", "get_experience");
// Property with placeholder
ADD_PROPERTY(PropertyInfo(Variant::STRING, "player_name",
PROPERTY_HINT_PLACEHOLDER_TEXT, "Enter name..."),
"set_player_name", "get_player_name");
// Resource type property
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "equipped_item",
PROPERTY_HINT_RESOURCE_TYPE, "Item"),
"set_equipped_item", "get_equipped_item");
// Array property
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "inventory",
PROPERTY_HINT_TYPE_STRING,
String::num(Variant::OBJECT) + "/" +
String::num(PROPERTY_HINT_RESOURCE_TYPE) + ":Item"),
"set_inventory", "get_inventory");
}
public:
// Property validation
void set_level(int p_level) {
level = CLAMP(p_level, 1, 100);
notify_property_list_changed();
}
// Dynamic property list
virtual void _get_property_list(List<PropertyInfo> *p_list) const override {
// Add dynamic properties based on state
if (level > 10) {
p_list->push_back(PropertyInfo(Variant::BOOL, "prestige_available"));
}
// Add properties for each inventory item
for (int i = 0; i < inventory.size(); i++) {
p_list->push_back(PropertyInfo(Variant::OBJECT,
"slot_" + itos(i),
PROPERTY_HINT_RESOURCE_TYPE, "Item"));
}
}
virtual bool _set(const StringName &p_name, const Variant &p_value) override {
String name = p_name;
if (name.begins_with("slot_")) {
int index = name.get_slicec('_', 1).to_int();
if (index >= 0 && index < inventory.size()) {
inventory[index] = p_value;
return true;
}
}
return false;
}
virtual bool _get(const StringName &p_name, Variant &r_ret) const override {
String name = p_name;
if (name.begins_with("slot_")) {
int index = name.get_slicec('_', 1).to_int();
if (index >= 0 && index < inventory.size()) {
r_ret = inventory[index];
return true;
}
}
return false;
}
};
Method Binding
Making your C++ functions callable: Method binding is how you expose your C++ functions to the rest of Godot. Once a method is bound, it can be called from GDScript, connected to signals, or invoked through the editor. The binding system handles converting between GDScript’s dynamic types and your C++ parameter types automatically.
How Bindings Work
Method binding creates the bridge between engine calls and C++ methods:
// The binding process
class BindingExample : public Node {
GDCLASS(BindingExample, Node)
protected:
static void _bind_methods() {
// 1. D_METHOD macro creates method info
// D_METHOD("set_value", "value") expands to:
// - Method name: "set_value"
// - Parameter names: ["value"]
// - Parameter count: 1
// 2. ClassDB::bind_method creates binding
MethodBind *bind = ClassDB::bind_method(
D_METHOD("calculate", "a", "b"),
&BindingExample::calculate
);
// 3. Binding stores:
// - Function pointer
// - Parameter types (deduced)
// - Return type (deduced)
// - Method flags
// 4. Optional: Set default arguments
bind->set_default_arguments({10, 20});
// 5. Method is now callable from:
// - GDScript
// - call() / callv()
// - Editor
// - Network RPC
}
public:
int calculate(int a, int b) {
return a + b;
}
// Vararg method binding
static void _bind_methods_vararg() {
MethodInfo mi;
mi.name = "print_all";
mi.arguments.push_back(PropertyInfo(Variant::NIL, "args"));
mi.flags = METHOD_FLAG_VARARG;
ClassDB::bind_vararg_method(
METHOD_FLAGS_DEFAULT,
"print_all",
&BindingExample::print_all,
mi
);
}
Variant print_all(const Variant **p_args, int p_argcount,
Callable::CallError &r_error) {
for (int i = 0; i < p_argcount; i++) {
print_line(p_args[i]->stringify());
}
return Variant();
}
};
Memory Management Patterns
Avoiding crashes and leaks: Godot uses different memory management strategies for different types of objects. Resources use reference counting (automatically deleted when no longer used), while Nodes use manual management through the scene tree (deleted when removed from the tree). Understanding these patterns helps you avoid memory leaks and crashes.
Reference Counting Best Practices
class MemoryPatterns : public Node {
GDCLASS(MemoryPatterns, Node)
private:
// Use Ref<T> for RefCounted objects
Ref<Resource> resource;
Ref<Texture2D> texture;
// Use raw pointers for Nodes (not ref-counted)
Node *child_node = nullptr;
Control *ui_element = nullptr;
// Cache frequently accessed nodes
HashMap<StringName, Node*> node_cache;
public:
virtual void _ready() override {
// Create ref-counted objects
resource = Ref<Resource>(memnew(Resource));
// Create nodes (not ref-counted)
child_node = memnew(Node);
add_child(child_node);
child_node->set_owner(get_owner());
// Load resources (automatically ref-counted)
texture = ResourceLoader::load("res://icon.png");
}
virtual void _exit_tree() override {
// Clear node references (don't delete - tree handles it)
child_node = nullptr;
ui_element = nullptr;
node_cache.clear();
// Ref<T> automatically decrements on destruction
// No manual cleanup needed for resource/texture
}
void safe_node_access() {
// Always check node validity
if (child_node && is_instance_valid(child_node)) {
child_node->set_name("Valid");
}
// Use ObjectID for long-term references
ObjectID child_id = child_node->get_instance_id();
// Later...
Object *obj = ObjectDB::get_instance(child_id);
Node *node = Object::cast_to<Node>(obj);
if (node) {
// Safe to use
}
}
};
Common Usage Patterns
Proven approaches for common needs: Over time, certain patterns have emerged as effective ways to structure Godot applications. These include factory patterns for creating objects, component patterns for modular functionality, and singleton patterns for global game state. These patterns help you organize your code in ways that work well with Godot’s architecture.
Factory Pattern
class NodeFactory : public Object {
GDCLASS(NodeFactory, Object)
protected:
static void _bind_methods() {
ClassDB::bind_method(D_METHOD("create_enemy", "type"),
&NodeFactory::create_enemy);
}
public:
Node *create_enemy(const String &type) {
Node *enemy = nullptr;
if (type == "zombie") {
enemy = memnew(Zombie);
} else if (type == "skeleton") {
enemy = memnew(Skeleton);
} else {
enemy = memnew(Enemy);
}
// Configure common properties
enemy->set_name(type.capitalize());
enemy->add_to_group("enemies");
enemy->set_meta("type", type);
return enemy;
}
};
Component Pattern
// Component base class
class Component : public Node {
GDCLASS(Component, Node)
protected:
Node *owner_node = nullptr;
public:
virtual void _enter_tree() override {
owner_node = get_parent();
}
virtual void setup(Node *p_owner) {
owner_node = p_owner;
}
};
// Health component
class HealthComponent : public Component {
GDCLASS(HealthComponent, Component)
private:
int max_health = 100;
int current_health = 100;
protected:
static void _bind_methods() {
ADD_SIGNAL(MethodInfo("died"));
ADD_SIGNAL(MethodInfo("health_changed",
PropertyInfo(Variant::INT, "health")));
}
public:
void damage(int amount) {
current_health = Math::max(0, current_health - amount);
emit_signal("health_changed", current_health);
if (current_health == 0) {
emit_signal("died");
}
}
};
// Entity using components
class Entity : public Node {
GDCLASS(Entity, Node)
private:
HealthComponent *health = nullptr;
public:
virtual void _ready() override {
// Add component
health = memnew(HealthComponent);
add_child(health);
health->setup(this);
// Connect to component signals
health->connect("died", Callable(this, "on_death"));
}
void on_death() {
queue_free();
}
};
Singleton Pattern
// Singleton accessible from GDScript
class GameManager : public Node {
GDCLASS(GameManager, Node)
private:
static GameManager *singleton;
int score = 0;
int high_score = 0;
protected:
static void _bind_methods() {
ClassDB::bind_method(D_METHOD("get_score"), &GameManager::get_score);
ClassDB::bind_method(D_METHOD("add_score", "points"), &GameManager::add_score);
ClassDB::bind_method(D_METHOD("get_high_score"), &GameManager::get_high_score);
}
public:
static GameManager *get_singleton() { return singleton; }
GameManager() {
ERR_FAIL_COND(singleton != nullptr);
singleton = this;
}
~GameManager() {
ERR_FAIL_COND(singleton != this);
singleton = nullptr;
}
void add_score(int points) {
score += points;
if (score > high_score) {
high_score = score;
}
}
int get_score() const { return score; }
int get_high_score() const { return high_score; }
};
GameManager *GameManager::singleton = nullptr;
// Register as singleton
void register_game_types() {
ClassDB::register_class<GameManager>();
Engine::get_singleton()->add_singleton(
Engine::Singleton("GameManager", GameManager::get_singleton())
);
}
Conclusion
The Object, Resource, and Node classes form the foundational trinity of Godot’s architecture. Object provides the core infrastructure for property management, signals, and method binding. Resource adds reference counting and serialization for data management. Node brings scene tree integration and game loop participation. Understanding their roles, state management, and interaction patterns is essential for effective GDExtension development. The extensive bookkeeping and binding systems enable seamless integration between C++ code and the Godot engine, providing a powerful and flexible development environment.