Building a Godot Plugin with GDExtension
Published • 4 minutes
Use the left and right arrow keys to navigate the slides







Install git
git config --global user.name "Your Name" git config --global user.email "[email protected]"
Install brew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Install python
brew install python
Install scons
python -m pip install scons
Install VSCode (or other text editor)
- Install
clangd
Extension
- Install

Install git
git config --global user.name "Your Name" git config --global user.email "[email protected]"
Install Visual Studio (with Desktop development with C++)
Install python (and make sure to check off add to env variable)
Install scons
python -m pip install scons
Install VSCode (or other text editor)
- Install
clangd
Extension
- Install

Install git
sudo apt install git
sudo apt install git
git config --global user.name "Your Name" git config --global user.email "[email protected]"
Install python and scons
sudo apt install python3 scons
Install vim
sudo apt install vim
sudo apt install vim
Note: If you are running Linux in a VM like me your might need to add this to your
project.godot
file.
[rendering]
renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"


Create a directory and setup a git repo
mkdir godot-cpp-plugin cd godot-cpp-plugin/ git init
Add the
godotengine/godot-cpp
repo as a submodule with:git submodule add https://github.com/godotengine/godot-cpp.git
Go into the
godot-cpp/
directory.Checkout the Godot
4.4
release with:git checkout godot-4.4-stable
Commit changes.

Create
SConstruct
in the root of the project#!/usr/bin/env python SConscript("godot-cpp/SConstruct") CacheDir(".scons_cache/")
Create
.gitignore
file.scons_cache/ .sconsign.dblite
Run scons in the root of your project:
scons

