Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

Módulos personalizados en C++

Módulos

Godot permite extender el motor de forma modular. Se pueden crear nuevos módulos, y después, activarlos o desactivarlos. Esto permite añadir nuevas funcionalidades para el motor en todos los niveles sin modificar el núcleo, el cual se puede escindir para su uso y reutilizar en diferentes módulos.

Los módulos se encuentran en el subdirectorio modules/ del sistema de compilación. Por defecto, decenas de módulos están activados, como GDScript (pues sí, no es parte del motor base), el tiempo de ejecución de Mono, un módulo de expresiones regulares, entre otros. Se pueden crear y combinar tantos módulos nuevos como se quiera. El sistema de compilación SCons se encargará de ello transparentemente.

¿Para qué?

Mientras se recomienda que la mayor parte de un juego se escriba en scripts (ya que ahorra mucho tiempo), resulta posible usar C++ en su lugar. Añadir módulos C++ puede resultar útil en los siguientes escenarios:

  • Vincular una biblioteca externa a Godot (como PhysX, FMOD, etc.).

  • Optimizar partes críticas de un juego.

  • Añadir una nueva funcionalidad al motor y/o al editor.

  • Porting an existing game to Godot.

  • Escribe un nuevo juego entero en C++ porque no puedes vivir sin C++.

Crear un nuevo módulo

Antes de crear un módulo, asegúrate de descargar el código fuente de Godot y compilarlo.

Para crear un nuevo módulo, el primer paso es crear un directorio dentro de modules/. Si quieres mantener el módulo por separado, puedes obtener un VCS diferente en módulos para utilizarlo.

The example module will be called "summator" (godot/modules/summator). Inside we will create a summator class:

/* summator.h */

#ifndef SUMMATOR_H
#define SUMMATOR_H

#include "core/object/ref_counted.h"

class Summator : public RefCounted {
    GDCLASS(Summator, RefCounted);

    int count;

protected:
    static void _bind_methods();

public:
    void add(int p_value);
    void reset();
    int get_total() const;

    Summator();
};

#endif // SUMMATOR_H

Y después, el archivo cpp.

/* summator.cpp */

#include "summator.h"

void Summator::add(int p_value) {
    count += p_value;
}

void Summator::reset() {
    count = 0;
}

int Summator::get_total() const {
    return count;
}

void Summator::_bind_methods() {
    ClassDB::bind_method(D_METHOD("add", "value"), &Summator::add);
    ClassDB::bind_method(D_METHOD("reset"), &Summator::reset);
    ClassDB::bind_method(D_METHOD("get_total"), &Summator::get_total);
}

Summator::Summator() {
    count = 0;
}

Esta nueva clase necesita ser registrada de alguna forma, así que se necesitan dos archivos más para ser creada:

register_types.h
register_types.cpp

Importante

Los archivos anteriores deben estar en la carpeta de nivel superior de tu módulo (junto a tus archivos SCsub y config.py) para que el módulo se registre correctamente.

Estos archivos deberán contener lo siguiente:

/* register_types.h */

#include "modules/register_module_types.h"

void initialize_summator_module(ModuleInitializationLevel p_level);
void uninitialize_summator_module(ModuleInitializationLevel p_level);
/* yes, the word in the middle must be the same as the module folder name */
/* register_types.cpp */

#include "register_types.h"

#include "core/object/class_db.h"
#include "summator.h"

void initialize_summator_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
            return;
    }
    ClassDB::register_class<Summator>();
}

void uninitialize_summator_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
            return;
    }
   // Nothing to do here in this example.
}

Lo siguiente que necesitamos es crear un archivo ``SCsub``para que se compile este módulo:

# SCsub

Import('env')

env.add_source_files(env.modules_sources, "*.cpp") # Add all cpp files to the build

Con múltiples fuentes, también puede agregar cada archivo individualmente a una lista de cadenas de Python:

src_list = ["summator.cpp", "other.cpp", "etc.cpp"]
env.add_source_files(env.modules_sources, src_list)

Esto permite grandes posibilidades usando Python para construir la lista de archivos usando bucles y declaraciones lógicas. Eche un vistazo a algunos módulos que vienen con Godot por defecto para ver ejemplos.

