Ejemplo de GDNative C

Introducción

Este tutorial introducira lo mínimo necesario para crear modulos de GDNative. Este debería ser tu inicio en el mundo del GDNative. Entender estos contenidos de este tutorial te ayudara a entender todo lo que vendrá luego de esto.

Antes de comenzar, puedes descargar el código fuente del ejemplo del objeto que describiremos a continuación en el repositorio GDNative-demos.

Este proyecto de ejemplo también contiene un archivo SConstruct que facilita un poco la compilación, pero en este tutorial haremos las cosas a mano para entender el proceso.

GDNative puede ser usado para crear bastantes tipos de adiciones a Godot, usar interfaces tales como PluginScript or ARVRInterfaceGDNative. En este tutorial vamos a ver como crear un módulo NativeScript. NativeScript te permite escribir lógica en C o C++ de manera similar a como escribirias un archivo GDScript. Estaresmos creando el equivalente en C de este GDScript:

extends Reference

var data

func _ready():
    data = "World from GDScript!"

func get_data():
    return data

Los próximos tutoriales se centrarán en los otros tipos de modulos GDNative y explicarán cuándo y cómo usar cada uno.

Pre-requisitos

Antes de comenzar, necesitará algunas cosas:

  1. Un ejecutable de Godot para tu versión objetivo.

  2. Un compilador de C. En Linux, instale gcc o clang mediante su gestor de paquetes. En macOS, puede instalar Xcode desde la App Store de Mac. En Windows, puede usar Visual Studio 2015 o superior, o MinGW-w64.

  3. Un clon de Git del repositorio godot-headers: estos son los encabezados de C para la API pública de Godot expuesta a GDNative.

Para esto le sugerimos que cree una carpeta para este proyecto de ejemplo de GDNative, después abra una terminal en esa carpeta y ejecutelo:

git clone https://github.com/godotengine/godot-headers.git --branch=3.4

Esto requiere que descarges los arhivos requeridos dentro de la carpeta.

Truco

Si planeas usar Git para tu proyecto en GDNative, puedes añadir godot_headers como un submódulo de Git.

Nota

El depósito Godot Headers tiene ramas distintas. Conforme Godot evoluciona, también lo hace GDNative. Mientras que intentamos conservar la compatibilidad entre cada versión, deberías de compilar tu módulo de GDNative de acuerdo a las cabeceras correspondientes a la rama estable de Godot (p. ej. 3.1), e idealmente la versión de lanzamiento (p. ej. 3.1.1-stable) que usas. Los módulos de GDNative compilados de acuerdo a versiones antiguas de cabeceras de Godot podrían funcionar con versiones nuevas del motor, pero no vice-versa.

La rama master del repositorio godot-headers se mantiene en línea con la rama master de Godot y, por lo tanto, contiene las definiciones de clases y estructuras de GDNative que funcionarán con las últimas compilaciones de desarrollo.

Si deseas escribir un módulo GDNative para una versión estable de Godot, debes mirar las etiquetas Git disponibles (con git tags) para encontrar aquella que coincida con la versión de tu motor. En el repositorio godot-headers, estas etiquetas están precedidas por godot-, por lo que puedes hacer un checkout en la etiqueta godot-3.4.4-stable, por ejemplo, para usar con Godot 3.4.4. En tu repositorio clonado, puedes hacer lo siguiente:

git checkout godot-3.4.4-stable

Si por alguna razón falta una etiqueta que coincida con tu versión estable, puedes utilizar la rama estable correspondiente (por ejemplo, 3.4) como alternativa, la cual también puedes seleccionar con git checkout 3.4.

Si está construyendo Godot desde una fuente con tus propios cambios que afectan GDNative, puede encontrar las clases actualizadas y la definición de la estructura en <godotsource>/modules/gdnative/include

Nuestra fuente C

Comencemos por escribir nuestro código principal. Eventualmente, queremos terminar con una estructura de archivos que se vea similar a esto:

+ <your development folder>
  + godot-headers
    - <lots of files here>
  + simple
    + bin
      - libsimple.dll/so/dylib
      - libsimple.gdnlib
      - simple.gdns
    main.tscn
    project.godot
  + src
    - simple.c

