Собственные модули на C++

Модули

Godot позволяет улучшать себя модульным путём. Новые модули могут быть созданы, а затем включены или выключены. Это позволяет добавлять новую функциональность в движок на любом уровне без модификации ядра, что позволяет разделить для использования или повторного использования разные модули.

Модули содержаться в под-директории modules/ системы сборки. Из коробки, существует много различных модулей, таких как GDScript (да он не часть базы движка), исполняемая среда Mono, модуль регулярных выражений, и другие. Многие новые модули которые желательны могут быть созданы и скомбинированы, и система сборки SCons делает это прозрачным.

Для чего это?

While it’s recommended that most of a game be written in scripting (as it is an enormous time saver), it’s perfectly possible to use C++ instead. Adding C++ modules can be useful in the following scenarios:

  • Связка внешней библиотеки с Godot (PhysX, FMOD, итд).
  • Оптимизация критических частей игры.
  • Добавление новой функциональности в движок и/или редактор.
  • Портирование существующей игры.
  • Написание целой, новой игры на C++ поскольку вы не можете жить без C++.

Создание нового модуля

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

Для создания нового модуля, первым шагом создайте директорию внутри modules/. Если вы хотите поддерживать модуль отдельно, вы можете назначить другую систему контроля версий(VCS) на модули и использовать её.

Для примера возьмём модуль который назовём «summator», и поместим его внутри дерева кода Godot (C:\godot ссылается на место где размещается коды Godot):

C:\godot> cd modules
C:\godot\modules> mkdir summator
C:\godot\modules> cd summator
C:\godot\modules\summator>

Внутри мы создадим простой класс для сложения суммы:

/* summator.h */

#ifndef SUMMATOR_H
#define SUMMATOR_H

#include "core/reference.h"

class Summator : public Reference {
    GDCLASS(Summator, Reference);

    int count;

protected:
    static void _bind_methods();

public:
    void add(int p_value);
    void reset();
    int get_total() const;

    Summator();
};

#endif // SUMMATOR_H

А затем файл cpp.

/* summator.cpp */

#include "summator.h"

void Summator::add(int p_value) {
    count += p_value;
}

void Summator::reset() {
    count = 0;
}

int Summator::get_total() const {
    return count;
}

void Summator::_bind_methods() {
    ClassDB::bind_method(D_METHOD("add", "value"), &Summator::add);
    ClassDB::bind_method(D_METHOD("reset"), &Summator::reset);
    ClassDB::bind_method(D_METHOD("get_total"), &Summator::get_total);
}

Summator::Summator() {
    count = 0;
}

Затем, новый класс нужно где-то зарегистрировать, для этого нужно создать еще два файла:

register_types.h
register_types.cpp

Со следующим наполнением:

/* register_types.h */

void register_summator_types();
void unregister_summator_types();
/* yes, the word in the middle must be the same as the module folder name */
/* register_types.cpp */

#include "register_types.h"

#include "core/class_db.h"
#include "summator.h"

void register_summator_types() {
    ClassDB::register_class<Summator>();
}

void unregister_summator_types() {
   // Nothing to do here in this example.
}

Далее, мы должны создать файл SCsub чтобы система сборки скомпилировала этот модуль:

# SCsub

Import('env')

env.add_source_files(env.modules_sources, "*.cpp") # Add all cpp files to the build

С несколькими файлами кода, вы также можете добавить каждый файл отдельно в список строк Python:

src_list = ["summator.cpp", "other.cpp", "etc.cpp"]
env.add_source_files(env.modules_sources, src_list)

Это открывает доступ к широким возможностям Python для собирания списка файлов используя циклы и логические выражения. Посмотрите в другие модули Godot для примеров.

Чтобы добавить директории включения(include) для компилятора, вы может добавить их в пути переменных сред:

env.Append(CPPPATH=["mylib/include"]) # this is a relative path
env.Append(CPPPATH=["#myotherlib/include"]) # this is an 'absolute' path

Если вы желаете добавить собственные флаги компиляции когда строите модуль, вы должны сначала клонировать env, чтобы он не добавлял флаги целиком в Godot (что вызовет ошибки). К примеру SCsub с собственными флагами:

# SCsub

Import('env')

module_env = env.Clone()
module_env.add_source_files(env.modules_sources, "*.cpp")
module_env.Append(CCFLAGS=['-O2']) # Flags for C and C++ code
module_env.Append(CXXFLAGS=['-std=c++11']) # Flags for C++ code only

И наконец, файл конфигурации для модуля, это просто файл скрипта питона который должен быть назван config.py:

# config.py

def can_build(env, platform):
    return True

def configure(env):
    pass

The module is asked if it’s OK to build for the specific platform (in this case, True means it will build for every platform).

Вот так. Надеемся это не было слишком сложным! Ваш модуль должен выглядеть примерно так:

godot/modules/summator/config.py
godot/modules/summator/summator.h
godot/modules/summator/summator.cpp
godot/modules/summator/register_types.h
godot/modules/summator/register_types.cpp
godot/modules/summator/SCsub

Затем вы можете сжать и раздать модуль всем желающим. При сборке под каждую платформу (инструкции в предыдущих разделах), ваш модуль будет включен.

Использование модуля

Теперь вы можете использовать ваш новый модуль из любого скрипта:

var s = Summator.new()
s.add(10)
s.add(20)
s.add(30)
print(s.get_total())
s.reset()

И вывод будет 60.

См.также

Предыдущий пример Summator был великолепен для маленьких, ручных модулей, но что если вы хотите использовать большую, внешнюю библиотеку? Перейдите в Связывание внешних библиотек для подробностей связки внешних библиотек.

Улучшение системы сборки для разработки

Итак мы определили чистый и простой SCsub что позволило нам добавить код в новый модуль как часть бинарного файла Godot.

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

