Советы для разработчиков DLL

Возможности Game Maker могут быть серьезно расширены при помощи DLL-библиотек - весь наш сайт посвящен, по сути, именно этому. Однако разработка расширений GM - дело достаточно непростое из-за строгих ограничений, накладываемых языком GML. Если вы впервые создаете свою DLL для Game Maker (будь то какая-нибудь структура данных, враппер чужой библиотеки или даже полноценный 3D-движок), то советуем сначала прочитать этот гайд. Само собой, для создания DLL нужен хотя бы минимальный опыт программирования на компилируемых языках - в частности, необходимо умение работать с динамической памятью и указателями.

Типы данных

Поскольку GML имеет всего два базовых типа - число и строка - функции в DLL также должны использовать только эти типы, а именно double и char*. Для незнакомых с C-подобным синтаксисом поясним - это число с плавающей запятой (IEEE) двойной точности (в некоторых языках оно называется real) и указатель на массив 8-битных символов, заканчивающийся нулем (C-подобная строка).
При этом если в функции используется более 4 аргументов, то все они должны иметь тип double - иными словами, поддерживается не более 4 строковых аргументов на функцию. Если попытаться вызвать функцию, не соответствующую этому требованию, GM выдаст ошибку.
Функции могут возвращать значения double и char*. void-функции не поддерживаются. Если функция не должна возвращать полезное значение, можно просто вернуть ноль или единицу.

Cоглашения о вызовах

Поддерживаются соглашения о вызовах cdecl и stdcall - то есть, стандартное C-шное соглашение и соглашение в стиле Win32.

Разрядность

GM поддерживает только 32-битные DLL.

Кодировки

Строки в GM должны быть в кодировке ASCII, либо ANSI (Windows-1251). Для поддержки Юникода придется писать функции-конвертеры.

Объекты

Если вы пишете DLL на C++ или каком-то другом ООП-языке (например, Delphi), возникнет вопрос, как обмениваться с GM указателями на объекты (и вообще указателями на что-либо). Очевидно, что их нужно преобразовывать в double. Простейший выход - приводить указатели к double и обратно.

Код на стороне DLL:
extern "C" __declspec(dllexport) double CreateMyObject()
{
    MyObject* pMyObj = new MyObject();
    return (double)(int)pMyObj;
}

extern "C" __declspec(dllexport) double DeleteMyObject(double obj)
{
    MyObject* pMyObj = (MyObject*)(int)obj;
    delete pMyObj;
    return 1.0;
}
Код на стороне GML:
obj = CreateMyObject();
DeleteMyObject(obj);
Это эффективный, но не очень безопасный способ. Главная его проблема - отсутствие валидации указателей. То есть, для DLL нет простой возможности узнать, является ли полученный double корректным указателем на существующий в памяти объект. Как результат, попытка обратиться по некорректному указателю, если таковой был передан по ошибке, приведет к Access Violation - отлаживать такие баги бывает очень непросто. Тем не менее, очень многие DLL для Game Maker используют именно этот способ, и Xtreme3D в их числе.
Более безопасным будет хранить создаваемые объекты в каком-нибудь контейнере - например, динамическом массиве. Тогда вместо указателей можно передавать индексы объектов в массиве. А уже по индексу довольно просто проверить, существует ли объект в памяти или нет.
Можно также хранить объекты в хэш-таблице со строковыми индексами, но обычно это нежелательно, поскольку использование строк вводит вышеупомянутое ограничение на количество аргументов функции.

Дескриптор окна

Довольно часто библиотеке требуется узнать дескриптор окна Game Maker (HWND). Например, это необходимо для рендеринга в это окно. Дескриптор можно получить функцией window_handle():

Код на стороне GML:
MyDLLFunction(window_handle());
Код на стороне DLL:
extern "C" __declspec(dllexport) double MyDLLFunction(double winHandle)
{
    HWND hwnd = (HWND)(int)winHandle;

    // ...

    return 1.0;
}
Рендеринг

Если вы хотите рендерить в окно GM, следует предварительно отключить автоматическую отрисовку встроенного графического движка - в противном случае содержимое окна будет мерцать.

Код на стороне GML:
set_automatic_draw(false);

Возврат нескольких значений из функции

Поскольку GML не поддерживает составные структуры данных, вернуть из функции что-либо, кроме double и char*, напрямую не получится. Самое простое решение - сохранить значения в глобальные переменные и ввести функции, читающие их. Например, вот так может выглядеть код для передачи в GML элементов вектора:

Код на стороне DLL:
double x, y, z; // глобальные переменные

double GetVector(double index)
{
    if (cast(int)index == 0)      return x;
    else if (cast(int)index == 1) return y;
    else if (cast(int)index == 2) return z;
}
Для получения всех трех элементов придется вызвать GetVector три раза:

Код на стороне GML:
x = GetVector(0);
y = GetVector(1);
z = GetVector(2);
Конечно, злоупотребление глобальными переменными - не комильфо, поэтому лучше всего размещать такие данные в динамической памяти, в виде объектов, которые пользователь может создавать заранее, а затем использовать для получения нужных значений. Это особенно актуально для больших наборов данных, вроде массивов или матриц. В плане произвозительности подход с динамической памятью не будет уступать глобальным переменным, единственный минус - необходимость управлять долгоживущими объектами (ведь их невозможно эффективно создавать и уничтожать в каждом шаге игрового цикла). Эта проблема, в принципе, решаема - можно написать собственный специализированный аллокатор, который будет быстро создавать объекты внутри заранее выделенного большого участка памяти. Но это, к сожалению, непростая задача. Для работы с объектами одного размера (например, теми же матрицами) проще всего написать аллокатор-пул - просто создать заранее массив из множества объектов и помечать каждый элемент как занятый/незанятый. В этом случае работа аллокатора сводится к поиску первого незанятого элемента, а это алгоритм сложности O(N). Для матричных операций еще можно сделать аллокатор-стек, где элементы каждый раз добавляются в конец массива и удаляются с конца, в той же последовательности, в какой добавлялись. Такой метод хорошо подходит для вычислений, и работает он даже быстрее пула, но стековая нотация может быть слишком неудобна для программиста - в ней легко запутаться и допустить ошибки, которые непросто отлаживать.



Hosted by uCoz