Puedes agregar directorios de inclusión a las rutas de entorno para que sean vistos por el compilador:

env.Append(CPPPATH=["mylib/include"]) # this is a relative path
env.Append(CPPPATH=["#myotherlib/include"]) # this is an 'absolute' path

Si desea añadir banderas personalizadas de compilador al construir su módulo, primero necesita clonar `` env , para que no añada esas banderas a toda la compilación de Godot (lo cual puede causar errores). Ejemplo de `` SCsub con banderas personalizadas:

# SCsub

Import('env')

module_env = env.Clone()
module_env.add_source_files(env.modules_sources, "*.cpp")
# Append CCFLAGS flags for both C and C++ code.
module_env.Append(CCFLAGS=['-O2'])
# If you need to, you can:
# - Append CFLAGS for C code only.
# - Append CXXFLAGS for C++ code only.

And finally, the configuration file for the module, this is a Python script that must be named config.py:

# config.py

def can_build(env, platform):
    return True

def configure(env):
    pass

Se le pregunta al módulo si está bien construirlo para la plataforma especificada (en este caso, True significa que se construirá para cada plataforma).

Y eso es todo. ¡Espero que no haya sido demasiado complicado! Su módulo debería verse así:

godot/modules/summator/config.py
godot/modules/summator/summator.h
godot/modules/summator/summator.cpp
godot/modules/summator/register_types.h
godot/modules/summator/register_types.cpp
godot/modules/summator/SCsub

Luego puede comprimirlo y compartir el módulo con todos los demás. Cuando se construya para cada plataforma (instrucciones en las secciones anteriores), se incluirá su módulo.

Nota

Hay un límite de 5 parámetros en los módulos de C ++ para cosas como subclases. Este límite puede llegar a alcanzar 13 si se incluye el archivo de encabezado core / method_bind_ext.gen.inc.

Usar el módulo

Ahora puede usar su módulo recién creado desde cualquier script:

var s = Summator.new()
s.add(10)
s.add(20)
s.add(30)
print(s.get_total())
s.reset()

La salida será 60.

Ver también

El ejemplo Summator anterior es excelente para módulos personalizados pequeños, pero podría preguntarse qué sucede si desea usar una biblioteca externa más grande. Consulte Vinculación a bibliotecas externas para más detalles sobre la vinculación de librerías externas.

Advertencia

Si se debe acceder a su módulo desde el proyecto en ejecución (no solo desde el editor), también debe recompilar cada plantilla de exportación que planea usar, y después especificar la ruta a la plantilla personalizada en cada ajuste preestablecido de exportación. De lo contrario, obtendrá errores al ejecutar el proyecto, ya que el módulo no se compila en la plantilla de exportación. Consulte las páginas Compilación de para obtener más información.

Compilar un módulo externamente

Compilar un módulo implica mover las fuentes del módulo directamente al directorio modules/. Si bien esta es la forma más sencilla de compilar un módulo, hay un par de razones por las que esto podría no ser algo práctico:

  1. Tener que copiar manualmente las fuentes de los módulos cada vez que desee compilar el motor con o sin el módulo, o tomar los pasos adicionales necesarios para desactivar manualmente un módulo durante la compilación con una opción de compilación similar a module_summator_enabled=no. La creación de enlaces simbólicos también puede ser una solución, pero es posible que deba superar las restricciones del sistema operativo como necesitar el privilegio de enlace simbólico si lo hace a través de un script.

  2. Dependiendo de si tienes que trabajar con el código fuente del motor, los archivos del módulo agregados directamente a la carpeta modules/ cambian el árbol de trabajo hasta el punto en que usar un Sistema de Control de Versiones (como git) puede ser complicado, ya que necesitas asegurarte de que solo se hagan commits de los cambios relacionados con el motor mediante la filtración de los cambios.

Si sientes que la estructura independiente de módulos personalizados es necesaria, tomemos nuestro módulo "summator" y muévelo al directorio principal del motor:

mkdir ../modules
mv modules/summator ../modules

Para compilar el motor con nuestro módulo, proporcionemos la opción de compilación custom_modules, que acepta una lista separada por comas de rutas de directorios que contienen módulos C++ personalizados, similar a lo siguiente:

scons custom_modules=../modules