However the trade-off is every single change means a full recompilation of the game. Even if SCons is able to detect and recompile only the file that have changed, finding such files and eventually linking the final binary is a long and costly part.

Решение для избежания таких расходов это сборка собственного модуля как общей(shared) библиотеки которая динамически загрузится при старте нашей игры.

# SCsub

Import('env')

sources = [
    "register_types.cpp",
    "summator.cpp"
]

# First, create a custom env for the shared library.
module_env = env.Clone()
module_env.Append(CCFLAGS=['-fPIC'])  # Needed to compile shared library
# We don't want godot's dependencies to be injected into our shared library.
module_env['LIBS'] = []

# Now define the shared library. Note that by default it would be built
# into the module's folder, however it's better to output it into `bin`
# next to the Godot binary.
shared_lib = module_env.SharedLibrary(target='#bin/summator', source=sources)

# Finally notify the main env it has our shared lirary as a new dependency.
# To do so, SCons wants the name of the lib with it custom suffixes
# (e.g. ".x11.tools.64") but without the final ".so".
# We pass this along with the directory of our library to the main env.
shared_lib_shim = shared_lib[0].name.rsplit('.', 1)[0]
env.Append(LIBS=[shared_lib_shim])
env.Append(LIBPATH=['#bin'])

Однажды скомпилировав, мы должны в конечном итоге создать папку bin содержащую бинарный файл``godot*`` и наш libsummator*.so. Хотя данный .so находится на в обычной директории (как /usr/lib), мы можем помочь нашему бинарному файлу найти его в среде выполнения через переменную среды LD_LIBRARY_PATH:

user@host:~/godot$ export LD_LIBRARY_PATH=`pwd`/bin/
user@host:~/godot$ ./bin/godot*

note: Pay attention you have to export the environ variable otherwise you won’t be able to play your project from within the editor.

Кроме того, было бы неплохо иметь возможность выбрать нужно ли скомпилировать ваш модуль как общую библиотеку (для разработки) или как часть бинарного файла Godot (для релиза). Для этого мы можем установить собственный флаг для SCons используя команду ARGUMENT:

# SCsub

Import('env')

sources = [
    "register_types.cpp",
    "summator.cpp"
]

module_env = env.Clone()
module_env.Append(CCFLAGS=['-O2'])
module_env.Append(CXXFLAGS=['-std=c++11'])

if ARGUMENTS.get('summator_shared', 'no') == 'yes':
    # Shared lib compilation
    module_env.Append(CCFLAGS=['-fPIC'])
    module_env['LIBS'] = []
    shared_lib = module_env.SharedLibrary(target='#bin/summator', source=sources)
    shared_lib_shim = shared_lib[0].name.rsplit('.', 1)[0]
    env.Append(LIBS=[shared_lib_shim])
    env.Append(LIBPATH=['#bin'])
else:
    # Static compilation
    module_env.add_source_files(env.modules_sources, sources)

Now by default scons command will build our module as part of Godot’s binary and as a shared library when passing summator_shared=yes.

Наконец вы можете даже ускорить сборку явно определяя ваш общий модуль как цель для команды scons:

user@host:~/godot$ scons summator_shared=yes platform=x11 bin/libsummator.x11.tools.64.so

Написание собственной документации

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

Существует много шагов для установки собственной документации в модуль:

  1. Создайте новую папку в корне модуля. Имя папки может быть любым, но мы используем doc_classes в этом разделе.

  2. Добавьте следующий кусочек кода в config.py:

    def get_doc_classes():
        return [
            "ClassName",
        ]
    
    def get_doc_path():
        return "doc_classes"
    

Метод get_doc_classes() необходим для системы сборки чтобы знать какие классы документации модуля должны быть слиты вместе, поскольку модуль может содержать несколько классов. Замените ClassName именем класса который вы хотите документировать. Если вам нужна документация для более чем одного класса, добавьте её также.

Метод get_doc_path() используется системой сборки для определения позиции документации. В нашем случае, они располагаются в папке doc_classes.

  1. Команда запуска:

    godot --doctool <path>
    

Это сбросит справку по API в определенный <path> в формате XML. Заметьте что вы должны настроить ваш PATH для нахождения бинарного файла Godot, и убедиться что вы имеете права доступа на запись. Если нет, вы столкнётесь с подобной ошибкой:

ERROR: Can't write doc file: docs/doc/classes/@GDScript.xml
   At: editor/doc/doc_data.cpp:956
  1. Получить сгенерированный файл документации из godot/doc/classes/ClassName.xml
  2. Скопируйте этот файл в doc_classes, опционально отредактируйте его, затем скомпилируйте движок.

Система сборки соберёт файлы документации из папки doc_classes и соединит их в базовые типы. После завершения процесса компиляции, документация будет доступна через встроенную систему документации движка.

Для содержания документации в актуальном состоянии, все что вам нужно делать это редактировать один из файлов ClassName.xml и компилировать движок.

Итоги

Запомните:

  • используйте GDCLASS для наследования, чтобы Godot мог распознать класс
  • используйте _bind_methods для связки ваших функций для скриптинга, и разрешите им работать как обратные вызовы для сигналов.

Но это еще не все, в зависимости от того что вы делаете, вы столкнётесь с некоторыми (надеемся позитивными) сюрпризами.

  • Если вы наследуете от class_Node (или другого наследника ноды, такой как Sprite), ваш новый класс будет показан в редакторе, в дереве наследований в диалоге «Add Node».
  • Если вы наследуете от class_Resource, он будет показан в списке ресурсов, и все ваши открытые свойства будут сериализованы во время загрузки/сохранения.
  • Следуя той же логике, вы можете расширить возможности Редактора и почти любой области движка.