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...
Exemplo de GDExtension C
Introdução
Este é um exemplo simples de como trabalhar com a GDExtension diretamente com código C. Note que a API não se destina a ser usada diretamente, portanto, isso definitivamente será bastante prolixo e exigirá muitas etapas, mesmo para um pequeno exemplo. No entanto, serve como referência para criar bindings para uma linguagem diferente. Ainda é possível usar a API diretamente se preferir, o que pode ser conveniente ao vincular apenas uma biblioteca de terceiros.
Neste exemplo, criaremos um nó personalizado que move um sprite na tela com base nos parâmetros do usuário. Embora seja muito simples, serve para mostrar como fazer algumas coisas com a GDExtension, como registrar classes personalizadas com métodos, propriedades e sinais. Isso dá uma visão sobre a API da GDExtension.
Configurando o projeto
Existem alguns pré-requisitos que você precisará:
um executável do Godot 4.2 (or posterior),
um compilador C,
SCons como ferramenta de compilação.
Como isso está usando a API diretamente, não há necessidade de usar o repositório godot-cpp.
Estrutura do arquivo
Para organizar nossos arquivos, vamos dividi-los principalmente em duas pastas:
gdextension_c_example/
|
+--project/ # game example/demo to test the extension
|
+--src/ # source code of the extension we are building
Também precisamos de uma cópia do cabeçalho gdextension_interface.h do código-fonte do Godot, que pode ser obtida diretamente do executável do Godot executando o seguinte comando:
godot --dump-gdextension-interface
Isso cria o cabeçalho na pasta atual, então você pode apenas copiá-lo para a pasta src no projeto de exemplo.
Por fim, há outra fonte de informação à qual precisamos nos referir, que é o arquivo JSON com a referência da API do Godot. Este arquivo não será usado diretamente pelo código; nós o usaremos apenas para extrair algumas informações manualmente.
Para obter este arquivo JSON, basta chamar o executável do Godot:
godot --dump-extension-api
O arquivo extension_api.json resultante será criado na pasta atual. Você pode copiar este arquivo para a pasta de exemplo para tê-lo em mãos.
Nota
Esta extensão tem como alvo o Godot 4.2, mas deve funcionar em versões posteriores também. Se você quiser ter como alvo uma versão mínima diferente, certifique-se de obter o cabeçalho e o JSON da versão do Godot que você tem como alvo.
Sistema de build
O uso de um sistema de compilação (buildsystem) torna nossa vida muito mais fácil ao lidar com código C. Por uma questão de conveniência, usaremos o SCons, pois é o mesmo que o próprio Godot usa.
O arquivo SConstruct a seguir é simples e compilará sua extensão para a plataforma atual que você está usando, seja ela Linux, macOS ou Windows. Esta será uma compilação não otimizada para fins de depuração. Ele também assume uma compilação de 64 bits, o que é relevante para algumas partes do código de exemplo. Fazer outros tipos de compilação e compilação cruzada está fora do escopo deste tutorial. Salve este arquivo na pasta raiz.
#!/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)
Isso incluirá todos os arquivos C na pasta src, então não precisamos alterar este arquivo ao adicionar novos arquivos de origem.
Inicializando a extensão
A primeira parte do código será responsável por inicializar a extensão. É isso que faz o Godot tomar conhecimento do que nossa GDExtension fornece, como classes e plugins.
Crie o arquivo init.h na pasta src, com o seguinte conteúdo:
#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);
As funções declaradas aqui têm as assinaturas esperadas pela API da GDExtension.
Observe a inclusão do arquivo defs.h. Este é um dos nossos auxiliares para simplificar a escrita do código da extensão. Por enquanto, ele conterá apenas a definição de GDE_EXPORT, uma macro que torna a função pública na biblioteca compartilhada para que o Godot possa chamá-la corretamente. Esta macro ajuda a abstrair o que cada compilador espera.
Crie o arquivo defs.h na pasta src com o seguinte conteúdo:
#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
Também incluímos alguns cabeçalhos padrão para facilitar as coisas. Agora só precisamos incluir defs.h e esses virão como um bônus.
Agora, vamos implementar os métodos que acabamos de declarar. Crie um arquivo chamado init.c na pasta src e adicione este código:
#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;
}
O que isso faz é configurar os dados de inicialização que o Godot espera. As funções para inicializar e desinicializar são definidas de modo que o Godot as chamará quando necessário. Também define o nível de inicialização, que varia por extensão. Como planejamos adicionar um nó personalizado, o nível SCENE é suficiente.
Preencheremos a função initialize_gdexample_module() mais tarde para registrar nossa classe personalizada.
Uma classe básica
Para fazer um nó real, primeiro criaremos uma struct C para conter dados e funções que atuarão como métodos. O plano é fazer com que este seja um nó personalizado que herda de Sprite2D.
Crie um arquivo chamado gdexample.h na pasta src com o seguinte conteúdo:
#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();
O que se destaca aqui é o campo object, que contém um ponteiro para o objeto do Godot, e a função gdexample_class_bind_methods(), que registrará os metadados de nossa classe personalizada (propriedades, métodos e sinais). Esta última não é inteiramente necessária, pois podemos fazê-lo ao registrar a classe, mas torna mais claro separar as responsabilidades e deixar nossa classe registrar seus próprios metadados.
O campo object é necessário porque nossa classe herdará de uma classe do Godot. Como não podemos herdá-la diretamente, pois não estamos interagindo com o código-fonte (e o C nem sequer tem classes), em vez disso, dizemos ao Godot para criar um objeto de um tipo que ele conhece e anexar nossa extensão a ele. Precisaremos da referência a tais objetos ao chamar métodos na classe pai, por exemplo.
Vamos criar a contraparte de código-fonte deste cabeçalho. Crie o arquivo gdexample.c na pasta src e adicione o seguinte código a ele:
#include "gdexample.h"
void gdexample_class_constructor(GDExample *self)
{
}
void gdexample_class_destructor(GDExample *self)
{
}
void gdexample_class_bind_methods()
{
}
Como ainda não temos nada para fazer com essas funções, elas permanecerão vazias por um tempo.
O próximo passo é registrar nossa classe. No entanto, para fazer isso, precisamos criar um StringName e, para isso, temos que obter uma função da API GDExtension. Como precisaremos disso algumas vezes e também precisaremos de outras coisas, vamos criar uma API wrapper para facilitar esse tipo de tarefa.
Uma API wrapper
Começaremos criando um arquivo api.h na pasta 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);
Este arquivo incluirá muitos outros auxiliares à medida que preenchemos nossa extensão com algo útil. Por enquanto, ele possui apenas um ponteiro para uma função que cria um StringName a partir de uma string C (na codificação Latin-1) e outro para destruir um StringName, que precisaremos usar para evitar o vazamento de memória, bem como a função para registrar uma classe, que é o nosso objetivo inicial.
Também mantemos uma referência ao class_library aqui. Isso é algo que o Godot nos fornece ao inicializar la extensão e precisaremos usá-lo ao registrar as coisas que criamos, para que o Godot saiba qual extensão está fazendo a chamada.
Há também uma função para carregar esses ponteiros de função da API GDExtension.
Vamos trabalhar na contraparte de código-fonte deste cabeçalho. Crie o arquivo api.c na pasta src, adicionando o seguinte código:
#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);
}
A primeira coisa importante aqui é o p_get_proc_address. Esta é uma função da API GDExtension que é passada durante a inicialização. Você pode usar essa função para solicitar funções específicas da API pelo nome delas. Aqui, estamos armazenando em cache (caching) os resultados para que não tenhamos que manter uma referência para p_get_proc_address em todos os lugares e possamos usar o nosso wrapper no lugar.
No início, solicitamos a função variant_get_ptr_destructor(). Ela não será usada fora desta função, portanto, não a adicionamos ao nosso wrapper e apenas a armazenamos em cache localmente. O cast é necessário para silenciar os avisos do compilador.
Em seguida, obtemos a função que cria um StringName a partir de uma string C, exatamente o que mencionamos antes como uma função necessária. Armazenamos isso na nossa struct constructors.
Depois, usamos a função variant_get_ptr_destructor() que acabamos de obter para consultar o destrutor do StringName, usando o valor enum da API gdextension_interface.h como parâmetro. Poderíamos obter destrutores para outros tipos de maneira semelhante, mas vamos nos limitar ao que é necessário para o exemplo.
Por fim, obtemos a função classdb_register_extension_class2(), que precisaremos para registrar nossa classe personalizada.
Nota
Você pode se perguntar por que o 2 está ali no nome da função. Isso significa que é a segunda versão desta função. A versão antiga é mantida para garantir a compatibilidade com versões anteriores de extensões mais antigas, mas como temos a segunda versão disponível, é melhor usar a nova, porque não pretendemos oferecer suporte a versões mais antigas do Godot neste exemplo.
O cabeçalho gdextension_interface.h documenta em qual versão do Godot cada função foi introduzida.
Também definimos a variável class_library aqui, que será configurada durante a inicialização.
Falando em inicialização, agora temos que alterar o arquivo init.c para preencher as coisas que acabamos de adicionar:
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);
...
Aqui, configuramos o class_library conforme necessário e chamamos nossa nova função load_api(). Não se esqueça de incluir também os novos cabeçalhos no topo deste arquivo:
#include "init.h"
#include "api.h"
#include "gdexample.h"
...
Já que estamos aqui, podemos registrar nossa nova classe personalizada. Vamos preencher a função 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);
}
A struct com as informações da classe é a maior parte aqui. Nenhum de seus campos é obrigatório, com exceção de create_instance_func e free_instance_func. Ainda não fizemos essas funções, então teremos que trabalhar nelas em breve. Note que pulamos a inicialização se ela não estiver no nível SCENE. Esta função pode ser chamada várias vezes, uma para cada nível, mas queremos apenas registrar nossa classe uma vez.
A outra coisa indefinida aqui é o StringName. Esta será uma struct opaca destinada a conter os dados de um StringName do Godot na nossa extensão. Vamos defini-la no arquivo apropriadamente nomeado 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;
Como mencionado no comentário, os tamanhos podem ser encontrados no arquivo extension_api.json que geramos anteriormente, sob a propriedade builtin_class_sizes. O BUILD_32 nunca é definido, pois assumimos que estamos trabalhando com uma compilação de 64 bits do Godot aqui, mas se precisar dele, você pode adicionar env.Append(CPPDEFINES=["BUILD_32"]) ao seu arquivo SConstruct.
O comentário // Types. prenuncia que adicionaremos mais tipos a este arquivo. Vamos deixar isso para mais tarde.
A struct StringName aqui serve apenas para conter os dados do Godot, então não nos importamos muito com o que está dentro dela. Embora, neste caso, seja apenas um ponteiro para os dados na heap. Usaremos essa struct quando precisarmos alocar dados para um StringName nós mesmos, como estamos fazendo ao registrar nossa classe.
Voltando ao registro, precisamos trabalhar nas nossas funções create e free. Vamos incluí-las em gdexample.h, já que são específicas para a classe personalizada:
...
// 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);
...
Before we can implement those functions, we'll need a few more things in our API.
We need a way to allocate and free memory. While we could do this with good ol'
malloc(), we can instead make use of Godot's memory management functions.
We'll also need a way to create a Godot object and set it with our custom
instance.
Então, vamos alterar o api.h para incluir essas novas funções:
...
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;
Em seguida, alteramos a função load_api() em api.c para capturar essas novas funções:
...
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");
}
Agora podemos voltar para gdexample.c e definir as novas funções, sem esquecer de incluir o cabeçalho 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);
}
Ao instanciar um objeto, primeiro criamos um novo objeto Sprite2D, já que esse é o pai da nossa classe. Em seguida, alocamos memória para nossa struct personalizada e chamamos seu construtor. Também salvamos o ponteiro para o objeto do Godot na struct, como mencionamos anteriormente.
Depois, definimos nossa struct personalizada como os dados da instância. Isso fará com que o Godot saiba que o objeto é uma instância da nossa classe personalizada e chame corretamente nossos métodos personalizados, por exemplo, além de passar esses dados de volta.
Note que retornamos o objeto do Godot que criamos, não nossa struct personalizada.
Para a função gdextension_free_instance(), apenas chamamos o destrutor e liberamos a memória que alocamos para os dados personalizados. Não é necessário destruir o objeto do Godot, pois isso será cuidado pelo próprio motor.
Um projeto de demonstração
Agora que podemos criar e liberar nosso objeto personalizado, devemos ser capazes de testá-lo em um projeto real. Para isso, você precisa abrir o Godot e criar um novo projeto na pasta project. O gerenciador de projetos pode avisar que a pasta não está vazia se você tiver compilado a extensão antes; você pode ignorar esse aviso com segurança desta vez.
Se você ainda não compilou a extensão, este é o momento de fazê-lo. Para isso, abra um terminal ou prompt de comando, navegue até a pasta raiz da extensão e execute scons. Deve compilar rapidamente, já que a extensão é bem simples.
Em seguida, crie um arquivo chamado gdexample.gdextension dentro da pasta project. Este é um recurso do Godot que descreve a extensão, permitindo que o motor a carregue corretamente. Coloque o seguinte conteúdo neste arquivo:
[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"
Como você pode ver, gdexample_library_init() é o mesmo nome da função que definimos no nosso arquivo init.c. É importante que os nomes coincidam porque é assim que o Godot chama o ponto de entrada da extensão.
Também definimos o mínimo de compatibilidade para 4.2, já que estamos visando essa versão. Ela ainda deve funcionar em versões posteriores. Se você estiver usando uma versão posterior do Godot e depender de novos recursos, precisará aumentar esse valor para um número de versão que tenha tudo o que você usa. Veja Compatibilidade de versões para mais informações.
Na seção [libraries], configuramos os caminhos para a biblioteca compartilhada em diferentes plataformas. Aqui há apenas as versões de depuração (debug), já que é nisso que estamos trabalhando para o exemplo. Usando tags de recursos, você pode ajustar isso para também fornecer versões de lançamento (release), adicionar mais sistemas operacionais de destino, bem como fornecer binários de 32 bits e 64 bits.
Você também pode adicionar dependências de biblioteca e ícones personalizados para suas classes neste arquivo, mas isso está fora do escopo deste tutorial.
Após salvar o arquivo, volte para o editor. O Godot deve carregar automaticamente a extensão. Nada será visto porque nossa extensão apenas registra uma nova classe. Para usar esta classe, adicione um Node2D como raiz da cena. Mova-o para o meio da viewport para melhor visibilidade. Em seguida, adicione um novo nó filho à raiz e, na caixa de diálogo Criar Novo Nó, pesquise por "GDExample", o nome da nossa classe, pois ela deve estar listada lá. Se não estiver, significa que o Godot não carregou a extensão corretamente, então tente reiniciar o editor e refazer os passos para ver se algo ficou faltando.
Nossa classe personalizada é derivada de Sprite2D, então ela possui uma propriedade Texture no Inspetor. Defina isso para o arquivo icon.svg que o Godot criou convenientemente para nós ao fazer o projeto. Salve esta cena como main.tscn e execute-a. Você pode querer defini-la como a cena principal por conveniência.
Voilà! Temos um nó personalizado rodando no Godot. No entanto, ele não faz nada e não tem nada de diferente de um nó Sprite2D regular. Vamos corrigir isso a seguir adicionando métodos e propriedades personalizados.
Métodos personalizados
Uma coisa comum em extensões é criar métodos para as classes personalizadas e expô-los para a API do Godot. Vamos criar um par de getters e setters que são necessários para vincular as propriedades posteriormente.
Primeiro, vamos adicionar os novos campos na nossa struct para conter os valores de amplitude e speed, que usaremos mais tarde ao criar o comportamento para o nó. Adicione-os ao arquivo gdexample.h, alterando a struct GDExample:
...
typedef struct
{
// Public properties.
double amplitude;
double speed;
// Metadata.
GDExtensionObjectPtr object; // Stores the underlying Godot object.
} GDExample;
...
No mesmo arquivo, adicione a declaração para os getters e setters, logo após o destrutor.
...
// 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);
...
No arquivo gdexample.c, inicializaremos esses valores no construtor e adicionaremos as implementações para essas novas funções, que são bastante triviais:
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;
}
Para fazer com que essas funções simples funcionem quando chamadas pelo Godot, precisaremos de alguns wrappers para nos ajudar a converter corretamente os dados de e para o motor.
Primeiro, criaremos wrappers para ptrcall. É isso que o Godot usa quando os tipos dos valores são conhecidos por serem exatos, o que evita o uso de Variant. Vamos precisar de dois deles: um para as funções que não recebem argumentos e retornam um double (para os getters) e outro para as funções que recebem um único argumento double e não retornam nada (para os setters).
Adicione as declarações ao arquivo 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);
Essas duas funções seguem o tipo GDExtensionClassMethodPtrCall, conforme definido em gdextension_interface.h. Usamos float como nome aqui porque no Godot o tipo float tem precisão dupla, então mantemos essa convenção.
Em seguida, implementamos essas funções no arquivo 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]));
}
O argumento method_userdata é um valor personalizado que damos ao Godot; neste caso, configuraremos como o ponteiro de função para aquela que queremos chamar. Então, primeiro o convertemos para o tipo de função e, em seguida, apenas o chamamos passando os argumentos quando necessário, ou configurando o valor de retorno.
O argumento p_instance contém a instância personalizada da nossa classe, que fornecemos com object_set_instance() ao criar o objeto.
p_args é uma matriz de argumentos. Note que isso contém ponteiros para os valores. É por isso que o desreferenciamos ao passar para as nossas funções. A quantidade de argumentos será declarada ao vincular a função (o que faremos em breve) e sempre incluirá os valores padrão, se existirem.
Por fim, o r_ret é un ponteiro para a variável onde o valor de retorno precisa ser configurado. Assim como os argumentos, será do tipo correto conforme declarado. Para a função que não retorna, temos que evitar configurá-lo.
Note como o tipo e as contagens de argumentos são exatos, portanto, se precisássemos de tipos diferentes, por exemplo, teríamos que criar mais wrappers. Isso poderia ser automatizado usando alguma geração de código, mas isso está fora do escopo deste tutorial.
Embora as funções ptrcall sejam usadas quando os tipos são exatos, às vezes o Godot não pode saber se esse é o caso (quando a chamada vem de uma linguagem dinamicamente tipada, como o GDScript). Nessas situações, ele usa funções call regulares, por isso precisamos fornecê-las também ao fazer a vinculação.
Vamos criar dois novos wrappers no arquivo 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);
Estes seguem o tipo GDExtensionClassMethodCall, que é um pouco diferente. Primeiro, você recebe ponteiros para Variants em vez de tipos exatos. Há também a quantidade de argumentos e uma struct de erro que você pode configurar se algo der errado.
Para verificar o tipo e também interagir com Variant, precisaremos de mais algumas funções da API GDExtension. Então, vamos expandir nossas structs 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;
Os nomes dizem tudo sobre o que eles fazem. Temos um par de construtores para criar e extrair um valor de ponto flutuante de e para um Variant. Também temos um par de auxiliares para realmente obter esses construtores, bem como uma função para descobrir o tipo de um Variant.
Vamos obtê-los da API, como fizemos antes, alterando a função load_api() no arquivo 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);
...
}
Agora que temos estes configurados, podemos implementar nossos wrappers de chamada no mesmo arquivo:
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);
}
Essas funções são um pouco mais longas, mas fáceis de acompanhar. Primeiro, elas verificam se a contagem de argumentos é a esperada e, se não for, configuram a struct de erro e retornam. Para a que possui um parâmetro, ela também verifica se o tipo do argumento está correto. Isso é importante porque tipos incompatíveis ao extrair de Variant podem causar travamentos.
Em seguida, ela prossegue para extrair o argumento usando o construtor que configuramos antes. A que não tem argumentos, em vez disso, configura o valor de retorno após chamar a função. Note como elas usam um ponteiro para uma variável double, já que é isso que esses construtores esperam.
Antes de podermos realmente vincular nossos métodos, precisamos de uma maneira de criar instâncias de GDExtensionPropertyInfo. Embora pudéssemos criá-las dentro das funções de vinculação que implementaremos posteriormente, é mais fácil ter um auxiliar para isso, já que precisaremos dele várias vezes, inclusive para quando vincularmos propriedades.
Vamos criar essas duas funções no arquivo 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);
A primeira é uma versão simplificada da segunda, já que geralmente não precisamos de todos os argumentos para a propriedade e estamos de acordo com os padrões. Depois, também temos uma função para destruir o PropertyInfo, pois precisamos criar Strings e StringNames que precisam ser descartados corretamente.
Falando nisso, também precisamos de uma maneira de criar e destruir Strings, então faremos uma adição às structs existentes neste mesmo arquivo. Também obteremos uma nova função de API para realmente vincular nosso método personalizado.
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;
Antes de implementá-las, vamos fazer uma rápida parada no arquivo defs.h e incluir o tamanho do tipo String e um par de enums:
// 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;
Embora seja o mesmo tamanho de StringName, é mais claro usar um nome diferente para ele.
Os enums aqui são apenas auxiliares para dar nomes aos números que representam. As informações sobre eles estão presentes no arquivo extension_api.json. Aqui, apenas configuramos os que precisamos para o tutorial, para mantê-lo mais conciso.
Indo agora para o api.c, precisamos carregar os ponteiros para as novas funções que adicionamos à 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);
}
Então, também podemos implementar as funções para criar a 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);
}
A versão simples de make_property() apenas chama a mais completa com alguns argumentos padrão. O que esses valores significam exatamente está fora do escopo deste tutorial, verifique a página sobre a classe Object para mais detalhes sobre a vinculação de métodos e propriedades.
A versão completa é mais complexa. Primeiro, ela cria String's e StringName's para os campos necessários, alocando memória e chamando seus construtores. Em seguida, cria uma struct GDExtensionPropertyInfo e define todos os campos com os argumentos fornecidos. Finalmente, retorna essa struct criada.
A função destruct_property() é direta: ela simplesmente chama os destrutores para os objetos criados e libera a memória alocada para eles.
Vamos voltar novamente ao cabeçalho api.h para criar as funções que realmente vincularão os métodos:
// 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);
Em seguida, mude de volta para o arquivo api.c para implementá-las:
// 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]);
}
Ambas as funções são muito semelhantes. Primeiro, elas criam um StringName com o nome do método. Isso é criado na pilha (stack), já que não precisamos mantê-lo após o término da função. Em seguida, criam variáveis locais para conter o call_func e o ptrcall_func, apontando para as funções auxiliares que definimos anteriormente.
No próximo passo, elas divergem um pouco. A primeira cria uma propriedade para o valor de retorno, que tem um nome vazio, já que não é necessário. A outra cria uma matriz de propriedades para os argumentos, que neste caso possui um único elemento. Esta também possui uma matriz de metadados, que pode ser usada se houver algo especial sobre o argumento (por exemplo, se um valor int tiver 32 bits de comprimento em vez do padrão de 64 bits).
Depois, elas criam o GDExtensionClassMethodInfo com os campos obrigatórios para cada caso. Em seguida, fazem um StringName para o nome da classe, a fim de associar o método com a classe. A seguir, chamam a função da API para realmente vincular este método à classe. Finalmente, destruímos os objetos que criamos, já que eles não são mais necessários.
Nota
Os auxiliares de vinculação aqui usam os auxiliares de chamada que criamos anteriormente, portanto, observe que esses auxiliares de chamada aceitam apenas o tipo FLOAT do Godot (que é equivalente a double em C). Se você pretende usar isso para outros tipos, precisará verificar o tipo dos argumentos e o tipo de retorno e selecionar um callback de função apropriado. Isso é evitado aqui apenas para evitar que o exemplo se torne ainda mais longo.
Agora que temos os meios para vincular métodos, podemos realmente fazer isso na nossa classe personalizada. Vá para o arquivo gdexample.c e preencha a função 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);
}
Como esta função já está sendo chamada pelo processo de inicialização, podemos parar por aqui. Esta função é muito mais direta depois que criamos toda a infraestrutura para fazer isso funcionar. Você pode ver que implementar as funções de vinculação inline aqui ocuparia bastante espaço e também seria bastante repetitivo. Isso também facilita a adição de outro método no futuro.
Se você compilar o código e reabrir o projeto do Godot, nada será diferente a princípio, já que apenas adicionamos dois novos métodos. Para garantir que eles foram registrados corretamente, você pode pesquisar por GDExample na ajuda do editor e verificar se eles estão presentes na página de documentação.
Propriedades personalizadas
Como agora já temos o getter e o setter para nossas propriedades vinculados, podemos seguir em frente para criar propriedades reais que serão exibidas no inspetor do editor do Godot.
Dado o nosso extenso setup na seção anterior, restam apenas algumas coisas necessárias para nos permitir vincular propriedades. Primeiro, vamos obter uma nova função da API no arquivo api.h:
extern struct API {
...
GDExtensionInterfaceClassdbRegisterExtensionClassProperty classdb_register_extension_class_property;
} api;
Vamos também declarar uma função aqui para vincular propriedades:
void bind_property(
const char *class_name,
const char *name,
GDExtensionVariantType type,
const char *getter,
const char *setter);
No arquivo api.c, podemos carregar a nova função da 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");
...
}
Então, podemos implementar nossa nova função auxiliar neste mesmo arquivo:
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);
}
Esta função é semelhante à de vinculação de métodos. A principal diferença é que não precisamos de uma struct extra, pois podemos simplesmente usar o GDExtensionPropertyInfo que é criado pela nossa função auxiliar, então é mais direto. Ela apenas cria os valores StringName a partir das strings C, cria uma struct de informações de propriedade usando nosso auxiliar, chama a função da API para registrar a propriedade na classe e, em seguida, destrói todos os objetos que criamos.
Com isso feito, podemos estender a função gdexample_class_bind_methods() no arquivo 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 você compilar a extensão com scons, verá no editor do Godot a nova propriedade exibida não apenas na página de documentação da classe personalizada, mas também no painel do Inspetor quando o nó GDExample estiver selecionado.
Vinculando métodos virtuais
Nosso nó personalizado agora possui propriedades para influenciar como ele opera, mas ainda não faz nada. Nesta seção, vincularemos o método virtual _process() e faremos nosso sprite personalizado se mover um pouco.
No arquivo gdexample.h, vamos adicionar uma função que representa o método virtual _process():
// Methods.
void gdexample_class_process(GDExample *self, double delta);
Também adicionaremos um campo "privado" para acompanhar o tempo decorrido na nossa struct personalizada. Isso é "privado" apenas no sentido de que não será vinculado à API do Godot, embora seja público no lado do C, já que a linguagem carece de modificadores de acesso.
typedef struct
{
// Private properties.
double time_passed;
...
} GDExample;
No arquivo de origem correspondente gdexample.c, precisamos inicializar o novo campo no construtor:
void gdexample_class_constructor(GDExample *self)
{
self->time_passed = 0.0;
self->amplitude = 10.0;
self->speed = 1.0;
}
Então, podemos criar a implementação mais simples para o método _process:
void gdexample_class_process(GDExample *self, double delta)
{
self->time_passed += self->speed * delta;
}
Por enquanto, ele não fará nada além de atualizar o campo privado que criamos. Voltaremos a isso após o método estar devidamente vinculado.
Os métodos virtuais são um pouco diferentes das vinculações normais. Em vez de registrar explicitamente o método em si, registraremos uma função especial que o Godot chamará para perguntar se um método virtual específico está implementado na nossa extensão. O motor passará um StringName como argumento, portanto, seguindo o espírito deste tutorial, criaremos uma função auxiliar para verificar se ele é igual a uma string C.
Vamos adicionar a declaração ao arquivo api.h:
// Compare a StringName with a C string.
bool is_string_name_equal(GDExtensionConstStringNamePtr p_a, const char *p_b);
Também adicionaremos uma nova struct a este arquivo para conter ponteiros de função para operadores personalizados:
extern struct Operators
{
GDExtensionPtrOperatorEvaluator string_name_equal;
} operators;
Então, no arquivo api.c, nós carregaremos o ponteiro de função da 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);
}
Como você pode ver, precisamos de um novo auxiliar local aqui para pegar o ponteiro de função para o operador.
Com isso em mãos, podemos criar facilmente nossa função de comparação no mesmo arquivo:
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;
}
Esta função cria um StringName a partir do argumento, compara com o outro usando o ponteiro de função do operador e retorna o resultado. Note que o valor de retorno para o operador é passado como uma referência de saída (out reference), o que é comum na API.
Vamos voltar para o arquivo gdexample.h e adicionar algumas funções que serão usadas como os callbacks para a API do 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);
Na verdade, existem duas maneiras de registrar métodos virtuais. Apenas uma tem a parte get, na qual você dá ao Godot un ponteiro de função devidamente construído que será chamado. Para isso, precisaríamos criar outro auxiliar para cada método virtual, algo que não é muito conveniente. Em vez disso, usamos o segundo método que nos permite retornar quaisquer dados, e então o Godot chamará um segundo callback e nos devolverá esses dados junto com as informações da chamada. Podemos simplesmente dar nosso próprio ponteiro de função como dados personalizados e, em seguida, ter um único callback para todos os métodos virtuais. Embora neste exemplo só vamos usá-lo para um método, esta maneira é mais simples de expandir.
Então vamos implementar essas duas funções no arquivo 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);
}
}
Essas funções também são bastante diretas após fazer todos os auxiliares anteriormente.
Para a primeira, simplesmente verificamos se o nome da função solicitado é _process e, se for, retornamos um ponteiro de função para a nossa implementação dele. Caso contrário, retornamos NULL, sinalizando que o método não está sendo sobrescrito. Não usamos o p_class_userdata aqui, já que esta função é destinada a apenas uma classe e não temos nenhum dado associado a ela.
A segunda é semelhante. Se for o método _process(), ela usa o ponteiro de função fornecido para chamar o auxiliar ptrcall, passando os argumentos da chamada adiante. Caso contrário, ela simplesmente não faz nada, já que não temos nenhum outro método virtual sendo implementado.
A única coisa que falta é usar esses callbacks quando a classe for registrada. Vá para o arquivo init.c e altere a inicialização de class_info para incluí-los, substituindo o valor NULL usado anteriormente:
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,
...
};
...
}
Isso é suficiente para vincular o método virtual. Se você compilar a extensão e rodar o projeto Godot novamente, a função _process() será chamada. Você só não será capaz de notar, já que a função em si não faz nada visível. Resolveremos isso agora fazendo o nó personalizado se mover seguindo um padrão.
Para fazer nosso nó fazer coisas, precisaremos chamar métodos do Godot. Não apenas as funções da API GDExtension como temos feito até agora, mas métodos reais da engine, como faríamos com scripts. Isso naturalmente requer uma configuração extra.
Primeiro, vamos adicionar Vector2 ao nosso arquivo defs.h, para que possamos usá-lo em nosso método:
// 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;
A definição REAL_T_IS_DOUBLE só é necessária se a sua versão do Godot foi compilada com suporte a precisão dupla, o que não é o padrão.
Agora, no arquivo api.h, adicionaremos algumas coisas às structs da API, incluindo uma nova para armazenar os métodos da engine que serão chamados.
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;
Então no arquivo api.c podemos pegar os ponteiros de função do 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.
...
}
A única parte digna de nota aqui é o construtor do Vector2, para o qual solicitamos o índice 3. Como existem múltiplos construtores com diferentes tipos de argumentos, precisamos especificar qual deles queremos. Neste caso, estamos pegando o que aceita dois números float como coordenadas x e y, daí o nome. Este índice pode ser recuperado do arquivo extension_api.json. Note que também precisamos de um novo auxiliar local para obtê-lo.
Fique atento que não pegamos nada para a struct de métodos aqui. Isso ocorre porque esta função é chamada muito cedo no processo de inicialização, de modo que as classes ainda não estarão devidamente registradas.
Em vez disso, vamos usar o callback de nível de inicialização para pegar esses métodos quando estivermos registrando nossa classe personalizada. Adicione isto ao arquivo 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);
...
}
Aqui criamos StringName para a classe e o método que queremos obter, então usamos a API GDExtension para recuperar seu MethodBind, que é um objeto que representa o método vinculado. Obtemos o método set_position de Node2D já que é onde ele foi registrado, embora vamos usá-lo em um Sprite2D, uma classe derivada.
O número aparentemente aleatório para obter o vínculo é na verdade um hash da assinatura do método. Isso permite que o Godot corresponda ao método que você está solicitando, mesmo se em uma versão futura do Godot essa assinatura mudar, fornecendo um método de compatibilidade que corresponda ao que você está pedindo. Este é um dos sistemas que permitem que a engine carregue extensões feitas para versões anteriores. Você pode obter o valor deste hash no arquivo extension_api.json.
Com tudo isso, podemos finalmente implementar nosso método personalizado _process() no arquivo 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);
}
Após atualizar o tempo decorrido escalonado pela propriedade speed, ele cria valores x e y baseados nisso, também modulados pela propriedade amplitude. Isso é o que dará o efeito de padrão. O cabeçalho math.h é necessário para as funções sin() e cos() usadas aqui.
Then it sets up an array of arguments to construct a Vector2, followed by
calling the constructor. It sets up another array of arguments and uses it to
call the set_position() method via the bind we obtained previously.
Como nada aqui aloca memória, não há necessidade de limpeza.
Agora podemos compilar a extensão novamente e reabrir o Godot. Mesmo no editor você verá o sprite personalizado se movendo.
Tente alterar as propriedades Speed e Amplitude e veja como o sprite reage.
Registrando e emitindo um sinal
Para concluir este tutorial, vamos ver como você pode registrar um sinal personalizado e emiti-lo quando apropriado. Como você já deve ter adivinhado, precisaremos de mais alguns ponteiros de função da API e de mais funções auxiliares.
No arquivo api.h estamos adicionando duas coisas. Uma é uma função da API para registrar um sinal, a outra é uma função auxiliar para envolver a vinculação do sinal.
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);
Neste caso, temos apenas uma versão para um argumento, já que é o que vamos usar.
Movendo para o arquivo api.c, podemos carregar esse novo ponteiro de função e implementar o auxiliar:
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]);
}
Este é muito semelhante à função para vincular métodos. A principal diferença é que não precisamos preencher outra struct, apenas passamos os nomes necessários e a array de argumentos. O 1 no final significa a quantidade de argumentos que o sinal fornece.
Com isso, podemos vincular o sinal em gdexample.c:
void gdexample_class_bind_methods()
{
...
bind_signal_1("GDExample", "position_changed", "new_position", GDEXTENSION_VARIANT_TYPE_VECTOR2);
}
Para emitir um sinal, precisamos chamar o método emit_signal() em nosso nó personalizado. Como esta é uma função vararg (o que significa que ela aceita qualquer quantidade de argumentos), não podemos usar ptrcall. Para fazer uma chamada regular, temos que criar Variants, o que requer mais alguns passos de encanamento estrutural para ser feito.
Primeiro, no arquivo defs.h criamos uma definição para 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;
Primeiro definimos o tamanho do Variant junto com o tamanho do Vector2 que adicionamos antes. Depois usamos isso para criar uma struct opaca que é suficiente para conter os dados do Variant. Novamente, definimos o tamanho para compilações com precisão dupla como uma alternativa, já que por padrão as compilações oficiais do Godot usam precisão simples.
A função emit_signal() será chamada com dois argumentos. O primeiro é o nome do sinal a ser emitido e o segundo é o argumento que estamos passando para as conexões do sinal, que é um Vector2 conforme declaramos ao vinculá-lo. Então vamos criar uma função auxiliar que possa chamar um MethodBind com esses tipos. Embora ela retorne algo (um código de erro), não precisamos lidar com isso, então, por enquanto, vamos apenas ignorar.
Em api.h, estamos adicionando algumas coisas às structs existentes, além de uma nova função auxiliar para a chamada:
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);
Agora vamos mudar para o arquivo api.c para carregar esses novos ponteiros de função e implementar la função auxiliar.
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);
}
Esta função auxiliar tem algum código padrão (boilerplate), mas é bastante direta. Ela configura os dois argumentos dentro de Variants alocados na pilha, e então cria uma array com ponteiros para eles. Ela também configura outro Variant para guardar o valor de retorno, que não precisamos construir, já que a chamada espera que ele não esteja inicializado.
Em seguida, ela realmente chama o MethodBind usando a instância que fornecemos e os argumentos. O NULL no final seria um ponteiro para uma struct GDExtensionCallError. Isso pode ser usado para tratar potenciais erros ao chamar as funções (como argumentos errados). Por uma questão de simplicidade, não vamos lidar com isso aqui.
No final, precisamos destruir os Variants que criamos. Embora tecnicamente o do Vector2 não exija destruição, é mais claro limpar tudo.
Também precisamos carregar o MethodBind, o que faremos no arquivo init.c, logo após carregar o do método set_position que fizemos antes:
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.
...
}
Note que reutilizamos as variáveis native_class_name e method_name aqui, para que não precisemos declarar novas.
Agora vá para o arquivo gdexample.h onde vamos adicionar um par de campos:
typedef struct
{
// Private properties.
..
double time_emit;
..
// Metadata.
StringName position_changed; // For signal.
} GDExample;
O primeiro armazenará o tempo decorrido desde que o último sinal foi emitido, já que faremos isso em intervalos regulares. O outro é apenas para armazenar em cache o nome do sinal, para que não precisemos criar um novo StringName toda vez.
No arquivo de origem gdexample.c podemos alterar o construtor e o destrutor para lidar com os novos campos:
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 destruir o StringName para evitar vazamentos de memória.
Agora podemos adicionar à função gdexample_class_process() para realmente emitir o sinal:
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;
}
}
Isso atualiza o tempo decorrido para a emissão do sinal e, se for maior que um segundo, chama a função emit_signal() na instância atual, passando o nome do sinal e a nova posição como argumentos.
Agora terminamos com nossa GDExtension em C. Compile-a mais uma vez e reabra o projeto Godot no editor.
Na página de documentação do GDExample você pode ver o novo sinal que vinculamos:
To verificar se está funcionando, vamos adicionar um pequeno script ao nó raiz, pai do nosso nó personalizado, que imprime a posição na saída toda vez que recebe o sinal:
extends Node2D
func _ready():
$GDExample.position_changed.connect(on_position_changed)
func on_position_changed(new_position):
prints("New position:", new_position)
Execute o projeto e você poderá observar os valores sendo impressos na aba Output no editor:
Conclusão
Este tutorial mostra uma extensão básica com métodos, propriedades e sinais personalizados. Embora exija uma boa quantidade de código padrão, ele pode ser bem dimensionado criando funções auxiliares para lidar com as tarefas tediosas.
Isso deve servir como uma boa base para entender a API GDExtension e como um ponto de partida para criar geradores de vinculação personalizados. De fato, seria possível criar vinculações para C usando esse tipo de gerador, fazendo com que a codificação real se pareça mais com o arquivo gdexample.c deste exemplo, que é bastante direto e não muito prolixo.
Se você deseja criar extensões reais, é preferível usar as vinculações de C++ em vez disso, pois isso remove todo o código padrão do seu código. Verifique a documentação do godot-cpp para ver como você pode fazer isso.