← Home

Building a Godot Plugin with GDExtension

Published • 4 minutes

Use the left and right arrow keys to navigate the slides

  1. Install git

    git config --global user.name "Your Name"
    git config --global user.email "[email protected]"
    
  2. Install brew

    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
    
  3. Install python

    brew install python
    
  4. Install scons

    python -m pip install scons
    
  5. Install VSCode (or other text editor)

    1. Install clangd Extension
  1. Install git

    git config --global user.name "Your Name"
    git config --global user.email "[email protected]"
    
  2. Install Visual Studio (with Desktop development with C++)

  3. Install python (and make sure to check off add to env variable)

  4. Install scons

    python -m pip install scons
    
  5. Install VSCode (or other text editor)

    1. Install clangd Extension
  1. Install git sudo apt install git

    sudo apt install git
    
    git config --global user.name "Your Name"
    git config --global user.email "[email protected]"
    
  2. Install python and scons

    sudo apt install python3 scons
    
  3. 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"
  1. Create a directory and setup a git repo

    mkdir godot-cpp-plugin
    cd godot-cpp-plugin/
    git init
    
  2. Add the godotengine/godot-cpp repo as a submodule with:

    git submodule add https://github.com/godotengine/godot-cpp.git
    
  3. Go into the godot-cpp/ directory.

  4. Checkout the Godot 4.4 release with:

    git checkout godot-4.4-stable
    
  5. Commit changes.

  1. Create SConstruct in the root of the project

    #!/usr/bin/env python
    
    SConscript("godot-cpp/SConstruct")
    
    CacheDir(".scons_cache/")
    
  2. Create .gitignore file

    .scons_cache/
    
    .sconsign.dblite
    
  3. Run scons in the root of your project:

    scons
    
  1. Create compile_flags.txt with the following contents:

    -std=c++17
    -Iinclude
    -Igodot-cpp/gdextension
    -Igodot-cpp/gen/include
    -Igodot-cpp/include
    
  2. (Optional) Create .clang-format with the following contents:

    AllowShortBlocksOnASingleLine: Never
    BreakBeforeBraces: Allman
    IndentWidth: 4
    PointerAlignment: Right
    TabWidth: 4
    UseTab: Never
    
  3. 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"
    
  4. 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);
    
  5. 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();
        }
    }
    
  6. 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)
    
  7. Update the .gitignore file to the following contents:

    .scons_cache/
    
    .sconsign.dblite
    
    build/
    
    include/*.os
    
  8. Run scons again to build the project with the new files.

    scons
    
  1. 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
    
  2. 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);
    };
    
  3. 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);
    }
    
  4. Update the initialize_godot_cpp_plugin method in include/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);
    }
    
  5. Run scons again to rebuild the plugin with the new changes.

  6. Create a new Godot project

  7. Copy the contents of the build folder into the root of the project

  8. Create an empty Node

  9. 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
    
  1. 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)
    
  2. 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);
    }
    
  3. 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; }
  1. 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");
        }
    
        // ...
    }
    
  2. 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.

  3. 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.

  1. 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;
    }
    
  2. 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);
    };
    
  3. 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); }
    
  4. 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.

  1. 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
    
  2. 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);
    };
    
  3. 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);
    }
    
  4. 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)
    
  1. Create .github/workflows/build.workflow.yml in your repo and push the changes. Go to the Actions tab and find the Build 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
    

Additional Links