Recall Engine. Часть 2.

Recall Engine. Часть 2.

AmeliePick


Вторая часть из цикла статей про Recall Engine, в которой будут рассмотрены два модуля: ввод-вывод и графический интерфейс.

Глава 1. Ввод-вывод.

Recall способен работать в 3 + 1 режимах обработки ввода:

1)     Стандартный ввод Windows.

2)     RawInput.

3)     DirectInput.

4)     XInput(только джойстики).

Переключаться между режимами можно прямо во время выполнения.

Клавиатура/Мышь.

Получать информацию о вводе с этих устройств можно двумя способами: запрашивать или подписаться на события. Менеджер ввода содержит методы, которые позволяют в ручном режиме проверять состояния устройств. Как «сигнальные», например, нажата или отпущена кнопка, так и получение конкретных данных, например, координаты курсора. Также Recall реализовывает события подключения/отключения устройств, множественного нажатия и многие другие. Событие множественного нажатия позволяет не только обрабатывать двойной-тройной клик мышки, но вплоть до 5 нажатий мыши и клавиатуры. Что, например, позволяет на одну и ту же кнопку на клавиатуре, назначить от одного до пяти разных действий. Хоть сделать 4-ой или 5-ой клик физически сложно, это так или иначе позволяет использовать макросы и всё равно расширяет возможности настройки управления.


Джойстик.

Как было упомянуто ранее, Recall способен работать с DirectInput и XInput джойстиками.  Последние же сильно ограничены в эффектах обратной связи(вибрации), что не позволяет создавать паттерны вибрации. Однако Recall старается исправить это и имеет собственную механику задания паттернов вибрации для джойстиков. При желании, у обоих типов джойстиков можно настроить эффект, например, пульсации. Механика не совершенна, и не гарантируется работа на абсолютно каждом джойстике, особенно, если джойстик не является DI или XI и эмулируется. Однако, большинство DI и XI устройств будут совместимы. Понятно, что к функционалу джоев отсносится и всё минимальное необходимое, например установка диапазонов осей, мёртвых зон, калибровка и т.д.


HID. USB. COM.

Recall также предоставляет возможность напрямую работать с HID устройствами, которыми являются как раз клавиатуры, мыши и контроллеры. А также, напрямую с USB и COM портами.


Терминал

Небольшой механизм, позволяющий перехватывать ввод с клавиатуры, мыши, файла и сети, в любой приёмник. Приёмник определяется самим разработчиком. Это может быть, как и файл, так и элемент GUI формы. Как пример, с помощью этого терминала можно в пару строк написать простейший кейлоггер.


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


FAQ

Если подключить джойстик не DirectInput и XInput(например DualShock), будет ли движок с ним работать? Будет, как с DirectInput устройством. Однако, при этом не гарантируется правильность вводимых данных. Например, диапазоны стиков могут быть неверными. Самым простым, как-то это исправить, способом является использование эмулятора DI или XI джойстиков. Но и в данном случае вовсе нет гарантий, что всё будет работать корректно, плюс банальные аппаратные проблемы совместимости. Как было упомянуто выше, такой эмулированный контроллер, скорее всего, будет иметь некорректную обратную связь. Сказывается обилие интерфейсов ввода для контроллеров, так и то, что движку не имеет смысла поддерживать каждый контроллер, при условии, что он всё равно хоть как-то, но будет работать. В конце концов, в случае необходимости, разработчик может написать собственный обработчик ввода для своего контроллера, используя возможности движка Recall или WDK как крайний случай.


Глава 2. Графический интерфейс.

Движок предоставляет доступ к элементам интерфейса Windows. Начиная от окна, заканчивая различными элементами управления. По сути, Recall предоставляет довольно простой и понятный интерфейс для работы с UI и является аналогом Windows Forms.

REWindow mainWindow("OwOWindow", IDC_ARROW, CS_HREDRAW | CS_GLOBALCLASS, WHITE_BRUSH, "OwO", WS_EX_WINDOWEDGE, WS_SYSMENU | WS_CAPTION | WS_MINIMIZEBOX | WS_SIZEBOX, 0, 0, 1000, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), nullptr, NULL, NULL);

mainWindow.Show();
mainWindow.Processing();

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

Ради упрощения использования данной обёртки, некоторые параметры остаются в неизменном виде, так, например, стили не переопределяются движком и их описание можно найти на MSDN. Тоже самое касается и некоторых других параметров у разных объектов. Если разработчик уже знаком с Win32 UI, то он будет понимать код движка. Также и наоборот, если программист, не зная Win32 UI, работает с движком, ему будет достаточно поверхностно ознакомится с документацией на том же MSDN, чтобы понимать некоторые особенности, например, какой стиль каким образом влияет на элемент.


FAQ

Можно ли изменить стиль элементов? На момент написания статьи нет, но планируется в последующих обновлениях.

Существует ли система разметки для создания граф. интерфейса, например как WPF? Частично разработана, но так как необходимости в ней особо нет на данный момент, то доработана будет со следующими обновлениями.