Abre Godot y crea un nuevo proyecto llamado "simple" junto a tu clon de Git de godot-headers. Esto creará la carpeta "simple" y el archivo "project.godot". Luego, manualmente crea una carpeta "src" junto a la carpeta "simple" y una subcarpeta "bin" dentro de la carpeta "simple".

Comenzaremos echando un vistazo a lo que nuestro archivo simple.c contiene. Por el momento, vamos a crear una fuente singular en C sin una cabecera para simplificar las cosas. Cuando comiences a escribir proyectos más grandes, es recomendable dividir tu proyecto en varios documentos; sin embargo, eso va más allá de la meta de este tutorial.

Veremos el código fuente poco a poco así que todas las partes debajo deberían estar juntas dentro de un gran archivo. Cada sección será explicada a medida que sea añadida.

#include <gdnative_api_struct.gen.h>

#include <string.h>

const godot_gdnative_core_api_struct *api = NULL;
const godot_gdnative_ext_nativescript_api_struct *nativescript_api = NULL;

El código superior incluye una cabecera en referencia a la estructura de la API de GDNative y una cabecera estándar, las cuales usaremos más adelante para operaciones de texto. Después define dos punteros a otras dos estructuras. GDNative incluye una gran repertorio de funciones para enviar llamados de vuelta al ejecutable principal de Godot. Con el fin de que tu módulo tenga acceso a tales funciones, GDNative provee a tu aplicación con una estructura que contiene punteros a todas estas funciones.

Para mantener esta implementación modular y fácilmante extendible, las funciones núcleo están disponibles a través del "núcleo" de la estructura de la API, pero funciones adicionales tienen su propia "estructura de GDNative" que son accesibles entre extensiones.

En nuestro ejemplo, accederemos a una de estas extensiones para ganar acceso a las funciones especificamente necesitadas para NativeScript.

Un archivo NativeScript se comporta como un script cualquiera en Godot. Debido a que la API de NativeScript es de un nivel bastante bajo, esta requiere que la biblioteca especifique varios elementos con mayor detalle que otros sistemas de scripting, como lo es GDScript. Cuando una instancia de NativeScript es creada, un constructor de la biblioteca es llamado. Cuando la misma instancia es eliminada, el destructor correspondiente es ejectuado.

void *simple_constructor(godot_object *p_instance, void *p_method_data);
void simple_destructor(godot_object *p_instance, void *p_method_data, void *p_user_data);
godot_variant simple_get_data(godot_object *p_instance, void *p_method_data,
        void *p_user_data, int p_num_args, godot_variant **p_args);

Estas son declaraciones frontales para las funciones que implementaremos para nuestro objeto. Necesitamos un constructor y un destructor. Adicionalmente, el objeto tendrá un método llamado get_data.

Lo siguiente es el primero de muchos puntos de entrada que Godot llamará cuando nuestra libería dinámica es cargada. Estos métodos están prefijados con godot_``(puedes cambiar esto luego) seguido de su nombre. ``gdnative_init es una función que inicia nuestra libería dinámica. Godot dará un "pointer" a una estructura que contenga varios bits de información que quizás encontremos útiles entre los cuales el "pointer" a nuestras estructuras de API.

Para cualquier estructura adicional de la API necesitamos hacer un bucle a través de nuestro array de extensiones y comprobar el tipo de extensión.

void GDN_EXPORT godot_gdnative_init(godot_gdnative_init_options *p_options) {
    api = p_options->api_struct;

    // Now find our extensions.
    for (int i = 0; i < api->num_extensions; i++) {
        switch (api->extensions[i]->type) {
            case GDNATIVE_EXT_NATIVESCRIPT: {
                nativescript_api = (godot_gdnative_ext_nativescript_api_struct *)api->extensions[i];
            }; break;
            default: break;
        }
    }
}

A continuación está gdnative_terminate la cual es llamada antes de que la biblioteca sea descargada. Godot descargará la biblioteca cuando no sea usada por ningún objeto. Aquí puedes ejecutar cualquier aseo que sea necesario, por ahora, vamos a limpiar nuestros punteros a la API.

void GDN_EXPORT godot_gdnative_terminate(godot_gdnative_terminate_options *p_options) {
    api = NULL;
    nativescript_api = NULL;
}

Finalmente, tenemos nativescript_init que es la función más importante que necesitaremos hoy. Esta función será llamada por Godot en el proceso de cargar una biblioteca de GDNative y comunica de vuelta al motor los objetos que hacemos disponibles.

