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.
Modules are located in the modules/ subdirectory of the build system.
By default, dozens of modules are enabled, such as GDScript (which, yes,
is not part of the base engine), GridMap support, a regular expressions
module, and others. As many new modules as desired can be
created and combined. The SCons build system will take care of it
transparently.
¿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++.
Nota
While it is possible to use modules for custom game logic, GDExtension is generally more suited as it doesn't require recompiling the engine after every code change.
C++ modules are mainly needed when GDExtension doesn't suffice and deeper engine integration is required.
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:
#pragma once
#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();
};
Y después, el archivo 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:
#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 */
#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.
}
Next, we need to create an SCsub file so the build system compiles
this module:
# 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 strings 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 deseas 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:
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.
Usar el módulo
Ahora puedes usar tu 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ías preguntarte qué sucede si deseas usar una biblioteca externa más grande. Véase Vinculación a bibliotecas externas para más detalles sobre la vinculación de bibliotecas externas.
Advertencia
Si se debe acceder a tu 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ás errores al ejecutar el proyecto, ya que el módulo no se compila en la plantilla de exportación. Consulta 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:
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.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 (comogit) 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 runtime 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:
#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.
#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.
}
Escribir 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:
Crear un nuevo directorio en la raíz del módulo. El nombre del directorio puede ser cualquier cosa, pero usaremos el nombre
doc_classesen esta sección.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
You can use Git to check if you have missed some of your classes by checking the
untracked files with git status. For example:
git status
Example output:
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
...
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.
Ejecuta el comando:
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 the class reference primer 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:
Create a new directory named
tests/under your module's root:
cd modules/summator
mkdir tests
cd tests
Create a new test suite:
test_summator.h. The header must be prefixed withtest_so that the build system can collect it and include it as part of thetests/test_main.cppwhere the tests are run.Write some test cases. Here's an example:
#pragma once
#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
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:
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.Mueve tus iconos
svgrecién creados (optimizados o no) a esa carpeta.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
GDCLASSpara herencia, así Godot puede encapsularla.Use
_bind_methodsto bind your functions to scripting, and to allow them to work as callbacks for signals.Avoid multiple inheritance for classes exposed to Godot, as
GDCLASSdoesn'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.
If you inherit from Resource, it will appear in the resource list, and all the exposed properties can be serialized when saved/loaded.
Con esta misma lógica, puedes ampliar el Editor y casi cualquier área del motor.