When it comes to game development in Godot, GDScript is the primary language in 90% of cases. It is simple, direct, easy to learn, and—most importantly—it is the language specifically designed for the engine. However, it isn't the only option. For instance, we have C#, which offers better performance in certain contexts, more language features, and vast libraries, making it a popular choice for those migrating from Unity. But what about C++?
Using C++ in Godot is the definitive choice for those seeking maximum performance, as it is the very language the engine was built in. Historically, this required recompiling the entire engine every time a modification was made—a complex process that made development extremely slow and tedious. Today, however, we have a much more practical and flexible solution: GDExtension. In this guide, we will focus on exactly that: how to create C++ code for your Godot projects and the essential concepts you need to master.
What exactly is GDExtension?
In simple terms, GDExtension is the official interface for Godot 4 that allows us to register classes written in C++ (or other languages) as if they were native Godot classes. In practice, it functions as a dynamic library. You compile your C++ logic separately using the API as a base, and Godot loads your binary at runtime. You can visualize GDExtension as a USB flash drive for Godot, where the engine is the computer; it allows you to "plug in" and utilize your custom C++ code.
The primary advantage is, of course, performance. If you have a heavy algorithm—such as complex physics processing, AI, or manipulation of large volumes of data—moving it to C++ can result in a massive speed gain compared to GDScript. Furthermore, it opens the door to the vast ecosystem of existing C and C++ libraries, such as OpenCV or SQLite.
The Trade-offs: Complexity and Workflow
There are, however, disadvantages to consider. The first is complexity. Beyond C++ itself being more difficult than GDScript, setting up the environment is undoubtedly more involved than simply opening the editor and coding. You will have to deal with compilers, build scripts, and a slower development cycle. You’ll need to configure significantly more within your C++ classes before they can be tested or read by GDScript.
Finally, you must compile your code for every operating system you intend to target. Unlike GDScript, where the same code is accepted across multiple platforms, C++ is platform-specific. The dynamic library you generate is only compatible with the target platform you choose, meaning you must recompile for different systems and, in specific cases, adapt the code to ensure it functions correctly.
Organizing the Workspace
Setting up GDExtension for the first time is the most labor-intensive part, but once completed, the process is repeatable. In this overview, we focus on the official path using godot-cpp to understand the foundation of how everything works.
If you prefer a more direct path, there are frameworks like Projekt J.E.N.O.V.A. that attempt to make C++ in Godot as simple as using GDScript. These tools offer "one-click" environment setups and hot-reloading. However, such frameworks act as an abstraction layer; you become dependent on their specific systems, which may not track Godot’s updates as quickly and can be harder to debug if something breaks.
One final consideration, this guide assumes a basic understanding of C++ and compilation tools. It is not a C++ tutorial, but rather a map of the Godot integration process.
Phase 1: Configuring godot-cpp
The configuration process is divided into two phases: the one-time setup of godot-cpp, and the ongoing development cycle of your own code.
For the setup, we need two essential tools: a C++ compiler and the SCons build tool (the same one Godot uses). On Linux, the compiler is usually included, while SCons is easily installed via package managers.
# Ubuntu
sudo apt install scons
# Arch Linux
sudo pacman -S scons
On Windows, tools like MSYS2, Cygwin or the Visual Studio Build Tools to provide the necessary environment.
Once the tools are ready, we need access to the godot-cpp repository, which contains the official bindings. The best method is to add this as a Git submodule in your project directory using git submodule add, ensuring you select the branch that matches your Godot version (e.g., branch 4.6 for Godot 4.6).
# Add the submodule (replace '4.6' with your version)
git submodule add -b 4.6 https://github.com/godotengine/godot-cpp.git
# Also, if you are cloning the repository, remember to use this command:
git submodule update --init --remote --recursive
After downloading, you must compile the bindings using your engine’s specific API information. By running the Godot executable with the command --dump-extension-api, you generate an extension_api.json file. This file contains the "blueprint" of how Godot works internally. Copy this JSON into the godot-cpp folder and run scons. This processes the JSON to generate the C++ code that "mirrors" Godot, resulting in a static library that serves as your project's foundation.
# 1. Export the engine API
./godot.exe --dump-extension-api
# 2. Move it to the bindings folder
mv extension_api.json godot-cpp/
# 3. Compile the bindings
cd godot-cpp
scons platform=[insert-platform] custom_api_file=extension_api.json
Phase 2: The Development Cycle
With the configuration complete, we move to the project itself. A clean folder structure is recommended: a folder for the Godot project, the godot-cpp folder, and a src folder for your C++ source code.
my_gdextension_project/
├── godot-cpp/ # The engine bindings (Git submodule)
├── src/ # Your C++ source code (.cpp and .h files)
├── SConstruct # The build script that ties it all together
└── project/ # Your actual Godot Project folder
Also, is important to note that, when creating C++ code with GDExentesion, there is three important steps:
1 - Creating classes
2 - Registering class data with "bind_methods"
3 - Registering the classes in Godot with ClassDB
1. Creating Classes
In the src folder, every class you wish to expose to Godot requires a header (.h) for definitions and a source file (.cpp) for logic. When defining a class, such as GDExample, it must inherit from a Godot class (like Sprite2D).
#pragma once
// We include the base class we want to inherit from
#include <godot_cpp/classes/sprite2d.hpp>
#include <godot_cpp/core/class_db.hpp>
namespace godot {
class GDExample : public Sprite2D {
// This macro is mandatory for all GDExtension classes.
// It allows Godot to handle type-checking and inheritance internally.
GDCLASS(GDExample, Sprite2D);
private:
// Internal variables for our logic
double time_passed;
double amplitude;
double time_emit;
protected:
// This is where we register our properties, methods, and signals to Godot
static void _bind_methods();
public:
GDExample();
~GDExample();
// Standard Godot virtual function for frame-by-frame logic
void _process(double delta) override;
// Setters and Getters for the 'amplitude' property
void set_amplitude(const double p_amplitude);
double get_amplitude();
// Example methods for logic and scene interaction
int sum_two_numbers(int a, float b);
void test_function();
};
} // namespace godot
Crucially, you must use the GDCLASS(GDExample, Sprite2D) macro to allow Godot to manage the class.
Binding data
You also declare standard node functions like _process or _ready, along with a mandatory static function called _bind_methods. This is where the magic happens: it is in _bind_methods that you tell Godot which functions, properties, and signals are recognized by the editor.
#include "gdexample.h"
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/classes/scene_tree.hpp>
using namespace godot;
// _bind_methods is where we register our C++ code with Godot's internal ClassDB.
// If you don't bind a method here, Godot (and GDScript) won't know it exists.
void GDExample::_bind_methods() {
// 1. REGISTERING METHODS
// D_METHOD provides the name Godot will use and the names of the parameters.
ClassDB::bind_method(D_METHOD("get_amplitude"), &GDExample::get_amplitude);
ClassDB::bind_method(D_METHOD("set_amplitude", "p_amplitude"), &GDExample::set_amplitude);
// 2. CREATING PROPERTIES (The @export equivalent)
// This allows you to edit "amplitude" directly in the Godot Inspector.
// We define the type (FLOAT), the name, and the Hint (a slider from 0 to 100).
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "amplitude", PROPERTY_HINT_RANGE, "0,100,0.1"),
"set_amplitude", "get_amplitude");
// Registering our custom math and utility functions
ClassDB::bind_method(D_METHOD("test_function"), &GDExample::test_function);
ClassDB::bind_method(D_METHOD("sum_two_numbers", "number1", "number2"), &GDExample::sum_two_numbers);
// 3. REGISTERING SIGNALS
// This allows our C++ node to broadcast events that GDScript can listen to.
// Here we define a signal "position_changed" that sends the Node itself and its new Vector2.
ADD_SIGNAL(MethodInfo("position_changed",
PropertyInfo(Variant::OBJECT, "node"),
PropertyInfo(Variant::VECTOR2, "new_pos")));
}
GDExample::GDExample() {
// Initialize our variables with default values.
time_passed = 0.0;
amplitude = 10.0;
time_emit = 0.0;
}
GDExample::~GDExample() {
// Clean up if necessary.
}
void GDExample::_process(double delta) {
// IMPORTANT: Check if we are inside the editor.
// If we don't do this, the script will run and move the node while you are designing!
if (godot::Engine::get_singleton()->is_editor_hint()) return;
time_passed += delta;
// A simple trigonometric movement pattern (Swaying motion)
Vector2 new_position = Vector2(
amplitude + (amplitude * sin(time_passed * 2.0)),
amplitude + (amplitude * cos(time_passed * 1.5))
);
// We call the native Godot 'set_position' method
set_position(new_position);
// 4. EMITTING SIGNALS
// We use a timer logic to emit our custom signal once every second.
time_emit += delta;
if (time_emit > 1.0) {
emit_signal("position_changed", this, new_position);
time_emit = 0.0;
}
}
// GETTERS AND SETTERS
// Required for the Property system to work.
void GDExample::set_amplitude(const double p_amplitude) {
amplitude = p_amplitude;
}
double GDExample::get_amplitude() {
return amplitude;
}
// EXAMPLE: Basic data processing
int GDExample::sum_two_numbers(int a, int b) {
return a + b;
}
// EXAMPLE: Interacting with the Scene Tree
void GDExample::test_function() {
// Printing to the Godot Output console
godot::UtilityFunctions::print("Hello from C++!");
// Accessing another node relative to this one (just like get_node in GDScript)
godot::Sprite2D* child_node = get_node<godot::Sprite2D>("Sprite2D");
godot::UtilityFunctions::print("Found child node: ", child_node);
// Accessing Groups (extremely useful for game logic)
Array nodes = get_tree()->get_nodes_in_group("MyGroup");
godot::UtilityFunctions::print("Nodes in group 'MyGroup': ", nodes);
}
3. Registration and Compilation
Next, you need an entry point for your library, typically called register_types. This file contains an initialization function where you use ClassDB to register each class you've created. This step is what makes your C++ class actually appear in the "Add Node" list inside Godot.
register_types.h
/**
* @file register_types.h
* @brief Module initialization and registration for the GDExtension plugin.
*/
#pragma once
#include <godot_cpp/core/class_db.hpp>
using namespace godot;
/**
* @brief Initializes the GDExtension module at the specified initialization level.
*
* This function is called by Godot Engine during plugin initialization. It registers
* all custom classes, methods, and properties defined in this GDExtension with the
* Godot ClassDB, making them available in the editor and at runtime.
*
* @param p_level The initialization level at which classes should be registered
* (e.g., MODULE_INITIALIZATION_LEVEL_SCENE for scene-related classes).
*/
void initialize_example_module(ModuleInitializationLevel p_level);
/**
* @brief Uninitializes the GDExtension module at the specified initialization level.
*
* This function is called by Godot Engine during plugin cleanup. It should perform
* any necessary cleanup operations for the module.
*
* @param p_level The initialization level at which cleanup should occur.
*/
void uninitialize_example_module(ModuleInitializationLevel p_level);
register_types.cpp
#include <gdexample.h>
#include <godot_cpp/core/class_db.hpp>
// This function is called by Godot when the extension is loaded
void initialize_example_module(ModuleInitializationLevel p_level) {
// We only want to register our classes at a specific 'level'
// (the Scene level is where Nodes live).
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
// This makes 'GDExample' appear in the "Create New Node" menu
ClassDB::register_class<GDExample>();
}
// This function is called when the extension is unloaded
void uninitialize_example_module(ModuleInitializationLevel p_level) {
// Cleanup code goes here if necessary
}
extern "C" {
// Initialization.
GDExtensionBool GDE_EXPORT example_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) {
godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization);
init_obj.register_initializer(initialize_example_module);
init_obj.register_terminator(uninitialize_example_module);
init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE);
return init_obj.init();
}
}
To compile, you create an SConstruct file at the project root. This file acts as a build script that configures the compilation and links your code with the godot-cpp library. Once you run scons at the root, it generates the final binary—a .dll for Windows or a .so for Linux.
#!/usr/bin/env python
import os
import sys
# We import error printing from SCons methods to keep output clean
from methods import print_error
# --- CONFIGURATION AREA ---
# Change these names to match your specific project
folder_name = "godot-cpp-example"
library_name = "GodotCppExample"
project_dir = "game_project" # This is the folder where your Godot 'project.godot' lives
# Initialize the SCons environment
localEnv = Environment(tools=["default"], PLATFORM="")
# Load custom build options (like compiler flags) if a 'custom.py' exists
customs = ["custom.py"]
customs = [os.path.abspath(path) for path in customs]
opts = Variables(customs, ARGUMENTS)
opts.Update(localEnv)
# Generate a 'help' text for when you run 'scons --help'
Help(opts.GenerateHelpText(localEnv))
env = localEnv.Clone()
# --- SAFETY CHECK ---
# Before we try to compile, we make sure the user didn't forget the submodules.
if not (os.path.isdir("godot-cpp") and os.listdir("godot-cpp")):
print_error("""The 'godot-cpp' bindings are missing!
You likely forgot to initialize Git submodules. Run this command:
git submodule update --init --recursive""")
sys.exit(1)
# --- THE BUILD PROCESS ---
# 1. We call the SConstruct file inside godot-cpp to prepare the base bindings
env = SConscript("godot-cpp/SConstruct", {"env": env, "customs": customs})
# 2. Tell SCons where our source code is
env.Append(CPPPATH=["src/"])
sources = Glob("src/*.cpp") # This automatically finds all .cpp files in the src folder
# 3. Handle Documentation (Godot 4.3+)
# If you are making an editor plugin, this compiles your XML documentation into the binary
if env["target"] in ["editor", "template_debug"]:
try:
doc_data = env.GodotCPPDocData("src/gen/doc_data.gen.cpp", source=Glob("doc_classes/*.xml"))
sources.append(doc_data)
except AttributeError:
print("Skipping class reference: targeting a pre-4.3 Godot baseline.")
# 4. Handle Naming Conventions
# Different OS's have different naming rules (e.g., .dll vs .so).
# This logic ensures the filename is formatted correctly for Godot.
suffix = env['suffix'].replace(".dev", "").replace(".universal", "")
lib_filename = "{}{}{}{}".format(env.subst('$SHLIBPREFIX'), library_name, suffix, env.subst('$SHLIBSUFFIX'))
# 5. The Compilation Step
# This actually triggers the compiler to create the Shared Library (binary)
library = env.SharedLibrary(
"bin/{}/{}".format(env['platform'], lib_filename),
source=sources,
)
# --- THE INSTALLATION STEP ---
# Instead of manually copying files, we tell SCons to automatically 'Install'
# the new binary directly into our Godot project's addons folder.
# This makes the development loop much faster!
copy = env.Install("{}/addons/{}/bin/{}/".format(project_dir, folder_name, env["platform"]), library)
# Set the library and the copy operation as the default action when 'scons' is run
default_args = [library, copy]
Default(*default_args)
3. The .gdextension File
The final step is telling Godot how to load your newly compiled binary. You do this by creating a simple text file with the .gdextension extension (e.g., my_project.gdextension).
Before we look at the file content, it is important to understand where this file sits. Typically, you want a clean separation between your C++ workspace and your Godot project files:
my_gdextension_project/
├── godot-cpp/ # The engine bindings (Git submodule)
├── src/ # Your C++ source code (.cpp and .h files)
├── SConstruct # The build script that ties it all together
└── project/ # Your actual Godot Project folder
├── project.godot
├── main.tscn
└── bin/ # Where your compiled binaries and .gdextension live
├── my_project.gdextension
├── libgdexample.windows.debug.x86_64.dll
└── libgdexample.windows.release.x86_64.dll
Observation: The deliberate omission of the "addons" folder (unlike in the Scons file) in this example, serves a purpose: to emphasize that Godot does not mandate placing the .gdextension file within the "addons" directory, which is the expectation for GDScript plugins.
This file acts as a manifest. It contains a [configuration] section pointing to your C++ entry point and a [libraries] section that maps specific platforms and build targets to the correct file paths.
Understanding Build Targets
It is important to understand that you will typically deal with two types of builds:
- Debug Builds: These are used during development and inside the Godot Editor. They include extra symbols that make debugging easier and allow the engine to give you descriptive error messages if the C++ code crashes.
- Release Builds: These are optimized for performance. They are smaller and faster because they strip away the debugging "clutter," and they are what you will actually distribute to your players.
Here is how you would set up your .gdextension file to handle both:
[configuration]
# This must match the entry function name defined in your registration file (register_types.cpp)
entry_symbol = "example_library_init"
compatibility_minimum = "4.6"
reloadable=true # add this to integrate hot-reload inside the editor!
[libraries]
# We map specific operating systems to the resulting compiled files.
# 'res://' points to the root of your Godot project folder.
# but you can also use relative paths.
windows.debug.x86_64 = "res://bin/libgdexample.windows.template_debug.x86_64.dll"
linux.debug.x86_64 = "res://bin/libgdexample.linux.template_debug.x86_64.so"
macos.debug.x86_64 = "res://bin/libgdexample.macos.template_debug.x86_64.dylib"
Why separate them?
By defining both debug and release paths, Godot is smart enough to swap the libraries automatically. When you are working in the editor, it uses the debug version. When you click Export Project and uncheck "Export with Debug," Godot will automatically bundle the release version of your C++ code, ensuring your players get the best possible performance.
But most importantly, when Godot sees the .gdextension file, it loads the correct binary for your platform, and your C++ classes are dynamically linked. They will appear in the editor ready to be used like any native node.
Observation: If your classes don't appear immediatly in the editor, reload the current project, and make sure during development to activate "hot reload" for a better developer experience.
Conclusion
You now understand the fundamental flow of GDExtension. This is only the beginning of the journey. While the initial setup is the steepest hurdle, the performance rewards and architectural flexibility are significant.
In a future deep-dive, we will explore the more complex challenges of GDExtension, including available macros, documenting classes for the editor, and—most importantly—a performance comparison to see exactly how much speed is gained when migrating a GDScript plugin over to C++.
Link to the repository:
https://github.com/GuaraProductions/Godot-GDExtension-minimalist-example


Top comments (1)