GDNative C++ ejemplo

Introducción

Este tutorial usa como base la información dada en el de GDNative C, así que recomendamos que leas ese tutorial primero.

Las ligaduras de C++ para GDNative son construidas en base de la API de NativeScript GDNative y proveen una manera más agradable de «extender» nodos en Godot usando C++. Lo que equivale a escribir scripts en GDScript, pero con C++ en su lugar.

Puedes descargar el ejemplo completo que crearemos en este tutorial en Github.

Configurando el proyecto

Hay algunos requisitos previos que necesitarás:

  • un ejecutable de Godot3.x,

  • un compilador de C++,

  • SCons como una herramienta de compilación,

  • una copia del repositorio godot-cpp.

También ve :ref:`Compilando<toc-devel-compiling>`ya que las herramientas de compilación son idénticas a las que necesitas para compilar Godot desde su código fuente.

Puedes descargar estos depósitos desde Github o dejar que Git lo haga por ti. Hay que hacer nota que estos depósitos tienen varias ramas hoy día para diferentes versiones de Godot. Los módulos escritos para versiones anteriores de GDNative funcionarán en versiones más nuevas (con la excepción de una ruptura en las interfaces de ARVR entre las versiones 3.0 y 3.1) pero no al revés, así que asegúrate de descargar la rama apropiada. También date cuenta de que la versión que uses para generar el api.json se convertirá en tu versión mínima.

Nota

GDExtension ha sido fusionado en la rama master de godot-cpp, pero solo es compatible con la próxima versión Godot 4.0. Por lo tanto, necesitas usar la rama 3.x de godot-cpp para usar GDNative y seguir este ejemplo.

This tutorial abarca únicamente GDNative en Godot 3.x, no GDExtension en Godot 4.0.

Si estás creando versiones de tu proyecto por medio de Git, es una buena idea agregarlos como submódulos de Git:

mkdir gdnative_cpp_example
cd gdnative_cpp_example
git init
git submodule add -b 3.x https://github.com/godotengine/godot-cpp
cd godot-cpp
git submodule update --init

Si decides sólo descargar los depósitos o clonarlos en tu carpeta de proyecto, asegúrate de mantener la estructura del proyecto idéntica al descrito aquí, ya que gran parte del código que enseñaremos asume que el proyecto seguirá esa estructura.

Asegúrate de clonar recursivamente para hacer Pull en ambos depósitos:

mkdir gdnative_cpp_example
cd gdnative_cpp_example
git clone --recursive -b 3.x https://github.com/godotengine/godot-cpp

Nota

Ahora, godot-cpp incluye godot-headers como un submódulo anidado. Si los has descargado manualmente, asegúrate de colocar godot-headers dentro de la carpeta godot-cpp.

No es necesario hacerlo de esta manera, pero hemos encontrado que es la forma más fácil de gestionarlo. Si decides descargar los repositorios o clonarlos en tu carpeta, asegúrate de mantener la estructura de carpetas tal como la hemos configurado aquí. Gran parte del código que mostraremos aquí asume que el proyecto tiene esta disposición.

Si clonaste el ejemplo del enlace especificado en la introducción, los submódulos no son inicializados automáticamente. Tendrás que ejecutar los siguientes comandos:

cd gdnative_cpp_example
git submodule update --init --recursive

Esto clonará ambos depósitos en tu carpeta de proyecto.

Compilando las ligaduras de C++

Ya que descargamos los prerequisitos, es hora de compilar las ligaduras de C++.

El depósito contiene una copia de los metadatos de la versión actual de Godot, pero si quieres compilar las ligaduras para una versión más nueva de Godot, simplemente llama el ejecutable de Godot:

godot --gdnative-generate-json-api api.json

Coloca el archivo api.json resultante en la carpeta del proyecto y agrega use_custom_apt_file=yes custom_api_file=../api.json al comando scons inferior.

Para generar y compilar las ligaduras, usa este comando (remplazando <platform> con windows, linux o osx dependiendo de tu sistema operativo):

Para acelerar la compilación, agrega -jN al final del comando SCons, substituyendo N con la cantidad de hilos del CPU que tiene tu sistema. El ejemplo siguiente usa 4 hilos.

cd godot-cpp
scons platform=<platform> generate_bindings=yes -j4
cd ..

Este paso tomará tiempo en finalizar, una vez completo, deberías de tener unas bibliotecas estáticas que pueden ser compiladas dentro de tu proyecto guardado en godot-cpp/bin/.

Nota

Es posible que necesites agregar bits=64 al comando en Windows o Linux.

Creación de un simple plugin