Глава 3. Кодим, собираем, запускаем.

Рассмотрим пример приложения, который создаёт окно и надпись, и по нажатию кнопки клавиатуры/мыши/джойстика меняет цвета фона и текста.

В качестве джойстика используется Thrustmaster Firestorm Digital 3 Gamepad.

Который поддерживает только DirectInput. Но в примере мы обработаем его ещё и как XInput. К сожалению, у этого джойстика нет вибрации, но код для работы с ней всё равно будет.

В качестве мыши - Bloody V8M.

У которой есть 2й и 3й клики и дополнительные кнопки. Мало кто обращает внимание, но в некоторых играх на одной и той же мышке, две этих кнопки бывают поменяны местами. Так как движок работает с Windows 10, а система определяет первую кнопку от колеса как первую дополнительную, а другую как вторую.

Приступим к коду.

int main()
{
    KernelBase kernel;
    IOAPI IOApi;

    REWindow mainWindow("OwOWindow", IDC_ARROW, CS_HREDRAW |
    CS_GLOBALCLASS, WHITE_BRUSH, "OwO", WS_EX_WINDOWEDGE, WS_SYSMENU |
    WS_CAPTION | WS_MINIMIZEBOX | WS_SIZEBOX, 0, 0, 1000,
    GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), 
    nullptr, NULL, NULL);

    REMenu owoMenu;
    owoMenu.ChangeTitle("OwO Menu");

    owoMenu.AddItem(REMenuItem
    (MFT_STRING, 0, NULL, NULL, NULL, "OwO", NULL));
    owoMenu.AddItem(REMenuItem
    (MFT_SEPARATOR, 0, NULL, NULL, NULL, nullptr, NULL));
    owoMenu.AddItem(REMenuItem
    (MFT_STRING, 0, NULL, NULL, NULL, "oWo", NULL));
    mainMenu.AddSubMenu(owoMenu);
    mainMenu.AddItem(REMenuItem
    (MFT_STRING, 0, NULL, NULL, NULL, "UwU", NULL));
    mainWindow.SetMenu(&mainMenu);


    RELabel text("OwO is the best", SS_CENTER, 0, 30, 1000, 450);
    text.SetTextColor(89, 76, 216);
    text.SetBackColor(0, 184, 159);
    text.SetFont("Montserrat");
    REButton bt1("OK", 0, 450, 150, 100, 100);
    bt1.SetFont("Montserrat");
    bt1.AddOnClickHandler(BtnCallback);

    text._control = mainWindow.AddControl(&text)._control;    
    bt1._control  = mainWindow.AddControl(&bt1)._control;

    AddKeyEventHandler(LambdaWrapper<void, uint32>([&text](int32 key)
    {
        int r, g, b;

        switch (key)
        {
            case 0x44:
            case 0x20:
                r = rand() % 254, g = rand() % 255, b = rand() % 255;
                break;
            case VK_XBUTTON1:
                r = 255, g = 0, b = 255;
                break;
            default:
                return;
            
            text.SetTextColor(r, g, b);
            text.SetBackColor(255, g, b);
         }
    }), KeyState::KEY_UP);

    mainWindow.Show();
    mainWindow.Processing();
}

Сперва необходимо инициализировать модули: основной и ввода-вывода. Делается это для корректного запуска всех необходимых систем, а также их корректного удаления и освобождения ресурсов при закрытии приложения.

Затем создаётся окно, о котором было рассказано несколько ранее. Чтобы оно не было пустым, создадим несколько элементов: два меню и элементы внутри них, надпись и кнопку. Для кнопки добавим функцию BtnCallback, которая будет при клике на эту кнопку, выводить в консоль текст.

Далее, два элемента: надпись и кнопку, мы добавляем к окно. После чего, контроль за ресурсами этих двух объектов возлагается на окно и по факту, те ссылки, которые у нас остаются, становятся некорректными. Но, окно всё равно возвращает интерфейс объекта, чтобы с ним можно было работать во время выполнения, поэтому мы обновляем адреса двух объектов и далее уже работаем с ними.

Имеем такое окно.

Создадим обработчик события отпускания кнопки. Создаётся лямбда, которая захватывает объект надписи, чтобы изменять её цвет, а принимает целочисленный параметр, который является кодом кнопки. В режимах ввода стандартный и RawInput, код 0x44 соответствует кнопке D на клавиатуре, а в DirectInput код этой кнопки будет уже 0x20. В обработчике мы проверяем, чтобы была отпущена кнопка D и затем меняем цвет надписи на случайный. Таким образом, что цвет надписи будет меняться по "клику". Для полноценной обработки клика, можно было использовать и множественное нажатие, где проверялось бы одиночное нажатие. Но для этого пришлось бы немного заморочитсья и отдельным объектом создать лямбду и подписываться на два события - для клавиатуры и мыши. В данном примере оно того не стоит 🙂 и потому подписываемся просто на одно событие.

Добавим теперь обработку ввода с джойстика, которая также будет менять цвет надписи. Поместим следующий код до вызова функции Show.

