Можно ли запустить DOOM в MetaTrader 5: DLL, рендеринг и ввод через MQL5?
Введение
В мире технологий существует любимая многими традиция, известная как «А запустится ли на этом DOOM?». С тех пор как id Software опубликовала исходный код оригинального движка DOOM 1993 года, программисты портировали его практически на всё, что только можно представить: тесты на беременность, банкоматы, тракторы, цифровые фотоаппараты и даже Minecraft. Идея проста: если у устройства есть экран и какой-либо способ ввода, кто-нибудь обязательно попытается запустить на нём DOOM. MetaTrader 5 с его окном графика, возможностями растрового рендеринга и поддержкой DLL — вполне достойный кандидат.
Эта статья не только о самой идее. Запуск DOOM внутри торгового терминала даёт практические навыки работы с MQL5. К ним относятся создание и интеграция пользовательских DLL для Windows, рендеринг пикселей на графиках в реальном времени с помощью ResourceCreate, обработка ввода с клавиатуры несмотря на ограничения MQL5, а также безопасное управление фоновыми потоками из советника. Независимо от того, планируете ли вы когда-нибудь запускать игру в терминале, описанные здесь методы напрямую применимы к пользовательским инструментам визуализации, мостам к внешним данным и любым проектам, требующим тесной интеграции между MQL5 и нативным кодом.
Мы рассмотрим следующее:
- Задача — что на самом деле требуется, чтобы запустить DOOM?
- Обзор архитектуры
- DLL-обёртка — встраивание DOOM в динамическую библиотеку
- Советник — рендеринг и ввод
- Сборка, развёртывание и запуск
- Результаты
- Заключение
Задача — что на самом деле требуется, чтобы запустить «DOOM»?
Для запуска DOOM на любой нестандартной платформе необходимо решить четыре задачи. Во-первых, вам нужен сам игровой движок — код, отвечающий за игровую логику, рендеринг, физику и искусственный интеллект. Оригинальный движок DOOM имеет открытый исходный код, но он был спроектирован как автономная программа, у которой есть собственный главный цикл, которая создаёт собственное окно и сама обрабатывает ввод. Мы не можем просто скомпилировать его и вызвать из MQL5. Во-вторых, вам нужна поверхность отображения — какой-то способ показать буфер кадра размером 320x200 пикселей, который DOOM отрисовывает в каждом кадре.
В MetaTrader 5 нет встроенного понятия буфера кадра, но есть растровые ресурсы, которые можно отображать на объектах графика. В-третьих, нужна обработка ввода — события клавиатуры должны поступать от клавиатуры пользователя в игровой движок. MQL5 поддерживает события нажатия клавиши, но, что особенно важно, не поддерживает события отпускания клавиши. И наконец, нужна фоновая многопоточность — игровой движок должен работать в собственном фоновом потоке, чтобы не блокировать основной поток пользовательского интерфейса MetaTrader.
Ни одна из этих проблем по отдельности не является непреодолимой, но их совместное решение в рамках чистой архитектуры требует тщательного проектирования. Выбранный нами подход разделяет проект на два компонента: DLL для Windows, которая содержит игровой движок и запускает его в фоновом потоке, и советник MQL5, который выступает как слой отображения и ввода.
Обзор архитектуры
Система состоит из двух компонентов, соединённых простым интерфейсом вызова функций:
Компонент 1: doomlib.dll — DLL для Windows, написанная на языке C. Она встраивает движок DOOM с помощью PureDOOM — исходного порта с одним заголовочным файлом и без зависимостей, специально разработанного для встраивания DOOM в другие приложения. PureDOOM предоставляет простой API: один раз вызывается doom_init(), в каждом кадре — doom_update(), после чего буфер кадра читается через doom_get_framebuffer(). Наша DLL оборачивает это в фоновый поток и предоставляет семь экспортируемых функций, которые может вызывать MQL5.
Компонент 2: DoomEA.mq5 — советник MQL5, который импортирует DLL, создаёт на графике графический объект "Метка-изображение" / OBJ_BITMAP_LABEL и запускает таймерный цикл с частотой 30 кадров в секунду. При каждом тике он получает последний кадр из DLL и передаёт его на график с помощью ResourceCreate. Он перехватывает ввод с клавиатуры через OnChartEvent и передаёт его в DLL. Для определения отпускания клавиши (что MQL5 изначально не поддерживает) выполняется опрос состояний клавиш с помощью функции GetAsyncKeyState из библиотеки user32.dll.
Поток данных в каждом кадре прост: фоновый поток DLL обновляет состояние движка и записывает пиксели в буфер кадра с двойной буферизацией. Таймер советника срабатывает, копирует передний буфер в массив MQL5 и вызывает ResourceCreate для обновления изображения на графике. Ввод идёт в обратном направлении: советник получает с графика события нажатия клавиш, преобразует коды виртуальных клавиш Windows в коды клавиш DOOM и помещает их в потокобезопасную очередь ввода DLL.