Es hora de hacer un plugin real. Iniciaremos creando un proyecto de Godot vacío en el que colocaremos unos cuantos archivos.

Abre Godot y crea un nuevo proyecto. Para este ejemplo, lo colocaremos en una carpeta llamada demo dentro de la estructura de la carpeta de nuestro módulo de GDNative.

En nuestro proyecto demo, crearemos una escena que contendrá un Nodo llamado "Main" y lo guardaremos como main.tscn. Volveremos a esto más tarde.

De vuelta en la carpeta del módulo GDNative de alto nivel, crearemos una sub-carpeta llamada src en la que colocaremos nuestros archivos fuente.

Ahora deberías tener los directorios demo, godot-cpp, godot-headers y src en tu módulo GDNative.

En la carpeta src comenzaremos con la creación de nuestro archivo principal para el nodo GDNative que estaremos creando. Lo llamaremos gdexample.h:

#ifndef GDEXAMPLE_H
#define GDEXAMPLE_H

#include <Godot.hpp>
#include <Sprite.hpp>

namespace godot {

class GDExample : public Sprite {
    GODOT_CLASS(GDExample, Sprite)

private:
    float time_passed;

public:
    static void _register_methods();

    GDExample();
    ~GDExample();

    void _init(); // our initializer called by Godot

    void _process(float delta);
};

}

#endif

Hay algunas cosas a destacar de lo anterior. Estamos incluyendo Godot.hpp el cuál contiene todas nuestras definiciones básicas. Después de esto, incluimos Sprite.hpp el cual contiene enlaces a la clase Sprite.

Usamos el espacio de nombre (namespace) godot, ya que todo en GDNative es definido dentro de este espacio de nombre.

Entonces tenemos nuestra definición de clase, el cual se hereda de nuestro Sprite a través de una clase contenedor. Veremos algunos efectos secundarios de esto más adelante. El macro GODOT_CLASS establece algunas cosas internas por nosotros.

Después de eso declaramos una sola variable de miembro llamada time_passed.

En el siguiente bloque, definimos nuestros métodos, obviamente tenemos nuestro constructor y destructor ya definidos, pero hay otras dos funciones que probablemente les resultarán familiares a algunos, y un nuevo método.

La primera es _register_methods, que es una función estática que Godot llamará para averiguar qué métodos pueden ser llamados en nuestro NativeScript y cuáles propiedades expone. La segunda es nuestra función _process, la cual funcionará exactamente igual que la función _process de GDScript a la que estamos acostumbrados. La tercera es nuestra función _init, la cual es llamada una vez que Godot ha creado nuestro objeto adecuadamente. Es necesario que exista incluso si no tiene código adentro.

Implementemos nuestras funciones creando nuestro archivo gdexample.cpp`:

#include "gdexample.h"

using namespace godot;

void GDExample::_register_methods() {
    register_method("_process", &GDExample::_process);
}

GDExample::GDExample() {
}

GDExample::~GDExample() {
    // add your cleanup here
}

void GDExample::_init() {
    // initialize any variables here
    time_passed = 0.0;
}

void GDExample::_process(float delta) {
    time_passed += delta;

    Vector2 new_position = Vector2(10.0 + (10.0 * sin(time_passed * 2.0)), 10.0 + (10.0 * cos(time_passed * 1.5)));

    set_position(new_position);
}

Esto debería ser sencillo. Implementamos cada método de nuestra clase que definimos en nuestro archivo de cabecera. Date cuenta que la llamada de register_method necesita exponer al método _process, de no ser así, Godot no podrá utilizarlo. Sin embargo, no necesitamos notificar a Godot sobre nuestras funciones constructoras, destructoras o _init.

El otro método relevante es nuestra función _process, que simplemente realiza un seguimiento de cuánto tiempo ha transcurrido y calcula una nueva posición para nuestro sprite utilizando las funciones seno y coseno. Lo que destaca es la llamada a owner->set_position para llamar a uno de los métodos incorporados de nuestro Sprite. Esto se debe a que nuestra clase es una clase contenedor; owner apunta al nodo Sprite real con el que se relaciona nuestro script.

Todavía hay un archivo de C++ más que necesitamos; lo llamaremos gdlibrary.cpp. Nuestro plugin de GDNative puede contener múltiples NativeScripts, cada uno con su propia cabecera y archivo fuente como lo hemos implementado en GDExample más arriba. Lo que necesitamos ahora es pequeño fragmento de código que le diga a Godot acerca de todos los NativeScripts en nuestro plugin.

#include "gdexample.h"

extern "C" void GDN_EXPORT godot_gdnative_init(godot_gdnative_init_options *o) {
    godot::Godot::gdnative_init(o);
}

extern "C" void GDN_EXPORT godot_gdnative_terminate(godot_gdnative_terminate_options *o) {
    godot::Godot::gdnative_terminate(o);
}

extern "C" void GDN_EXPORT godot_nativescript_init(void *handle) {
    godot::Godot::nativescript_init(handle);

    godot::register_class<godot::GDExample>();
}

Nótese que no estamos usando godot como nombre aquí, ya que las tres funciones implementadas aquí necesitan estar definidas sin un namespace.

Las funciones godot_gdnative_init y godot_gdnative_terminate son llamadas respectivamente cuando Godot carga nuestro plugin y cuando lo descarga. Todo lo que tenemos que hacemos aquí es analizar a través de estas funciones en nuestro módulo de enlaces para inicializarlo, pero podrías tener que configurar más cosas dependiendo de tus necesidades.

La función importante aquí es la tercera llamada godot_nativescript_init. Primero llamamos una función en nuestra librería de enlaces que hace su trabajo habitual, llamamos a la función `register_class por cada una de nuestras clases en la librería.