void GDN_EXPORT godot_nativescript_init(void *p_handle) {
    godot_instance_create_func create = { NULL, NULL, NULL };
    create.create_func = &simple_constructor;

    godot_instance_destroy_func destroy = { NULL, NULL, NULL };
    destroy.destroy_func = &simple_destructor;

    nativescript_api->godot_nativescript_register_class(p_handle, "SIMPLE", "Reference",
            create, destroy);

    godot_instance_method get_data = { NULL, NULL, NULL };
    get_data.method = &simple_get_data;

    godot_method_attributes attributes = { GODOT_METHOD_RPC_MODE_DISABLED };

    nativescript_api->godot_nativescript_register_method(p_handle, "SIMPLE", "get_data",
            attributes, get_data);
}

Primero le indicamos al motor cuáles archivos son implementados al llamar nativescript_register_class. El primer parámetro es el puntero de referencia que se nos entrega. El segundo es el nombre de la clase de nuestro objeto. El tercero es el tipo de Objeto del cual «heredamos»; no es herencia real, sino una aproximación suficiente. Finalmente, nuestros parámetros cuatro y cinco son descripciones de nuestro constructor y destructor.

Luego le informamos a Godot acerca de nuestros métodos (en este caso, solo tenemos un método) llamando nativescript_register_method para cada método de nuestra clase. En nuestro caso, eso es simplemente get_data. Nuestro primer parámetro es, nuevamente, nuestro puntero de manejo. El segundo es nuevamente el nombre de la clase de objeto que estamos registrando. El tercero es el nombre de nuestra función tal como será conocida por GDScript. El cuarto es nuestra configuración de atributos (consulta la enumeración godot_method_rpc_mode en godot-headers/nativescript/godot_nativescript.h para ver los posibles valores). El quinto y último parámetro es una descripción de qué función llamar cuando se llame al método.

La estructura descriptiva instance_method contiene el puntero de la función como su primer campo. Los otros dos campos en estas estructuras son para especificar datos de usuario por método; el segundo es el campo method_data el cual es pasado en todas las llamadas a funciones bajo el argumento p_method_data. La utilidad de esto es que puedes reusar una función para métodos diferentes en varias clases-script. Si el valor method_data es un puntero a memoria que necesita ser liberada, el tercer campo free_func puede contener un puntero a una función que liberará esa memoria. Esa función de liberación es llamada cuando el script mismo (¡no la instancia!) Es descargado (comúnmente al momento de descargar bibliotecas).

Ahora, comencemos a trabajar en las funciones de nuestro objeto. Primero, definiremos una estructura que usaremos para almacenar la información de miembro de una instancia de nuestra clase GDNative.

typedef struct user_data_struct {
    char data[256];
} user_data_struct;

Después, definiremos nuestro constructor. Todo lo que hacemos en nuestro constructor es asignar memoria para nuestra estructura y llenarla con información. Pon atención a que usamos las funciones de memoria de Godot con el fin de que la memoria sea rastreada, y regresamos el puntero a nuestra nueva estructura. Este puntero funcionará como nuestro identificador de instancia en caso de que varios objetos sean inicializados.

Este puntero será pasado a cualquiera de nuestras funciones relacionadas a nuestro objeto en forma de un parámetro llamado p_user_data, y puede ser utilizado para identificar nuestra instancia y acceder su información interna.

void *simple_constructor(godot_object *p_instance, void *p_method_data) {
    user_data_struct *user_data = api->godot_alloc(sizeof(user_data_struct));
    strcpy(user_data->data, "World from GDNative!");

    return user_data;
}

Nuestro destructor es llamado cuando Godot ha terminado de usar nuestro objeto y liberamos la información interna de nuestra instancia.

void simple_destructor(godot_object *p_instance, void *p_method_data, void *p_user_data) {
    api->godot_free(p_user_data);
}

Finalmente, implementaremos nuestra función get_data. La información siempre es enviada y regresada como variantes, así que para regresar nuestra información (que es una cadena de caracteres), primero tenemos que convertir nuestra cadena de C a un objeto de texto de Godot, para después copiar ese objeto a la variante que regresaremos.

