GDScript and GDExtension Interaction
Overview
What is GDScript interaction? This is the two-way communication between your C++ GDExtension code and GDScript (Godot’s built-in scripting language). Once properly set up, your C++ classes appear in GDScript just like built-in Godot classes - GDScript can create instances, call methods, access properties, and connect to signals.
Why use both C++ and GDScript? This hybrid approach gives you the best of both worlds: write performance-critical systems (like physics simulations or data processing) in C++ for speed, while keeping game logic (like AI behavior or UI interactions) in GDScript for rapid iteration and easier debugging.
GDExtension classes seamlessly integrate with GDScript, appearing as native engine classes. This bidirectional communication enables hybrid architectures where performance-critical code runs in C++ while game logic remains in GDScript.
Communication Architecture
---
config:
theme: 'base'
curve: 'straight'
themeVariables:
darkMode: true
clusterBkg: '#22272f62'
clusterBorder: '#6a6f77ff'
clusterTextColor: '#6a6f77ff'
lineColor: '#C1C4CAAA'
background: '#262B33'
primaryColor: '#2b4268ff'
primaryTextColor: '#C1C4CAff'
primaryBorderColor: '#6a6f77ff'
primaryLabelBkg: '#262B33'
secondaryColor: '#425f5fff'
secondaryBorderColor: '#8c9c81ff'
secondaryTextColor: '#C1C4CAff'
tertiaryColor: '#4d4962ff'
tertiaryBorderColor: '#8983a5ff'
tertiaryTextColor: '#eeeeee55'
nodeTextColor: '#C1C4CA'
defaultLinkColor: '#C1C4CA'
edgeLabelBackground: '#262B33'
edgeLabelBorderColor: '#C1C4CA'
labelTextColor: '#C1C4CA'
errorBkgColor: '#724848ff'
errorTextColor: '#C1C4CA'
flowchart:
curve: 'basis'
nodeSpacing: 50
rankSpacing: 50
subGraphTitleMargin:
top: 15
bottom: 15
left: 15
right: 15
---
flowchart LR
subgraph GDSCRIPT["GDScript Layer"]
A[Game Logic] --> B[Script Instance]
end
subgraph BINDING["Binding Layer"]
C[Method Calls] --> D[Property Access]
D --> E[Signal Emission]
end
subgraph GDEXT["GDExtension Layer"]
F[C++ Classes] --> G[Native Performance]
end
B <--> C
E <--> F
linkStyle default stroke:#C1C4CAaa,stroke-width:2px,color:#C1C4CA
style A fill:#7a7253ff,stroke:#c7c19bff,stroke-width:2px,color:#C1C4CA,rx:8,ry:8
style B fill:#7a6253ff,stroke:#c7ac9bff,stroke-width:2px,color:#C1C4CA,rx:8,ry:8
style C fill:#2b4268ff,stroke:#779DC9ff,stroke-width:2px,color:#C1C4CA,rx:8,ry:8
style D fill:#4d4962ff,stroke:#8983a5ff,stroke-width:2px,color:#C1C4CA,rx:8,ry:8
style E fill:#3a3f47ff,stroke:#6a6f77ff,stroke-width:2px,color:#C1C4CA,rx:8,ry:8
style F fill:#425f5fff,stroke:#8c9c81ff,stroke-width:2px,color:#C1C4CA,rx:8,ry:8
style G fill:#425f5fff,stroke:#8c9c81ff,stroke-width:2px,color:#C1C4CA,rx:8,ry:8
Calling GDExtension from GDScript
Using your C++ classes in GDScript: Once you’ve registered your C++ class with proper bindings, GDScript can use it exactly like any other Godot class. You can instantiate it with .new(), call its methods, access its properties, and connect to its signals. The binding system handles all the complexity of converting between GDScript’s dynamic types and your C++ types.
Basic Usage
Once registered, GDExtension classes appear like any other Godot class:
# GDScript using GDExtension class
extends Node
func _ready():
# Create instance of GDExtension class
var my_extension = MyExtensionClass.new()
# Call methods
var result = my_extension.calculate(10, 20)
print("Result: ", result)
# Access properties
my_extension.health = 100
var current_health = my_extension.health
# Connect to signals
my_extension.health_changed.connect(_on_health_changed)
# Add as child node (if extends Node)
add_child(my_extension)
# Use static methods
var processed = MyExtensionClass.process_data("input")
func _on_health_changed(new_health):
print("Health is now: ", new_health)
GDExtension Class Definition
The C++ class that GDScript interacts with:
class MyExtensionClass : public Node {
GDCLASS(MyExtensionClass, Node)
private:
int health = 100;
float speed = 5.0f;
String player_name = "Player";
protected:
static void _bind_methods() {
// Make methods available to GDScript
ClassDB::bind_method(D_METHOD("calculate", "a", "b"),
&MyExtensionClass::calculate);
// Properties
ClassDB::bind_method(D_METHOD("set_health", "value"),
&MyExtensionClass::set_health);
ClassDB::bind_method(D_METHOD("get_health"),
&MyExtensionClass::get_health);
ADD_PROPERTY(PropertyInfo(Variant::INT, "health",
PROPERTY_HINT_RANGE, "0,100"),
"set_health", "get_health");
// Signals
ADD_SIGNAL(MethodInfo("health_changed",
PropertyInfo(Variant::INT, "new_health")));
// Static methods
ClassDB::bind_static_method("MyExtensionClass",
D_METHOD("process_data", "input"),
&MyExtensionClass::process_data);
// Constants
BIND_CONSTANT(MAX_HEALTH);
// Enums
BIND_ENUM_CONSTANT(STATE_IDLE);
BIND_ENUM_CONSTANT(STATE_ACTIVE);
BIND_ENUM_CONSTANT(STATE_COMPLETE);
}
public:
enum State {
STATE_IDLE,
STATE_ACTIVE,
STATE_COMPLETE
};
static const int MAX_HEALTH = 100;
int calculate(int a, int b) {
return a + b;
}
void set_health(int p_health) {
int old_health = health;
health = CLAMP(p_health, 0, MAX_HEALTH);
if (health != old_health) {
emit_signal("health_changed", health);
}
}
int get_health() const {
return health;
}
static String process_data(const String &input) {
return "Processed: " + input;
}
};
Using in GDScript with Type Hints
GDScript can use type hints with GDExtension classes:
# Type-safe GDScript
class_name GameController
extends Node
var extension_node: MyExtensionClass
var health_bar: ProgressBar
func _ready() -> void:
# Type-safe instantiation
extension_node = MyExtensionClass.new() as MyExtensionClass
add_child(extension_node)
# Type checking
if extension_node:
setup_extension()
func setup_extension() -> void:
# Accessing enum constants
extension_node.state = MyExtensionClass.STATE_IDLE
# Using constants
extension_node.health = MyExtensionClass.MAX_HEALTH
# Method with return type
var calc_result: int = extension_node.calculate(5, 3)
func process_data(input: String) -> String:
# Call static method
return MyExtensionClass.process_data(input)
Calling GDScript from GDExtension
Invoking GDScript code from C++: Your C++ code can call GDScript methods using the call() function, access GDScript properties with get() and set(), and even load and run GDScript code dynamically. This is useful for creating extensible systems where GDScript acts as a scripting layer on top of your C++ foundation.
Script Instance Access
GDExtension can call GDScript methods through the script instance:
class ScriptInteraction : public Node {
GDCLASS(ScriptInteraction, Node)
public:
void call_gdscript_method() {
// Check if node has script
if (!has_method("custom_gdscript_method")) {
return;
}
// Call GDScript method
Variant result = call("custom_gdscript_method", 42, "parameter");
// Call with array of arguments
Array args;
args.append(10);
args.append("test");
Variant result2 = callv("another_method", args);
}
void check_script_properties() {
// Check for script
Ref<Script> script = get_script();
if (script.is_valid()) {
// Script is attached
// Get script property
if (has_method("get_custom_property")) {
Variant prop = call("get_custom_property");
}
// Set script property
if (has_method("set_custom_property")) {
call("set_custom_property", "new_value");
}
}
}
void interact_with_script_signals() {
// Connect to GDScript signal
if (has_signal("gdscript_signal")) {
connect("gdscript_signal",
Callable(this, "on_gdscript_signal"));
}
// Emit signal that GDScript can receive
if (has_signal("request_processed")) {
emit_signal("request_processed", true);
}
}
void on_gdscript_signal(const Variant &data) {
print_line("Received from GDScript: " + data.stringify());
}
};
Dynamic Script Loading
Loading and instantiating GDScript from C++:
class DynamicScriptLoader : public Node {
GDCLASS(DynamicScriptLoader, Node)
public:
Node *load_gdscript_node(const String &script_path) {
// Load GDScript file
Ref<Script> script = ResourceLoader::load(script_path);
if (script.is_null()) {
ERR_PRINT("Failed to load script: " + script_path);
return nullptr;
}
// Check if script can be instantiated
if (!script->can_instantiate()) {
ERR_PRINT("Script cannot be instantiated: " + script_path);
return nullptr;
}
// Get base type
StringName base_type = script->get_instance_base_type();
// Create base node
Object *obj = ClassDB::instantiate(base_type);
Node *node = Object::cast_to<Node>(obj);
if (node) {
// Attach script
node->set_script(script);
// Call GDScript _ready if needed
if (node->has_method("_ready")) {
node->call("_ready");
}
return node;
}
return nullptr;
}
void create_and_configure_gdscript_node() {
Node *gdscript_node = load_gdscript_node("res://scripts/Player.gd");
if (gdscript_node) {
// Configure through GDScript methods
if (gdscript_node->has_method("initialize")) {
Dictionary config;
config["health"] = 100;
config["speed"] = 5.5;
gdscript_node->call("initialize", config);
}
// Add to scene
add_child(gdscript_node);
// Set owner for scene saving
gdscript_node->set_owner(get_tree()->get_edited_scene_root());
}
}
};
Evaluating GDScript Code
Running GDScript dynamically:
class GDScriptEvaluator : public Node {
GDCLASS(GDScriptEvaluator, Node)
public:
Variant evaluate_expression(const String &expression) {
// Create expression
Ref<Expression> expr;
expr.instantiate();
// Parse expression
Array input_names;
input_names.append("x");
input_names.append("y");
Error err = expr->parse(expression, input_names);
if (err != OK) {
ERR_PRINT("Failed to parse expression: " + expression);
return Variant();
}
// Execute expression
Array input_values;
input_values.append(10);
input_values.append(20);
Variant result = expr->execute(input_values, this);
if (expr->has_execute_failed()) {
ERR_PRINT("Expression execution failed");
return Variant();
}
return result;
}
void run_gdscript_code() {
// For more complex GDScript execution, use GDScript class
Ref<GDScript> gdscript;
gdscript.instantiate();
String code = R"(
extends RefCounted
func calculate(a, b):
return a * b + 10
func get_message():
return "Hello from dynamic GDScript"
)";
gdscript->set_source_code(code);
Error err = gdscript->reload();
if (err == OK) {
// Create instance
Ref<RefCounted> instance = gdscript->new();
if (instance.is_valid()) {
// Call methods
Variant result = instance->call("calculate", 5, 3);
print_line("Result: " + result.stringify());
String message = instance->call("get_message");
print_line("Message: " + message);
}
}
}
};
Property Synchronization
Keeping data in sync between C++ and GDScript: When both C++ and GDScript can modify the same property, you need to ensure they stay synchronized. This involves proper setter methods, change notifications, and sometimes caching strategies to avoid constant cross-language calls in performance-critical code.
Automatic Property Syncing
Properties are automatically synchronized between GDScript and GDExtension:
class PropertySync : public Node {
GDCLASS(PropertySync, Node)
private:
float sync_value = 0.0f;
bool dirty = false;
protected:
static void _bind_methods() {
// Property with notification
ClassDB::bind_method(D_METHOD("set_sync_value", "value"),
&PropertySync::set_sync_value);
ClassDB::bind_method(D_METHOD("get_sync_value"),
&PropertySync::get_sync_value);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "sync_value",
PROPERTY_HINT_RANGE, "0.0,1.0,0.01"),
"set_sync_value", "get_sync_value");
// Property change signal
ADD_SIGNAL(MethodInfo("sync_value_changed",
PropertyInfo(Variant::FLOAT, "new_value")));
}
public:
void set_sync_value(float p_value) {
if (sync_value != p_value) {
sync_value = p_value;
dirty = true;
// Notify GDScript
emit_signal("sync_value_changed", sync_value);
// Update property list for editor
notify_property_list_changed();
// Call GDScript handler if exists
if (has_method("_on_sync_value_changed")) {
call("_on_sync_value_changed", sync_value);
}
}
}
float get_sync_value() const {
return sync_value;
}
virtual void _process(double delta) override {
if (dirty) {
// Check if GDScript modified the value
Variant script_value = get("sync_value");
if (script_value.get_type() == Variant::FLOAT) {
float val = script_value;
if (val != sync_value) {
// GDScript changed the value
sync_value = val;
}
}
dirty = false;
}
}
};
GDScript side:
extends PropertySync
var local_sync_value: float = 0.0
func _ready():
# Connect to property change signal
sync_value_changed.connect(_on_sync_changed)
# Set initial value
sync_value = 0.5
func _on_sync_changed(new_value: float):
print("C++ changed sync_value to: ", new_value)
local_sync_value = new_value
func _on_sync_value_changed(value: float):
# Called by C++ when property changes
print("Property changed handler: ", value)
func _process(delta):
# Modify from GDScript
if Input.is_action_pressed("ui_up"):
sync_value += delta * 0.1
sync_value = clamp(sync_value, 0.0, 1.0)
Signal Communication
Event-driven communication between languages: Signals provide an excellent way for C++ and GDScript code to communicate without tight coupling. Your C++ code can emit signals that GDScript connects to, and vice versa. This enables clean event-driven architectures where different parts of your system can respond to events without needing direct references to each other.
Bidirectional Signal Flow
class SignalCommunication : public Node {
GDCLASS(SignalCommunication, Node)
protected:
static void _bind_methods() {
// Signals emitted to GDScript
ADD_SIGNAL(MethodInfo("data_received",
PropertyInfo(Variant::DICTIONARY, "data")));
ADD_SIGNAL(MethodInfo("processing_complete",
PropertyInfo(Variant::BOOL, "success"),
PropertyInfo(Variant::STRING, "message")));
// Method to receive GDScript signals
ClassDB::bind_method(D_METHOD("on_gdscript_request", "request_data"),
&SignalCommunication::on_gdscript_request);
}
public:
virtual void _ready() override {
// Connect to GDScript signals if they exist
if (has_signal("gdscript_custom_signal")) {
connect("gdscript_custom_signal",
Callable(this, "handle_gdscript_signal"));
}
// Emit initial signal
Dictionary init_data;
init_data["status"] = "ready";
init_data["version"] = 1;
emit_signal("data_received", init_data);
}
void on_gdscript_request(const Dictionary &request_data) {
// Process request from GDScript
String action = request_data.get("action", "");
if (action == "process") {
bool success = process_request(request_data);
emit_signal("processing_complete", success,
success ? "Success" : "Failed");
}
}
void handle_gdscript_signal(const Variant &arg1, const Variant &arg2) {
print_line("Received signal from GDScript");
}
private:
bool process_request(const Dictionary &data) {
// Process the request
return true;
}
};
GDScript implementation:
extends SignalCommunication
signal gdscript_custom_signal(param1, param2)
func _ready():
# Connect to C++ signals
data_received.connect(_on_data_received)
processing_complete.connect(_on_processing_complete)
# Emit signal to C++
gdscript_custom_signal.emit("test", 123)
# Call C++ method that expects signal
var request = {
"action": "process",
"data": "test_data"
}
on_gdscript_request(request)
func _on_data_received(data: Dictionary):
print("Received data from C++: ", data)
func _on_processing_complete(success: bool, message: String):
if success:
print("Processing succeeded: ", message)
else:
print("Processing failed: ", message)
Using CustomCallable for Lambda and std::function Callbacks
Modern C++ signal handling: CustomCallable allows you to connect signals to lambdas, std::function objects, or any callable in C++. This provides a more flexible and modern approach to signal handling compared to traditional method binding, especially useful for one-off callbacks, capturing local state, or creating dynamic signal handlers.
#include <godot_cpp/core/custom_callable.hpp>
#include <functional>
class ModernSignalHandling : public Node {
GDCLASS(ModernSignalHandling, Node)
private:
int counter = 0;
std::function<void(int)> stored_callback;
protected:
static void _bind_methods() {
ADD_SIGNAL(MethodInfo("value_changed",
PropertyInfo(Variant::INT, "value")));
ADD_SIGNAL(MethodInfo("process_complete"));
}
public:
virtual void _ready() override {
// Lambda with capture
int local_value = 42;
auto lambda_callback = [this, local_value](const Variant &value) {
print_line(vformat("Lambda received: %d (captured: %d)",
value.operator int(), local_value));
counter++;
};
// Connect using CustomCallable with lambda
connect("value_changed",
Callable::create(lambda_callback));
// std::function callback
stored_callback = [this](int value) {
print_line(vformat("std::function callback: %d", value));
if (value > 100) {
emit_signal("process_complete");
}
};
// Stateful lambda capturing this
auto stateful_lambda = [this](const Array &args) -> Variant {
if (args.size() > 0) {
int value = args[0];
stored_callback(value);
}
return Variant();
};
// Connect with CustomCallable
connect("value_changed",
Callable::create(stateful_lambda));
// Connect child node signals with lambdas
Node *child = get_node_or_null(NodePath("ChildNode"));
if (child && child->has_signal("custom_signal")) {
auto child_handler = [this, child](const Variant &data) {
print_line(vformat("Child %s sent: %s",
child->get_name(),
data.stringify()));
};
child->connect("custom_signal",
Callable::create(child_handler));
}
}
void connect_with_context() {
// Lambda with shared_ptr for lifetime management
auto shared_data = std::make_shared<Dictionary>();
(*shared_data)["count"] = 0;
auto context_lambda = [shared_data](const Variant &value) mutable {
int count = (*shared_data)["count"];
count++;
(*shared_data)["count"] = count;
print_line(vformat("Called %d times with value: %s",
count, value.stringify()));
};
connect("value_changed",
Callable::create(context_lambda));
}
void create_dynamic_handlers() {
// Create multiple handlers dynamically
for (int i = 0; i < 5; i++) {
auto handler = [i](const Variant &value) {
print_line(vformat("Handler %d received: %s",
i, value.stringify()));
};
connect("value_changed",
Callable::create(handler));
}
}
void connect_one_shot() {
// One-shot connection that disconnects after first call
std::shared_ptr<Callable> callable_ref;
auto one_shot = [this, &callable_ref](const Variant &value) {
print_line("One-shot handler called!");
// Disconnect after handling
if (callable_ref) {
disconnect("value_changed", *callable_ref);
}
};
auto callable = Callable::create(one_shot);
callable_ref = std::make_shared<Callable>(callable);
connect("value_changed", callable, CONNECT_ONE_SHOT);
}
};
// Advanced CustomCallable patterns
class CallableFactory : public RefCounted {
GDCLASS(CallableFactory, RefCounted)
public:
// Factory for creating typed callbacks
template<typename T>
static Callable create_typed_handler(std::function<void(T)> handler) {
return Callable::create([handler](const Variant &value) {
if (value.get_type() == Variant::get_type<T>()) {
handler(value.operator T());
}
});
}
// Create callback with error handling
static Callable create_safe_handler(std::function<void(Variant)> handler) {
return Callable::create([handler](const Variant &value) {
try {
handler(value);
} catch (const std::exception &e) {
ERR_PRINT(vformat("Callback error: %s", e.what()));
}
});
}
// Chain multiple callbacks
static Callable create_chained_handler(
const std::vector<std::function<void(Variant)>> &handlers) {
return Callable::create([handlers](const Variant &value) {
for (const auto &handler : handlers) {
handler(value);
}
});
}
// Conditional callback
static Callable create_conditional_handler(
std::function<bool(Variant)> condition,
std::function<void(Variant)> handler) {
return Callable::create([condition, handler](const Variant &value) {
if (condition(value)) {
handler(value);
}
});
}
// Rate-limited callback
static Callable create_throttled_handler(
std::function<void(Variant)> handler,
double min_interval) {
struct ThrottleState {
uint64_t last_call = 0;
double interval;
std::function<void(Variant)> handler;
};
auto state = std::make_shared<ThrottleState>();
state->interval = min_interval * 1000000; // Convert to microseconds
state->handler = handler;
return Callable::create([state](const Variant &value) {
uint64_t now = OS::get_singleton()->get_ticks_usec();
if (now - state->last_call >= state->interval) {
state->handler(value);
state->last_call = now;
}
});
}
};
// Practical example: Event bus with lambda handlers
class EventBus : public Node {
GDCLASS(EventBus, Node)
private:
HashMap<String, Vector<Callable>> event_handlers;
protected:
static void _bind_methods() {
ClassDB::bind_method(D_METHOD("subscribe", "event", "callable"),
&EventBus::subscribe);
ClassDB::bind_method(D_METHOD("publish", "event", "data"),
&EventBus::publish);
}
public:
void subscribe(const String &event, const Callable &callable) {
if (!event_handlers.has(event)) {
event_handlers[event] = Vector<Callable>();
}
event_handlers[event].append(callable);
}
void subscribe_lambda(const String &event,
std::function<void(Variant)> handler) {
subscribe(event, Callable::create(handler));
}
void publish(const String &event, const Variant &data) {
if (event_handlers.has(event)) {
for (const Callable &handler : event_handlers[event]) {
handler.call(data);
}
}
}
// Usage example
void setup_event_handling() {
// Subscribe with lambda
subscribe_lambda("player_health_changed", [this](const Variant &data) {
int health = data;
if (health <= 0) {
publish("player_died", Dictionary());
}
});
// Subscribe with capturing lambda
int max_health = 100;
subscribe_lambda("player_healed", [this, max_health](const Variant &data) {
int heal_amount = data;
print_line(vformat("Healed for %d (max: %d)",
heal_amount, max_health));
});
// Chain reactions
subscribe_lambda("enemy_defeated", [this](const Variant &data) {
Dictionary enemy_data = data;
int exp = enemy_data.get("exp", 0);
publish("experience_gained", exp);
if (enemy_data.has("loot")) {
publish("loot_dropped", enemy_data["loot"]);
}
});
}
};
Usage from GDScript:
extends Node
func _ready():
# Get the event bus (assuming it's a singleton)
var event_bus = get_node("/root/EventBus")
# Subscribe to events with callables
var health_callback = func(data):
print("Health changed to: ", data)
event_bus.subscribe("player_health_changed", health_callback)
# Publish events
event_bus.publish("player_health_changed", 75)
# Create and use ModernSignalHandling
var modern_node = ModernSignalHandling.new()
add_child(modern_node)
# Emit signal that triggers lambda handlers
modern_node.emit_signal("value_changed", 150)
Custom Types in GDScript
Making C++ data structures available to GDScript: Beyond simple methods and properties, you can expose complex C++ data types to GDScript. This includes custom Resource classes for saveable data, custom Node classes for game entities, and even complex data structures with their own methods and properties.
Exposing Complex Types
class CustomDataType : public Resource {
GDCLASS(CustomDataType, Resource)
private:
String name;
int level = 1;
Dictionary attributes;
Array inventory;
protected:
static void _bind_methods() {
// Properties
ClassDB::bind_method(D_METHOD("set_name", "name"),
&CustomDataType::set_name);
ClassDB::bind_method(D_METHOD("get_name"),
&CustomDataType::get_name);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "name"),
"set_name", "get_name");
ClassDB::bind_method(D_METHOD("set_level", "level"),
&CustomDataType::set_level);
ClassDB::bind_method(D_METHOD("get_level"),
&CustomDataType::get_level);
ADD_PROPERTY(PropertyInfo(Variant::INT, "level",
PROPERTY_HINT_RANGE, "1,100"),
"set_level", "get_level");
ClassDB::bind_method(D_METHOD("set_attributes", "attributes"),
&CustomDataType::set_attributes);
ClassDB::bind_method(D_METHOD("get_attributes"),
&CustomDataType::get_attributes);
ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "attributes"),
"set_attributes", "get_attributes");
// Methods
ClassDB::bind_method(D_METHOD("add_item", "item"),
&CustomDataType::add_item);
ClassDB::bind_method(D_METHOD("remove_item", "index"),
&CustomDataType::remove_item);
ClassDB::bind_method(D_METHOD("get_item", "index"),
&CustomDataType::get_item);
ClassDB::bind_method(D_METHOD("get_item_count"),
&CustomDataType::get_item_count);
// Factory method
ClassDB::bind_static_method("CustomDataType",
D_METHOD("create_default"),
&CustomDataType::create_default);
}
public:
void set_name(const String &p_name) { name = p_name; }
String get_name() const { return name; }
void set_level(int p_level) { level = CLAMP(p_level, 1, 100); }
int get_level() const { return level; }
void set_attributes(const Dictionary &p_attributes) {
attributes = p_attributes;
}
Dictionary get_attributes() const { return attributes; }
void add_item(const Variant &item) {
inventory.append(item);
}
void remove_item(int index) {
if (index >= 0 && index < inventory.size()) {
inventory.remove_at(index);
}
}
Variant get_item(int index) const {
if (index >= 0 && index < inventory.size()) {
return inventory[index];
}
return Variant();
}
int get_item_count() const {
return inventory.size();
}
static Ref<CustomDataType> create_default() {
Ref<CustomDataType> data;
data.instantiate();
data->set_name("Default");
data->set_level(1);
Dictionary default_attrs;
default_attrs["strength"] = 10;
default_attrs["speed"] = 5;
data->set_attributes(default_attrs);
return data;
}
};
Using in GDScript:
extends Node
var player_data: CustomDataType
func _ready():
# Create using factory method
player_data = CustomDataType.create_default()
# Modify properties
player_data.name = "Hero"
player_data.level = 10
# Work with attributes
var attrs = player_data.attributes
attrs["intelligence"] = 15
player_data.attributes = attrs
# Use methods
player_data.add_item("Sword")
player_data.add_item("Potion")
print("Player: ", player_data.name)
print("Level: ", player_data.level)
print("Items: ", player_data.get_item_count())
# Save as resource
ResourceSaver.save(player_data, "user://player_data.tres")
# Load resource
var loaded_data = load("user://player_data.tres") as CustomDataType
if loaded_data:
print("Loaded: ", loaded_data.name)
Editor Integration
Making your C++ classes work in the Godot editor: Properly bound C++ classes automatically appear in the Godot editor’s node creation dialog, their properties show up in the inspector with appropriate UI widgets, and they can be used in tool scripts that run inside the editor. This makes your C++ extensions feel like native Godot features.
Custom Inspector Properties
class EditorIntegration : public Node {
GDCLASS(EditorIntegration, Node)
private:
int dropdown_value = 0;
String file_path;
Color color_value = Color(1, 1, 1);
protected:
static void _bind_methods() {
// Dropdown property
ClassDB::bind_method(D_METHOD("set_dropdown_value", "value"),
&EditorIntegration::set_dropdown_value);
ClassDB::bind_method(D_METHOD("get_dropdown_value"),
&EditorIntegration::get_dropdown_value);
ADD_PROPERTY(PropertyInfo(Variant::INT, "dropdown_value",
PROPERTY_HINT_ENUM,
"Option1,Option2,Option3"),
"set_dropdown_value", "get_dropdown_value");
// File path property
ClassDB::bind_method(D_METHOD("set_file_path", "path"),
&EditorIntegration::set_file_path);
ClassDB::bind_method(D_METHOD("get_file_path"),
&EditorIntegration::get_file_path);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "file_path",
PROPERTY_HINT_FILE, "*.png,*.jpg"),
"set_file_path", "get_file_path");
// Color property
ClassDB::bind_method(D_METHOD("set_color_value", "color"),
&EditorIntegration::set_color_value);
ClassDB::bind_method(D_METHOD("get_color_value"),
&EditorIntegration::get_color_value);
ADD_PROPERTY(PropertyInfo(Variant::COLOR, "color_value"),
"set_color_value", "get_color_value");
// Export hints for GDScript
ADD_GROUP("Export Settings", "export_");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "export_enabled"),
"set_export_enabled", "get_export_enabled");
}
// Property list for dynamic properties
virtual void _get_property_list(List<PropertyInfo> *p_list) const override {
// Add dynamic properties visible in editor
p_list->push_back(PropertyInfo(Variant::STRING,
"dynamic_property",
PROPERTY_HINT_MULTILINE_TEXT));
// Conditional properties
if (dropdown_value == 1) {
p_list->push_back(PropertyInfo(Variant::FLOAT,
"conditional_value",
PROPERTY_HINT_RANGE,
"0.0,1.0,0.01"));
}
}
virtual bool _set(const StringName &p_name, const Variant &p_value) override {
String name = p_name;
if (name == "dynamic_property") {
// Handle dynamic property
return true;
}
return false;
}
virtual bool _get(const StringName &p_name, Variant &r_ret) const override {
String name = p_name;
if (name == "dynamic_property") {
r_ret = "Dynamic value";
return true;
}
return false;
}
public:
void set_dropdown_value(int p_value) {
dropdown_value = p_value;
notify_property_list_changed();
}
int get_dropdown_value() const { return dropdown_value; }
void set_file_path(const String &p_path) { file_path = p_path; }
String get_file_path() const { return file_path; }
void set_color_value(const Color &p_color) { color_value = p_color; }
Color get_color_value() const { return color_value; }
};
Tool Scripts
Making GDExtension classes work with tool scripts:
class ToolMode : public Node {
GDCLASS(ToolMode, Node)
protected:
static void _bind_methods() {
ClassDB::bind_method(D_METHOD("update_in_editor"),
&ToolMode::update_in_editor);
}
public:
virtual void _ready() override {
if (Engine::get_singleton()->is_editor_hint()) {
// Running in editor
set_process(true);
update_in_editor();
}
}
virtual void _process(double delta) override {
if (Engine::get_singleton()->is_editor_hint()) {
// Update in editor
update_gizmos();
} else {
// Game runtime logic
update_gameplay(delta);
}
}
void update_in_editor() {
// Called from GDScript tool scripts
print_line("Updating in editor");
}
private:
void update_gizmos() {
// Editor visualization
}
void update_gameplay(double delta) {
// Game logic
}
};
Common Patterns
Proven architectures for C++/GDScript integration: Several patterns have emerged as effective ways to structure hybrid applications: plugin systems where GDScript extends C++ base classes, data exchange patterns for efficiently sharing large amounts of data, and component architectures where C++ and GDScript components work together on the same entities.
Plugin Architecture
Creating extensible systems with GDScript plugins:
class PluginSystem : public Node {
GDCLASS(PluginSystem, Node)
private:
Array registered_plugins;
Dictionary plugin_data;
protected:
static void _bind_methods() {
// Plugin registration
ClassDB::bind_method(D_METHOD("register_plugin", "plugin_script"),
&PluginSystem::register_plugin);
ClassDB::bind_method(D_METHOD("unregister_plugin", "plugin_name"),
&PluginSystem::unregister_plugin);
// Plugin communication
ClassDB::bind_method(D_METHOD("call_plugin_method", "plugin_name",
"method", "args"),
&PluginSystem::call_plugin_method);
// Plugin discovery
ClassDB::bind_method(D_METHOD("discover_plugins", "directory"),
&PluginSystem::discover_plugins);
// Signals for plugin events
ADD_SIGNAL(MethodInfo("plugin_registered",
PropertyInfo(Variant::STRING, "plugin_name")));
ADD_SIGNAL(MethodInfo("plugin_message",
PropertyInfo(Variant::STRING, "plugin_name"),
PropertyInfo(Variant::DICTIONARY, "message")));
}
public:
bool register_plugin(const Ref<Script> &plugin_script) {
if (plugin_script.is_null()) {
return false;
}
// Create plugin instance
Ref<RefCounted> plugin = plugin_script->new();
if (plugin.is_null()) {
return false;
}
// Validate plugin interface
if (!plugin->has_method("get_plugin_name") ||
!plugin->has_method("initialize") ||
!plugin->has_method("process")) {
ERR_PRINT("Plugin missing required methods");
return false;
}
String plugin_name = plugin->call("get_plugin_name");
// Initialize plugin
Dictionary init_data;
init_data["version"] = 1;
init_data["system"] = this;
plugin->call("initialize", init_data);
// Store plugin
registered_plugins.append(plugin);
plugin_data[plugin_name] = plugin;
emit_signal("plugin_registered", plugin_name);
return true;
}
void unregister_plugin(const String &plugin_name) {
if (plugin_data.has(plugin_name)) {
Ref<RefCounted> plugin = plugin_data[plugin_name];
if (plugin.is_valid() && plugin->has_method("cleanup")) {
plugin->call("cleanup");
}
plugin_data.erase(plugin_name);
// Remove from array
for (int i = 0; i < registered_plugins.size(); i++) {
Ref<RefCounted> p = registered_plugins[i];
if (p == plugin) {
registered_plugins.remove_at(i);
break;
}
}
}
}
Variant call_plugin_method(const String &plugin_name,
const String &method,
const Array &args) {
if (plugin_data.has(plugin_name)) {
Ref<RefCounted> plugin = plugin_data[plugin_name];
if (plugin.is_valid() && plugin->has_method(method)) {
return plugin->callv(method, args);
}
}
return Variant();
}
void discover_plugins(const String &directory) {
DirAccess *dir = DirAccess::open(directory);
if (!dir) {
return;
}
dir->list_dir_begin();
String file_name = dir->get_next();
while (!file_name.is_empty()) {
if (file_name.ends_with(".gd")) {
String full_path = directory.path_join(file_name);
Ref<Script> script = ResourceLoader::load(full_path);
if (script.is_valid()) {
register_plugin(script);
}
}
file_name = dir->get_next();
}
dir->list_dir_end();
}
virtual void _process(double delta) override {
// Process all plugins
for (int i = 0; i < registered_plugins.size(); i++) {
Ref<RefCounted> plugin = registered_plugins[i];
if (plugin.is_valid() && plugin->has_method("process")) {
plugin->call("process", delta);
}
}
}
};
Example GDScript plugin:
# Plugin script
extends RefCounted
var plugin_name = "ExamplePlugin"
var enabled = true
func get_plugin_name() -> String:
return plugin_name
func initialize(data: Dictionary) -> void:
print("Plugin initialized with data: ", data)
enabled = true
func process(delta: float) -> void:
if enabled:
# Plugin processing logic
pass
func cleanup() -> void:
print("Plugin cleanup")
enabled = false
func custom_action(params: Dictionary) -> Variant:
# Custom plugin functionality
return "Action completed"
Data Exchange Patterns
Efficient data exchange between GDScript and C++:
class DataExchange : public Node {
GDCLASS(DataExchange, Node)
protected:
static void _bind_methods() {
// Bulk data transfer
ClassDB::bind_method(D_METHOD("send_bulk_data", "data"),
&DataExchange::send_bulk_data);
ClassDB::bind_method(D_METHOD("receive_bulk_data"),
&DataExchange::receive_bulk_data);
// Streaming data
ClassDB::bind_method(D_METHOD("start_stream"),
&DataExchange::start_stream);
ClassDB::bind_method(D_METHOD("stream_data", "chunk"),
&DataExchange::stream_data);
ClassDB::bind_method(D_METHOD("end_stream"),
&DataExchange::end_stream);
// Shared buffer access
ClassDB::bind_method(D_METHOD("get_shared_buffer"),
&DataExchange::get_shared_buffer);
ClassDB::bind_method(D_METHOD("update_shared_buffer", "data"),
&DataExchange::update_shared_buffer);
}
private:
PackedByteArray shared_buffer;
Array stream_buffer;
bool streaming = false;
public:
void send_bulk_data(const Dictionary &data) {
// Convert complex data for GDScript
Dictionary processed;
for (const KeyValue<Variant, Variant> &E : data) {
// Process each entry
processed[E.key] = process_value(E.value);
}
// Emit to GDScript
call_deferred("_on_bulk_data_received", processed);
}
Dictionary receive_bulk_data() {
// Return data to GDScript
Dictionary result;
result["timestamp"] = OS::get_singleton()->get_unix_time();
result["data"] = shared_buffer;
return result;
}
void start_stream() {
streaming = true;
stream_buffer.clear();
}
void stream_data(const Variant &chunk) {
if (streaming) {
stream_buffer.append(chunk);
// Process when buffer is full
if (stream_buffer.size() >= 100) {
process_stream_buffer();
}
}
}
void end_stream() {
if (streaming) {
process_stream_buffer();
streaming = false;
}
}
PackedByteArray get_shared_buffer() const {
return shared_buffer;
}
void update_shared_buffer(const PackedByteArray &data) {
shared_buffer = data;
// Notify GDScript of update
if (has_method("_on_buffer_updated")) {
call_deferred("_on_buffer_updated", shared_buffer.size());
}
}
private:
Variant process_value(const Variant &value) {
// Custom processing
return value;
}
void process_stream_buffer() {
// Process accumulated stream data
print_line("Processing " + itos(stream_buffer.size()) + " chunks");
stream_buffer.clear();
}
};
Performance Optimization
Minimizing the cost of cross-language calls: Every call between C++ and GDScript has some overhead for type conversion and marshalling. For performance-critical code, consider batching operations, caching frequently accessed data, and keeping hot paths within a single language to minimize cross-language overhead.
Minimizing Call Overhead
class PerformanceOptimized : public Node {
GDCLASS(PerformanceOptimized, Node)
private:
// Cache frequently accessed script methods
bool has_update_method = false;
bool has_process_method = false;
protected:
static void _bind_methods() {
// Batch operations
ClassDB::bind_method(D_METHOD("batch_update", "items"),
&PerformanceOptimized::batch_update);
// Direct data access
ClassDB::bind_method(D_METHOD("get_data_ptr"),
&PerformanceOptimized::get_data_ptr);
}
public:
virtual void _ready() override {
// Cache method existence
has_update_method = has_method("gdscript_update");
has_process_method = has_method("gdscript_process");
}
virtual void _process(double delta) override {
// Avoid repeated has_method checks
if (has_process_method) {
call("gdscript_process", delta);
}
// Batch updates instead of individual calls
if (should_batch_update()) {
Array batch = collect_batch_data();
call_deferred("process_batch", batch);
}
}
void batch_update(const Array &items) {
// Process multiple items in one call
for (int i = 0; i < items.size(); i++) {
process_item_internal(items[i]);
}
// Single callback to GDScript
if (has_update_method) {
call("gdscript_update", items.size());
}
}
int64_t get_data_ptr() {
// Return pointer for direct memory access (advanced)
// GDScript can use this with GDExtension for zero-copy
return reinterpret_cast<int64_t>(shared_buffer.data());
}
private:
Vector<float> shared_buffer;
bool should_batch_update() {
// Batching logic
return Engine::get_singleton()->get_process_frames() % 10 == 0;
}
Array collect_batch_data() {
Array batch;
// Collect data for batching
return batch;
}
void process_item_internal(const Variant &item) {
// Internal processing
}
};
Debugging Integration
Tools and techniques for debugging hybrid code: Debugging code that spans both C++ and GDScript requires special considerations. This includes techniques for tracing calls across language boundaries, debugging tools that work with both languages, and strategies for isolating issues to specific parts of your hybrid system.
Debug Helpers
class DebugIntegration : public Node {
GDCLASS(DebugIntegration, Node)
protected:
static void _bind_methods() {
// Debug methods
ClassDB::bind_method(D_METHOD("debug_print", "message"),
&DebugIntegration::debug_print);
ClassDB::bind_method(D_METHOD("get_debug_info"),
&DebugIntegration::get_debug_info);
ClassDB::bind_method(D_METHOD("enable_profiling", "enabled"),
&DebugIntegration::enable_profiling);
// Debug signals
ADD_SIGNAL(MethodInfo("debug_message",
PropertyInfo(Variant::STRING, "message"),
PropertyInfo(Variant::INT, "level")));
}
private:
bool profiling_enabled = false;
HashMap<String, uint64_t> profile_data;
public:
void debug_print(const String &message) {
// Print with context
String context = vformat("[%s:%d] ",
get_class(),
get_instance_id());
print_line(context + message);
// Also emit to GDScript
emit_signal("debug_message", message, 0);
// Log to file if needed
if (OS::get_singleton()->is_debug_build()) {
log_to_file(message);
}
}
Dictionary get_debug_info() {
Dictionary info;
// Object info
info["class"] = get_class();
info["instance_id"] = get_instance_id();
info["script"] = get_script().is_valid();
// Performance info
info["memory_usage"] = OS::get_singleton()->get_static_memory_usage();
info["frame"] = Engine::get_singleton()->get_process_frames();
// Profile data
if (profiling_enabled) {
Dictionary profile;
for (const KeyValue<String, uint64_t> &E : profile_data) {
profile[E.key] = E.value;
}
info["profile"] = profile;
}
return info;
}
void enable_profiling(bool enabled) {
profiling_enabled = enabled;
if (!enabled) {
profile_data.clear();
}
}
void profile_function(const String &name) {
if (profiling_enabled) {
uint64_t start = OS::get_singleton()->get_ticks_usec();
// Function execution
uint64_t elapsed = OS::get_singleton()->get_ticks_usec() - start;
profile_data[name] = elapsed;
}
}
private:
void log_to_file(const String &message) {
// File logging implementation
}
};
Conclusion
The seamless interaction between GDScript and GDExtension enables powerful hybrid architectures. GDExtension provides performance-critical functionality while GDScript handles game logic with rapid iteration. Understanding the communication patterns, property synchronization, signal flow, and optimization techniques ensures efficient and maintainable codebases that leverage the strengths of both systems.