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.

Godot 3.1 vio la introducción de las adiciones a NativeScript 1.1 que permitieron al equipo de GDNative crear una biblioteca de ligaduras con C++ más agradable. Estos cambios han sido incorporados a la rama maestra y serán el camino a seguir en el futuro. Si quieres escribir un plugin de GDNative en C++ que también funcione con Godot 3.0, tendrás que usar la rama 3.0 y la sintaxis de NativeScript 1.0. Demostraremos ambas versiones lado a lado en esta revisión.

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.

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 https://github.com/godotengine/godot-cpp
cd godot-cpp
git submodule update --init
mkdir gdnative_cpp_example
cd gdnative_cpp_example
git init
git submodule add -b 3.0 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 https://github.com/godotengine/godot-cpp
mkdir gdnative_cpp_example
cd gdnative_cpp_example
git clone --recursive -b 3.0 https://github.com/godotengine/godot-cpp

Nota

godot-cpp now includes godot-headers as a nested submodule, if you've manually downloaded them please make sure to place godot-headers inside of the godot-cpp folder.

No tienes que hacerlo de esta manera, pero es lo que hemos encontrado más fácil de administrar. Si decides descargar los depósitos o simplemente clonarlos dentro de tu carpeta, asegúrate de mantener la estructura del proyecto de la misma manera que la hemos configurado aquí, ya que gran parte del código que enseñaremos asume que el proyecto tiene esa configuració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/.

En algún momento futuro, habrá binarios compilados disponibles, haciendo que este sea un paso opcional.

Nota

Podrías tener que agregar bits=64 al comando en Windows o Linux. Seguimos trabajando en una auto detección más efectiva.

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.

You should now have demo, godot-cpp, godot-headers, and src directories in your GDNative module.

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
#ifndef GDEXAMPLE_H
#define GDEXAMPLE_H

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

namespace godot {

class GDExample : public godot::GodotScript<Sprite> {
    GODOT_CLASS(GDExample)

private:
    float time_passed;

public:
    static void _register_methods();

    GDExample();
    ~GDExample();

    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);
}
#include "gdexample.h"

using namespace godot;

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

GDExample::GDExample() {
    // Initialize any variables here
    time_passed = 0.0;
}

GDExample::~GDExample() {
    // Add your cleanup procedure here
}

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)));

    owner->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 destacable es nuestra función _process, la cual simplemente registra la cantidad de tiempo que ha transcurrido y calcula la posición de nuestro sprite usando una simple función de seno y coseno. Lo que resalta es llamar owner->set_position para llamar uno de los métodos de construcción de nuestro Sprite. Esto sucede porque nuestra clase es una clase contenedora; owner señala al nodo de Sprite real al cual nuestro script está relacionado. En el próximo NativeScript 1.1, set_position podrá ser llamado directamente en nuestra clase.

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.

Once you've downloaded the SConstruct file, place it in your GDNative module folder besides godot-cpp, godot-headers and demo, then run:

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.

The first one is a file that lets Godot know what dynamic libraries should be loaded for each platform and is called 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=[]

This file contains a general section that controls how the module is loaded. It also contains a prefix section which should be left on godot_ for now. If you change this, you'll need to rename various functions that are used as entry points. This was added for the iPhone platform because it doesn't allow dynamic libraries to be deployed, yet GDNative modules are linked statically.

The entry section is the important bit: it tells Godot the location of the dynamic library in the project's filesystem for each supported platform. It will also result in just that file being exported when you export the project, which means the data pack won't contain libraries that are incompatible with the target platform.

Finally, the dependencies section allows you to name additional dynamic libraries that should be included as well. This is important when your GDNative plugin implements someone else's library and requires you to supply a third-party dynamic library with your project.

If you double click on the gdexample.gdnlib file within Godot, you'll see there are far more options to set:

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

The second file we need to create is a file used by each NativeScript we've added to our plugin. We'll name it gdexample.gdns for our gdexample NativeScript.

[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 )

This is a standard Godot resource; you could just create it directly in your scene, but saving it to a file makes it much easier to reuse it in other places. This resource points to our gdnlib file, so that Godot can know which dynamic library contains our NativeScript. It also defines the class_name which identifies the NativeScript in our plugin we want to use.

Time to jump back into Godot. We load up the main scene we created way back in the beginning and now add a Sprite to our scene:

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

We're going to assign the Godot logo to this sprite as our texture, disable the centered property and drag our gdexample.gdns file onto the script property of the sprite:

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

We're finally ready to run the project:

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

Agregando propiedades

GDScript allows you to add properties to your script using the export keyword. In GDNative you have to register the properties and there are two ways of doing this. You can either bind directly to a member or use a setter and getter function.

Nota

There is a third option, just like in GDScript you can directly implement the _get_property_list, _get and _set methods of an object but that goes far beyond the scope of this tutorial.

We'll examine both starting with the direct bind. Lets add a property that allows us to control the amplitude of our wave.

