Примеры GDNative C

Введение

Этот туториал познакомит вас с минимально необходимым для создания модулей GDNative. Это должно стать вашей отправной точкой в мир GDNative. Понимание этого урока поможет вам понять все, что будет после него.

Прежде чем мы начнем, вы можете загрузить исходный код примера, который мы описываем ниже, из репозитория GDNative-demos.

Этот пример проекта также содержит файл SConstruct, который немного облегчает компиляцию, но в этом туториале мы будем делать все вручную, чтобы понять процесс.

GDNative можно использовать для создания нескольких типов дополнений к Godot, используя такие интерфейсы, как PluginScript или ARVRInterfaceGDNative. В этом руководстве мы рассмотрим создание модуля NativeScript. NativeScript позволяет вам писать логику на C или C++ аналогично тому, как вы бы написали файл GDScript. Мы создадим C-эквивалент этого GDScript:

extends Reference

var data

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

func get_data():
    return data

В последующих уроках будут рассмотрены другие типы модулей GDNative и объяснено, когда и как использовать каждый из них.

Требования

Прежде чем мы начнем, вам понадобится несколько вещей:

  1. Исполняемый файл Godot для вашей выбранной версии.

  2. Компилятор языка Си. В Linux установите gcc или clang из менеджера пакетов. На macOS вы можете установить Xcode из Mac App Store. В Windows вы можете использовать Visual Studio 2015 или более позднюю версию, или MinGW-w64.

  3. Клон Git-репозитория godot-headers: это заголовки на C для публичного API Godot, используемого в GDNative.

Мы предлагаем вам создать специальную папку для этого проекта GDNative, открыть терминал в этой папке и выполнить команду:

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

Это позволит загрузить необходимые файлы в эту папку.

Совет

Если вы планируете использовать Git для своего проекта GDNative, вы также можете добавить godot-headers в качестве подмодуля Git.

Примечание

Репозиторий godot-headers имеет разные ветки. По мере развития Godot развивается и GDNative. Хотя мы стараемся сохранять совместимость между версиями, вы всегда должны собирать свой GDNative модуль на основе заголовков, соответствующих стабильной ветке Godot (например, 3.4) и, в идеале, актуальному релизу (например, 3.4.4-stable), который вы используете. Модули GDNative, собранные на основе старых версий заголовков Godot , могут работать с более новыми версиями движка, но не наоборот.

Ветка master репозитория godot-headers поддерживается в соответствии с веткой master Godot и поэтому содержит определения классов и структур GDNative, которые будут работать с последними разработанными сборками.

If you want to write a GDNative module for a stable version of Godot, look at the available Git tags (with git tags) for the one matching your engine version. In the godot-headers repository, such tags are prefixed with godot-, so you can e.g. checkout the godot-3.4.4-stable tag for use with Godot 3.4.4. In your cloned repository, you can do:

git checkout godot-3.4.4-stable

If a tag matching your stable release is missing for any reason, you can fall back to the matching stable branch (e.g. 3.4), which you would also check out with git checkout 3.4.

Если вы собираете Godot из исходного кода с собственными изменениями, влияющими на GDNative, вы можете найти обновленное определение классов и структур в <godotsource>/modules/gdnative/include

Исходный файл на С

Давайте начнем с написания нашего основного кода. В конечном итоге мы хотим получить файловую структуру, которая будет выглядеть примерно так:

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

Откройте Godot и создайте новый проект под названием "simple" рядом с вашим Git-клоном godot-headers. Это создаст папку simple и файл project.godot. Затем вручную создайте папку rc рядом с папкой simple и подпапку bin в папке simple.

Для начала посмотрим, что содержит наш файл simple.c. В нашем примере мы создаем один исходный файл на языке Си без заголовка, чтобы все было просто. Когда вы начинаете писать более крупные проекты, рекомендуется разбить проект на несколько файлов. Однако это выходит за рамки данного руководства.

Мы будем рассматривать исходный код по частям, поэтому все части ниже должны быть собраны в один большой файл. Каждый фрагмент будет объясняться по мере его добавления.

#include <gdnative_api_struct.gen.h>

#include <string.h>

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

Приведенный выше код включает в себя заголовок структуры GDNative API и стандартный заголовок, который мы будем использовать далее для операций со строками. Затем он определяет два указателя на две различные структуры. GDNative поддерживает большую коллекцию функций для обратного вызова в основной исполняемый файл Godot. Для того чтобы ваш модуль имел доступ к этим функциям, GDNative предоставляет вашему приложению структуру, содержащую указатели на все эти функции.

Чтобы сохранить модульность и простоту расширения этой реализации, основные функции доступны непосредственно через API структуру "core", но дополнительные функции имеют свои собственные "структуры GDNative", доступные через расширения.