El sistema de compilación detectará automáticamente todos los módulos en el directorio ../modules y los compilará según corresponda, incluido nuestro módulo "summator".

Advertencia

Cualquier ruta proporcionada a custom_modules se convertirá internamente en una ruta absoluta como una forma de distinguir entre módulos personalizados e integrados. Esto significa que cosas como generar la documentación del módulo pueden depender de una estructura de ruta específica en tu máquina.

Customizing module types initialization

Modules can interact with other built-in engine classes during runtime and even affect the way core types are initialized. So far, we've been using register_summator_types as a way to bring in module classes to be available within the engine.

A crude order of the engine setup can be summarized as a list of the following type registration methods:

preregister_module_types();
preregister_server_types();
register_core_singletons();
register_server_types();
register_scene_types();
EditorNode::register_editor_types();
register_platform_apis();
register_module_types();
initialize_physics();
initialize_navigation_server();
register_server_singletons();
register_driver_types();
ScriptServer::init_languages();

Our Summator class is initialized during the register_module_types() call. Imagine that we need to satisfy some common module run-time dependency (like singletons), or allow us to override existing engine method callbacks before they can be assigned by the engine itself. In that case, we want to ensure that our module classes are registered before any other built-in type.

This is where we can define an optional preregister_summator_types() method which will be called before anything else during the preregister_module_types() engine setup stage.

We now need to add this method to register_types header and source files:

/* register_types.h */

#define MODULE_SUMMATOR_HAS_PREREGISTER
void preregister_summator_types();

void register_summator_types();
void unregister_summator_types();

Nota

Unlike other register methods, we have to explicitly define MODULE_SUMMATOR_HAS_PREREGISTER to let the build system know what relevant method calls to include at compile time. The module's name has to be converted to uppercase as well.

/* register_types.cpp */

#include "register_types.h"

#include "core/object/class_db.h"
#include "summator.h"

void preregister_summator_types() {
    // Called before any other core types are registered.
    // Nothing to do here in this example.
}

void register_summator_types() {
    ClassDB::register_class<Summator>();
}

void unregister_summator_types() {
   // Nothing to do here in this example.
}

Mejorando el sistema de compilación para el desarrollo

Advertencia

This shared library support is not designed to support distributing a module to other users without recompiling the engine. For that purpose, use a GDExtension instead.

Hasta ahora, hemos definido un SCsub limpio que nos permite agregar las fuentes de nuestro nuevo módulo como parte del binario de Godot.

Este enfoque estático es adecuado cuando queremos compilar una versión de lanzamiento de nuestro juego y deseamos que todos los módulos estén en un solo binario.

Sin embargo, el inconveniente es que cada cambio requiere una recompilación completa del juego. Aunque SCons puede detectar y recompilar solo el archivo que ha sido modificado, encontrar dichos archivos y finalmente vincular el binario final lleva mucho tiempo.

La solución para evitar ese costo es construir nuestro propio módulo como una biblioteca compartida que se cargará dinámicamente al iniciar el binario de nuestro juego.

# SCsub

Import('env')

sources = [
    "register_types.cpp",
    "summator.cpp"
]

# First, create a custom env for the shared library.
module_env = env.Clone()

# Position-independent code is required for a shared library.
module_env.Append(CCFLAGS=['-fPIC'])

# Don't inject Godot's dependencies into our shared library.
module_env['LIBS'] = []

# Define the shared library. By default, it would be built in the module's
# folder, however it's better to output it into `bin` next to the
# Godot binary.
shared_lib = module_env.SharedLibrary(target='#bin/summator', source=sources)

# Finally, notify the main build environment it now has our shared library
# as a new dependency.

# LIBPATH and LIBS need to be set on the real "env" (not the clone)
# to link the specified libraries to the Godot executable.

env.Append(LIBPATH=['#bin'])

# SCons wants the name of the library with it custom suffixes
# (e.g. ".linuxbsd.tools.64") but without the final ".so".
shared_lib_shim = shared_lib[0].name.rsplit('.', 1)[0]
env.Append(LIBS=[shared_lib_shim])