In our gdexample.h file we simply need to add a member variable like so:

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

In our gdexample.cpp file we need to make a number of changes, we will only show the methods we end up changing, don't remove the lines we're omitting:

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);
}
void GDExample::_register_methods() {
    register_method((char *)"_process", &GDExample::_process);
    register_property<GDExample, float>("amplitude", &GDExample::amplitude, 10.0);
}

GDExample::GDExample() {
    // 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))
    );

    owner->set_position(new_position);
}

Once you compile the module with these changes in place you will see that a property has been added to our interface. You can now change this property and when you run your project, you will see that our Godot icon travels along a larger figure.

Nota

The reloadable property in the gdexample.gdnlib file must be set to true for the Godot editor to automatically pick up the newly added property.

However, this setting should be used with care especially when tool classes are used, as the editor might hold objects then that have script instances attached to them that are managed by a GDNative library.

Lets do the same but for the speed of our animation and use a setter and getter function. Our gdexample.h header file again only needs a few more lines of code:

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

This requires a few more changes to our gdexample.cpp file, again we're only showing the methods that have changed so don't remove anything we're omitting:

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;
}
void GDExample::_register_methods() {
    register_method((char *)"_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);
}

GDExample::GDExample() {
    // 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))
    );

    owner->set_position(new_position);
}

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

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

Now when the project is compiled we'll see another property called speed. Changing its value will make the animation go faster or slower.

For this example there is no obvious advantage of using a setter and getter. It is just more code to write. For a simple example as this there may be a good reason for a setter if you want to react on the variable being changed but in many cases just binding the variable will be enough.

Getters and setters become far more useful in more complex scenarios where you need to make additional choices based on the state of your object.

Nota

For simplicity we've left out the optional parameters in the register_property<class, type> method call. These parameters are rpc_mode, usage, hint and hint_string. These can be used to further configure how properties are displayed and set on the Godot side.

Modern C++ compilers are able to infer the class and variable type and allow you to omit the <GDExample, float> part of our register_property method. We've had mixed experiences with this however.

Señales

Last but not least, signals fully work in GDNative as well. Having your module react to a signal given out by another object requires you to call connect on that object. We can't think of a good example for our wobbling Godot icon, we would need to showcase a far more complete example.

This however is the required syntax:

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

Note that you can only call my_method if you've previously registered it in your _register_methods method.

Having your object sending out signals is far more common. For our wobbling Godot icon we'll do something silly just to show how it works. We're going to emit a signal every time a second has passed and pass the new location along.

In our gdexample.h header file we just need to define a new member time_emit:

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

The changes in gdexample.cpp are a bit more elaborate this time. First you'll need to set time_emit = 0.0; in either our _init method or in our constructor. But the other two needed changes we'll look at one by one.

In our _register_methods method we need to declare our signal and we do this as follows:

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);
}
void GDExample::_register_methods() {
    register_method((char *)"_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);

    Dictionary args;
    args[Variant("node")] = Variant(Variant::OBJECT);
    args[Variant("new_pos")] = Variant(Variant::VECTOR2);
    register_signal<GDExample>((char *)"position_changed", args);
}

Here we see a nice improvement in the latest version of godot-cpp where our register_signal method can be a single call first taking the signals name, then having pairs of values specifying the parameter name and type of each parameter we'll send along with this signal.

For NativeScript 1.0 we first build a dictionary in which we tell Godot about the types of arguments we will pass to our signal, and then register it.

Next we'll need to change our _process method:

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;
    }
}
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))
    );

    owner->set_position(new_position);

    time_emit += delta;
    if (time_emit > 1.0) {
        Array args;
        args.push_back(Variant(owner));
        args.push_back(Variant(new_position));
        owner->emit_signal("position_changed", args);

        time_emit = 0.0;
    }
}

Después de un segundo emitimos nuestra señal y reiniciamos nuestro contador. De nuevo en la nueva versión de godot-cpp podemos añadir los valores de nuestros parámetros directamente a emit_signal. En NativeScript 1.0 primero construimos un array de valores y luego llamamos a emit_signal.

Once compiled we can go into Godot and select our sprite node. On our Node tab we find our new signal and link it up by pressing connect. We've added a script on our main node and implemented our signal like this:

extends Node

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

Every second we simply output our position to the console.

NativeScript 1.1 vs NativeScript 1.0

So far in our example above there doesn't seem to be a lot of difference between the old and new syntax. The class is defined slightly differently and we no longer use the owner member to call methods on the Godot side of our object. A lot of the improvements are hidden under the hood.

This example only deals with simple variables and simple methods. Especially once you start passing references to other objects or when you start calling methods that require more complex parameters, NativeScript 1.1 does start to show its benefits.

Siguientes pasos

The above is only a simple example, but we hope it shows you the basics. You can build upon this example to create full-fledged scripts to control nodes in Godot using C++.

You should be able to edit and recompile the plugin while the Godot editor remains open; just rerun the project after the library has finished building.