В нашем примере мы обращаемся к одному из этих расширений, чтобы получить доступ к функциям, необходимым для NativeScript.

NativeScript ведет себя в Godot как любой другой сценарий. Поскольку API NativeScript довольно низкоуровневый, он требует от библиотеки более подробного описания многих вещей, чем в других скриптовых системах, таких как GDScript. Когда создается экземпляр NativeScript, вызывается конструктор, заданный библиотекой. Когда экземпляр уничтожается, выполняется заданный деструктор.

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

Это внешние объявления для функций, которые мы будем реализовывать для нашего объекта. Также необходимы конструктор и деструктор. Кроме того, у объекта будет один метод get_data.

Далее следует первая из точек входа, которые Godot будет вызывать при загрузке нашей динамической библиотеки. Все эти методы имеют префикс godot_ (вы можете изменить его позже), за которым следует их имя. gdnative_init - это функция, которая инициализирует нашу динамическую библиотеку. Godot передаст ей указатель на структуру, содержащую различные биты информации, которые мы можем найти полезными, среди которых указатели на структуры API.

Для любых дополнительных структур API нам нужно пройтись по массиву расширений и проверить тип расширения.

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

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

Далее следует gdnative_terminate, который вызывается перед выгрузкой библиотеки. Godot выгрузит библиотеку, когда ни один объект больше не будет ее использовать. Здесь вы можете выполнить любую очистку, которая вам может понадобиться. В нашем примере мы просто очистим указатели API.

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

Наконец, у нас есть nativescript_init, которая является самой важной функцией, которая нам понадобится. Эта функция будет вызываться Godot в процессе загрузки библиотеки GDNative и сообщает движку, какие объекты мы делаем доступными.

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

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

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

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

    godot_method_attributes attributes = { GODOT_METHOD_RPC_MODE_DISABLED };

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

Сначала мы сообщаем движку, какие классы реализованы, вызывая nativescript_register_class. Первым параметром здесь является указатель на дескриптор, переданный нам. Второй - имя класса нашего объекта. Третий - это тип объекта в Godot, от которого мы "наследуем"; это не настоящее наследование, но достаточно близкое. Наконец, четвертый и пятый параметры - это описания наших конструктора и деструктора.

Затем мы сообщаем Godot о наших методах (в данном случае об одном методе), вызывая nativescript_register_method для каждого метода нашего класса. В нашем случае это просто get_data. Первым параметром снова будет указатель нашего дескриптора. Второй - снова имя класса объекта, который мы регистрируем. Третий - имя нашей функции, как оно будет известно GDScript. Четвертый - это установка наших атрибутов (см. перечисление godot_method_rpc_mode в godot-headers/nativescript/godot_nativescript.h для возможных значений). Пятый и последний параметр - это описание того, какую функцию следует вызывать при вызове метода.

Описание структуры instance_method содержит указатель на саму функцию в качестве первого поля. Два других поля в этих структурах предназначены для указания пользовательских данных каждого метода. Второе - это поле method_data, которое передается при каждом вызове функции как аргумент p_method_data. Это полезно для повторного использования одной функции для различных методов, возможно, нескольких различных классов скриптов. Если значение method_data является указателем на память, которую необходимо освободить, третье поле free_func может содержать указатель на функцию, которая освободит эту память. Эта функция освобождения вызывается при выгрузке самого скрипта (не экземпляра!) (обычно во время выгрузки библиотеки).

Теперь пора приступить к работе над функциями нашего объекта. Сначала мы определим структуру, которую используем для хранения данных-членов экземпляра нашего класса GDNative.

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

Затем мы определяем наш конструктор. Все, что мы делаем в нашем конструкторе, это выделяем память для нашей структуры и заполняем ее некоторыми данными. Обратите внимание, что мы используем функции памяти Godot, чтобы память отслеживалась, а затем возвращаем указатель на нашу новую структуру. Этот указатель будет служить идентификатором нашего экземпляра в случае инстанцирования нескольких объектов.

Этот указатель будет передаваться в любую из наших функций, связанных с нашим объектом, как параметр p_user_data, и может использоваться как для идентификации нашего экземпляра, так и для доступа к его данным-членам.

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

    return user_data;
}

Наш деструктор вызывается, когда Godot завершает работу с нашим объектом, и мы освобождаем данные-члены нашего экземпляра.

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

И, наконец, мы реализуем нашу функцию get_data. Данные всегда отправляются и возвращаются в виде вариантов, поэтому, чтобы вернуть наши данные, которые являются строкой, нам сначала нужно преобразовать нашу строку C в строковый объект Godot, а затем скопировать этот строковый объект в вариант, который мы возвращаем.

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

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

    return ret;
}