Compilando el plugin

No podemos escribir fácilmente un archivo SConstruct a mano que SCons pueda usar para construir. Por propósito de este ejemplo, sólo :descarga: este archivo SConstruct <files/cpp_example/SConstruct> que hemos preparado. Cubriremos un ejemplo más personalizable y detallado de cómo usar estos archivos en próximos tutoriales.

Nota

Este archivo SConstruct ha sido escrito para ser usado con la ultima versión de godot-cpp master, puedes necesitar hacer algunos cambios para usarlo en versiones más antiguas o revisar en la documentación de Godot 3.0 acerca del archivo SConstruct.

Una vez que hayas descargado el archivo SConstruct, colócalo en la carpeta de tu módulo GDNative junto a godot-cpp, godot-headers y demo, luego ejecuta:

scons platform=<platform>

Ahora deberías poder encontrar el módulo en demo/bin/<platform>.

Nota

Aquí hemos compilado tanto godot-cpp y nuestro gdxample como versiones de debug. Para versiones optimizadas, deberías compilarlos usando el interruptor``target=realease``.

Usando el módulo de GDNative

Antes de que podamos volver a Godot, necesitamos crear dos archivos más en demo/bin/. Ambos pueden ser creados usando el editor de Godot, pero sera más crearlos directamente.

El primer archivo es el que le permite a Godot saber qué bibliotecas dinámicas deben cargarse para cada plataforma y se llama gdexample.gdnlib.

[general]

singleton=false
load_once=true
symbol_prefix="godot_"
reloadable=false

[entry]

X11.64="res://bin/x11/libgdexample.so"
Windows.64="res://bin/win64/libgdexample.dll"
OSX.64="res://bin/osx/libgdexample.dylib"

[dependencies]

X11.64=[]
Windows.64=[]
OSX.64=[]

Este archivo contiene una sección general que controla cómo se carga el módulo. También contiene una sección de prefijo que debe dejarse como godot_ por ahora. Si lo cambias, deberás cambiar el nombre de varias funciones que se utilizan como puntos de entrada. Esto se agregó para la plataforma de iPhone porque no permite el despliegue de bibliotecas dinámicas, pero los módulos de GDNative están vinculados estáticamente.

La sección entry es la parte importante: le indica a Godot la ubicación de la biblioteca dinámica en el sistema de archivos del proyecto para cada plataforma admitida. Esto también resultará en que solo ese archivo se exporte cuando exportes el proyecto, lo que significa que el paquete de datos no contendrá bibliotecas incompatibles con la plataforma objetivo.

Por último, la sección dependencies te permite nombrar bibliotecas dinámicas adicionales que también deben incluirse. Esto es importante cuando tu complemento GDNative implementa la biblioteca de otra persona y requiere que suministres una biblioteca dinámica de terceros con tu proyecto.

Si haces doble clic en el archivo gdexample.gdnlib dentro de Godot, verás que hay muchas más opciones para configurar:

../../../_images/gdnative_library.png

El segundo archivo que necesitamos crear es un archivo utilizado por cada NativeScript que hemos agregado a nuestro complemento. Lo nombraremos gdexample.gdns para nuestro NativeScript gdexample.

[gd_resource type="NativeScript" load_steps=2 format=2]

[ext_resource path="res://bin/gdexample.gdnlib" type="GDNativeLibrary" id=1]

[resource]

resource_name = "gdexample"
class_name = "GDExample"
library = ExtResource( 1 )