Create
compile_flags.txt
with the following contents:-std=c++17 -Iinclude -Igodot-cpp/gdextension -Igodot-cpp/gen/include -Igodot-cpp/include
(Optional) Create
.clang-format
with the following contents:AllowShortBlocksOnASingleLine: Never BreakBeforeBraces: Allman IndentWidth: 4 PointerAlignment: Right TabWidth: 4 UseTab: Never
Create
GodotCppPlugin.gdextension
with the following contents:[configuration] entry_symbol = "godot_cpp_plugin_entry" compatibility_minimum = 4.4 reloadable = true [libraries] macos.debug = "res://addons/GodotCppPlugin/libGodotCppPlugin.macos.template_debug.framework" macos.release = "res://addons/GodotCppPlugin/libGodotCppPlugin.macos.template_release.framework" linux.debug.x86_64 = "res://addons/GodotCppPlugin/libGodotCppPlugin.linux.template_debug.x86_64.so" linux.release.x86_64 = "res://addons/GodotCppPlugin/libGodotCppPlugin.linux.template_release.x86_64.so" windows.debug.x86_32 = "res://addons/GodotCppPlugin/libGodotCppPlugin.windows.template_debug.x86_32.dll" windows.release.x86_32 = "res://addons/GodotCppPlugin/libGodotCppPlugin.windows.template_release.x86_32.dll" windows.debug.x86_64 = "res://addons/GodotCppPlugin/libGodotCppPlugin.windows.template_debug.x86_64.dll" windows.release.x86_64 = "res://addons/GodotCppPlugin/libGodotCppPlugin.windows.template_release.x86_64.dll"
Create
include/register_types.hpp
with the following contents:#pragma once #include <godot_cpp/core/class_db.hpp> using namespace godot; void initialize_godot_cpp_plugin(ModuleInitializationLevel p_level); void uninitialize_godot_cpp_plugin(ModuleInitializationLevel p_level);
Create
include/register_types.cpp
with the following contents:#include "register_types.hpp" #include <gdextension_interface.h> #include <godot_cpp/core/class_db.hpp> #include <godot_cpp/core/defs.hpp> #include <godot_cpp/godot.hpp> using namespace godot; void initialize_godot_cpp_plugin(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { return; } } void uninitialize_godot_cpp_plugin(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { return; } } extern "C" { auto GDE_EXPORT godot_cpp_plugin_entry( GDExtensionInterfaceGetProcAddress p_get_proc_address, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) -> GDExtensionBool { godot::GDExtensionBinding::InitObject init_obj( p_get_proc_address, p_library, r_initialization); init_obj.register_initializer(initialize_godot_cpp_plugin); init_obj.register_terminator(uninitialize_godot_cpp_plugin); init_obj.set_minimum_library_initialization_level( MODULE_INITIALIZATION_LEVEL_SCENE); return init_obj.init(); } }
Update the
SConstruct
file to the following contents:#!/usr/bin/env python env = SConscript("godot-cpp/SConstruct") env.Append(CPPPATH=["include/"]) sources = Glob("include/*.cpp") folder = "build/addons/GodotCppPlugin" if env["platform"] == "macos": file_name = "libGodotCppPlugin.{}.{}".format(env["platform"], env["target"]) library = env.SharedLibrary( "{}/{}.framework/{}".format(folder, file_name, file_name), source=sources ) else: library = env.SharedLibrary( "{}/libGodotCppPlugin{}{}" .format(folder, env["suffix"], env["SHLIBSUFFIX"]), source=sources, ) gdextension_copy = env.Command( target="{}/GodotCppPlugin.gdextension".format(folder), source="GodotCppPlugin.gdextension", action=Copy("$TARGET", "$SOURCE") ) env.Depends(gdextension_copy, library) CacheDir(".scons_cache/") Default(library) Default(gdextension_copy)
Update the
.gitignore
file to the following contents:.scons_cache/ .sconsign.dblite build/ include/*.os
Run
scons
again to build the project with the new files.scons

Create
include/mathf.hpp
with the following contents:#pragma once #include <algorithm> namespace Mathf { auto lerp(float a, float b, float t) -> float { return (a + t) * (b - a); } auto inverse_lerp(float a, float b, float v) -> float { return std::clamp((v - a) / (b - a), 0.0F, 1.0F); } } // namespace Mathf
Create
include/godot_cpp_plugin.hpp
with the following contents:#pragma once #include <godot_cpp/classes/object.hpp> #include <godot_cpp/core/class_db.hpp> #include <godot_cpp/variant/string.hpp> using namespace godot; class godot_cpp_plugin : public Object { GDCLASS(godot_cpp_plugin, Object) protected: static void _bind_methods(); public: static float lerp(float a, float b, float t); static float inverse_lerp(float a, float b, float v); };
Create
include/godot_cpp_plugin.cpp
with the following contents:#include "godot_cpp_plugin.hpp" #include "mathf.hpp" void godot_cpp_plugin::_bind_methods() { ClassDB::bind_static_method("godot_cpp_plugin", D_METHOD("lerp", "a", "b", "t"), &godot_cpp_plugin::lerp); ClassDB::bind_static_method("godot_cpp_plugin", D_METHOD("inverse_lerp", "a", "b", "v"), &godot_cpp_plugin::inverse_lerp); } float godot_cpp_plugin::lerp(float a, float b, float t) { return Mathf::lerp(a, b, t); } float godot_cpp_plugin::inverse_lerp(float a, float b, float v) { return Mathf::inverse_lerp(a, b, v); }
Update the
initialize_godot_cpp_plugin
method ininclude/register_types.cpp
with the following contents:#include "godot_cpp_plugin.hpp" void initialize_godot_cpp_plugin(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { return; } // Add this line to existing initialize_godot_cpp_plugin method: GDREGISTER_VIRTUAL_CLASS(godot_cpp_plugin); }
Run
scons
again to rebuild the plugin with the new changes.Create a new Godot project
Copy the contents of the build folder into the root of the project
Create an empty Node
Attach a script with the following contents:
extends Node func _ready() -> void: var value = godot_cpp_plugin.lerp(0, 10, 0.5) print(value) # 5.0

First, create a GDScript script in Godot extending the Sprite2D node:
extends Sprite2D @export var speed: int = 200; var screen_size: Vector2 var sprite_size: Vector2 var x_direction: int = 1 var y_direction: int = 1 func _ready(): screen_size = get_viewport_rect().size sprite_size = get_rect().size func _process(delta): var min_x = -(screen_size.x / 2) + (sprite_size.x / 2) var min_y = -(screen_size.y / 2) + (sprite_size.y / 2) var max_x = (screen_size.x / 2) - (sprite_size.x / 2) var max_y = (screen_size.y / 2) - (sprite_size.y / 2) position.x += speed * x_direction * delta position.y += speed * y_direction * delta if position.x > max_x || position.x < min_x: x_direction = -x_direction if position.y > max_y || position.y < min_y: y_direction = -y_direction position.x = clamp(position.x, min_x, max_x) position.y = clamp(position.y, min_y, max_y)
Next, create a matching header file (
include/screensaver.hpp
) and source file (include/screensaver.cpp
) in your plugin repo:include/screensaver.hpp
#pragma once #include <godot_cpp/classes/sprite2d.hpp> #include <godot_cpp/variant/vector2.hpp> namespace godot { class Screensaver : public Sprite2D { GDCLASS(Screensaver, Sprite2D) private: int speed = 200; Vector2 screen_size; Vector2 sprite_size; int x_direction = 1; int y_direction = 1; protected: static void _bind_methods(); public: void _ready() override; void _process(double delta) override; }; } // namespace godot
include/screensaver.cpp
#include "screensaver.hpp" #include <algorithm> #include <godot_cpp/core/class_db.hpp> using namespace godot; void Screensaver::_bind_methods() {} void Screensaver::_ready() { screen_size = get_viewport_rect().size; sprite_size = get_rect().size; } void Screensaver::_process(double delta) { auto min_x = -(screen_size.x / 2) + (sprite_size.x / 2); auto min_y = -(screen_size.y / 2) + (sprite_size.y / 2); auto max_x = (screen_size.x / 2) - (sprite_size.x / 2); auto max_y = (screen_size.y / 2) - (sprite_size.y / 2); auto position = get_position(); position.x += speed * x_direction * delta; position.y += speed * y_direction * delta; if (position.x > max_x || position.x < min_x) { x_direction = -x_direction; } if (position.y > max_y || position.y < min_y) { y_direction = -y_direction; } position.x = std::clamp(position.x, min_x, max_x); position.y = std::clamp(position.y, min_y, max_y); set_position(position); }
Finally, we let the plugin know that this new node exists by adding it to the
include/register_types.cpp
file:#include "screensaver.hpp" void initialize_godot_cpp_plugin(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { return; } GDREGISTER_VIRTUAL_CLASS(godot_cpp_plugin); // Add this line to existing initialize_godot_cpp_plugin method: GDREGISTER_RUNTIME_CLASS(Screensaver); }

screensaver.gd
@export var speed: int = 200;
include/screensaver.hpp
class Screensaver : public Sprite2D
{
GDCLASS(Screensaver, Sprite2D)
private:
int speed = 200;
void set_speed(const int value);
int get_speed();
include/screensaver.cpp
void Screensaver::_bind_methods()
{
ClassDB::bind_method(D_METHOD("set_speed", "speed"),
&Screensaver::set_speed);
ClassDB::bind_method(D_METHOD("get_speed"), &Screensaver::get_speed);
ADD_PROPERTY(PropertyInfo(Variant::INT, "speed"), "set_speed", "get_speed");
}
void Screensaver::set_speed(const int value) { speed = value; }
int Screensaver::get_speed() { return speed; }

Next we a going to add a callback signal to our screensaver for when the Sprite2d bounces off a wall.
void Screensaver::_bind_methods() { // ... ADD_SIGNAL(MethodInfo("wall_bounce")); }
void Screensaver::_process(double delta) { // ... if (position.x > max_x || position.x < min_x) { x_direction = -x_direction; emit_signal("wall_bounce"); } if (position.y > max_y || position.y < min_y) { y_direction = -y_direction; emit_signal("wall_bounce"); } // ... }
Go back to Godot, select the Screensaver, click on Node in the inspector panel, then select
wall_bounce
, right-click and select Connect.... Select the first node we created and press the Connect button.In the newly created method, add a
print
statement for testing.

So far we have worked with primatives, but what if we wanted to send an array of values from godot to our plugin and run a calculation.
Lets start with sending an array of integers to a method to add the values together.
First start with adding the following to
include/mathf.hpp
:#include <godot_cpp/classes/object.hpp> #include <godot_cpp/variant/array.hpp> using namespace godot; auto sum(Array values) -> int { auto count = 0; for (auto i = 0; i < values.size(); i += 1) { if (values[i].get_type() == Variant::INT) { int variant = values[i]; count += variant; } } return count; }
Add the following to
include/godot_cpp_plugin.hpp
#pragma once #include <godot_cpp/classes/object.hpp> #include <godot_cpp/core/class_db.hpp> #include <godot_cpp/variant/string.hpp> using namespace godot; class godot_cpp_plugin : public Object { GDCLASS(godot_cpp_plugin, Object) protected: static void _bind_methods(); public: static float lerp(float a, float b, float t); static float inverse_lerp(float a, float b, float v); // Add the following new static method to the existing godot_cpp_plugin class: static int sum(const Array &values); };
And then add the following to
include/godot_cpp_plugin.cpp
void godot_cpp_plugin::_bind_methods() { // Add this line to existing godot_cpp_plugin::_bind_methods method: ClassDB::bind_static_method("godot_cpp_plugin", D_METHOD("sum", "values"), &godot_cpp_plugin::sum); } int godot_cpp_plugin::sum(const Array &values) { return Mathf::sum(values); }
And then use it in Godot with the following:
var sum = godot_cpp_plugin.sum([1, 2, 3, 4, 5]) print(sum)
We can also take a dictionary of values and return a subset of the values based on a key.
Create a new file
include/convert.hpp
#pragma once #include <string> #include <vector> #include <godot_cpp/classes/object.hpp> #include <godot_cpp/variant/array.hpp> using namespace godot; namespace Convert { auto get_key_values(Array values, const String &key) -> Array { std::vector<std::string> internalValues; for (auto i = 0; i < values.size(); i += 1) { if (values[i].get_type() == Variant::DICTIONARY) { Dictionary variant = values[i]; Array keys = variant.keys(); if (keys.has(key)) { String value = variant[key]; internalValues.push_back(value.utf8().get_data()); } } } Array godot_values; for (auto const &value : internalValues) { godot_values.append(value.c_str()); } return godot_values; } } // namespace Convert
Add the following to
include/godot_cpp_plugin.hpp
#pragma once #include <godot_cpp/classes/object.hpp> #include <godot_cpp/core/class_db.hpp> #include <godot_cpp/variant/string.hpp> #include <godot_cpp/variant/array.hpp> using namespace godot; class godot_cpp_plugin : public Object { GDCLASS(godot_cpp_plugin, Object) protected: static void _bind_methods(); public: static float lerp(float a, float b, float t); static float inverse_lerp(float a, float b, float v); static int sum(const Array &values); // Add the following new static method to the existing godot_cpp_plugin class: static Array get_key_values(const Array &values, const String &key); };
And then add the following to
include/godot_cpp_plugin.cpp
#include "convert.hpp" void godot_cpp_plugin::_bind_methods() { // Add this line to existing godot_cpp_plugin::_bind_methods method: ClassDB::bind_static_method("godot_cpp_plugin", D_METHOD("get_key_values", "values", "key"), &godot_cpp_plugin::get_key_values); } Array godot_cpp_plugin::get_key_values(const Array &values, const String &key) { return Convert::get_key_values(values, key); }
And then use it in Godot with the following:
var urls = godot_cpp_plugin.get_key_values([{"name": "Google", "url":"http://google.com"}], "url") print(urls)

Create
.github/workflows/build.workflow.yml
in your repo and push the changes. Go to the Actions tab and find theBuild Godot Plugin
item in the sidebar. Click Run Workflow. This can take up to a half hour the first time, and 3 - 5 minutes on subsequent builds.name: Build Godot Plugin on: workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build-plugin: strategy: matrix: include: - os: ubuntu-latest platform: linux arch: x86_64 - os: macos-latest platform: macos arch: universal - os: windows-latest platform: windows arch: x86_64 runs-on: ${{ matrix.os }} steps: - name: Check out repository uses: actions/[email protected] with: fetch-depth: 0 - name: Check out submodules run: | git submodule update --init - if: matrix.platform == 'linux' name: Install dependencies (Linux) run: | sudo apt-get update sudo apt-get install -y build-essential scons pkg-config libx11-dev \ libxcursor-dev libxinerama-dev libgl1-mesa-dev libglu1-mesa-dev \ libasound2-dev libpulse-dev libudev-dev libxi-dev libxrandr-dev \ libwayland-dev - if: matrix.platform == 'macos' name: Install dependencies (macOS) run: | brew install scons - if: matrix.platform == 'windows' name: Install dependencies (Windows) run: | python -m pip install scons choco install mingw - name: Restore SCons Cache uses: actions/cache/[email protected] with: path: | .scons_cache/ .sconsign.dblite key: ${{ matrix.os }}-scons-cache - name: Restore Godot C++ Generated Files uses: actions/cache/[email protected] with: path: | godot-cpp/bin/ godot-cpp/gen/ key: ${{ matrix.os }}-godot-cpp-cache - name: Build Plugin shell: bash run: | scons platform=${{ matrix.platform }} target=template_release arch=${{ matrix.arch }} scons platform=${{ matrix.platform }} target=template_debug arch=${{ matrix.arch }} - name: Store SCons Cache uses: actions/cache/[email protected] with: path: | .scons_cache/ .sconsign.dblite key: ${{ matrix.os }}-scons-cache - name: Store Godot C++ Generated Files uses: actions/cache/[email protected] with: path: | godot-cpp/bin/ godot-cpp/gen/ key: ${{ matrix.os }}-godot-cpp-cache - name: Upload build artifacts uses: actions/[email protected] with: name: build-${{ matrix.platform }}-${{ matrix.arch }} path: build/ retention-days: 1 commit-changes: needs: build-plugin runs-on: ubuntu-latest permissions: contents: write steps: - name: Check out repository uses: actions/[email protected] with: fetch-depth: 0 - name: Download all build artifacts uses: actions/[email protected] with: path: artifacts/ - name: Move artifacts to build directory run: | mkdir -p build/ cp -r artifacts/*/* build/ - name: Setup git run: | git config user.name 'github-actions[bot]' git config user.email 'github-actions[bot]@users.noreply.github.com' - name: Git commit changes run: | git pull git add build/ git commit -m "Updated build files [skip ci]" || exit 0 git push
