Godot 4: GDExtension for C++ using CMake
With the release of the new Godot 4 version on 2023-03-01 the Godot engine the team behind it worked on many features. One of those features was the complete redesign of the GDNative bindings which I talked about some time ago in a blog post here. GDExtensions is the new successor to this GDNative bindings and you will find out that there are some major changes and some things that stay the same. Let’s dive in!
Table of Contents
Intro
For Godot there are quite a few languages available that you can “script” in. There is the official and recommended GDScript and the popular for game engines these days C#. But if you’re like me and you like it low-level you could go for developing directly in C++. This is also the language that the engine was written in and you could in fact develop a module for it.
C++ through Godot Modules
Modules are one way to develop C++ for Godot. The engine itself is actually based on modularity and a lot of the functionality is split into modules. So they have also been made available for you to extend the engine. The modules though are limiting in one particular way. That is the fact that you need to compile the whole engine from scratch and you’re very much limited to their build system called scons.
This is not very robust or convenient for me and this is where GDExtension will take place.
So what is GDExtension
The GDExtension bindings allow you to build code that is meant to be used by Godot without recompiling. You essentially build your shared library in C++ (or any other language that has C++ bindings in fact) and then when you create your game you link to that extension. This is what I will be showing you how to do today.
GDExtension Project
So what I will do is a very simple CMake project where we’re going to build a shared library. This shared library will define a simple Movement class which will represent our custom node (script) that will handle simple movement. This will showcase the most basic things:
- Registering our GDExtension
- Registering our class as an extension to Node2D
- Overriding Godot functions like process
- Input handling
- Defining properties like speed
The CMakeList.txt files
I use CMake since it is the most popular and well-defined project configuration tool. I am aware that the Godot team prefers SCons but their decision is not the one that the general public would choose when starting a new project. CMake has the widest adoption by IDEs and companies and is actually really easy to work with. If you want to study more about CMake you can check out my course on the topic.
We will start by doing our root CMakeLists.txt file which should download the gdextension bindings and then link to them and add our library subdirectory CMakeLists.txt file. To do this I do the following setup:
cmake_minimum_required(VERSION 3.19)
project(gdextension-gameplay)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
include(FetchContent)
FetchContent_Declare(
GDExtension
GIT_REPOSITORY https://github.com/godotengine/godot-cpp.git
GIT_TAG godot-4.0-stable
)
FetchContent_MakeAvailable(GDExtension)
add_subdirectory(gameplay)
Update: You can also select a newer version by setting the GIT_TAG to something else for example “godot-4.1-stable”
I use FetchContent here to be able to link to a specific git tag which represents the bindings for the stable Godot 4.0 engine. If you’re reading this at a later date you could link to another tag. For this command to execute you would need to have git installed on your system. If you want to learn different ways to manage dependencies take a look at this post.
Now the way I have set this up is that we would have a subdirectory called gameplay which would define our library in the following way:
project(gameplay)
# Automatically pick up files added to src
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
# Add a dynamic library called gameplay - this builds gameplay.dll
add_library(${PROJECT_NAME} SHARED ${SOURCES})
target_include_directories(${PROJECT_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(${PROJECT_NAME} PUBLIC godot::cpp)
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/src" PREFIX src FILES ${SOURCES})
Library Registration
Now we need a new source file that will register our library. Take a note here that most of hte things in the following code must be as-is or it will not work properly and you will have a hard time debugging it later. First I create a file called <root>/gameplay/src/gdextension_registration.cpp . This file will contain the following code:
#include <godot_cpp/godot.hpp>
#include <godot_cpp/core/class_db.hpp>
void register_gameplay_types(godot::ModuleInitializationLevel p_level) {
if (p_level != godot::ModuleInitializationLevel::MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
// REGISTER CLASSES HERE LATER
}
void unregister_gameplay_types(godot::ModuleInitializationLevel p_level) {
// DO NOTHING
}
extern "C" {
GDExtensionBool GDE_EXPORT gameplay_library_init(const GDExtensionInterface *p_interface, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) {
godot::GDExtensionBinding::InitObject init_object(p_interface, p_library, r_initialization);
init_object.register_initializer(register_gameplay_types);
init_object.register_terminator(unregister_gameplay_types);
init_object.set_minimum_library_initialization_level(godot::ModuleInitializationLevel::MODULE_INITIALIZATION_LEVEL_SCENE);
return init_object.init();
}
}
Update: Since 4.1 the furst argument of the
gameplay_library_init
needs to be of the typeGDExtensionInterfaceGetProcAddress
and no longer a pointer. It is still passed on to theinit_object
.
It is very important to note the function gameplay_library_init – you can call it whatever you like but it must take the same parameters, return GDExtensionBool, Have GDE_EXPORT before the name, and most importantly have extern “C” around the whole function. Otherwise, this function creates your GDExtension init object and registers the above functions for adding types.
Defining & Implementing a Movement Class
The next step in our journey is to add two new files for defining our movement class – movement.h and movement.cpp. For simplicity, I will not explain this too much and just have the well-documented code pasted here:
#ifndef GDEXTENSION_GAMEPLAY_MOVEMENT_H_
#define GDEXTENSION_GAMEPLAY_MOVEMENT_H_
#include <cstdint>
#include <godot_cpp/godot.hpp>
#include <godot_cpp/classes/node2d.hpp>
namespace godot {
class Movement : public Node2D {
GDCLASS(Movement, Node2D)
public:
// Will be called by Godot when the class is registered
// Use this to add properties to your class
static void _bind_methods();
void _process(double_t delta) override;
// property setter
void set_speed(float_t speed) {
m_Speed = speed;
}
// property getter
[[nodiscard]] float_t get_speed() const {
return m_Speed;
}
private:
Vector2 m_Velocity;
// This will be a property later (look into _bind_methods)
float_t m_Speed = 500.0f;
void process_movement(double_t delta);
};
}
#endif //GDEXTENSION_GAMEPLAY_MOVEMENT_H_
Now this is our class definition. The main thing here is that we define the variables that we will use privately as well as private methods. We also extend one method that you might have seen in typical GDScript called _process. This method will be called on every frame with the passed time in seconds (portion of the second).
I also want to showcase how you can define a property that is editable through the Godot inspector. This property is speed. It doesn’t matter how we call it in our C++ code as you will notice later. We will pass to Godot a string how we want it to be called in the editor. So let’s see how this property is registered and how we handle the input events:
#include <movement.h>
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/classes/input.hpp>
namespace godot {
void Movement::_process(double_t delta) {
Node::_process(delta);
process_movement(delta);
}
void Movement::process_movement(double_t delta) {
m_Velocity = Vector2(0.0f, 0.0f);
Input& intutSingleton = *Input::get_singleton();
if (intutSingleton.is_action_pressed("ui_right")) {
m_Velocity.x += 1.0f;
}
if (intutSingleton.is_action_pressed("ui_left")) {
m_Velocity.x -= 1.0f;
}
if (intutSingleton.is_action_pressed("ui_up")) {
m_Velocity.y -= 1.0f;
}
if (intutSingleton.is_action_pressed("ui_down")) {
m_Velocity.y += 1.0f;
}
set_position(get_position() + (m_Velocity * m_Speed * delta));
}
void Movement::_bind_methods() {
UtilityFunctions::print("Binding methods");
ClassDB::bind_method(D_METHOD("get_speed"), &Movement::get_speed);
ClassDB::bind_method(D_METHOD("set_speed", "speed"), &Movement::set_speed);
ADD_GROUP("Movement", "movement_");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "movement_speed"), "set_speed", "get_speed");
}
}
You will notice this is a bit more verbose than GDScript. To define the property in _bind_methods you need to first bind both functions for getting and setting the value and then add the property. I also add the property to a group for nice styling.
For processing the movement I get the input manager which is a singleton in this case (I could also override the _input method for getting event callbacks) and then I check for some of the UI events. The idea is to move the node at some speed based on the passed frame time in the direction that was pressed.
Registering the class
Earlier in the register_types.h file we had a place where I commented // REGISTER CLASSES HERE LATER
. We now also need to add our movement class. This can be done through the following line godot::ClassDB:register_class<Movement>();
.
Building
Now the last part of this tutorial is to invoke the build. If you’re aware of how CMake works this will be very easy otherwise check my tutorial on the very basics of cmake. Basically we need to open up a terminal in the root directory of our project and run the command:
> cmake -S . -B ./build
Then to build it run:
> cmake --build ./build
After the build completes you will find your shared library inside “<root>/build/gameplay” and based on the platform you’re on it will be either libgameplay.so or gamplay.dll or something like that. Remember this because we will need this file in the next section. You need to copy it in your godot project.
Godot Project
Next thing is to download Godot 4 from the official website and run it. You will be greeted with the default project wizard and you can create whatever project you like. Make sure to choose a 2D scene for this tutorial though when it starts.
Next step is to open the folder where your godot project was created and we will create a new resource folder for our gdextension. I call this folder gameplay to match my gameplay library and inside I add a directory called bin and a file called gameplay.gdextension. Inside the bin folder you will paste your shared library (gameplay.dll). Last thing is to open up the gameplay.gdextension with a notepad application and write the follwing code:
[configuration]
entry_symbol = "gameplay_library_init"
[libraries]
windows.debug.x86_64 = "bin/gameplay.dll"
Update: Since 4.1 you need to add at least
compatibility = "4.1"
inside the[configuration]
section
Entry symbol here is that function I talked about when registering the library in the cpp code. That function that has to be wrapped in extern “C” you need to use now as and entry_symbol. The other thing is to set the libraries where you specify a path that is relative to the gameplay.gdextension file.
If you open up your Godot project freshly now you will be able to create a “Movement” node. I add the default icon on it and run the project. You can see how I set the scene up in the following video:
Conclusion
Well this is it! It isn’t as complicated as it seems in the beginning and with this knowledge you will be able to do some basic gameplay logic. This is close to how you would also work with Godot Modules which are directly embedded in the engine and this makes me very happy that GDExtension is here. It will allow us to make libraries for the Godot engine without actually having to recompile the whole engine.
This is how I managed to make it work with Godot 4.2.1 on Win11 with VS22. 1.compatibility_minimum = "4.1"
needs to be added under [configuration] in gameplay.gdextension. 4.1 is the minimum you can set. 2. Set the same GIT_TAG for FetchContent, otherwise you'll miss some definitions. 3. Add thegodot::ClassDB::register_class();
line after the// REGISTER CLASSES HERE LATER
line. 4. That requires you to include movement.h. 5. Change the 1st parameter of gameplay_library_init toGDExtensionInterfaceGetProcAddress p_get_proc_address
, and also change it in the next line in the constructor. For testing start a command prompt and start Godot console version from there, in order to get error messages in the console, without the console being closed immediately. This was the only way I could get usable error messages. Otherwise Godot just crashed without any logs. But I am totally new to Godot, so there might be simpler ways. Thank you for the tutorial. It was super useful to see how is this done with CMake.
Thanks for this guide. Thanks to Fee for the missing lines heads-up. I got this working with MSYS2/MINGW. Here's a few notes for anyone else trying the same. 1. When calling cmake add -G "MinGW Makefiles", this tells cmake to generate a build for MINGW 2. If you are using the Godot 4.1 or later tag, the GDE_EXPORT init function has changed slightly, refer to Godot docs example. 3. "lib" is generally prefixed to the dll when building with MINGW, so remember to include it in the .gdextension file. 4. When launching the extension, Godot will complain about missing dlls. These are found in the bin folder of MINGW, if they are copied to the same folder as your extension dll, it will work. There may be a way to statically link this code, but I have not tried yet.
So where it said//REGISTER CLASSES HERE LATER
it is actually important to register classes for your Movement node type to show up in the editor. e.g#include "movement.h" //or movement.hpp if you named it like that //other imports void register_gameplay_types(godot::ModuleInitializationLevel p_level) { if (p_level != godot::ModuleInitializationLevel::MODULE_INITIALIZATION_LEVEL_SCENE) { return; } // REGISTER CLASSES HERE godot::ClassDB::register_class(); }
I followed this entire tutorial (on Linux, so I had to tell it to locate libgameplay.so), but then the Movement did not show up as a node type in the editor. Any idea how to debug this?
Leave a comment