godot_variant simple_get_data(godot_object *p_instance, void *p_method_data,
        void *p_user_data, int p_num_args, godot_variant **p_args) {
    godot_string data;
    godot_variant ret;
    user_data_struct *user_data = (user_data_struct *)p_user_data;

    api->godot_string_new(&data);
    api->godot_string_parse_utf8(&data, user_data->data);
    api->godot_variant_new_string(&ret, &data);
    api->godot_string_destroy(&data);

    return ret;
}

Las cadenas de texto en Godot son distribuidas en la pila de memoria, por lo tanto, tienen un destructor que libera esa memoria. Los destructores son llamados godot_NOMBREDELTIPO_destroy. Cuando una variante se crea con una cadena de texto, hace referencia a la misma; lo que significa que la cadena original puede ser «destruida» para reducir la cuenta de referencias. Si eso no sucede, la memoria de cadenas de texto va a fugarse, ya que la cuenta de referencias jamás será cero y la memoria jamás será liberada. La variante regresada es destruida automáticamente por Godot.

Nota

En operaciones más complicadas puede ser confuso mantener rastro de qué valor necesita ser liberado y cual no. Como regla general: llama godot_NOMBREDELTIPO_destroy cuando sea necesario en lugar del destructor de C++. El destructor de cadenas de texto sería llamado en C++ después de que la variante fue creada, así que lo mismo es necesario en C.

La variante que regresamos es destruida automáticamente por Godot.

Y este es el código fuente entero de nuestro módulo.

Compilando

Ahora necesitamos compilar nuestro código fuente. Como mencionamos, nuestro proyecto de ejemplo en GitHub contiene una configuración de SCons que se encarga del trabajo duro por ti, pero en nuestro tutorial llamaremos los compiladores directamente.

Asumiendo que te estás apegando a la estructura de carpetas sugerida previamente, lo mejor es abrir una sesión de tu terminal en la carpeta src y ejecutar los comandos desde allí. Asegúrate de crear la carpeta bin antes de continuar.

En Linux:

gcc -std=c11 -fPIC -c -I../godot-headers simple.c -o simple.o
gcc -rdynamic -shared simple.o -o ../simple/bin/libsimple.so

En macOS:

clang -std=c11 -fPIC -c -I../godot-headers simple.c -o simple.os
clang -dynamiclib simple.os -o ../simple/bin/libsimple.dylib

En Windows:

cl /Fosimple.obj /c simple.c /nologo -EHsc -DNDEBUG /MD /I. /I..\godot-headers
link /nologo /dll /out:..\simple\bin\libsimple.dll /implib:..\simple\bin\libsimple.lib simple.obj

Nota

En la compilación de Windows también tendrás una biblioteca libsimple.lib. Esta es una biblioteca que se puede compilar en un proyecto para proporcionar acceso a la DLL. La obtenemos como subproducto y no lo necesitamos :) Al exportar el juego para lanzamiento, este archivo será ignorado.

Creando el archivo GDNativeLibrary(.gdnlib)

Con nuestro módulo compilado, ahora tenemos que crear un recurso GDNativeLibrary con la extensión .gdnlib, la cual colocaremos junto con las bibliotecas dinámicas. Este archivo le dice a Godot cuáles bibliotecas dinámicas son parte de nuestro módulo y necesitan ser cargadas por plataforma.

Podemos usar Godot para generar este archivo, así que abre el proyecto «simple» en el editor.

Inicia presionando el botón de crear recurso en el inspector:

../../../_images/new_resource.gif

Y selecciona GDNativeLibrary:

../../../_images/gdnativelibrary_resource.png

Deberías de ver un editor contextual aparecer en el panel inferior. Usa el botón de «Expandir Panel Inferior» en la parte inferior derecha para expandirlo a su altitud completa:

../../../_images/gdnativelibrary_editor.png

Propiedades generales

En el inspector, tienes varias propiedades para controlar la carga de la Biblioteca.

Si Load Once está habilitada, nuestra biblioteca se cargará sólo una vez y cada script individual que use nuestra biblioteca usará la misma información. Cada variable que definas globalmente será accesible por cada instancia del objeto que crees. Si Load Once está deshabilitada, una nueva copia de la biblioteca será cargada a la memoria cada vez que un script acceda a la biblioteca.

Si Singleton está habilitado, nuestra librería se carga automáticamente y se llama a una función llamada godot_gdnative_singleton. Dejaremos eso para otro tutorial.