Рис. 1. Архитектурная схема, показывающая взаимодействие DLL и EA
DLL-обёртка — встраивание DOOM в динамическую библиотеку
DLL представляет собой один файл на языке C (doomlib.c), который включает PureDOOM.h и дополняет его многопоточностью, двойной буферизацией и экспортируемыми функциями. PureDOOM настраивается с помощью набора флагов препроцессора, которые включают возможности стандартной библиотеки, такие как malloc, файловый ввод-вывод и измерение времени:
/* PureDOOM configuration */ #define DOOM_IMPLEMENTATION #define DOOM_IMPLEMENT_MALLOC #define DOOM_IMPLEMENT_FILE_IO #define DOOM_IMPLEMENT_GETTIME #define DOOM_IMPLEMENT_EXIT #define DOOM_IMPLEMENT_PRINT #include "../PureDOOM.h"
После определения этих флагов PureDOOM использует стандартную библиотеку C для выделения памяти, доступа к файлам и измерения времени. На платформах, где эти заголовочные файлы недоступны (встроенные системы, микроволновые печи и т. д.), PureDOOM позволяет переопределить каждый из них с помощью пользовательских обратных вызовов. Для наших целей стандартных реализаций вполне достаточно.
Экспортируемый API
DLL экспортирует семь функций с использованием соглашения о вызовах __stdcall для совместимости с MQL5. Основной интерфейс намеренно сделан минималистичным:
| Функция | Назначение |
|---|---|
| DoomInit(wadDir) | Инициализация движка, настройка параметров по умолчанию, запуск Игрового потока |
| DoomShutdown() | Остановить игровой поток и освободить ресурсы |
| DoomGetFramebuffer(buf, len) | Скопировать текущий кадр в буфер, предоставленный вызывающей стороной |
| DoomGetScreenWidth() | Вернуть масштабированную ширину вывода (960 пикселей) |
| DoomGetScreenHeight() | Вернуть масштабированную высоту вывода (600 пикселей) |
| DoomKeyDown(key) | Поставить в очередь событие нажатия клавиши |
| DoomKeyUp(key) | Поставить в очередь событие отпускания клавиши |
Игровой поток
Самая важная часть DLL — фоновый поток. Движок DOOM не является потокобезопасным и должен работать в одном выделенном потоке. Наш игровой поток работает в тесном цикле: опустошает очередь ввода, обновляет движок, захватывает буфер кадра, преобразует и масштабирует его, а затем меняет буферы местами. Вот ядро этого цикла:
static DWORD WINAPI game_thread(LPVOID param) { (void)param; while (g_running) { DWORD t0 = timeGetTime(); /* Process pending input */ drain_input(); /* Tick the engine */ doom_update(); /* Grab framebuffer and convert into back buffer */ const unsigned char* fb = doom_get_framebuffer(4); if (fb) { convert_and_scale(fb, g_back); /* Swap front/back */ EnterCriticalSection(&g_frame_cs); { unsigned int* tmp = g_front; g_front = g_back; g_back = tmp; } LeaveCriticalSection(&g_frame_cs); } /* Sleep to maintain target FPS */ DWORD elapsed = timeGetTime() - t0; if (elapsed < FRAME_MS) Sleep(FRAME_MS - elapsed); } return 0; }
Вызов doom_get_framebuffer(4) запрашивает кадр в формате RGBA (4 байта на пиксель). Затем функция convert_and_scale выполняет две операции за один проход: переставляет байты из RGBA в ARGB (формат, который MetaTrader 5 ожидает для COLOR_FORMAT_ARGB_NORMALIZE) и применяет 3-кратное масштабирование методом ближайшего соседа с исходных 320x200 до 960x600. Двойная буферизация гарантирует, что EA всегда считывает полный, согласованный кадр: Игровой поток записывает данные в задний буфер и меняет указатели местами только под блокировкой CRITICAL_SECTION.
Преобразование формата пикселей
Это малозаметная, но критически важная деталь. DOOM выводит изображение в порядке байтов RGBA, однако ResourceCreate в MetaTrader 5 с COLOR_FORMAT_ARGB_NORMALIZE ожидает пиксели, упакованные как 0xAARRGGBB в значении uint32. При преобразовании считываются четыре байта каждого пикселя и заново упаковываются:
const unsigned char* p = src + (sy * DOOM_WIDTH + sx) * 4; unsigned int argb = ((unsigned int)p[3] << 24) | ((unsigned int)p[0] << 16) | ((unsigned int)p[1] << 8) | ((unsigned int)p[2]);
Здесь p[0] — красный, p[1] — зелёный, p[2] — синий, а p[3] — альфа-канал. Результат упаковывается следующим образом: в старшем байте — альфа-канал, затем красный, зелёный и синий — именно так, как ожидает MetaTrader. Неправильный порядок байтов — самая распространённая причина чёрного экрана или психоделической мешанины, поэтому этому стоит уделить внимание.
Загрузка WAD-файлов и работа со строками в MQL5
Одна важная деталь: MQL5 передаёт строки в DLL как широкие строки (UTF-16, wchar_t*), а не как char*. Функция DoomInit принимает путь к каталогу WAD как широкую строку и перед передачей в PureDOOM преобразует его в многобайтовую строку с помощью WideCharToMultiByte. Затем этот путь передаётся движку через пользовательское переопределение getenv: когда DOOM внутри запрашивает переменную окружения DOOMWADDIR, наша функция возвращает путь, полученный от советника:
static char g_wad_dir[1024] = {0}; static char* custom_getenv(const char* var) { if (strcmp(var, "DOOMWADDIR") == 0) return g_wad_dir; return getenv(var); }
Это позволяет избежать вызова SetCurrentDirectory, которая действует на уровне всего процесса и повлияла бы на весь MetaTrader 5, а также сохраняет разрешение пути к WAD чистым и изолированным.
Советник — рендеринг и обработка ввода
Именно в части проекта на MQL5 всё становится особенно интересным для разработчиков MQL5. Советник импортирует функции из двух DLL: нашей собственной библиотеки doomlib.dll для игрового движка и user32.dll для опроса состояния клавиатуры.
Импорт из DLL
#import "doomlib.dll" int DoomInit(string wadDir); void DoomShutdown(); int DoomGetFramebuffer(uint &buf[], int bufLen); int DoomGetScreenWidth(); int DoomGetScreenHeight(); void DoomKeyDown(int doomKey); void DoomKeyUp(int doomKey); #import #import "user32.dll" short GetAsyncKeyState(int vKey); #import
Обратите внимание, что функция DoomGetFramebuffer принимает массив uint по ссылке. MQL5 передаёт динамические массивы в DLL как указатели, поэтому на стороне C это приходит как unsigned int* — именно то, что ожидает наша DLL. Параметр bufLen сообщает DLL, сколько элементов содержит массив, обеспечивая дополнительную защиту от переполнения буфера.
Инициализация
В OnInit советник подготавливает график как чистую поверхность отображения, инициализирует DLL и настраивает конвейер рендеринга:
int OnInit() { // Построить таблицу сопоставления клавиш BuildKeyMap(); // Настроить график — скрыть весь торговый интерфейс ChartSetInteger(0, CHART_SHOW, false); ChartSetInteger(0, CHART_KEYBOARD_CONTROL, false); // Инициализировать движок DOOM string wadDir = TerminalInfoString(TERMINAL_DATA_PATH) + "\\MQL5\\Experts\\Doom"; if(!DoomInit(wadDir)) { Print("DOOM: Failed to initialize engine"); return INIT_FAILED; } // Запросить из DLL масштабированное разрешение g_screenW = DoomGetScreenWidth(); g_screenH = DoomGetScreenHeight(); g_totalPixels = g_screenW * g_screenH; ArrayResize(g_pixels, g_totalPixels); // Создать графический объект "Метка-изображение" / OBJ_BITMAP_LABEL и ресурс ObjectCreate(0, OBJ_NAME, OBJ_BITMAP_LABEL, 0, 0, 0); ObjectSetInteger(0, OBJ_NAME, OBJPROP_XDISTANCE, 0); ObjectSetInteger(0, OBJ_NAME, OBJPROP_YDISTANCE, 0); ObjectSetInteger(0, OBJ_NAME, OBJPROP_XSIZE, g_screenW); ObjectSetInteger(0, OBJ_NAME, OBJPROP_YSIZE, g_screenH); ObjectSetString(0, OBJ_NAME, OBJPROP_BMPFILE, RES_NAME); ResourceCreate(RES_NAME, g_pixels, g_screenW, g_screenH, 0, 0, g_screenW, COLOR_FORMAT_ARGB_NORMALIZE); // Запустить таймер с частотой 30 FPS EventSetMillisecondTimer(33); g_initialized = true; return INIT_SUCCEEDED; }
Здесь стоит отметить две вещи. Сначала мы устанавливаем CHART_SHOW в значение false, что скрывает все элементы графика (свечи, сетку, ценовую шкалу, отображение OHLC) и даёт нам чистый чёрный холст. Кроме того, мы отключаем CHART_KEYBOARD_CONTROL, чтобы MetaTrader не перехватывал нажатия клавиш для собственных сочетаний клавиш навигации по графику. Во-вторых, вместо использования CCanvas (класса холста в MQL5) мы работаем напрямую с ObjectCreate, чтобы создать графический объект "Метка-изображение", и с ResourceCreate, чтобы передавать в него данные пикселей. Так проще и быстрее — по сути, мы рассматриваем график как обычный буфер кадра.
Цикл рендеринга
Обработчик OnTimer работает с частотой примерно 30 кадров в секунду и отвечает за получение последнего кадра из DLL и его отображение на графике:
void OnTimer() { if(!g_initialized) return; // Опрашивать отпускания клавиш PollKeyStates(); // Получить буфер кадра из DLL if(DoomGetFramebuffer(g_pixels, g_totalPixels)) { ResourceCreate(RES_NAME, g_pixels, g_screenW, g_screenH, 0, 0, g_screenW, COLOR_FORMAT_ARGB_NORMALIZE); ChartRedraw(0); } }
Это и есть весь конвейер рендеринга — всего три строки реальной работы. DoomGetFramebuffer копирует последний кадр (уже масштабированный и преобразованный DLL в ARGB) в наш массив пикселей. ResourceCreate обновляет растровый ресурс новыми данными пикселей с использованием COLOR_FORMAT_ARGB_NORMALIZE, что сообщает MetaTrader, что массив содержит пиксели ARGB с предумноженной альфой. Наконец, ChartRedraw принудительно вызывает немедленное обновление экрана.
Решение проблемы отпускания клавиши
Это, пожалуй, самая интересная техническая проблема во всём проекте. MQL5 генерирует событие CHARTEVENT_KEYDOWN при нажатии клавиши, но соответствующего события CHARTEVENT_KEYUP не существует. DOOM требуется и то и другое: нажатие клавиши, чтобы начать двигаться вперёд, и отпускание клавиши, чтобы остановиться. Без событий отпускания клавиши ваш персонаж после однократного нажатия W бесконечно шёл бы вперёд.
Решение заключается в том, чтобы импортировать функцию GetAsyncKeyState из user32.dll и опрашивать фактическое состояние аппаратных клавиш при каждом тике таймера. Мы поддерживаем таблицу, сопоставляющую коды виртуальных клавиш Windows с кодами клавиш DOOM, а также булево значение, отслеживающее, считается ли каждая клавиша нажатой в текущий момент. При каждом тике таймера функция PollKeyStates проверяет все отслеживаемые клавиши:
void PollKeyStates() { int count = ArraySize(g_keymap); for(int i = 0; i < count; i++) { short state = GetAsyncKeyState(g_keymap[i].vk); bool isDown = (state & 0x8000) != 0; if(g_keyStates[i] && !isDown) { // Клавиша была нажата и теперь отпущена DoomKeyUp(g_keymap[i].doomKey); g_keyStates[i] = false; } } }
Функция GetAsyncKeyState возвращает состояние аппаратной клавиши в реальном времени. Старший бит (0x8000) указывает, удерживается ли клавиша в данный момент. Когда мы обнаруживаем, что клавиша, которую мы пометили как нажатую, больше не удерживается, мы отправляем соответствующее событие DoomKeyUp. Опрос выполняется с частотой 30 Гц (каждые 33 мс), что добавляет не более 33 мс задержки при отпускании клавиши — это совершенно незаметно при игровом тике DOOM с частотой 35 Гц. Этот обходной способ хорошо известен в сообществе MQL5 и применим к любому проекту, в котором требуется обнаружение отпускания клавиши.
Обработка нажатия клавиш
Для событий нажатия клавиш мы используем стандартный обработчик OnChartEvent. Когда срабатывает CHARTEVENT_KEYDOWN, мы преобразуем код виртуальной клавиши Windows в соответствующий код клавиши DOOM и передаём его в DLL:
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(!g_initialized) return; if(id == CHARTEVENT_KEYDOWN) { int vk = (int)lparam; int doomKey = VkToDoomKey(vk); if(doomKey != DOOM_KEY_UNKNOWN) { int idx = FindKeyIndex(vk); if(idx >= 0) g_keyStates[idx] = true; DoomKeyDown(doomKey); } } }
Функция VkToDoomKey выполняет поиск по таблице сопоставления клавиш, которая охватывает все буквы (A-Z, преобразуемые в строчные a-z), цифры (0-9), функциональные клавиши (F1-F12), клавиши со стрелками и модификаторы (Ctrl, Shift, Alt, Space, Enter, Escape, Tab, Backspace). Полная таблица сопоставления приведена в прилагаемом исходном коде.
Сборка, развертывание и запуск
Из соображений безопасности предварительно скомпилированный файл doomlib.dll не предоставляется. Следуйте приведённым ниже инструкциям, чтобы собрать DLL и запустить DOOM.
Предварительные требования:
DLL собирается с помощью CMake и MinGW-w64. Вам потребуется полная структура проекта, включающая PureDOOM.h (движок в виде одного заголовочного файла), doomlib.c (обёртка) и CMakeLists.txt. Все эти файлы включены в приложенный ZIP-архив. Репозиторий PureDOOM также доступен на GitHub.
Для сборки DLL также потребуется набор инструментов C. Установите CMake и MinGW-w64. После установки обеих программ приступайте к сборке DLL.
Сборка за 5 простых шагов:
- Скачайте и распакуйте ZIP-файл.
- Откройте папку «DLL».
- Щёлкните правой кнопкой мыши по пустому месту и выберите «Открыть в терминале» (при этом откроется PowerShell с текущим каталогом)
- После открытия PowerShell выполните следующие команды одну за другой:
cmake . -G "MinGW Makefiles"
cmake --build . --config Release В результате будет создан файл doomlib.dll, который затем нужно скопировать в папку MQL5\Libraries\. Если вы предпочитаете Visual Studio, замените генератор на «Visual Studio 17 2022» и добавьте -A x64. Главное требование: DLL должна быть 64-разрядной, если ваша установка MetaTrader 5 64-разрядная (что является стандартом для современных систем).
Получение doom1.wad:
В этой статье файл doom1.wadне прилагается. Этот файл критически важен для работы DOOM, так как в нём содержатся данные игры. Этот файл представляет собой свободно распространяемый shareware-WAD из DOOM, и вам следует загрузить собственную копию из репозитория PureDOOM на GitHub. Откройте doom1.wad in the PureDOOM repository, затем воспользуйтесь опцией GitHub Download raw file , чтобы скачать файл.
Сохраните файл под именем doom1.wad и поместите его в папку MQL5\Experts\Doom\. Ожидаемый полный путь: MQL5\Experts\Doom\doom1.wad.
Результаты

Рис. 2. Геймплей DOOM в MetaTrader 5
DOOM плавно работает в MetaTrader 5 при полном разрешении 960x600 (трёхкратное увеличение по сравнению с исходным разрешением 320x200). Игровой поток поддерживает стабильные 35 кадров в секунду на стороне движка, а обновление экрана советником со скоростью 30 кадров в секунду обеспечивает заметно плавный игровой процесс. Передвижение — клавишами WASD, стрельба — Ctrl, открытие дверей — E (или пробел), навигация по меню — всё работает как ожидается. В игру можно полноценно играть.
Однако стоит отметить несколько ограничений. Звука нет — в MetaTrader 5 отсутствует API для вывода звука, поэтому игра работает в беззвучном режиме. В этой версии поддержка мыши не реализована, хотя PureDOOM предоставляет функции для обработки ввода с мыши, и для этой цели можно было бы перехватывать события CHARTEVENT_MOUSE_MOVE. Некоторые сочетания клавиш, которые перехватывает MetaTrader (например, Alt+F4), не дойдут до советника. Это незначительные компромиссы для проекта, который по сути является демонстрацией концепции.
Заключение
Мы успешно продемонстрировали, что MetaTrader 5 действительно может запускать DOOM. Помимо развлекательной составляющей, этот проект демонстрирует несколько приёмов, полезных при разработке на MQL5:
- Интеграция пользовательской DLL. Сборка нативной DLL с экспортируемыми функциями и их импорт в MQL5 через #import. Этот подход применим к любой внешней библиотеке, для которой вы хотите построить мост к MetaTrader, — к движкам машинного обучения, пользовательским источникам данных, аппаратным интерфейсам и не только.
- Рендеринг в буфер кадра в реальном времени. Использование OBJ_BITMAP_LABEL и ResourceCreate с параметром COLOR_FORMAT_ARGB_NORMALIZE для вывода сырых пиксельных данных на график со скоростью 30 кадров в секунду. Это применимо к пользовательским тепловым картам, панелям визуализации в реальном времени или любым другим средствам отображения, выходящим за рамки стандартных графических объектов.
- Фоновая многопоточность из MQL5. Выполнение ресурсоёмкой задачи в фоновом потоке внутри DLL с потокобезопасным обменом данными через CRITICAL_SECTION. Это правильный подход для любых длительных вычислений, которые не должны блокировать поток интерфейса MetaTrader.
- Обходной способ обнаружения отпускания клавиши. Использование функции GetAsyncKeyState из user32.dll для определения отпускания клавиш, что позволяет обойти хорошо известное ограничение модели событий MQL5. Этот метод полезен для любого интерактивного советника, которому нужно реагировать как на события нажатия клавиш, так и на события отпускания клавиш.
Среди возможных улучшений для дальнейшего изучения можно отметить добавление поддержки управления обзором с помощью CHARTEVENT_MOUSE_MOVE, реализацию вывода звука через внешнюю аудиобиблиотеку в DLL, а также эксперименты с более высокими коэффициентами масштабирования или билинейной фильтрацией для более плавного изображения.
| Имя файла | Описание |
|---|---|
| DoomEA.mq5 | Исходный код советника — рендерит DOOM и обрабатывает ввод |
| doomlib.c | Исходный код обёртки DLL |
| CMakeLists.txt | Конфигурация сборки CMake |
| PureDOOM.h | Однозаголовочный движок DOOM от Daivuk |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21858
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Возможности Мастера MQL5, которые вам нужно знать (Часть 79): Использование осцилляторов Gator и A/D в обучении с учителем
Система аудита счета в MQL5 (Часть 1): Проектирование пользовательского интерфейса
Анализ CSV-данных (Часть 1): Механизм экспорта CSV при многоядерной оптимизации в MQL5
Возможности Мастера MQL5, которые вам нужно знать (Часть 78): Стратегии на основе осцилляторов Gator и A/D для повышения устойчивости на рынке
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Это одна из самых увлекательных и интересных статей на сайте MQL5. :)
Браво! Твой конвертер «Пайн» тоже попался. Ты настоящий мастер!