Strings are heap-allocated in Godot, so they have a destructor which frees the memory. Destructors are named godot_TYPENAME_destroy. When a Variant gets created with a String, it references the String. That means that the original String can be "destroyed" to decrease the ref-count. If that does not happen the String memory will leak since the ref-count will never be zero and the memory never deallocated. The returned variant gets automatically destroyed by Godot.

Примечание

In more complex operations it can be confusing the keep track of which value needs to be deallocated and which does not. As a general rule: call godot_TYPENAME_destroy when a C++ destructor would be called instead. The String destructor would be called in C++ after the Variant was created, so the same is necessary in C.

The variant we return is destroyed automatically by Godot.

And that is the whole source code of our module.

Компиляция

We now need to compile our source code. As mentioned our example project on GitHub contains a SCons configuration that does all the hard work for you, but for our tutorial here we are going to call the compilers directly.

Assuming you are sticking to the folder structure suggested above, it is best to open a terminal session in the src folder and execute the commands from there. Make sure to create the bin folder before you proceed.

On Linux:

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

On macOS:

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

On Windows:

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

Примечание

On the Windows build you also end up with a libsimple.lib library. This is a library that you can compile into a project to provide access to the DLL. We get it as a byproduct and we do not need it :) When exporting your game for release this file will be ignored.

Creating the GDNativeLibrary (.gdnlib) file

With our module compiled, we now need to create a corresponding GDNativeLibrary resource with .gdnlib extension which we place alongside our dynamic libraries. This file tells Godot what dynamic libraries are part of our module and need to be loaded per platform.

We can use Godot to generate this file, so open the "simple" project in the editor.

Start by clicking the create resource button in the Inspector:

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

And select GDNativeLibrary:

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

You should see a contextual editor appear in the bottom panel. Use the "Expand Bottom Panel" button in the bottom right to expand it to full height:

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

General properties

In the Inspector, you have various properties to control loading the library.

If Load Once is enabled, our library is loaded only once and each individual script that uses our library will use the same data. Any variable you define globally will be accessible from any instance of your object you create. If Load Once is disabled, a new copy of the library is loaded into memory each time a script accesses the library.

If Singleton is enabled, our library is automatically loaded and a function called godot_gdnative_singleton is called. We'll leave that for another tutorial.

The Symbol Prefix is a prefix for our core functions, such as godot_ in godot_nativescript_init seen earlier. If you use multiple GDNative libraries that you wish to statically link, you will have to use different prefixes. This again is a subject to dive into deeper in a separate tutorial, it is only needed at this time for deployment to iOS as this platform does not like dynamic libraries.

Reloadable defines whether the library should be reloaded when the editor loses and gains focus, typically to pick up new or modified symbols from any change made to the library externally.

Platform libraries

The GDNativeLibrary editor plugin lets you configure two things for each platform and architecture that you aim to support.

The Dynamic Library column (entry section in the saved file) tells us for each platform and feature combination which dynamic library has to be loaded. This also informs the exporter which files need to be exported when exporting to a specific platform.

The Dependencies column (also dependencies section) tells Godot what other files need to be exported for each platform in order for our library to work. Say that your GDNative module uses another DLL to implement functionality from a 3rd party library, this is where you list that DLL.

For our example, we only built libraries for Linux, macOS and/or Windows, so you can link them in the relevant fields by clicking the folder button. If you built all three libraries, you should have something like this:

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

Saving the resource

We can then save our GDNativeLibrary resource as bin/libsimple.gdnlib with the Save button in the Inspector:

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

The file is saved in a text-based format and should have contents similar to this:

[general]

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

[entry]

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

[dependencies]

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

Creating the NativeScript (.gdns) file

With our .gdnlib file we've told Godot how to load our library, now we need to tell it about our "SIMPLE" object class. We do this by creating a NativeScript resource file with .gdns extension.

Like done for the GDNativeLibrary resource, click the button to create a new resource in the Inspector and select NativeScript:

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

The inspector will show a few properties that we need to fill. As Class Name we enter "SIMPLE" which is the object class name that we declared in our C source when calling godot_nativescript_register_class. We also need to select our .gdnlib file by clicking on Library and selecting Load:

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

Примечание

The Class Name must have the same spelling as the one given in godot_nativescript_init when registering the class.

Finally, click on the save icon and save this as bin/simple.gdns:

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

Now it's time to build our scene. Add a Control node to your scene as your root and call it main. Then add a Button and a Label as child nodes. Place them somewhere nice on screen and give your button a name.

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

Select the control node and attach a script to it:

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

Next link up the pressed signal on the button to your script:

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

Don't forget to save your scene, call it main.tscn.

Now we can implement our main.gd code:

extends Control

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

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

After all that, our project should work. The first time you run it Godot will ask you what your main scene is and you select your main.tscn file and presto:

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