Una vez compilado, obtendremos un directorio bin que contiene tanto el ejecutable godot* como nuestra librería libsummator*.so. Sin embargo, dado que la librería .so no se encuentra en un directorio estándar (como /usr/lib), debemos ayudar a nuestro ejecutable a encontrarla durante la ejecución usando la variable de entorno LD_LIBRARY_PATH:

export LD_LIBRARY_PATH="$PWD/bin/"
./bin/godot*

Nota

Debes exportar la variable de entorno. De lo contrario, no podrás ejecutar tu proyecto desde el editor.

Además, sería conveniente poder seleccionar si queremos compilar nuestro módulo como una biblioteca compartida (para desarrollo) o como parte del binario de Godot (para distribución). Para hacer esto, podemos definir una bandera personalizada que se pasará a SCons usando el comando ARGUMENT:

# SCsub

Import('env')

sources = [
    "register_types.cpp",
    "summator.cpp"
]

module_env = env.Clone()
module_env.Append(CCFLAGS=['-O2'])

if ARGUMENTS.get('summator_shared', 'no') == 'yes':
    # Shared lib compilation
    module_env.Append(CCFLAGS=['-fPIC'])
    module_env['LIBS'] = []
    shared_lib = module_env.SharedLibrary(target='#bin/summator', source=sources)
    shared_lib_shim = shared_lib[0].name.rsplit('.', 1)[0]
    env.Append(LIBS=[shared_lib_shim])
    env.Append(LIBPATH=['#bin'])
else:
    # Static compilation
    module_env.add_source_files(env.modules_sources, sources)

Ahora, por defecto, el comando scons construirá nuestro módulo como parte del binario de Godot y como una biblioteca compartida cuando se le pase summator_shared=yes.

Finalmente, incluso puedes acelerar aún más la compilación especificando explícitamente tu módulo compartido como objetivo en el comando SCons. Por ejemplo:

scons summator_shared=yes platform=linuxbsd bin/libsummator.linuxbsd.tools.64.so

Escribiendo documentación personalizada

Writing documentation may seem like a boring task, but it is highly recommended to document your newly created module to make it easier for users to benefit from it. Not to mention that the code you've written one year ago may become indistinguishable from the code that was written by someone else, so be kind to your future self!

Hay varios pasos para configurar la documentación personalizada para el módulo:

  1. Crear un nuevo directorio en la raíz del módulo. El nombre del directorio puede ser cualquier cosa, pero usaremos el nombre doc_classes en esta sección.

  2. Ahora necesitamos editar config.py, agrega el siguiente código:

    def get_doc_path():
        return "doc_classes"
    
    def get_doc_classes():
        return [
            "Summator",
        ]
    

La función get_doc_path() se utiliza por el sistema de compilación para determinar la ubicación de la documentación. En este caso, estará ubicada en el directorio modules/summator/doc_classes. Si no defines esto, la ruta de la documentación para tu módulo se establecerá por defecto en el directorio principal doc/classes.

El método get_doc_classes() es necesario para que el sistema de compilación sepa qué clases registradas pertenecen al módulo. Debes listar todas tus clases aquí. Las clases que no listes se ubicarán en el directorio principal doc/classes.

Truco

Puedes usar Git para verificar si has omitido algunas de tus clases al revisar los archivos no rastreados con git status. Por ejemplo:

user@host:~/godot$ git status

Ejemplo de salida:

Untracked files:
    (use "git add <file>..." to include in what will be committed)

    doc/classes/MyClass2D.xml
    doc/classes/MyClass4D.xml
    doc/classes/MyClass5D.xml
    doc/classes/MyClass6D.xml
    ...
  1. Ahora podemos generar la documentación:

Podemos hacer esto ejecutando la herramienta de documentación de Godot, es decir, godot --doctool <ruta>, lo que volcará la referencia de la API del motor en el directorio proporcionado <ruta> en formato XML.

En nuestro caso, lo apuntaremos al directorio raíz del repositorio clonado. También puedes apuntarlo a otra carpeta y simplemente copiar los archivos que necesitas.

Ejecute el comando:

user@host:~/godot$ ./bin/<godot_binary> --doctool .

Ahora, si vas a la carpeta godot/modules/summator/doc_classes, verás que contiene un archivo llamado Summator.xml, o cualquier otra clase que hayas referenciado en tu función get_doc_classes.