List<EngineAllocator> lst;
GetJoysticksList((TList<Joystick>*)&lst);

JoyHandle joy =
DirectInputJoystick::AttachToJoy(lst.Index<Joystick>(0));

DIEFFECT effect;
effect.dwSize = sizeof(effect);
REHandle effectHandle = nullptr;
DirectInputJoystick::PlayFeedback(joy, GUID_ConstantForce, effect, &effectHandle);

Как было упомянуто в первой статье, движок предоставляет типизированный список, который принимает функция GetJoysticksList. Но в примере у нас используется простой нетипизированный, просто потому что. Функция заполняет список структурой, которая содержит информацию о подключённых к компьютеру игровых устройствах. В моём случае подключён только один джойстик, а потому список будет иметь только один элемент, который и используется функцией AttachToJoy, осуществляющей «захват» устройства. Затем создаётся и проигрывается эффект постоянной вибрации.

Отличительной особенностью некоторых джойстиков, является возможность получать от них события о вводе. К сожалению, на моём такого нет, это значит, что проверять ввод нужно вручную. Интерфейс окна позволяет добавлять 2D и 3D рендереры, а также собственный код или функции, которые должны быть обработаны на каждой итерации цикла обновления окна.

Для нашего примера хватило бы и таймера, чтобы опрашивать джойстик раз в 1 мс. В приложениях же «реального времени» лучше использовать для этого само окно. И хотя наше простое приложение не нуждается в больших мощностях ЦП, мы всё равно напишем код и для этого случая, а также для таймера.

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

После предыдущего кода напишем следующий:

DIJOYSTATE joyData;
mainWindow.AddHandler(DynamicLambda([&text, &joy, &joyData]()
{
    DirectInputJoystick::GetInput(joy, &joyData);
    if (joyData.lX < 0)
        text.SetTextColor(255, 255, 255);
}));

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

Далее извлекаются данные джойстика и если его ось X отклонена влево(в данном примере, диапазон значений этой оси от -1024 по 1024, где 0 это центр), то меняем цвет надписи на белый.

Код для таймера же ничем, особо, не отличается от кода обработчика:

Timer t1(1, DynamicLambda([&text, &joy, &joyData]()
{
    DirectInputJoystick::GetInput(joy, &joyData);
    if (joyData.lX < 0)
        text.SetTextColor(255, 255, 255);
}), true, false);

Указывается интервал в 1 мс, сама лямбда, автостарт и не уничтожение таймера после первого тика.


Обработаем теперь джойстик как XInput. Для этого будем использовать эмулятор x360ce. Следующий код имеет общую логику с предыдущим, меняются по сути только используемые методы:

JoyHandle joy = XInputJoystick::AttachToJoy(index);

XIJoyEffect effect;
effect.duration = 10000;
effect.activePeriod = 1000;
effect.inactivePeriod = 2000;
effect.motor = XIJoyEffect::Motor::BOTH;
effect.power = 100;

XInputJoystick::PlayFeedback(joy, effect);

Функция AttachToJoy принимает номер порта, на котором находится XInput джой. XInput поддерживает только 4 одновременно работающих джоев в системе, поэтому это диапазон [0; 3;].

Получить индексы портов можно следующим образом:

uint8 indexes[4] = { 0 };
GetXInputJoysticksIndexes(indexes);

Функция заполняет массив единицами, соответственно индксам, используемых портов. Тоесть, если джой находится на 0 порте, то элемент в массиве по индексу 0 будет равен единице. Возвращает же эта функция количество подключённых джойстиков.

После того, как «захватили» устройство, создаём эффект вибрации, который будет длится 10 секунд, работать вибриция будет в течении секунды, а пауза 2 секунды. Так же указывается какой вибромотор будет задействован и с какой силой он будет вибрировать. В зависимости от джоя, у него могут быть как и два одинаковых мотора(как на многих DI), так и разные. По итогу один и тот же эффект будет по-разному ощущаться на разных типах джойстиков, особенно если это не геймпад, а например, руль. Подробнее узнать об эффектах вибрации можно на MSDN.

График вибрации.


Код обработчика для окна(как и таймера) тоже не имеет глобальных изменений:

XINPUT_STATE joyData;
mainWindow.AddHandler(DynamicLambda([&text, &joy, &joyData]()
{
    XInputJoystick::GetInput(joy, &joyData);
    if (joyData.Gamepad.sThumbLX < 0)
        text.SetTextColor(255, 255, 255);
}));

И в итоге получаем следующее приложение:

Гифка пережата для телеграфа, лучше её открыть в новом окне браузера.


Заключение

В статье были рассмотрены основные возможности модулей ввода-вывода и графического интерфейса. Есть ещё много разного фунционала, который попросту не входит в статью по размерам, например, не продемонстрирована "горячая" смена режимов обработки ввода, расширенные возможности джойстиков(события, получения данных) и т.д. Да и к тому же, суть этой статьи продемонстрировать возможности ПО, которое является закрытым ( > ‿ < )

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


Дорожная карта и описание проекта находятся тут.

Конец.







Report Page