Esto es un recurso estándar de Godot; podrías crearlo directamente en tu escena, pero guardarlos en un archivo facilita mucho su reutilización en otros lugares. Este recurso apunta a nuestro archivo gdnlib, para que Godot pueda saber qué biblioteca dinámica contiene nuestro NativeScript. También define el class_name que identifica el NativeScript en nuestro complemento que queremos usar.

Es hora de volver a Godot. Cargamos la escena principal que creamos al principio y ahora agregamos un Sprite a nuestra escena:

../../../_images/gdnative_cpp_nodes.png

Vamos a asignar el logotipo de Godot a este sprite como nuestra textura, deshabilitar la propiedad centered y arrastrar nuestro archivo gdexample.gdns sobre la propiedad script del sprite:

../../../_images/gdnative_cpp_sprite.png

Finalmente, estamos listos para ejecutar el proyecto:

../../../_images/gdnative_cpp_animated.gif

Agregando propiedades

GDScript te permite agregar propiedades a tu script utilizando la palabra clave export. En GDNative, debes registrar las propiedades y existen dos formas de hacerlo. Puedes enlazar directamente a un miembro o utilizar funciones setter y getter.

Nota

Hay una tercera opción, al igual que en GDScript, puedes implementar directamente los métodos _get_property_list, _get y _set de un objeto, pero esto va mucho más allá del alcance de este tutorial. Es una opción más avanzada y compleja que puede ser útil en casos específicos, pero no la cubriremos aquí.

Examinaremos ambos, comenzando con la vinculación directa. Vamos a agregar una propiedad que nos permita controlar la amplitud de nuestra onda.

En nuestro archivo gdexample.h, simplemente necesitamos agregar una variable miembro de la siguiente manera:

...
private:
    float time_passed;
    float amplitude;
...

En nuestro archivo gdexample.cpp, necesitaremos realizar una serie de cambios. Solo mostraremos los métodos que terminamos modificando, sin eliminar las líneas que estamos omitiendo:

void GDExample::_register_methods() {
    register_method("_process", &GDExample::_process);
    register_property<GDExample, float>("amplitude", &GDExample::amplitude, 10.0);
}

void GDExample::_init() {
    // initialize any variables here
    time_passed = 0.0;
    amplitude = 10.0;
}

void GDExample::_process(float delta) {
    time_passed += delta;

    Vector2 new_position = Vector2(
        amplitude + (amplitude * sin(time_passed * 2.0)),
        amplitude + (amplitude * cos(time_passed * 1.5))
    );

    set_position(new_position);
}

Una vez que compiles el módulo con estos cambios implementados, verás que se ha agregado una propiedad a nuestra interfaz. Ahora puedes cambiar esta propiedad y, cuando ejecutes tu proyecto, notarás que el ícono de Godot se desplaza a lo largo de una figura más grande.

Nota

La propiedad reloadable en el archivo gdexample.gdnlib debe establecerse en true para que el editor de Godot pueda detectar automáticamente la nueva propiedad agregada.

Sin embargo, esta configuración debe utilizarse con precaución, especialmente cuando se utilizan clases de herramientas (tool classes), ya que el editor podría mantener objetos que tienen instancias de scripts adjuntas a ellos que son administradas por una biblioteca de GDNative.

Hagamos lo mismo, pero para la velocidad de nuestra animación y usemos una función setter y getter. Nuestro archivo de encabezado gdexample.h solo necesita unas pocas líneas más de código:

...
    float amplitude;
    float speed;
...
    void _process(float delta);
    void set_speed(float p_speed);
    float get_speed();
...

Esto requiere algunos cambios adicionales en nuestro archivo gdexample.cpp. Nuevamente, solo mostraremos los métodos que han cambiado, así que no eliminaré nada de lo que estamos omitiendo:

void GDExample::_register_methods() {
    register_method("_process", &GDExample::_process);
    register_property<GDExample, float>("amplitude", &GDExample::amplitude, 10.0);
    register_property<GDExample, float>("speed", &GDExample::set_speed, &GDExample::get_speed, 1.0);
}

void GDExample::_init() {
    // initialize any variables here
    time_passed = 0.0;
    amplitude = 10.0;
    speed = 1.0;
}

void GDExample::_process(float delta) {
    time_passed += speed * delta;

    Vector2 new_position = Vector2(
        amplitude + (amplitude * sin(time_passed * 2.0)),
        amplitude + (amplitude * cos(time_passed * 1.5))
    );

    set_position(new_position);
}

void GDExample::set_speed(float p_speed) {
    speed = p_speed;
}

float GDExample::get_speed() {
    return speed;
}