Edit the file(s) following Guías de referencia de clases and recompile the engine.

Una vez que el proceso de compilación haya finalizado, la documentación estará disponible en el sistema de documentación integrado del motor.

Para mantener la documentación actualizada, simplemente tendrás que modificar uno de los archivos XML y recompilar el motor a partir de ahora.

Si cambias la API de tu módulo, también puedes extraer nuevamente la documentación; esta contendrá las cosas que agregaste previamente. Por supuesto, si apuntas la extracción a tu carpeta de Godot, asegúrate de no perder el trabajo al extraer documentación más antigua de una versión anterior del motor sobre las más recientes.

Ten en cuenta que si no tienes derechos de escritura en la <ruta> que proporcionaste, es posible que encuentres un error similar al siguiente:

ERROR: Can't write doc file: docs/doc/classes/@GDScript.xml
   At: editor/doc/doc_data.cpp:956

Writing custom unit tests

It's possible to write self-contained unit tests as part of a C++ module. If you are not familiar with the unit testing process in Godot yet, please refer to Unit testing.

The procedure is the following:

  1. Create a new directory named tests/ under your module's root:

cd modules/summator
mkdir tests
cd tests
  1. Create a new test suite: test_summator.h. The header must be prefixed with test_ so that the build system can collect it and include it as part of the tests/test_main.cpp where the tests are run.

  2. Write some test cases. Here's an example:

// test_summator.h
#ifndef TEST_SUMMATOR_H
#define TEST_SUMMATOR_H

#include "tests/test_macros.h"

#include "modules/summator/summator.h"

namespace TestSummator {

TEST_CASE("[Modules][Summator] Adding numbers") {
    Ref<Summator> s = memnew(Summator);
    CHECK(s->get_total() == 0);

    s->add(10);
    CHECK(s->get_total() == 10);

    s->add(20);
    CHECK(s->get_total() == 30);

    s->add(30);
    CHECK(s->get_total() == 60);

    s->reset();
    CHECK(s->get_total() == 0);
}

} // namespace TestSummator

#endif // TEST_SUMMATOR_H
  1. Compile the engine with scons tests=yes, and run the tests with the following command:

./bin/<godot_binary> --test --source-file="*test_summator*" --success

You should see the passing assertions now.

Añadir iconos de editor personalizados

De manera similar a cómo puedes escribir documentación autocontenida dentro de un módulo, también puedes crear tus propios iconos personalizados para que aparezcan en el editor junto a las clases.

Para obtener información detallada sobre el proceso de creación de iconos de editor para integrarlos en el motor, consulta primero la sección Iconos del editor.

Una vez que hayas creado tus iconos, sigue los siguientes pasos:

  1. Crea un nuevo directorio en la raíz del módulo llamado icons. Esta es la ubicación predeterminada para que el motor busque los iconos del editor del módulo.

  2. Mueve tus iconos svg recién creados (optimizados o no) a esa carpeta.

  3. Vuelve a compilar el motor y ejecuta el editor. Ahora los iconos aparecerán en la interfaz del editor donde corresponda.

Si deseas almacenar tus iconos en algún otro lugar dentro de tu módulo, agrega el siguiente fragmento de código a config.py para anular la ruta predeterminada:

def get_icons_path():
    return "path/to/icons"

Resumiendo

Recordar a:

  • Usa la macro GDCLASS para herencia, así Godot puede encapsularla.

  • Use _bind_methods to bind your functions to scripting, and to allow them to work as callbacks for signals.

  • Avoid multiple inheritance for classes exposed to Godot, as GDCLASS doesn't support this. You can still use multiple inheritance in your own classes as long as they're not exposed to Godot's scripting API.

Pero eso no es todo, dependiendo de lo que hagas, serás recibido con algunas (espero que positivas) sorpresas.

  • If you inherit from Node (or any derived node type, such as Sprite2D), your new class will appear in the editor, in the inheritance tree in the "Add Node" dialog.

  • Si hereda de: ref: class_Resource, aparecerá en la lista de recursos y todas las propiedades expuestas se pueden serializar cuando se guardan / cargan.

  • Con esta misma lógica, usted puede ampliar el Editor y casi cualquier área del motor.