Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
Esempio di GDExtension in C
Introduzione
Questo è un semplice esempio su come lavorare con GDExtension direttamente con codice C. Si noti che l'API non è pensata per l'uso diretto, quindi questo esempio, sebbene piccolo, sarà sicuramente piuttosto verboso e richiederà molti passaggi. Tuttavia, serve come riferimento per creare binding per un linguaggio diverso. È comunque possibile utilizzare l'API direttamente, se si preferisce, il che potrebbe essere comodo quando il binding serve solo per una libreria di terze parti.
In questo esempio creeremo un nodo personalizzato che sposta uno sprite sullo schermo in base ai parametri dell'utente. Pur essendo molto semplice, serve a mostrare come realizzare alcune operazioni con GDExtension, come la registrazione di classi personalizzate con metodi, proprietà e segnali. Offre inoltre un'approfondimento dell'API di GDExtension.
Configurare il progetto
Ci sono alcuni prerequisiti necessari:
Un eseguibile di Godot 4.2 (o successivo)
un compilatore per C,
SCons come strumento di compilazione.
Poiché ciò utilizza direttamente l'API, non è necessario utilizzare il repository godot-cpp.
Struttura dei file
Per organizzare i nostri file, li divideremo principalmente in due cartelle:
gdextension_c_example/
|
+--project/ # game example/demo to test the extension
|
+--src/ # source code of the extension we are building
Abbiamo inoltre bisogno di una copia dell'intestazione in gdextension_interface.h dal codice sorgente di Godot, che si può ottenere direttamente dall'eseguibile di Godot eseguendo il seguente comando:
godot --dump-gdextension-interface
Questo crea l'intestazione nella cartella attuale, quindi basta copiarla nella cartella src del progetto di esempio.
Infine, c'è un'altra fonte di informazioni a cui dobbiamo fare riferimento, ovvero il file JSON con il riferimento all'API di Godot. Questo file non sarà utilizzato direttamente dal codice, lo useremo solo per estrarre manualmente alcune informazioni.
Per ottenere questo file JSON, è sufficiente chiamare l'eseguibile di Godot:
godot --dump-extension-api
Il file extension_api.json risultante verrà creato nella cartella attuale. Si può copiare questo file nella cartella di esempio per averlo a portata di mano.
Nota
Questa estensione è progettata per Godot 4.2, ma dovrebbe funzionare anche su versioni successive. Se desideri utilizzare una versione minima diversa, assicurarsi di ottenere l'intestazione e il file JSON dalla versione di Godot che si sta puntando.
Buildsystem
Usare un sistema di compilazione ci semplifica notevolmente la vita quando si ha a che fare con il codice C. Per comodità, useremo SCons, poiché è lo stesso sistema usato da Godot.
Il seguente file SConstruct è un semplice file che compilerà l'estensione per la piattaforma che si sta utilizzando, che sia Linux, macOS o Windows. Si tratta di una build non ottimizzata a scopo di debug. Presuppone inoltre una build a 64 bit, rilevante per alcune parti del codice di esempio. Creare altri tipi di build e la compilazione incrociata vanno fuori dallo scopo di questo tutorial. Salva questo file nella cartella radice.
#!/bin/env python
from SCons.Script import Environment
from os import path
import sys
env = Environment()
# Set the target path and name.
target_path = "project/bin/"
target_name = "libgdexample"
# Set the compiler and flags.
env.Append(CPPPATH=["src"]) # Add the src folder to the include path.
env.Append(CFLAGS=["-O0", "-g"]) # Make it a debug build.
# Use Clang on macOS.
if sys.platform == "darwin":
env["CC"] = "clang"
# Add all C files in "src" folder as sources.
sources = env.Glob("src/*.c")
# Create a shared library.
library = env.SharedLibrary(
target=path.join(target_path, target_name),
source=sources,
)
# Set the library as the default target.
env.Default(library)
Questo includerà tutti i file C nella cartella src, quindi non sarà necessario modificare questo file quando si aggiungono nuovi file sorgente.
Inizializzare l'estensione
La prima parte del codice si occuperà di iniziallizare l'estensione. In questo modo Godot sarà a conoscenza di ciò che la nostra GDExtension fornisce, come classi e plugin.
Crea il file init.h nella cartella src con il seguente contenuto:
#pragma once
#include "defs.h"
#include "gdextension_interface.h"
void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level);
void deinitialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level);
GDExtensionBool GDE_EXPORT gdexample_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization);
Le funzioni qui dichiarate hanno le firme previste dall'API GDExtension.
Si noti l'inclusione del file defs.h. Questo è uno dei nostri strumenti di supporto per semplificare la scrittura di codice per estensioni. Per ora conterrà solo la definizione di GDE_EXPORT, una macro che rende la funzione pubblica nella libreria condivisa in modo che Godot possa richiamarla correttamente. Questa macro aiuta ad astrarre cosa si aspetta ciascun compilatore.
Crea il file defs.h nella cartella src con il seguente contenuto:
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#if !defined(GDE_EXPORT)
#if defined(_WIN32)
#define GDE_EXPORT __declspec(dllexport)
#elif defined(__GNUC__)
#define GDE_EXPORT __attribute__((visibility("default")))
#else
#define GDE_EXPORT
#endif
#endif // ! GDE_EXPORT
Abbiamo incluso anche alcuni file di intestazione standard per semplificare le cose. Ora dobbiamo solo includere defs.h e questi saranno un bonus.
Ora, implementiamo le funzioni che abbiamo appena dichiarato. Crea un file chiamato init.c nella cartella src e aggiungi questo codice:
#include "init.h"
void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
}
void deinitialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
}
GDExtensionBool GDE_EXPORT gdexample_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization)
{
r_initialization->initialize = initialize_gdexample_module;
r_initialization->deinitialize = deinitialize_gdexample_module;
r_initialization->userdata = NULL;
r_initialization->minimum_initialization_level = GDEXTENSION_INITIALIZATION_SCENE;
return true;
}
Questo codice imposta i dati di inizializzazione previsti da Godot. Sono definite le funzioni di inizializzazione e deinizializzazione, che Godot richiamerà quando necessario. È inoltre impostato il livello di inizializzazione, che varia a seconda dell'estensione. Poiché intendiamo aggiungere un nodo personalizzato, il livello SCENE è sufficiente.
Dopo compileremo la funzione initialize_gdexample_module() per registrare la nostra classe personalizzata.
Una classe base
Per creare un nodo vero e proprio, innanzitutto creeremo una struct C per contenere dati e funzioni che fungeranno da metodi. L'obiettivo è creare un nodo personalizzato che erediti da Sprite2D.
Crea un file chiamato gdexample.h nella cartella src con il seguente contenuto:
#pragma once
#include "gdextension_interface.h"
#include "defs.h"
// Struct to hold the node data.
typedef struct
{
// Metadata.
GDExtensionObjectPtr object; // Stores the underlying Godot object.
} GDExample;
// Constructor for the node.
void gdexample_class_constructor(GDExample *self);
// Destructor for the node.
void gdexample_class_destructor(GDExample *self);
// Bindings.
void gdexample_class_bind_methods();
Da notare il campo object, che contiene un puntatore all'oggetto di Godot, e la funzione gdexample_class_bind_methods(), che registrerà i metadati della nostra classe personalizzata (proprietà, metodi e segnali). Quest'ultima non è strettamente necessaria, poiché possiamo farlo anche durante la registrazione della classe, ma rende più chiara la separazione delle responsabilità e permette alla nostra classe di registrare autonomamente i propri metadati.
Il campo object è necessario perché la nostra classe erediterà una classe Godot. Dato che non possiamo ereditarla direttamente, poiché non interagiamo con il codice sorgente (e il linguaggio C non ha nemmeno le classi), diciamo a Godot di creare un oggetto di un tipo che conosce e di collegarvi la nostra estensione. Avremo bisogno del riferimento a tali oggetti quando chiameremo i metodi della classe padre, ad esempio.
Creiamo la controparte sorgente di questo file di intestazione. Crea il file gdexample.c nella cartella src e aggiungi il seguente codice:
#include "gdexample.h"
void gdexample_class_constructor(GDExample *self)
{
}
void gdexample_class_destructor(GDExample *self)
{
}
void gdexample_class_bind_methods()
{
}
Visto che al momento non abbiamo nulla a che fare con quelle funzioni, rimarranno vuote per un po'.
Il prossimo passo è registrare la nostra classe. Tuttavia, per farlo dobbiamo creare un StringName e per questo dobbiamo ottenere una funzione dall'API GDExtension. Dato che ci servirà più volte e ci serviranno anche altre cose, creiamo un'API wrapper per semplificare questo tipo di operazione.
Un'API wrapper
Inizieremo creando un file api.h nella cartella src:
#pragma once
/*
This file works as a collection of helpers to call the GDExtension API
in a less verbose way, as well as a cache for methods from the discovery API,
just so we don't have to keep loading the same methods again.
*/
#include "gdextension_interface.h"
#include "defs.h"
extern GDExtensionClassLibraryPtr class_library;
// API methods.
extern struct Constructors
{
GDExtensionInterfaceStringNameNewWithLatin1Chars string_name_new_with_latin1_chars;
} constructors;
extern struct Destructors
{
GDExtensionPtrDestructor string_name_destructor;
} destructors;
extern struct API
{
GDExtensionInterfaceClassdbRegisterExtensionClass2 classdb_register_extension_class2;
} api;
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address);
Questo file includerà molte altre funzioni ausiliari man mano che arricchiremo la nostra estensione con elementi utili. Per ora contiene solo un puntatore a una funzione che crea un oggetto StringName da una stringa C (in codifica Latin-1) e a un'altra per distruggere un oggetto StringName, che dovremo usare per evitare perdite di memoria, nonché la funzione per registrare una classe, che è il nostro obiettivo iniziale.
Qui conserviamo anche un riferimento alla class_library. Questa è qualcosa che Godot ci fornisce durante l'inizializzazione dell'estensione e che dovremo utilizzare quando registriamo gli elementi che creiamo, affinché Godot possa identificare quale estensione sta effettuando la chiamata.
È presente anche una funzione per caricare i puntatori di funzioni dall'API GDExtension.
Lavoriamo ora sulla controparte sorgente di questo file di intestazione. Creiamo il file api.c nella cartella src, aggiungendo il seguente codice:
#include "api.h"
GDExtensionClassLibraryPtr class_library = NULL;
struct Constructors constructors;
struct Destructors destructors;
struct API api;
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
// Get helper functions first.
GDExtensionInterfaceVariantGetPtrDestructor variant_get_ptr_destructor = (GDExtensionInterfaceVariantGetPtrDestructor)p_get_proc_address("variant_get_ptr_destructor");
// API.
api.classdb_register_extension_class2 = (GDExtensionInterfaceClassdbRegisterExtensionClass2)p_get_proc_address("classdb_register_extension_class2");
// Constructors.
constructors.string_name_new_with_latin1_chars = (GDExtensionInterfaceStringNameNewWithLatin1Chars)p_get_proc_address("string_name_new_with_latin1_chars");
// Destructors.
destructors.string_name_destructor = variant_get_ptr_destructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME);
}
La prima cosa importante qui è p_get_proc_address. Questa è una funzione dell'API GDExtension che viene passata durante l'inizializzazione. È possibile utilizzare questa funzione per richiedere funzioni specifiche dall'API tramite il loro nome. Qui stiamo memorizzando i risultati in modo da non dover mantenere un riferimento a p_get_proc_address ovunque e utilizzare invece il nostro wrapper.
All'inizio richiediamo la funzione variant_get_ptr_destructor(). Questa non sarà utilizzata al di fuori di questa funzione, quindi non la aggiungiamo al nostro wrapper e la memorizziamo solo localmente. Il cast è necessario per silenziare gli avvisi del compilatore.
Poi otteniamo la funzione che crea un oggetto StringName da una stringa C, esattamente la funzione di cui abbiamo parlato prima. La memorizziamo nella nostra struct constructors.
Successivamente, utilizziamo la funzione variant_get_ptr_destructor() che abbiamo appena ottenuto per richiedere il distruttore di StringName, usando il valore di enumerazione dall'API gdextension_interface.h come parametro. Potremmo ottenere i distruttori per altri tipi in modo simile, ma ci limiteremo a ciò che è necessario per l'esempio.
Infine, otteniamo la funzione classdb_register_extension_class2(), che ci servirà per registrare la nostra classe personalizzata.
Nota
Potreste chiederti perché c'è un numero 2 nel nome della funzione. Questo indica che è la seconda versione di questa funzione. La vecchia versione è mantenuta per garantire la compatibilità con le estensioni più vecchie, ma poiché abbiamo a disposizione la seconda versione, è preferibile utilizzare quella più recente, dato che in questo esempio non intendiamo supportare le versioni precedenti di Godot.
Il file di intestazione gdextension_interface.h documenta in quale versione di Godot è stata introdotta ciascuna funzione.
Qui definiamo anche la variabile class_library, che verrà impostata durante l'inizializzazione.
A proposito di inizializzazione, ora dobbiamo modificare il file init.c per completare le cose che abbiamo appena aggiunto:
GDExtensionBool GDE_EXPORT gdexample_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization)
{
class_library = p_library;
load_api(p_get_proc_address);
...
Qui impostiamo la class_library come necessario e chiamiamo la nostra nuova funzione load_api(). Non dimenticare di includere anche i nuovi file di intestazione all'inizio di questo file:
#include "init.h"
#include "api.h"
#include "gdexample.h"
...
Visto che siamo qui, possiamo registrare la nostra nuova classe personalizzata. Completiamo la funzione initialize_gdexample_module():
void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
if (p_level != GDEXTENSION_INITIALIZATION_SCENE)
{
return;
}
// Register class.
StringName class_name;
constructors.string_name_new_with_latin1_chars(&class_name, "GDExample", false);
StringName parent_class_name;
constructors.string_name_new_with_latin1_chars(&parent_class_name, "Sprite2D", false);
GDExtensionClassCreationInfo2 class_info = {
.is_virtual = false,
.is_abstract = false,
.is_exposed = true,
.set_func = NULL,
.get_func = NULL,
.get_property_list_func = NULL,
.free_property_list_func = NULL,
.property_can_revert_func = NULL,
.property_get_revert_func = NULL,
.validate_property_func = NULL,
.notification_func = NULL,
.to_string_func = NULL,
.reference_func = NULL,
.unreference_func = NULL,
.create_instance_func = gdexample_class_create_instance,
.free_instance_func = gdexample_class_free_instance,
.recreate_instance_func = NULL,
.get_virtual_func = NULL,
.get_virtual_call_data_func = NULL,
.call_virtual_with_data_func = NULL,
.get_rid_func = NULL,
.class_userdata = NULL,
};
api.classdb_register_extension_class2(class_library, &class_name, &parent_class_name, &class_info);
// Bind methods.
gdexample_class_bind_methods();
// Destruct things.
destructors.string_name_destructor(&class_name);
destructors.string_name_destructor(&parent_class_name);
}
La struct con le informazioni sulla classe è la cosa più importante qui. Nessuno dei suoi campi è obbligatorio, ad eccezione di create_instance_func e free_instance_func. Non abbiamo ancora creato queste funzioni, quindi dovremo lavorarci presto. Si noti che saltiamo l'inizializzazione se non si trova al livello SCENE. Questa funzione potrebbe essere chiamata più volte, una per ogni livello, ma noi vogliamo registrare la nostra classe una sola volta.
L'altra cosa non definita qui è StringName. Sarà una struct opaca destinata a contenere i dati di uno StringName di Godot nella nostra estensione. La definiremo nel file appropriatamente denominato defs.h:
...
// The sizes can be obtained from the extension_api.json file.
#ifdef BUILD_32
#define STRING_NAME_SIZE 4
#else
#define STRING_NAME_SIZE 8
#endif
// Types.
typedef struct
{
uint8_t data[STRING_NAME_SIZE];
} StringName;
Come accennato nel commento, le dimensioni si trovano nel file extension_api.json che abbiamo generato in precedenza, nella proprietà builtin_class_sizes. BUILD_32 non viene mai definito, poiché presupponiamo di lavorare con una build a 64 bit di Godot, ma se ne ce n'è bisogno è possibile aggiungere env.Append(CPPDEFINES=["BUILD_32"]) al file SConstruct.
Il commento // Types. anticipa che aggiungeremo altri tipi a questo file. Rimandiamolo a più tardi.
La struct StringName serve solo a contenere i dati di Godot, quindi non ci interessa cosa contenga. Tuttavia, in questo caso, è semplicemente un puntatore ai dati nell'heap. Useremo questa struct quando dovremo allocare noi stessi i dati per uno StringName, come stiamo facendo quando registriamo la nostra classe.
Back to registering, we need to work on our create and free functions. Let's
include them in gdexample.h since they're specific to the custom class:
...
// Bindings.
void gdexample_class_bind_methods();
GDExtensionObjectPtr gdexample_class_create_instance(void *p_class_userdata);
void gdexample_class_free_instance(void *p_class_userdata, GDExtensionClassInstancePtr p_instance);
...
Prima di poter implementare queste funzioni, avremo bisogno di alcune altre cose nella nostra API. Ci serve un modo per allocare e liberare la memoria. Sebbene potremmo farlo con il buon vecchio malloc(), possiamo invece utilizzare le funzioni di gestione della memoria di Godot. Avremo anche bisogno di un modo per creare un oggetto di Godot e impostarlo con la nostra istanza personalizzata.
Modifichiamo quindi il file api.h per includere queste nuove funzioni:
...
extern struct API
{
GDExtensionInterfaceClassdbRegisterExtensionClass2 classdb_register_extension_class2;
GDExtensionInterfaceClassdbConstructObject classdb_construct_object;
GDExtensionInterfaceObjectSetInstance object_set_instance;
GDExtensionInterfaceObjectSetInstanceBinding object_set_instance_binding;
GDExtensionInterfaceMemAlloc mem_alloc;
GDExtensionInterfaceMemFree mem_free;
} api;
Poi modifichiamo la funzione load_api() nel file api.c per prendere queste nuove funzioni:
...
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
...
// API.
api.classdb_register_extension_class2 = p_get_proc_address("classdb_register_extension_class2");
api.classdb_construct_object = (GDExtensionInterfaceClassdbConstructObject)p_get_proc_address("classdb_construct_object");
api.object_set_instance = (GDExtensionInterfaceObjectSetInstance)p_get_proc_address("object_set_instance");
api.object_set_instance_binding = (GDExtensionInterfaceObjectSetInstanceBinding)p_get_proc_address("object_set_instance_binding");
api.mem_alloc = (GDExtensionInterfaceMemAlloc)p_get_proc_address("mem_alloc");
api.mem_free = (GDExtensionInterfaceMemFree)p_get_proc_address("mem_free");
}
Ora possiamo tornare a gdexample.c e definire le nuove funzioni, senza dimenticare di includere il file di intestazione api.h:
#include "gdexample.h"
#include "api.h"
...
const GDExtensionInstanceBindingCallbacks gdexample_class_binding_callbacks = {
.create_callback = NULL,
.free_callback = NULL,
.reference_callback = NULL,
};
GDExtensionObjectPtr gdexample_class_create_instance(void *p_class_userdata)
{
// Create native Godot object;
StringName class_name;
constructors.string_name_new_with_latin1_chars(&class_name, "Sprite2D", false);
GDExtensionObjectPtr object = api.classdb_construct_object(&class_name);
destructors.string_name_destructor(&class_name);
// Create extension object.
GDExample *self = (GDExample *)api.mem_alloc(sizeof(GDExample));
gdexample_class_constructor(self);
self->object = object;
// Set the extension instance in the native Godot object.
constructors.string_name_new_with_latin1_chars(&class_name, "GDExample", false);
api.object_set_instance(object, &class_name, self);
api.object_set_instance_binding(object, class_library, self, &gdexample_class_binding_callbacks);
destructors.string_name_destructor(&class_name);
return object;
}
void gdexample_class_free_instance(void *p_class_userdata, GDExtensionClassInstancePtr p_instance)
{
if (p_instance == NULL)
{
return;
}
GDExample *self = (GDExample *)p_instance;
gdexample_class_destructor(self);
api.mem_free(self);
}
Quando creiamo un'istanza di un oggetto, per prima cosa creiamo un nuovo oggetto Sprite2D, poiché è il padre della nostra classe. Quindi allochiamo memoria per la nostra struct personalizzata e chiamiamo il suo costruttore. Salviamo anche il puntatore all'oggetto Godot nella struct, come accennato in precedenza.
Then we set our custom struct as the instance data. This will make Godot know that the object is an instance of our custom class and properly call our custom methods for instance, as well as passing this data back.
Si noti che restituiamo l'oggetto Godot che abbiamo creato, non la nostra struct personalizzata.
Per la funzione gdextension_free_instance(), chiamiamo solo il distruttore e liberiamo la memoria che abbiamo allocato per i dati personalizzati. Non è necessario distruggere l'oggetto di Godot poiché questo sarà gestito dal motore stesso.
Un progetto di dimostrazione
Ora che possiamo creare e liberare il nostro oggetto personalizzato, dovremmo essere in grado di provarlo in un progetto reale. Per fare ciò, è necessario aprire Godot e creare un nuovo progetto nella cartella project. Il gestore dei progetti potrebbe avvisarti che la cartella non è vuota se hai già compilato l'estensione in precedenza; questa volta puoi tranquillamente ignorare questo avviso.
Se non hai ancora compilato l'estensione, è il momento di farlo. Apri un terminale o un prompt dei comandi, vai alla cartella radice dell'estensione ed esegui scons. La compilazione dovrebbe essere rapida, dato che l'estensione è molto semplice.
Successivamente, crea un file chiamato gdexample.gdextension all'interno della cartella project. È una risorsa di Godot che descrive l'estensione, consentendo al motore di caricarla correttamente. Inserisci il seguente contenuto in questo file:
[configuration]
entry_symbol = "gdexample_library_init"
compatibility_minimum = "4.2"
[libraries]
macos.debug = "res://bin/libgdexample.dylib"
linux.debug = "res://bin/libgdexample.so"
windows.debug = "res://bin/libgdexample.dll"
Come puoi vedere, gdexample_library_init() ha lo stesso nome della funzione che abbiamo definito nel nostro file init.c. È importante che i nomi corrispondano perché è così che Godot chiama il punto di ingresso dell'estensione.
We also set the compatibility minimum to 4.2, since we are targeting this version. It should still work on later versions. If you are using a later Godot version and rely on the new features, you need to increase this value to a version number that has everything you use. See Compatibilità tra versioni for more information.
In the [libraries] section we set up the paths to the shared library on
different platforms. Here there's only the debug versions since that's what we
are working on for the example. Using feature tags you
can fine tune this to also provide release versions, add more target operating systems, as
well as providing 32-bit and 64-bit binaries.
In questo file è anche possibile aggiungere dipendenze a librerie e icone personalizzate per le classi, ma questo argomento è fuori dallo scopo di questo tutorial.
Dopo aver salvato il file, torna all'editor. Godot dovrebbe caricare automaticamente l'estensione. Non vedrai nulla perché la nostra estensione registra solo una nuova classe. Per utilizzare questa classe, aggiungi un Node2D come radice della scena. Spostalo al centro della viewport per una migliore visibilità. Quindi aggiungi un nuovo nodo figlio alla radice e nella finestra di dialogo Crea un nuovo nodo cerca "GDExample", il nome della nostra classe, poiché dovrebbe apparire nell'elenco. Se no, significa che Godot non ha caricato correttamente l'estensione, quindi prova a riavviare l'editor e a ripetere i passaggi per verificare se manca qualcosa.
La nostra classe personalizzata deriva da Sprite2D, quindi ha una proprietà Texture nell'Ispettore. Impostala sul file icon.svg che Godot ha creato comodamente per noi alla creazione del progetto. Salva questa scena come main.tscn ed eseguila. Per comodità, puoi impostarla come scena principale.
Voilà! We have a custom node running in Godot. However, it does not do anything
and has nothing different than a regular Sprite2D node. We will fix that next by
adding custom methods and properties.
Metodi personalizzati
Una pratica comune nelle estensioni è creare metodi per le classi personalizzate e esporli all'API di Godot. Creeremo un paio di metodi getter e setter necessari per l'eventuale binding delle proprietà più avanti.
Innanzitutto, aggiungiamo i nuovi campi alla nostra struct per memorizzare i valori di amplitude e speed, che useremo in seguito mentre creeremo il comportamento del nodo. Aggiungiamoli al file gdexample.h, modificando la struct GDExample:
...
typedef struct
{
// Public properties.
double amplitude;
double speed;
// Metadata.
GDExtensionObjectPtr object; // Stores the underlying Godot object.
} GDExample;
...
Nello stesso file, aggiungi la dichiarazione dei metodi getter e setter, subito dopo il distruttore.
...
// Destructor for the node.
void gdexample_class_destructor(GDExample *self);
// Properties.
void gdexample_class_set_amplitude(GDExample *self, double amplitude);
double gdexample_class_get_amplitude(const GDExample *self);
void gdexample_class_set_speed(GDExample *self, double speed);
double gdexample_class_get_speed(const GDExample *self);
...
Nel file gdexample.c, inizializzeremo questi valori nel costruttore e aggiungeremo le implementazioni per queste nuove funzioni, che sono piuttosto banali:
void gdexample_class_constructor(GDExample *self)
{
self->amplitude = 10.0;
self->speed = 1.0;
}
void gdexample_class_set_amplitude(GDExample *self, double amplitude)
{
self->amplitude = amplitude;
}
double gdexample_class_get_amplitude(const GDExample *self)
{
return self->amplitude;
}
void gdexample_class_set_speed(GDExample *self, double speed)
{
self->speed = speed;
}
double gdexample_class_get_speed(const GDExample *self)
{
return self->speed;
}
Per assicurare che queste semplici funzioni funzionino quando sono richiamate da Godot, avremo bisogno di alcuni wrapper che ci aiutino a convertire correttamente i dati da e verso il motore.
Innanzitutto, creeremo dei wrapper per ptrcall. Questo è ciò che Godot utilizza quando i tipi dei valori sono noti con precisione, evitando così l'uso di Variant. Ne serviranno due: uno per le funzioni che non accettano argomenti e restituiscono un double (per i getter) e un altro per le funzioni che accettano un singolo argomento double e non restituiscono nulla (per i setter).
Aggiungi le dichiarazioni al file api.h:
void ptrcall_0_args_ret_float(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret);
void ptrcall_1_float_arg_no_ret(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret);
Queste due funzioni seguono il tipo GDExtensionClassMethodPtrCall, come definito in gdextension_interface.h. Utilizziamo float come nome perché in Godot il tipo float ha doppia precisione, quindi manteniamo questa convenzione.
Poi implementiamo queste funzioni nel file api.c:
void ptrcall_0_args_ret_float(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret)
{
// Call the function.
double (*function)(void *) = method_userdata;
*((double *)r_ret) = function(p_instance);
}
void ptrcall_1_float_arg_no_ret(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret)
{
// Call the function.
void (*function)(void *, double) = method_userdata;
function(p_instance, *((double *)p_args[0]));
}
L'argomento method_userdata è un valore personalizzato che forniamo a Godot; in questo caso, lo imposteremo come puntatore alla funzione che vogliamo chiamare. Quindi, prima lo convertiamo nel tipo funzione, poi la chiamiamo semplicemente passando gli argomenti quando necessario, oppure impostando il valore di ritorno.
L'argomento p_instance contiene l'istanza personalizzata della nostra classe, che abbiamo fornito con object_set_instance() al momento della creazione dell'oggetto.
p_args è un array di argomenti. Si noti che contiene puntatori ai valori. Ecco perché lo dereferenziamo quando lo passiamo alle nostre funzioni. Il numero di argomenti sarà dichiarato al momento del binding della funzione (cosa che faremo a breve) e includerà sempre quelli predefiniti, se esistono.
Infine, r_ret è un puntatore alla variabile in cui deve essere impostato il valore di ritorno. Come gli argomenti, avrà il tipo corretto come dichiarato. Per la funzione che non restituisce nulla, dobbiamo evitare di impostarlo.
Si noti come il tipo e il numero di argomenti siano esatti, quindi se avessimo bisogno di tipi diversi, ad esempio, dovremmo creare più wrapper. Sarebbe possibile automatizzarlo tramite generazione di codice, ma ciò è fuori dallo scopo di questo tutorial.
Sebbene le funzioni ptrcall siano utilizzate quando i tipi sono esatti, a volte Godot non può saperlo (quando la chiamata proviene da un linguaggio a tipizzazione dinamica, come GDScript). In queste situazioni utilizza le normali funzioni call, quindi dobbiamo fornirle anche noi al momento del binding.
Creiamo due nuovi wrapper nel file api.h:
void call_0_args_ret_float(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionVariantPtr r_return, GDExtensionCallError *r_error);
void call_1_float_arg_no_ret(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionVariantPtr r_return, GDExtensionCallError *r_error);
Questi seguono il tipo GDExtensionClassMethodCall, che è leggermente diverso. Innanzitutto, si ricevono puntatori a Variant invece di tipi esatti. C'è anche il numero di argomenti e una struct di errore che è possibile impostare se qualcosa va male.
Per controllare il tipo e interagire con Variant, avremo bisogno di alcune funzioni in più dall'API GDExtension. Quindi espandiamo le nostre struct wrapper:
extern struct Constructors {
...
GDExtensionVariantFromTypeConstructorFunc variant_from_float_constructor;
GDExtensionTypeFromVariantConstructorFunc float_from_variant_constructor;
} constructors;
extern struct API
{
...
GDExtensionInterfaceGetVariantFromTypeConstructor get_variant_from_type_constructor;
GDExtensionInterfaceGetVariantToTypeConstructor get_variant_to_type_constructor;
GDExtensionInterfaceVariantGetType variant_get_type;
} api;
I nomi stessi spiegano la loro funzione. Abbiamo un paio di costruttori per creare ed estrarre un valore in virgola mobile da e verso un Variant. Abbiamo anche un paio di funzioni ausiliari per accedere a questi costruttori, oltre a una funzione per ricavare il tipo di un Variant.
Recuperiamoli dall'API, come abbiamo fatto prima, modificando la funzione load_api() nel file api.c:
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
...
// API.
...
api.get_variant_from_type_constructor = (GDExtensionInterfaceGetVariantFromTypeConstructor)p_get_proc_address("get_variant_from_type_constructor");
api.get_variant_to_type_constructor = (GDExtensionInterfaceGetVariantToTypeConstructor)p_get_proc_address("get_variant_to_type_constructor");
api.variant_get_type = (GDExtensionInterfaceVariantGetType)p_get_proc_address("variant_get_type");
...
// Constructors.
...
constructors.variant_from_float_constructor = api.get_variant_from_type_constructor(GDEXTENSION_VARIANT_TYPE_FLOAT);
constructors.float_from_variant_constructor = api.get_variant_to_type_constructor(GDEXTENSION_VARIANT_TYPE_FLOAT);
...
}
Ora che li abbiamo impostati, possiamo implementare i nostri wrapper di chiamate nello stesso file:
void call_0_args_ret_float(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionVariantPtr r_return, GDExtensionCallError *r_error)
{
// Check argument count.
if (p_argument_count != 0)
{
r_error->error = GDEXTENSION_CALL_ERROR_TOO_MANY_ARGUMENTS;
r_error->expected = 0;
return;
}
// Call the function.
double (*function)(void *) = method_userdata;
double result = function(p_instance);
// Set resulting Variant.
constructors.variant_from_float_constructor(r_return, &result);
}
void call_1_float_arg_no_ret(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionVariantPtr r_return, GDExtensionCallError *r_error)
{
// Check argument count.
if (p_argument_count < 1)
{
r_error->error = GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS;
r_error->expected = 1;
return;
}
else if (p_argument_count > 1)
{
r_error->error = GDEXTENSION_CALL_ERROR_TOO_MANY_ARGUMENTS;
r_error->expected = 1;
return;
}
// Check the argument type.
GDExtensionVariantType type = api.variant_get_type(p_args[0]);
if (type != GDEXTENSION_VARIANT_TYPE_FLOAT)
{
r_error->error = GDEXTENSION_CALL_ERROR_INVALID_ARGUMENT;
r_error->expected = GDEXTENSION_VARIANT_TYPE_FLOAT;
r_error->argument = 0;
return;
}
// Extract the argument.
double arg1;
constructors.float_from_variant_constructor(&arg1, (GDExtensionVariantPtr)p_args[0]);
// Call the function.
void (*function)(void *, double) = method_userdata;
function(p_instance, arg1);
}
Queste funzioni sono un po' più lunghe, ma facili da seguire. Innanzitutto, verificano se il numero di argomenti è quello previsto e, se non, impostano la struct di errore e ritornano. Per quella con un solo parametro, verifica anche se il tipo di argomento è corretto. Questo è importante perché tipi non corrispondenti durante l'estrazione da Variant possono causare arresti anomali.
Poi procede all'estrazione dell'argomento attraverso il costruttore che abbiamo impostato prima. Quello senza argomenti, invece, imposta il valore restituito dopo aver chiamato la funzione. Si noti come utilizzino un puntatore a una variabile di tipo double, poiché è ciò che quei costruttori si aspettano.
Prima di poter effettivamente associare i nostri metodi, abbiamo bisogno di un modo per creare istanze di GDExtensionPropertyInfo. Sebbene potremmo farlo all'interno delle funzioni di binding che implementeremo dopo, è più semplice avere una funzione ausiliare, poiché ne avremo bisogno più volte, anche quando associamo le proprietà.
Creiamo queste due funzioni nel file api.h:
// Create a PropertyInfo struct.
GDExtensionPropertyInfo make_property(
GDExtensionVariantType type,
const char *name);
GDExtensionPropertyInfo make_property_full(
GDExtensionVariantType type,
const char *name,
uint32_t hint,
const char *hint_string,
const char *class_name,
uint32_t usage_flags);
void destruct_property(GDExtensionPropertyInfo *info);
La prima è una versione semplificata della seconda, poiché di solito non abbiamo bisogno di tutti gli argomenti per la proprietà e ci accontentiamo dei valori predefiniti. Abbiamo anche una funzione per distruggere l'oggetto PropertyInfo, poiché dobbiamo creare String e StringName che si devono rilasciare correttamente.
A proposito, abbiamo anche bisogno di un modo per creare e distruggere le stringhe, quindi aggiungeremo una funzionalità alle struct esistenti in questo stesso file. Faremo anche una nuova funzione API per associare il nostro metodo personalizzato.
extern struct Constructors
{
...
GDExtensionInterfaceStringNewWithUtf8Chars string_new_with_utf8_chars;
} constructors;
extern struct Destructors
{
...
GDExtensionPtrDestructor string_destructor;
} destructors;
extern struct API
{
...
GDExtensionInterfaceClassdbRegisterExtensionClassMethod classdb_register_extension_class_method;
} api;
Prima di implementarle, facciamo una breve sosta nel file defs.h e includiamo la dimensione del tipo String e un paio di enumerazioni:
// The sizes can be obtained from the extension_api.json file.
#ifdef BUILD_32
#define STRING_SIZE 4
#define STRING_NAME_SIZE 4
#else
#define STRING_SIZE 8
#define STRING_NAME_SIZE 8
#endif
...
typedef struct
{
uint8_t data[STRING_SIZE];
} String;
// Enums.
typedef enum
{
PROPERTY_HINT_NONE = 0,
} PropertyHint;
typedef enum
{
PROPERTY_USAGE_NONE = 0,
PROPERTY_USAGE_STORAGE = 2,
PROPERTY_USAGE_EDITOR = 4,
PROPERTY_USAGE_DEFAULT = PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR,
} PropertyUsageFlags;
Sebbene abbia la stessa dimensione di StringName, è più chiaro utilizzare un nome diverso.
Le enumerazioni qui esistono solo per dare un nome ai numeri che rappresentano. Le informazioni su di essi sono presenti nel file extension_api.json. Qui impostiamo solo quelli necessari per il tutorial, per renderlo più conciso.
Passando ora al file api.c, dobbiamo caricare i puntatori alle nuove funzioni che abbiamo aggiunto all'API.
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
...
// API
...
api.classdb_register_extension_class_method = (GDExtensionInterfaceClassdbRegisterExtensionClassMethod)p_get_proc_address("classdb_register_extension_class_method");
// Constructors.
...
constructors.string_new_with_utf8_chars = (GDExtensionInterfaceStringNewWithUtf8Chars)p_get_proc_address("string_new_with_utf8_chars");
// Destructors.
...
destructors.string_destructor = variant_get_ptr_destructor(GDEXTENSION_VARIANT_TYPE_STRING);
}
Possiamo poi implementare anche le funzioni per creare la struct PropertyInfo.
GDExtensionPropertyInfo make_property(
GDExtensionVariantType type,
const char *name)
{
return make_property_full(type, name, PROPERTY_HINT_NONE, "", "", PROPERTY_USAGE_DEFAULT);
}
GDExtensionPropertyInfo make_property_full(
GDExtensionVariantType type,
const char *name,
uint32_t hint,
const char *hint_string,
const char *class_name,
uint32_t usage_flags)
{
StringName *prop_name = api.mem_alloc(sizeof(StringName));
constructors.string_name_new_with_latin1_chars(prop_name, name, false);
String *prop_hint_string = api.mem_alloc(sizeof(String));
constructors.string_new_with_utf8_chars(prop_hint_string, hint_string);
StringName *prop_class_name = api.mem_alloc(sizeof(StringName));
constructors.string_name_new_with_latin1_chars(prop_class_name, class_name, false);
GDExtensionPropertyInfo info = {
.name = prop_name,
.type = type,
.hint = hint,
.hint_string = prop_hint_string,
.class_name = prop_class_name,
.usage = usage_flags,
};
return info;
}
void destruct_property(GDExtensionPropertyInfo *info)
{
destructors.string_name_destructor(info->name);
destructors.string_destructor(info->hint_string);
destructors.string_name_destructor(info->class_name);
api.mem_free(info->name);
api.mem_free(info->hint_string);
api.mem_free(info->class_name);
}
La versione semplificata di make_property() si limita a chiamare la versione più completa con alcuni argomenti predefiniti. Cosa questi valori significano è fuori dallo scopo di questo tutorial; consultare la pagina relativa alla classe Object per maggiori dettagli sui binding dei metodi e proprietà.
La versione completa è più complessa. Innanzitutto, crea oggetti di tipo String e StringName per i campi necessari, allocando memoria e chiamando i relativi costruttori. Poi crea una struct GDExtensionPropertyInfo e imposta tutti i campi con gli argomenti forniti. Infine, restituisce la struct creata.
La funzione destruct_property() è semplice: chiama i distruttori degli oggetti creati e libera la memoria ad essi allocata.
Torniamo ora al file di intestazione api.h per creare le funzioni che effettivamente assoceranno i metodi:
// Version for 0 arguments, with return.
void bind_method_0_r(
const char *class_name,
const char *method_name,
void *function,
GDExtensionVariantType return_type);
// Version for 1 argument, no return.
void bind_method_1(
const char *class_name,
const char *method_name,
void *function,
const char *arg1_name,
GDExtensionVariantType arg1_type);
Poi torniamo al file api.c per implementarle:
// Version for 0 arguments, with return.
void bind_method_0_r(
const char *class_name,
const char *method_name,
void *function,
GDExtensionVariantType return_type)
{
StringName method_name_string;
constructors.string_name_new_with_latin1_chars(&method_name_string, method_name, false);
GDExtensionClassMethodCall call_func = call_0_args_ret_float;
GDExtensionClassMethodPtrCall ptrcall_func = ptrcall_0_args_ret_float;
GDExtensionPropertyInfo return_info = make_property(return_type, "");
GDExtensionClassMethodInfo method_info = {
.name = &method_name_string,
.method_userdata = function,
.call_func = call_func,
.ptrcall_func = ptrcall_func,
.method_flags = GDEXTENSION_METHOD_FLAGS_DEFAULT,
.has_return_value = true,
.return_value_info = &return_info,
.return_value_metadata = GDEXTENSION_METHOD_ARGUMENT_METADATA_NONE,
.argument_count = 0,
};
StringName class_name_string;
constructors.string_name_new_with_latin1_chars(&class_name_string, class_name, false);
api.classdb_register_extension_class_method(class_library, &class_name_string, &method_info);
// Destruct things.
destructors.string_name_destructor(&method_name_string);
destructors.string_name_destructor(&class_name_string);
destruct_property(&return_info);
}
// Version for 1 argument, no return.
void bind_method_1(
const char *class_name,
const char *method_name,
void *function,
const char *arg1_name,
GDExtensionVariantType arg1_type)
{
StringName method_name_string;
constructors.string_name_new_with_latin1_chars(&method_name_string, method_name, false);
GDExtensionClassMethodCall call_func = call_1_float_arg_no_ret;
GDExtensionClassMethodPtrCall ptrcall_func = ptrcall_1_float_arg_no_ret;
GDExtensionPropertyInfo args_info[] = {
make_property(arg1_type, arg1_name),
};
GDExtensionClassMethodArgumentMetadata args_metadata[] = {
GDEXTENSION_METHOD_ARGUMENT_METADATA_NONE,
};
GDExtensionClassMethodInfo method_info = {
.name = &method_name_string,
.method_userdata = function,
.call_func = call_func,
.ptrcall_func = ptrcall_func,
.method_flags = GDEXTENSION_METHOD_FLAGS_DEFAULT,
.has_return_value = false,
.argument_count = 1,
.arguments_info = args_info,
.arguments_metadata = args_metadata,
};
StringName class_name_string;
constructors.string_name_new_with_latin1_chars(&class_name_string, class_name, false);
api.classdb_register_extension_class_method(class_library, &class_name_string, &method_info);
// Destruct things.
destructors.string_name_destructor(&method_name_string);
destructors.string_name_destructor(&class_name_string);
destruct_property(&args_info[0]);
}
Entrambe le funzioni sono molto simili. Innanzitutto, creano una StringName con il nome del metodo. Questa viene creata nello stack poiché non è necessario conservarla dopo la fine della funzione. Successivamente, creano variabili locali per contenere call_func e ptrcall_func, che puntano alle funzioni ausiliari definite prima.
Nel passaggio successivo, le due funzioni si differenziano un po'. La prima crea una proprietà per il valore restituito, che ha un nome vuoto poiché non è necessario. L'altra crea un array di proprietà per gli argomenti, che in questo caso ha un singolo elemento. Anche questo ha un array di metadati, che può servire se c'è qualcosa di particolare nell'argomento (ad esempio, se un valore int è lungo 32 bit invece del valore predefinito di 64 bit).
In seguito, creano l'oggetto GDExtensionClassMethodInfo con i campi richiesti per ciascun caso. Quindi creano un oggetto StringName per il nome della classe, al fine di associare il metodo alla classe. Successivamente, chiamano la funzione dell'API per associare effettivamente questo metodo alla classe. Infine, distruggiamo gli oggetti creati poiché non sono più necessari.
Nota
Le funzioni ausiliari di binding qui utilizzate si basano sulle funzioni ausiliari di chiamate create prima, quindi tiene presente bene che queste accettano solo il tipo FLOAT di Godot (equivalente a double in C). Se intendi utilizzare questo approccio per altri tipi, è necessario verificare il tipo degli argomenti e il tipo restituito e selezionare una funzione di callback appropriata. Questo passaggio è stato omesso in questo esempio solo per evitare che diventi troppo lungo.
Ora che abbiamo i mezzi per associare i metodi, possiamo effettivamente farlo nella nostra classe personalizzata. Vai al file gdexample.c e implementa la funzione gdexample_class_bind_methods():
void gdexample_class_bind_methods()
{
bind_method_0_r("GDExample", "get_amplitude", gdexample_class_get_amplitude, GDEXTENSION_VARIANT_TYPE_FLOAT);
bind_method_1("GDExample", "set_amplitude", gdexample_class_set_amplitude, "amplitude", GDEXTENSION_VARIANT_TYPE_FLOAT);
bind_method_0_r("GDExample", "get_speed", gdexample_class_get_speed, GDEXTENSION_VARIANT_TYPE_FLOAT);
bind_method_1("GDExample", "set_speed", gdexample_class_set_speed, "speed", GDEXTENSION_VARIANT_TYPE_FLOAT);
}
Poiché questa funzione viene già chiamata durante il processo di inizializzazione, possiamo fermarci qui. Questa funzione è molto più semplice da implementare dopo aver creato tutta l'infrastruttura necessaria. Come poi vedere, implementare le funzioni di binding direttamente nel codice richiederebbe molto spazio e risulterebbe piuttosto ripetitivo. Facendo così, inoltre, sarà più facile aggiungere un altro metodo in futuro.
Se compili il codice e riapri il progetto Godot, inizialmente non noterai alcuna differenza, poiché abbiamo aggiunto solo due nuovi metodi. Per assicurarti che siano registrati correttamente, puoi cercare GDExample nella guida dell'editor e verificare che siano presenti nella pagina della documentazione.
Proprietà personalizzate
Ora che abbiamo già associato i metodi getter e setter per le nostre proprietà, possiamo procedere alla creazione di proprietà vere e proprie che saranno visualizzate nell'ispettore dell'editor Godot.
Considerata la configurazione dettagliata nella sezione precedente, basta solo qualche altra cosa per consentirci di associare le proprietà. Innanzitutto, aggiungiamo una nuova funzione di API al file api.h:
extern struct API {
...
GDExtensionInterfaceClassdbRegisterExtensionClassProperty classdb_register_extension_class_property;
} api;
Dichiariamo anche qui una funzione per associare le proprietà:
void bind_property(
const char *class_name,
const char *name,
GDExtensionVariantType type,
const char *getter,
const char *setter);
Nel file api.c possiamo caricare la nuova funzione API:
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
// API
...
api.classdb_register_extension_class_property = (GDExtensionInterfaceClassdbRegisterExtensionClassProperty)p_get_proc_address("classdb_register_extension_class_property");
...
}
Possiamo quindi implementare la nostra nuova funzione ausiliare nello stesso file:
void bind_property(
const char *class_name,
const char *name,
GDExtensionVariantType type,
const char *getter,
const char *setter)
{
StringName class_string_name;
constructors.string_name_new_with_latin1_chars(&class_string_name, class_name, false);
GDExtensionPropertyInfo info = make_property(type, name);
StringName getter_name;
constructors.string_name_new_with_latin1_chars(&getter_name, getter, false);
StringName setter_name;
constructors.string_name_new_with_latin1_chars(&setter_name, setter, false);
api.classdb_register_extension_class_property(class_library, &class_string_name, &info, &setter_name, &getter_name);
// Destruct things.
destructors.string_name_destructor(&class_string_name);
destruct_property(&info);
destructors.string_name_destructor(&getter_name);
destructors.string_name_destructor(&setter_name);
}
Questa funzione è simile a quella per i metodi di binding. La differenza principale è che non abbiamo bisogno di una struct aggiuntiva poiché possiamo usare la GDExtensionPropertyInfo che viene creata dalla nostra funzione ausiliare, quindi è più semplice. Crea solo i valori StringName dalle stringhe C, crea una struct di property info usando la nostra funzione ausiliare, chiama la funzione dell'API per registrare la proprietà nella classe e poi distrugge tutti gli oggetti che abbiamo creato.
Fatto ciò, possiamo estendere la funzione gdexample_class_bind_methods() nel file gdexample.c:
void gdexample_class_bind_methods()
{
bind_method_0_r("GDExample", "get_amplitude", gdexample_class_get_amplitude, GDEXTENSION_VARIANT_TYPE_FLOAT);
bind_method_1("GDExample", "set_amplitude", gdexample_class_set_amplitude, "amplitude", GDEXTENSION_VARIANT_TYPE_FLOAT);
bind_property("GDExample", "amplitude", GDEXTENSION_VARIANT_TYPE_FLOAT, "get_amplitude", "set_amplitude");
bind_method_0_r("GDExample", "get_speed", gdexample_class_get_speed, GDEXTENSION_VARIANT_TYPE_FLOAT);
bind_method_1("GDExample", "set_speed", gdexample_class_set_speed, "speed", GDEXTENSION_VARIANT_TYPE_FLOAT);
bind_property("GDExample", "speed", GDEXTENSION_VARIANT_TYPE_FLOAT, "get_speed", "set_speed");
}
Se compili l'estensione con scons, apparirà nell'editor di Godot la nuova proprietà non solo nella pagina di documentazione per la classe personalizzata, ma anche nel pannello Ispettore quando il nodo GDExample è selezionato.
Vincolare i metodi virtuali
Il nostro nodo personalizzato ora ha delle proprietà che ne influenzano il funzionamento, ma al momento non fa ancora nulla. In questa sezione, assoceremo il metodo virtuale _process() e faremo muovere un po' il nostro sprite personalizzato.
Nel file gdexample.h, aggiungiamo una funzione che rappresenta il metodo personalizzato _process():
// Methods.
void gdexample_class_process(GDExample *self, double delta);
Aggiungeremo anche un campo "privato" per tenere traccia del tempo trascorso nella nostra struct personalizzata. Questo campo è "privato" solo nel senso che non sarà associato all'API di Godot, anche se è pubblico nel codice C, dato che il linguaggio non supporta i modificatori di accesso.
typedef struct
{
// Private properties.
double time_passed;
...
} GDExample;
Nel file sorgente corrispondente gdexample.c, dobbiamo inizializzare il nuovo campo nel costruttore:
void gdexample_class_constructor(GDExample *self)
{
self->time_passed = 0.0;
self->amplitude = 10.0;
self->speed = 1.0;
}
Poi possiamo creare l'implementazione più semplice per il metodo _process:
void gdexample_class_process(GDExample *self, double delta)
{
self->time_passed += self->speed * delta;
}
Per ora non farà altro che aggiornare il campo privato che abbiamo creato. Ci ritorneremo dopo che il metodo sarà stato correttamente associato.
I metodi virtuali sono leggermente diversi dai normali binding. Invece di registrare esplicitamente il metodo stesso, registreremo una funzione speciale che Godot chiamerà per verificare se un particolare metodo virtuale è implementato nella nostra estensione. Il motore passerà un StringName come argomento, quindi, seguendo lo spirito di questo tutorial, creeremo una funzione ausiliare per verificare se è uguale a una stringa C.
Aggiungiamo la dichiarazione al file api.h:
// Compare a StringName with a C string.
bool is_string_name_equal(GDExtensionConstStringNamePtr p_a, const char *p_b);
Aggiungeremo anche una nuova struct a questo file, per contenere i puntatori di funzioni per gli operatori personalizzati:
extern struct Operators
{
GDExtensionPtrOperatorEvaluator string_name_equal;
} operators;
Successivamente nel file api.c caricheremo il puntatore di funzione dall'API:
struct Operators operators;
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
// Get helper functions first.
...
GDExtensionInterfaceVariantGetPtrOperatorEvaluator variant_get_ptr_operator_evaluator = (GDExtensionInterfaceVariantGetPtrOperatorEvaluator)p_get_proc_address("variant_get_ptr_operator_evaluator");
...
// Operators.
operators.string_name_equal = variant_get_ptr_operator_evaluator(GDEXTENSION_VARIANT_OP_EQUAL, GDEXTENSION_VARIANT_TYPE_STRING_NAME, GDEXTENSION_VARIANT_TYPE_STRING_NAME);
}
Come puoi vedere, abbiamo bisogno di una nuova funzione ausiliare locale per recuperare il puntatore di funzione per l'operatore.
Con questo a disposizione, possiamo creare facilmente la nostra funzione di confronto nello stesso file:
bool is_string_name_equal(GDExtensionConstStringNamePtr p_a, const char *p_b)
{
// Create a StringName for the C string.
StringName string_name;
constructors.string_name_new_with_latin1_chars(&string_name, p_b, false);
// Compare both StringNames.
bool is_equal = false;
operators.string_name_equal(p_a, &string_name, &is_equal);
// Destroy the created StringName.
destructors.string_name_destructor(&string_name);
// Return the result.
return is_equal;
}
Questa funzione crea un oggetto StringName dall'argomento, lo confronta con un altro oggetto utilizzando il puntatore alla funzione operatore e restituisce il risultato. Si noti che il valore restituito dell'operatore viene passato come riferimento out; questa è una cosa comune nell'API.
Torniamo al file gdexample.h e aggiungiamo un paio di funzioni che saranno utilizzate come callback per l'API di Godot:
void *gdexample_class_get_virtual_with_data(void *p_class_userdata, GDExtensionConstStringNamePtr p_name);
void gdexample_class_call_virtual_with_data(GDExtensionClassInstancePtr p_instance, GDExtensionConstStringNamePtr p_name, void *p_virtual_call_userdata, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret);
In realtà esistono due modi per registrare i metodi virtuali. Solo uno prevede la parte get, in cui si fornisce a Godot un puntatore di funzione opportunamente strutturato che verrà chiamato. Per farlo dovremmo creare un altra funzione ausiliare per ogni metodo virtuale, il che non è molto pratico. Invece, utilizziamo il secondo metodo che ci permette di restituire qualsiasi dato, dopodiché Godot chiamerà un secondo callback che ci restituirà questi dati insieme alle informazioni sulla chiamata. Possiamo semplicemente fornire il nostro puntatore di funzione come dati personalizzati e quindi avere un unico callback per tutti i metodi virtuali. Sebbene in questo esempio lo useremo solo per un metodo, questo approccio è più semplice da espandere.
Implementiamo quindi queste due funzioni nel file gdexample.c:
void *gdexample_class_get_virtual_with_data(void *p_class_userdata, GDExtensionConstStringNamePtr p_name)
{
// If it is the "_process" method, return a pointer to the gdexample_class_process function.
if (is_string_name_equal(p_name, "_process"))
{
return (void *)gdexample_class_process;
}
// Otherwise, return NULL.
return NULL;
}
void gdexample_class_call_virtual_with_data(GDExtensionClassInstancePtr p_instance, GDExtensionConstStringNamePtr p_name, void *p_virtual_call_userdata, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret)
{
// If it is the "_process" method, call it with a helper.
if (p_virtual_call_userdata == &gdexample_class_process)
{
ptrcall_1_float_arg_no_ret(p_virtual_call_userdata, p_instance, p_args, r_ret);
}
}
Queste funzioni sono anche piuttosto semplici dopo aver creato tutte le funzioni ausiliari in precedenza.
Per la prima, controlliamo semplicemente se il nome della funzione richiesta è _process e, se sì, restituiamo un puntatore alla nostra implementazione di tale funzione. Altrimenti, restituiamo NULL, a indicare che il metodo non viene sovrascritto. Non utilizziamo p_class_userdata qui, poiché questa funzione è destinata a una sola classe e non abbiamo dati associati ad essa.
La seconda è simile. Se è il metodo _process(), utilizza il puntatore di funzione fornito per chiamare la funzione ausiliare ptrcall, passando gli argomenti della chiamata. Altrimenti, non fa nulla, poiché non abbiamo altri metodi virtuali implementati.
L'unica cosa che manca è usare quei callback quando la classe viene registrata. Vai al file init.c e modifica l'inizializzazione di class_info per includerli, sostituendo il valore NULL utilizzato in precedenza:
void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
...
GDExtensionClassCreationInfo2 class_info = {
...
.get_virtual_call_data_func = gdexample_class_get_virtual_with_data,
.call_virtual_with_data_func = gdexample_class_call_virtual_with_data,
...
};
...
}
Questo è sufficiente per associare il metodo virtuale. Se compili l'estensione ed esegui nuovamente il progetto Godot, la funzione _process() verrà chiamata. Non te ne accorgerai perché la funzione stessa non fa nulla di evidente. Lo risolviamo ora facendo muovere il nodo personalizzato seguendo uno schema.
Affinché il nostro nodo faccia qualcosa, dovremo chiamare i metodi di Godot. Non solo le funzioni dell'API GDExtension, come abbiamo fatto finora, ma i metodi veri e propri del motore, proprio come avremmo fatto in uno script. Questo naturalmente richiede un altro po' di configurazione.
Innanzitutto, aggiungiamo Vector2 al nostro file defs.h, così da poterlo usare nel nostro metodo:
// The sizes can be obtained from the extension_api.json file.
...
#ifdef REAL_T_IS_DOUBLE
#define VECTOR2_SIZE 16
#else
#define VECTOR2_SIZE 8
#endif
...
// Types.
...
typedef struct
{
uint8_t data[VECTOR2_SIZE];
} Vector2;
La definizione REAL_T_IS_DOUBLE è necessaria solo se la propria versione di Godot è stata compilata con il supporto per la doppia precisione, che non è il caso predefinito.
Ora, nel file api.h, aggiungeremo alcune cose alle struct dell'API, tra cui una nuova per contenere i metodi del motore da chiamare.
extern struct Constructors
{
...
GDExtensionPtrConstructor vector2_constructor_x_y;
} constructors;
...
extern struct Methods
{
GDExtensionMethodBindPtr node2d_set_position;
} methods;
extern struct API
{
...
GDExtensionInterfaceClassdbGetMethodBind classdb_get_method_bind;
GDExtensionInterfaceObjectMethodBindPtrcall object_method_bind_ptrcall;
} api;
Poi nel file api.c possiamo recuperare i puntatori di funzioni da Godot:
struct Methods methods;
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
// Get helper functions first.
...
GDExtensionInterfaceVariantGetPtrConstructor variant_get_ptr_constructor = (GDExtensionInterfaceVariantGetPtrConstructor)p_get_proc_address("variant_get_ptr_constructor");
// API.
...
api.classdb_get_method_bind = (GDExtensionInterfaceClassdbGetMethodBind)p_get_proc_address("classdb_get_method_bind");
api.object_method_bind_ptrcall = (GDExtensionInterfaceObjectMethodBindPtrcall)p_get_proc_address("object_method_bind_ptrcall");
// Constructors.
...
constructors.vector2_constructor_x_y = variant_get_ptr_constructor(GDEXTENSION_VARIANT_TYPE_VECTOR2, 3); // See extension_api.json for indices.
...
}
L'unica parte degna di nota qui è il costruttore Vector2, per il quale richiediamo l'indice 3. Poiché ci sono più costruttori con diversi tipi di argomenti, dobbiamo specificare quale vogliamo. In questo caso stiamo ottenendo quello che accetta due numeri float come coordinate x e y, da cui il nome. È possibile ricavare questo indice dal file extension_api.json. Nota che abbiamo anche bisogno di una nuova funzione ausiliare locale per ottenerlo.
Tieni presente che qui non otteniamo nulla per la struct dei metodi. Questo perché questa funzione viene chiamata troppo presto nel processo di inizializzazione, quindi le classi non saranno ancora state registrate correttamente.
Invece, useremo il callback a livello di inizializzazione per recuperarli mentre stiamo registrando la nostra classe personalizzata. Aggiungi questo al file init.c:
void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
if (p_level != GDEXTENSION_INITIALIZATION_SCENE)
{
return;
}
// Get ClassDB methods here because the classes we need are all properly registered now.
// See extension_api.json for hashes.
StringName native_class_name;
StringName method_name;
constructors.string_name_new_with_latin1_chars(&native_class_name, "Node2D", false);
constructors.string_name_new_with_latin1_chars(&method_name, "set_position", false);
methods.node2d_set_position = api.classdb_get_method_bind(&native_class_name, &method_name, 743155724);
destructors.string_name_destructor(&native_class_name);
destructors.string_name_destructor(&method_name);
...
}
Qui creiamo gli StringName per la classe e il metodo che vogliamo ottenere, quindi utilizziamo l'API GDExtension per recuperare il loro MethodBind, che è un oggetto che rappresenta il metodo associato. Otteniamo il metodo set_position da Node2D poiché è lì che è stato registrato, anche se lo useremo in un Sprite2D, una classe derivata.
Il numero apparentemente casuale utilizzato per ottenere il bind è in realtà un hash della firma del metodo. Questo permette a Godot di associare il metodo richiesto anche se in una futura versione di Godot la firma dovesse cambiare, fornendo un metodo di compatibilità che corrisponda a quanto richiesto. Questo è uno dei sistemi che consentono al motore di caricare estensioni create per versioni precedenti. È possibile ottenere il valore di questo hash dal file extension_api.json.
Con tutto ciò, possiamo finalmente implementare il nostro metodo personalizzato _process() nel file gdexample.c:
...
#include <math.h>
...
void gdexample_class_process(GDExample *self, double delta)
{
self->time_passed += self->speed * delta;
Vector2 new_position;
// Set up the arguments for the Vector2 constructor.
double x = self->amplitude + (self->amplitude * sin(self->time_passed * 2.0));
double y = self->amplitude + (self->amplitude * cos(self->time_passed * 1.5));
GDExtensionConstTypePtr args[] = {&x, &y};
// Call the Vector2 constructor.
constructors.vector2_constructor_x_y(&new_position, args);
// Set up the arguments for the set_position method.
GDExtensionConstTypePtr args2[] = {&new_position};
// Call the set_position method.
api.object_method_bind_ptrcall(methods.node2d_set_position, self->object, args2, NULL);
}
Dopo aver aggiornato il tempo trascorso, scalato dalla proprietà speed, vengono creati i valori x e y in base a tale valore, modulati anche dalla proprietà amplitude. Questo è ciò che genererà l'effetto ripetitivo. L'intestazione math.h è necessaria per le funzioni sin() e cos() utilizzate qui.
Poi imposta un array di argomenti per costruire un Vector2, seguito da una chiamata al costruttore. Imposta un altro array di argomenti e lo utilizza per chiamare il metodo set_position() tramite il bind acquisito in precedenza.
Poiché nulla qui alloca memoria, non è necessario effettuare operazioni di pulizia.
Ora possiamo ricompilare l'estensione e riaprire Godot. Anche nell'editor lo sprite personalizzato apparirà muoversi.
Prova a modificare le proprietà Speed e Amplitude e osserva come reagisce lo sprite.
Registrare ed emettere un segnale
Per completare questo tutorial, vediamo come registrare un segnale personalizzato ed emetterlo quando necessario. Come avrai intuito, avremo bisogno di alcuni puntatori di funzioni dall'API e di altre funzioni ausiliari.
Nel file api.h aggiungiamo due cose. Uno è una funzione di API per registrare un segnale, l'altro è una funzione ausiliare per incapsulare il binding del segnale.
extern struct API
{
...
GDExtensionInterfaceClassdbRegisterExtensionClassSignal classdb_register_extension_class_signal;
} api;
...
// Version for 1 argument.
void bind_signal_1(
const char *class_name,
const char *signal_name,
const char *arg1_name,
GDExtensionVariantType arg1_type);
In questo caso abbiamo solo una versione con un argomento, poiché è quella che useremo.
Passando al file api.c, possiamo caricare questo nuovo puntatore di funzione e implementare la funzione ausiliare:
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
// API.
...
api.classdb_register_extension_class_signal = (GDExtensionInterfaceClassdbRegisterExtensionClassSignal)p_get_proc_address("classdb_register_extension_class_signal");
...
}
void bind_signal_1(
const char *class_name,
const char *signal_name,
const char *arg1_name,
GDExtensionVariantType arg1_type)
{
StringName class_string_name;
constructors.string_name_new_with_latin1_chars(&class_string_name, class_name, false);
StringName signal_string_name;
constructors.string_name_new_with_latin1_chars(&signal_string_name, signal_name, false);
GDExtensionPropertyInfo args_info[] = {
make_property(arg1_type, arg1_name),
};
api.classdb_register_extension_class_signal(class_library, &class_string_name, &signal_string_name, args_info, 1);
// Destruct things.
destructors.string_name_destructor(&class_string_name);
destructors.string_name_destructor(&signal_string_name);
destruct_property(&args_info[0]);
}
Questa funzione è molto simile alla funzione per associare i metodi. La differenza principale è che non è necessario riempire un'altra struct, basta passare i nomi necessari e l'array di argomenti. Il 1 alla fine indica il numero di argomenti forniti dal segnale.
Con questo possiamo associare il segnale in gdexample.c:
void gdexample_class_bind_methods()
{
...
bind_signal_1("GDExample", "position_changed", "new_position", GDEXTENSION_VARIANT_TYPE_VECTOR2);
}
Per emettere un segnale, dobbiamo chiamare il metodo emit_signal() sul nostro nodo personalizzato. Poiché è una funzione vararg (il che significa che accetta un numero qualsiasi di argomenti), non possiamo usare ptrcall. Per effettuare una chiamata normale, dobbiamo creare delle Variant, il che richiede qualche passaggio in più.
Innanzitutto, nel file defs.h creiamo una definizione per Variant:
...
// The sizes can be obtained from the extension_api.json file.
...
#ifdef REAL_T_IS_DOUBLE
#define VARIANT_SIZE 40
#define VECTOR2_SIZE 16
#else
#define VARIANT_SIZE 24
#define VECTOR2_SIZE 8
#endif
...
// Types.
...
typedef struct
{
uint8_t data[VARIANT_SIZE];
} Variant;
Inizialmente impostiamo la dimensione di Variant insieme alla dimensione di Vector2 che abbiamo aggiunto prima. Quindi la utilizziamo per creare una struct opaca sufficiente a contenere i dati di Variant. Anche in questo caso, impostiamo la dimensione per le build a doppia precisione come alternative di riserva, in quanto le build ufficiali di Godot utilizzano la precisione singola.
La funzione emit_signal() sarà chiamata con due argomenti. Il primo è il nome del segnale da emettere e il secondo è l'argomento che passiamo alle connessioni del segnale, che è un Vector2 come dichiarato durante il binding. Quindi creeremo una funzione ausiliare che può chiamare un MethodBind con questi tipi. Anche se restituisce qualcosa (un codice di errore), non c'è bisogno di gestirlo, quindi per ora lo ignoreremo e basta.
Nel file api.h, stiamo aggiungendo alcune cose alle struct esistenti, oltre a una nuova funzione ausiliare per la chiamata:
extern struct Constructors
{
...
GDExtensionVariantFromTypeConstructorFunc variant_from_string_name_constructor;
GDExtensionVariantFromTypeConstructorFunc variant_from_vector2_constructor;
} constructors;
extern struct Destructors
{
..
GDExtensionInterfaceVariantDestroy variant_destroy;
} destructors;
...
extern struct Methods
{
...
GDExtensionMethodBindPtr object_emit_signal;
} methods;
extern struct API
{
...
GDExtensionInterfaceObjectMethodBindCall object_method_bind_call;
} api;
...
// Helper to call with Variant arguments.
void call_2_args_stringname_vector2_no_ret_variant(
GDExtensionMethodBindPtr p_method_bind,
GDExtensionObjectPtr p_instance,
const GDExtensionTypePtr p_arg1,
const GDExtensionTypePtr p_arg2);
Ora passiamo al file api.c per caricare questi nuovi puntatori di funzioni e implementare la funzione ausiliare.
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
// API.
...
api.object_method_bind_call = (GDExtensionInterfaceObjectMethodBindCall)p_get_proc_address("object_method_bind_call");
// Constructors.
...
constructors.variant_from_string_name_constructor = api.get_variant_from_type_constructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME);
constructors.variant_from_vector2_constructor = api.get_variant_from_type_constructor(GDEXTENSION_VARIANT_TYPE_VECTOR2);
// Destructors.
...
destructors.variant_destroy = (GDExtensionInterfaceVariantDestroy)p_get_proc_address("variant_destroy");
...
}
...
void call_2_args_stringname_vector2_no_ret_variant(GDExtensionMethodBindPtr p_method_bind, GDExtensionObjectPtr p_instance, const GDExtensionTypePtr p_arg1, const GDExtensionTypePtr p_arg2)
{
// Set up the arguments for the call.
Variant arg1;
constructors.variant_from_string_name_constructor(&arg1, p_arg1);
Variant arg2;
constructors.variant_from_vector2_constructor(&arg2, p_arg2);
GDExtensionConstVariantPtr args[] = {&arg1, &arg2};
// Add dummy return value storage.
Variant ret;
// Call the function.
api.object_method_bind_call(p_method_bind, p_instance, args, 2, &ret, NULL);
// Destroy the arguments.
destructors.variant_destroy(&arg1);
destructors.variant_destroy(&arg2);
destructors.variant_destroy(&ret);
}
Questa funzione ausiliare contiene del codice boilerplate, ma è piuttosto semplice. Configura i due argomenti all'interno di Variant allocati nello stack, quindi crea un array con puntatori a questi. Configura anche un altro oggetto Variant per mantenere il valore restituito, che non è necessario costruire poiché la chiamata si aspetta che non sia inizializzato.
Poi chiama il MethodBind attraverso l'istanza fornita e gli argomenti. Il NULL alla fine sarebbe un puntatore a una struct GDExtensionCallError. Questa può servire per gestire eventuali errori durante la chiamata alle funzioni (come argomenti errati). Per semplicità, non la gestiremo qui.
Infine, dobbiamo distruggere le Variant create. Sebbene tecnicamente la Variant Vector2 non richieda la distruzione, è più chiaro ripulire tutto.
Dobbiamo inoltre caricare il MethodBind, cosa che faremo nel file init.c, subito dopo aver caricato quello per il metodo set_position che abbiamo fatto prima:
void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
...
constructors.string_name_new_with_latin1_chars(&native_class_name, "Object", false);
constructors.string_name_new_with_latin1_chars(&method_name, "emit_signal", false);
methods.object_emit_signal = api.classdb_get_method_bind(&native_class_name, &method_name, 4047867050);
destructors.string_name_destructor(&native_class_name);
destructors.string_name_destructor(&method_name);
// Register class.
...
}
Si noti che qui riutilizziamo le variabili native_class_name e method_name, quindi non è necessario dichiararne di nuove.
Ora andiamo al file gdexample.h dove aggiungeremo un paio di campi:
typedef struct
{
// Private properties.
..
double time_emit;
..
// Metadata.
StringName position_changed; // For signal.
} GDExample;
Il primo memorizzerà il tempo trascorso dall'ultima emissione del segnale, poiché lo faremo a intervalli regolari. L'altro serve solo a conservare il nome del segnale in modo da non dover creare una nuova StringName ogni volta.
Nel file sorgente gdexample.c possiamo modificare il costruttore e il distruttore per gestire i nuovi campi:
void gdexample_class_constructor(GDExample *self)
{
...
self->time_emit = 0.0;
// Construct the StringName for the signal.
constructors.string_name_new_with_latin1_chars(&self->position_changed, "position_changed", false);
}
void gdexample_class_destructor(GDExample *self)
{
// Destruct the StringName for the signal.
destructors.string_name_destructor(&self->position_changed);
}
È importante distruggere la StringName per evitare perdite di memoria.
Ora possiamo aggiungere alla funzione gdexample_class_process() il codice per emettere effettivamente il segnale:
void gdexample_class_process(GDExample *self, double delta)
{
...
self->time_emit += delta;
if (self->time_emit >= 1.0)
{
// Call the emit_signal method.
call_2_args_stringname_vector2_no_ret_variant(methods.object_emit_signal, self->object, &self->position_changed, &new_position);
self->time_emit = 0.0;
}
}
Questo aggiorna il tempo trascorso dall'emissione del segnale e, se è superiore a un secondo, chiama la funzione emit_signal() sull'istanza attuale, passando come argomenti il nome del segnale e la nuova posizione.
Ora abbiamo finito con la nostra estensione GDExtension in C. Compilala nuovamente e riapri il progetto Godot nell'editor.
Nella pagina di documentazione di GDExample apparirà il nuovo segnale che abbiamo associato:
Per verificare che funzioni, aggiungiamo un piccolo script al nodo radice, padre del nostro nodo personalizzato, che stampi la posizione nell'output ogni volta che riceve il segnale:
extends Node2D
func _ready():
$GDExample.position_changed.connect(on_position_changed)
func on_position_changed(new_position):
prints("New position:", new_position)
Esegui il progetto e potrai osservare i valori stampati nel pannello Output nell'editor:
Conclusione
Questo tutorial mostra un'estensione basilare con metodi, proprietà e segnali personalizzati. Sebbene richieda una discreta quantità di boilerplate, è facilmente scalabile creando funzioni ausiliari per gestire le attività più seccanti.
Questo dovrebbe servire come una buona base per comprendere l'API GDExtension e come punto di partenza per creare generatori di binding personalizzati. Infatti, sarebbe possibile creare binding per C attraverso un tipo di generatore come questo, rendendo il codice effettivo più simile al file gdexample.c di questo esempio, che è piuttosto semplice e poco verboso.
Se desideri creare estensioni vere e proprie, è preferibile utilizzare i binding di C++, in quanto tolgono tutto il boilerplate dal codice. Consulta la documentazione di godot-cpp per vedere come potresti farlo.