A streamlined GDExtension template for Godot C++ development that reduces boilerplate code through intelligent macros and automatic code generation.
This project provides a convenient development template for creating Godot 4.x GDExtensions in C++. Instead of manually writing repetitive binding code, you annotate your classes with simple macros, and an automatic code generator creates all the necessary boilerplate for you.
- Less Boilerplate: Declare properties, methods, and signals with simple macros
- Automatic Code Generation: Python script generates binding code automatically
- Directory Structure Preserved: Subdirectories in
src/are preserved in generated files - Recursive Compilation: Automatically finds and compiles
.cppfiles from all subdirectories - Single Entry Point: Classes auto-register without explicit registration calls
The template provides four main macros to simplify GDExtension development:
Replaces the boilerplate needed inside a Godot class definition. It automatically generates:
GDCLASS()declaration (registers the class with Godot)_bind_methods()static method (for binding properties and methods)- Getter and setter declarations for all
GDPROPERTY()fields
class MyClass : public Node {
GD_GENERATED_BODY() // ← Replaces ~15 lines of boilerplate
};Exposes a member variable as a property in the Godot editor. The generator automatically creates getter/setter methods and property registration.
GDPROPERTY()
float speed;
GDPROPERTY()
String class_name;
GDPROPERTY()
Camera3D* camera;
GDPROPERTY()
float speed = 10.0f; // default values are supportedSupported types:
float,double→ GodotFLOATint→ GodotINTbool→ GodotBOOLString→ GodotSTRINGVector2,Vector3→ GodotVECTOR2,VECTOR3Color→ GodotCOLORNodePath→ GodotNODE_PATHSomeNode*(pointer to any Godot object) → GodotNODE_PATH(exposed as a path that resolves to the pointer at runtime)
Marks a method to be exposed to Godot (callable from GDScript and the editor).
GDFUNCTION()
int calculate(int a, int b);
GDFUNCTION()
void apply_effect(Vector3 position);Declares a signal that can be emitted and connected to from GDScript.
GDSIGNAL("player_spawned", Vector3, spawn_position, int, player_id)
GDSIGNAL("health_changed", float, new_health)Signal syntax: GDSIGNAL("signal_name", Type1, param1, Type2, param2, ...)
The generator automatically binds Godot's lifecycle methods. Declare them in your class and the generator creates the necessary overrides:
void ready(); // Called when node enters scene tree
void process(double delta); // Called every frame
void physics_process(double delta); // Called every physics frameWhen implemented in your .cpp file, these methods are called automatically at the appropriate lifecycle stage. The generator handles the override boilerplate:
void MyClass::ready() {
// This is called when the node enters the scene tree
initialize_stuff();
}
void MyClass::process(double delta) {
// This is called every frame
update_position(delta);
}You can also create editor-only variants by adding _editor suffix:
void ready_editor(); // Called only in the editor
void process_editor(double delta); // Called only in the editor
void physics_process_editor(double delta); // Called only in the editorThe generator automatically routes to the _editor variant when Engine::get_singleton()->is_editor_hint() returns true:
void MyClass::ready_editor() {
// This only runs in the Godot editor
visualize_debug_info();
}Reference other nodes using C++ pointers. The generator automatically:
- Exposes a
NodePathproperty to the editor - Resolves the path to the actual node pointer when
_ready()is called
GDPROPERTY()
Camera3D* camera;
GDPROPERTY()
AudioStreamPlayer* audio_player;Access the resolved pointers directly in your code:
void MyClass::ready() {
if (camera) {
camera->set_position(Vector3(0, 5, 10));
}
}In the Godot editor, the property appears as a NodePath that you can set by dragging nodes into the inspector.
Create a header file in src/ (e.g., src/my_plugin.h):
#pragma once
#include "internal/stubs.h"
#include <godot_cpp/classes/node.hpp>
#include "generated/my_plugin.gen.h"
namespace godot {
class MyPlugin : public Node {
GD_GENERATED_BODY()
GDSIGNAL("ready")
private:
GDPROPERTY()
float max_speed;
public:
GDFUNCTION()
void initialize();
GDFUNCTION()
int get_count();
};
}Create the implementation file (src/my_plugin.cpp):
#include "my_plugin.h"
using namespace godot;
void MyPlugin::initialize() {
emit_signal("ready");
}
int MyPlugin::get_count() {
return 42;
}The build system automatically handles everything:
# Build for the current platform
scons target=template_release
scons target=template_debug
# The generator runs automatically on every build
# It scans src/ and generates src/generated/ filesThe generated .gen.h and .gen.cpp files:
- Are automatically created in
src/generated/ - Contain getter/setter implementations
- Include property and signal registration
- Should NOT be edited manually
Your class is now available in Godot:
- In the editor: Drag the generated
.gdextensionfile into your project - In GDScript: Use your class like any other node
var plugin = MyPlugin.new()
plugin.max_speed = 100.0
plugin.initialize()
plugin.ready.connect(func(): print("Ready!"))godot-cpp-template-macros/
├── src/ # Your source files
│ ├── example_macro.h
│ ├── example_macro.cpp
│ ├── generated/ # AUTO-GENERATED (do not edit)
│ │ ├── example_macro.gen.h
│ │ └── example_macro.gen.cpp
│ └── internal/
│ ├── stubs.h # Macro definitions
│ ├── class_registry.h # Class auto-registration
│ ├── register_types.h # Godot entry point
│ └── register_types.cpp
├── tools/
│ ├── gdheader_gen.py # Code generator (runs automatically)
│ └── gen_vcxproj.py # Generates .sln/.vcxproj/.run for Rider/VS
├── .run/ # Rider run configurations (AUTO-GENERATED)
│ ├── windows-editor.run.xml
│ ├── windows-game.run.xml
│ └── ...
├── godot-cpp/ # Godot C++ bindings (submodule)
├── project/ # Godot project files
│ └── bin/ # Build outputs (debug/ and release/)
├── SConstruct # Build configuration
└── godot.exe # Godot binary (place here for Rider debugging)
- Build starts:
SConstructruns the generator before compilation - Scanner:
gdheader_gen.pyrecursively scanssrc/for.hfiles - Parser: Extracts class names, properties, methods, and signals using regex
- Generator: Creates:
.gen.hfiles with macro expansions.gen.cppfiles with implementations
- Compilation: Godot-cpp compiles everything into a GDExtension
Classes are registered automatically via a static initializer in the .gen.cpp file:
namespace {
bool _registered_MyClass = []() {
godot::ClassRegistry::get().add([]() { GDREGISTER_CLASS(MyClass); });
return true;
}();
}This runs at library load time, before _enter_tree() is called.
You can organize your source files in subdirectories, and the generator preserves the structure:
src/
├── plugins/
│ ├── audio_plugin.h
│ └── audio_plugin.cpp
└── utils/
├── math_utils.h
└── math_utils.cpp
# Generated files will be:
src/generated/plugins/audio_plugin.gen.h
src/generated/plugins/math_utils.gen.h
# etc.
The generated getters and setters are simple pass-through methods. You can override them in your .cpp file for custom logic:
float MyClass::get_speed() const {
// The setter auto-generates this, but you can customize it
return speed * 1.5f; // Apply a multiplier, for example
}Lifecycle methods are automatically called by Godot at the appropriate times. You don't need to call them manually:
// Header
class GameController : public Node {
GD_GENERATED_BODY()
GDPROPERTY()
float spawn_delay;
void ready();
void process(double delta);
void process_editor(double delta);
GDFUNCTION()
void reset_game();
};
// Implementation
void GameController::ready() {
// Called when node enters the scene tree
initialize_spawner();
}
void GameController::process(double delta) {
// Called every frame during gameplay
update_enemies(delta);
}
void GameController::process_editor(double delta) {
// Called every frame only in the editor
update_debug_visualization(delta);
}The generator automatically:
- Creates
_ready(),_process(),_physics_process()overrides - Resolves node references in
_ready() - Routes to
*_editorvariants when in editor mode - Calls your user-defined lifecycle methods
Node references use NodePaths internally, allowing them to be edited in the Godot inspector. Resolution happens automatically in _ready():
// Header
class CameraController : public Node3D {
GD_GENERATED_BODY()
GDPROPERTY()
Camera3D* camera;
GDPROPERTY()
AudioStreamPlayer* background_music;
void ready();
};
// Implementation
void CameraController::ready() {
if (camera) {
camera->set_position(Vector3(0, 5, 10));
}
if (background_music) {
background_music->play();
}
}In the editor:
- The property appears as a
NodePathfield - Drag a node into the field to set the reference
- The path is resolved to the actual pointer when
_ready()is called - Use null checks before accessing pointers
- Only supported types: The generator knows about float, int, bool, String, Vector2, Vector3, Color, and NodePath. Node pointers (
SomeNode*) are automatically converted to NodePath properties. Other types default toVariant::NIL. - No nested classes: The parser doesn't support nested class definitions.
- Method parameter names: Required in function declarations for proper binding.
- Do not edit generated files: Changes to
.gen.hand.gen.cppwill be overwritten on the next build. - Lifecycle methods: Only
ready,process, andphysics_processare supported. Editor variants (*_editor) are automatically detected if the method exists. - Node path resolution: Node pointers are resolved in
_ready(), so they're guaranteed to be valid after that point. Always check for null pointers if accessed before_ready()is called.
# Linux
scons platform=linuxbsd target=template_release
# macOS
scons platform=macos target=template_release
# Windows
scons platform=windows target=template_release
# Web (Emscripten)
scons platform=web target=template_releaseFor C++ IntelliSense via ReSharper C++ (without requiring clangd), generate the project files using the included script:
python tools/gen_vcxproj.pyThis creates in the project root:
<project_name>.slnand<project_name>.vcxproj— the MSBuild project (IntelliSense, build/clean/rebuild).run/*.run.xml— pre-configured Rider run configurations
The project name is read from SConstruct (project_name = "...") and falls back to the folder name.
Workflow in Rider:
- Select a build configuration from the toolbar (e.g.
Debug Windows) - Select a run configuration from the toolbar (e.g.
Windows Editor) - Press Play (run) or Debug
Build configurations (Debug/Release × Windows/Linux/Android):
| Configuration | scons command |
|---|---|
| Debug Windows | scons platform=windows target=template_debug debug_symbols=yes |
| Release Windows | scons platform=windows target=template_release |
| Debug Linux | scons platform=linux target=template_debug |
| Release Linux | scons platform=linux target=template_release |
| Debug Android | scons platform=android target=template_debug |
| Release Android | scons platform=android target=template_release |
Run configurations (what gets launched when you press Play):
| Run config | Launches | Args |
|---|---|---|
| Windows Editor | godot.exe |
--editor --path project/ |
| Windows Game | godot.exe |
--path project/ |
| Release Windows | godot.exe |
--path project/ |
| Debug/Release Linux | (no local launcher) | — |
| Debug/Release Android | (no local launcher) | — |
Requirements:
- Place a
godot.exe(Windows export template or editor binary) in the project root for the Windows run configs to work
Notes:
- Re-run
gen_vcxproj.pyif you rename the project or add new platforms - The generated files are committed to the repo so teammates don't need to run the script
- New
.cpp/.hfiles insrc/are picked up automatically via wildcards — no need to regenerate
For a better development experience in VS Code, create the following configuration files:
{
"version": "2.0.0",
"tasks": [
{
"label": "Build Debug",
"type": "shell",
"command": "scons",
"args": ["target=template_debug"],
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": []
},
{
"label": "Build Release",
"type": "shell",
"command": "scons",
"args": ["target=template_release"],
"group": "build",
"problemMatcher": []
},
{
"label": "Generate Code",
"type": "shell",
"command": "python",
"args": ["tools/gdheader_gen.py"],
"group": "build",
"problemMatcher": []
},
{
"label": "Clean Build",
"type": "shell",
"command": "scons",
"args": ["-c"],
"group": "build",
"problemMatcher": []
}
]
}{
"version": "0.2.0",
"configurations": [
{
"name": "Godot Debug",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/godot-cpp/bin/godot.exe",
"args": ["-e"],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/project",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"preLaunchTask": "Build Debug",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}Tips:
- Use Ctrl+Shift+B to run the default build task
- Use F5 to start debugging (requires Godot executable in path)
- Run "Generate Code" task if you modify macro annotations
- Adjust
programpath if Godot is installed elsewhere
Install the C++ Child Process Debugger by Alber Ziegenhagel:
- Search for
C++ Child Process Debuggerin VS Code extensions - This debugger can attach to child processes, which is useful for debugging GDExtensions that are loaded by the Godot engine
- Alternative: Use
ms-vscode.cpptools(C/C++ extension) for standard debugging
Generated files not updated?
- Run
python tools/gdheader_gen.pymanually to regenerate - Check that your
.hfile includesGD_GENERATED_BODY()
Build fails with undefined references?
- Ensure your implementation
.cppfile exists - Check that the
.hfile has the correct#includefor the.gen.hfile
Properties not showing in Godot?
- Verify the type is in the
TYPE_MAPdictionary ingdheader_gen.py - Ensure you used
GDPROPERTY()with no arguments - Rebuild and reimport the
.gdextensionfile
This template builds on top of godot-cpp, which is licensed under the MIT license.