A custom C++ module
Complete files, class registration, build integration, export and C# SDK limitations.
A C++ module is compiled into the engine. Use it to change native subsystems or register classes that require internal headers. It is not a loadable library for the installed editor. Use C# for ordinary gameplay logic.
Below is the complete file set for the teaching module demo_counter. It registers DemoCounter, stores an integer and exposes set_value / get_value. The C++ portion was reviewed against source but was not compiled or executed: this check did not rebuild the working engine.
Module directory
Build paths are relative to Source. Place these six files in modules/demo_counter/ in a separate checkout:
modules/demo_counter/
config.py
SCsub
register_types.h
register_types.cpp
demo_counter.h
demo_counter.cppDiscovery requires at least register_types.h, SCsub and config.py. A C++ file alone does not connect a module. The directory name supplies the option module_demo_counter_enabled and initialization function names; choose a unique name suitable for a C++ identifier.
Class and method binding
demo_counter.h:
#pragma once
#include "core/object/ref_counted.h"
class DemoCounter : public RefCounted {
RECKFELL_CLASS(DemoCounter, RefCounted);
int value = 0;
protected:
static void _bind_methods();
public:
void set_value(int p_value) { value = p_value; }
int get_value() const { return value; }
};demo_counter.cpp:
#include "demo_counter.h"
#include "core/object/class_db.h"
void DemoCounter::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_value", "value"), &DemoCounter::set_value);
ClassDB::bind_method(D_METHOD("get_value"), &DemoCounter::get_value);
}RECKFELL_CLASS is the current macro for classes registered in ClassDB. _bind_methods() exposes methods through the engine’s call system. A public C++ method alone is not available through that system. This minimal example adds no Inspector property, value serialization or thread safety.
Initialization and build configuration
register_types.h:
#pragma once
#include "modules/register_module_types.h"
void initialize_demo_counter_module(ModuleInitializationLevel p_level);
void uninitialize_demo_counter_module(ModuleInitializationLevel p_level);register_types.cpp:
#include "register_types.h"
#include "core/object/class_db.h"
#include "demo_counter.h"
void initialize_demo_counter_module(ModuleInitializationLevel p_level) {
if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) {
RECKFELL_REGISTER_CLASS(DemoCounter);
}
}
void uninitialize_demo_counter_module(ModuleInitializationLevel p_level) {
// No module-owned global resources to release.
}The generator calls initialize_demo_counter_module(p_level) and uninitialize_demo_counter_module(p_level). This example registers its class only at MODULE_INITIALIZATION_LEVEL_SCENE. Do not use the older register_demo_counter_types() functions or MODULE_DEMO_COUNTER_HAS_PREREGISTER scheme: the current generator does not call them.
Do not manually delete the registered class during shutdown. Use uninitialize_* for resources owned by your module: finish background work and release global objects if you actually created them. This example owns no such resources.
config.py:
def can_build(env, platform):
return True
def configure(env):
passcan_build(env, platform) checks dependencies for the selected platform; this example’s True means there are no additional restrictions. It does not prove successful compilation on every OS. Where needed, declare module dependencies through env.module_add_dependencies(...) in can_build.
SCsub:
Import("env")
Import("env_modules")
module_env = env_modules.Clone()
module_env.add_source_files(env.modules_sources, "*.cpp")Cloning env_modules preserves the module environment and isolates extra compiler flags. Objects are added to env.modules_sources, which the build uses to create the current module’s library. Do not change global env for one module’s flags. A SCons path beginning with # is rooted at the build tree, not the disk root.
Keeping sources outside the engine
Instead of copying into Source/modules/, keep the module outside the engine and pass its directory through custom_modules. This option takes comma-separated paths; custom_modules_recursive defaults to enabled. A path may point to the module itself or its parent. Paths become absolute, must exist and must not refer to the built-in modules directory.
An external module with a matching name replaces the detected built-in module. Avoid collisions unless replacement is intentional. Disable this example with module_demo_counter_enabled=no; changing the configuration requires a new binary.
Editor, export and C#
Start with the build prerequisites. Test the new build independently of the installed engine. If the game needs your class, build the corresponding export templates too: the stock template does not include it. Select your templates in the export preset.
Registering a class in ClassDB does not add a DemoCounter type to the installed C# SDK. Static C# wrappers and glue must match the new build; see building with C# for the workflow and known limitations. Do not replace an arbitrary single DLL in a working SDK.
After building, check class registration, instance creation, writing 42, reading 42, releasing the instance and repeating the test in an exported application. Check the new binary and template, rather than the working editor without your module.
If the class is missing
| Symptom | Check |
|---|---|
| Module not discovered | The three required files, directory name, custom_modules path and search depth. |
| Discovered but not built | module_demo_counter_enabled, can_build and dependencies. |
| Initialization linker error | Directory name, both initialize_* / uninitialize_* functions and inclusion of register_types.cpp. |
| Class exists but method is missing | ClassDB::bind_method, exact call name and the running binary. |
| Works in the editor but not the export | A matching custom template containing the module, selected in the preset. |
C# cannot find DemoCounter | Wrappers and glue matching the new build; native registration alone does not change the SDK. |
Module discovery and initialization source generation were exercised on isolated fixtures. Native compilation, class behavior, export and managed wrappers remain separate checks after building.