El prefijo Symbol Prefix trabaja con nuestras funciones principales, tales como godot_ en godot_nativescript_init que vimos previamente. SI usas varias bibliotecas GDNative que quieres conectar estáticamente, tendrás que usar prefijos distintos. De nuevo, este es un tema para enfocarse en un tutorial distinto, sólo es necesario en este momento para el despliegue en iOS, ya que a esta plataforma no le gustan las bibliotecas dinámicas.

Reloadable define si la biblioteca debería de ser recargada cuando el editor pierde y recupera el enfoque, principalmente para encontrar símbolos nuevos o modificados por cualquier cambio hecho a la biblioteca de manera externa.

Librerías de la plataforma

El plugin GDNativeLibrary del editor te permite configurar dos elementos por cada plataforma y arquitectura que planees apoyar.

La columna Dinamyc Library (sección entry en el archivo guardado) nos dice para cada combinación de plataforma y característica qué librería dinámica debe ser cargada. Esto también informa al exportador los ficheros que deben exportarse al exportar a una plataforma específica.

La columna Dependencies (o la sección dependencies) le dice a Godot cuáles otros archivos tienen que ser exportados por plataforma con el fin de que nuestra librería funcione. Imagina que tu módulo GDNative usa otro DLL para implementar funcionalidad de una librería tercera o externa, aquí es dónde alistas tales DLLs.

Para nuestro ejemplo, sólo construimos bibliotecas para Linux, macOS y/o Windows, así que puedes enlazar los campos relevantes haciendo clic en el botón de carpeta. Si construiste las tres bibliotecas, deberías tener algo como:

../../../_images/gdnativelibrary_editor_complete.png

Guardando el recurso

Podemos guardar nuestro recurso GDNativeLibrary como bin/libsimple.gdnlib con el botón Guardar en el Inspector:

../../../_images/gdnativelibrary_save.png

El archivo es guardado en un formato texto y debería tener contenido similar a este:

[general]

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

[entry]

OSX.64="res://bin/libsimple.dylib"
OSX.32="res://bin/libsimple.dylib"
Windows.64="res://bin/libsimple.dll"
X11.64="res://bin/libsimple.so"

[dependencies]

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

Creando el archivo NativeScript (.gdns)

Con nuestro archivo .gdnlib, le hemos indicado a Godot cómo cargar nuestra biblioteca. Ahora necesitamos informarle acerca de nuestra clase de objeto "SIMPLE". Lo hacemos creando un archivo de recurso NativeScript con la extensión .gdns.

De la misma manera que hicimos con nuestro recurso GDNativeLibrary, haz clic en el botón para crear un nuevo recurso en el inspector y selecciona NativeScript:

../../../_images/nativescript_resource.png

El inspector mostrará algunas propiedades que debemos completar. En el campo Nombre de Clase, ingresamos "SIMPLE", que es el nombre de la clase de objeto que declaramos en nuestro código fuente en C al llamar a godot_nativescript_register_class. También necesitamos seleccionar nuestro archivo .gdnlib haciendo clic en Biblioteca y seleccionando Cargar:

../../../_images/nativescript_library.png

Nota

El campo Class Name tiene que estar escrito de la misma manera que fue asignado en godot_nativescript_init al registrar la clase.

Finalmente, haz clic en el icono de guardado y guarda el archivo bajo el nombre bin/simple.gdns:

../../../_images/save_gdns.gif

Es momento de crear nuestra escena. Agrega un nodo de Control a tu escena como raíz y llámalo main. Después, agrégale los nodos hijos Button y Label (Botón y Etiqueta, respectivamente). Posiciónalos en algún lugar agradable de la pantalla y dale un nombre a tu Botón.

../../../_images/c_main_scene_layout.png

Selecciona el nodo de Control y adjúntale un script:

../../../_images/add_main_script.gif

Ahora, conecta la señal pressed del botón a tu script:

../../../_images/connect_button_signal.gif

No olvides guardar tu escena, llámala main.tscn.

Ahora podemos implementar nuestro código de main.gd:

extends Control

# load the Simple library
onready var data = preload("res://bin/simple.gdns").new()

func _on_Button_pressed():
    $Label.text = "Data = " + data.get_data()

Después de todo esto, nuestro proyecto debería funcionar. La primera vez que lo ejecutes Godot te preguntará cuál es tu escena principal, selecciona main.tscn y ya está listo:

../../../_images/c_sample_result.png