Ahora, cuando el proyecto se compila, veremos otra propiedad llamada speed. Cambiar su valor hará que la animación vaya más rápido o más lento.

Para este ejemplo, no hay una ventaja evidente de utilizar un setter y getter. Una buena razón para usar un setter sería si quisieras reaccionar ante el cambio de la variable. Si no necesitas hacer algo así, simplemente vincular la variable es suficiente.

Los getters y setters se vuelven mucho más útiles en escenarios más complejos donde necesitas tomar decisiones adicionales basadas en el estado de tu objeto.

Nota

Para simplificar, hemos omitido los parámetros opcionales en la llamada al método register_property<class, type>. Estos parámetros son rpc_mode, usage, hint y hint_string. Estos se pueden utilizar para configurar aún más cómo se muestran y establecen las propiedades en el lado de Godot.

Los compiladores de C++ modernos pueden inferir la clase y el tipo de variable, lo que te permite omitir la parte <GDExample, float> de nuestro método register_property. Sin embargo, hemos tenido experiencias mixtas con esto.

Señales

Por último, pero no menos importante, las señales (signals) también funcionan completamente en GDNative. Para que tu módulo reaccione a una señal emitida por otro objeto, debes llamar a connect en ese objeto. No se nos ocurre un buen ejemplo para nuestro icono de Godot tembloroso; necesitaríamos mostrar un ejemplo mucho más completo.

Esta es la sintaxis requerida:

some_other_node->connect("the_signal", this, "my_method");

Ten en cuenta que solo puedes llamar a my_method si previamente lo has registrado en tu método _register_methods.

Enviar señales desde tu objeto es más común. Para nuestro icono de Godot tembloroso, haremos algo simple solo para mostrar cómo funciona. Emitiremos una señal cada vez que pase un segundo y pasaremos la nueva ubicación a lo largo de ella.

En nuestro archivo de encabezado gdexample.h, necesitamos definir un nuevo miembro llamado time_emit:

...
    float time_passed;
    float time_emit;
    float amplitude;
...

Esta vez, los cambios en gdexample.cpp son más elaborados. Primero, necesitarás establecer time_emit = 0.0; ya sea en nuestro método _init o en nuestro constructor. Veremos los otros 2 cambios necesarios uno por uno.

En nuestro método _register_methods, debemos declarar nuestra señal. Esto se hace de la siguiente manera:

void GDExample::_register_methods() {
    register_method("_process", &GDExample::_process);
    register_property<GDExample, float>("amplitude", &GDExample::amplitude, 10.0);
    register_property<GDExample, float>("speed", &GDExample::set_speed, &GDExample::get_speed, 1.0);

    register_signal<GDExample>((char *)"position_changed", "node", GODOT_VARIANT_TYPE_OBJECT, "new_pos", GODOT_VARIANT_TYPE_VECTOR2);
}

Aquí, nuestro método register_signal puede ser una llamada única que primero tome el nombre de la señal y luego tenga pares de valores que especifiquen el nombre del parámetro y el tipo de cada parámetro que enviaremos junto con esta señal.

A continuación, necesitaremos cambiar nuestro método _process:

void GDExample::_process(float delta) {
    time_passed += speed * delta;

    Vector2 new_position = Vector2(
        amplitude + (amplitude * sin(time_passed * 2.0)),
        amplitude + (amplitude * cos(time_passed * 1.5))
    );

    set_position(new_position);

    time_emit += delta;
    if (time_emit > 1.0) {
        emit_signal("position_changed", this, new_position);

        time_emit = 0.0;
    }
}

Después de haber pasado un segundo, emitimos nuestra señal y reiniciamos nuestro contador. Podemos agregar los valores de los parámetros directamente a emit_signal.

Una vez que la biblioteca GDNative esté compilada, podemos ir a Godot y seleccionar nuestro nodo sprite. En el área de Nodos (Node), podemos encontrar nuestra nueva señal y vincularla presionando el botón Conectar o haciendo doble clic en la señal. Hemos agregado un script en nuestro nodo principal e implementado nuestra señal de la siguiente manera:

extends Node

func _on_Sprite_position_changed(node, new_pos):
    print("The position of " + node.name + " is now " + str(new_pos))

Cada segundo, mostramos nuestra posición en la consola.

Siguientes pasos

Lo anterior es solo un ejemplo simple, pero esperamos que te muestre los conceptos básicos. Puedes desarrollar a partir de este ejemplo para crear scripts completos que controlen nodos en Godot utilizando C++.

Para editar y recompilar el complemento mientras el editor de Godot sigue abierto, vuelve a ejecutar el proyecto después de que la biblioteca haya terminado de compilar.