Использование ImGui с SFML для создания инструментов для разработки игр

Habrahabr 2

Привет, Хабр! Данная статья — вольный перевод моей статьи на русский с некоторыми небольшими изменениями и улучшениями. Хотелось бы показать как просто и полезно использовать ImGui с SFML. Приступим.

Введение

При разработке игр очень важно иметь хорошие инструменты для создания контента (редактор уровней, ресурсов и пр.), а также для дебаггинга. Наличие данных вещей повышает производительность и креативность. Гораздо проще отлавливаются баги и исправляются уже найденные: легко выводить значение различных переменных, а также создавать виджеты для их изменения, чтобы посмотреть, что происходит с игрой при определённых их значениях. Виджеты для изменения переменных также очень полезны для полировки геймплея. Например, можно легко изменять скорость передвижения персонажей, скорость перезарядки оружия и пр.

Вот какие инструменты я создал с помощью ImGui для своей игры:

Редактор уровней

Консоль Lua

Редактор анимаций

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

ImGui и концепция immediate GUI

Immediate mode GUI немного отличается от классической методики программирования интерфейсов, которая называется retained mode GUI. ImGui виджеты создаются и рисуются в каждом кадре игрового цикла. Сами виджеты не хранят внутри себя своё состояние, либо хранят абсолютно минимальный необходимый минимум, который обычно скрыт от программиста.

В отличие от того же Qt, где для создания кнопки нужно создавать объект QPushButton, а затем связывать с ней какую-нибудь функцию-callback, вызываемую при нажатии, в ImGui всё делается гораздо проще. В коде достаточно написать:

if (ImGui::Button("Some Button")) {
    ... // код, вызываемый при нажатии кнопки
}

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

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

Советую посмотреть вот это видео Кейси Муратори про ImGui, если вы хотите узнать чуть больше о данной методике.

Итак, каковы же достоинства ImGui?

  • MIT-лицензия
  • Очень быстрая и занимает мало памяти
  • Постоянно обновляется и расширяется
  • Почти не производит динамическую аллокацию/деалокаццию (и это можно контролировать, устанавливая каким образом ImGui будет получать необходимую память)
  • Очень портабельна: есть множество биндингов для различных библиотек и платформ
  • Очень легко расширяется путём создания новых виджетов на основе существующих, либо написанных с нуля.

Настройка

Итак, начнём

  1. Создайте простую программу на SFML, которая показывает пустое окно. Если вы раньше этим не занимались, то можете воспользоваться туториалом.
  2. Скачайте ImGui.
  3. Скачайте ImGui SFML биндинг и положите его в папку, в которую скачали ImGui. Важно: добавьте содержимое imconfig-SFML.h в imconfig.h
  4. Добавьте папку ImGui в include директории вашего проекта
  5. Добавьте следующие файлы в билд вашего проекта:

    • imgui.cpp
    • imgui_draw.cpp
    • imgui-SFML.cpp
    • imgui_demo.cpp
  6. Если вы будете получать ошибки линковки, то залинкуйте OpenGL к своему проекту.

Вот небольшой пример кода, который создаёт окошко ImGui и позволяет менять цвет заднего фона и заголовок окна. Объяснения того, что происходит, будут далее.

#include "imgui.h"
#include "imgui-sfml.h"

#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/System/Clock.hpp>
#include <SFML/Window/Event.hpp>

int main()
{
    sf::RenderWindow window(sf::VideoMode(640, 480), "");
    window.setVerticalSyncEnabled(true);
    ImGui::SFML::Init(window);

    sf::Color bgColor;
    float color[3] = { 0.f, 0.f, 0.f };

    // здесь мы будем использовать массив char. Чтобы использовать
    // std::string нужно сделать действия, описанные во второй части
    char windowTitle[255] = "ImGui + SFML = <3";
    window.setTitle(windowTitle);

    sf::Clock deltaClock;
    while (window.isOpen()) {
        sf::Event event;
        while (window.pollEvent(event)) {
            ImGui::SFML::ProcessEvent(event);

            if (event.type == sf::Event::Closed) {
                window.close();
            }
        }

        ImGui::SFML::Update(window, deltaClock.restart());

        ImGui::Begin("Sample window"); // создаём окно

        // Инструмент выбора цвета
        if (ImGui::ColorEdit3("Background color", color)) {
            // код вызывается при изменении значения, поэтому всё
            // обновляется автоматически
            bgColor.r = static_cast<sf::Uint8>(color[0] * 255.f);
            bgColor.g = static_cast<sf::Uint8>(color[1] * 255.f);
            bgColor.b = static_cast<sf::Uint8>(color[2] * 255.f);
        }

        ImGui::InputText("Window title", windowTitle, 255);

        if (ImGui::Button("Update window title")) {
            // этот код выполняется, когда юзер жмёт на кнопку
            // здесь можно было бы написать 
            // if(ImGui::InputText(...))
            window.setTitle(windowTitle);
        }
        ImGui::End(); // end window

        window.clear(bgColor); // заполняем окно заданным цветом
        ImGui::SFML::Render(target);
        window.display();
    }

    ImGui::SFML::Shutdown();
}

Вы должны увидеть что-то вроде этого:

Попробуйте изменить что-нибудь. Если кликнуть два раза на одно из полей RGB, то можно ввести соответствующее значение. Если одно из полей потянуть, то можно плавно изменять текущее введённое значение. Поле ввода позволяет изменить заголовок окна после нажатия на кнопку.

Отлично, теперь разберёмся как всё работает.

ImGui инициализируется вызовом ImGui::SFML::Init, при вызове в функцию передаётся ссылка на окно sf::RenderWindow. В этот момент также создаётся стандартный шрифт, который будет использоваться в дальнейшем. (см. раздел Fonts how-to в описании imgui-sfml, чтобы увидеть как использовать другие шрифты).

При выходе из программы важно вызывать функцию ImGui::SFML::Shutdown, которая освобождает ресурсы, которые использует ImGui.

В игровом цикле ImGui имеет две фазы: обновление и рендеринг.

Обновление состоит из обработки событий, обновления состояния ImGui и обновления/создания виджетов. Обработка событий происходит через вызов ImGui::SFML::ProcessEvent. ImGui обрабатывает события клавиатуры, мыши, изменения фокуса и размера окна. Обновление состояния ImGui производится в ImGui::SFML::Update, в неё передаётся delta time (время между двумя обновлениями), который ImGui использует для обновления состояния виджетов (например, для анимации). Также в данной функции вызывается ImGui::NewFrame, после вызова которой уже можно создавать новые виджеты.

Рендеринг ImGui осуществляется вызовом ImGui::SFML::Render. Очень важно создавать/обновлять виджеты между вызовами ImGui::SFML::Update и ImGui::SFML::Render, иначе ImGui будет ругаться на нарушение состояния.

Если вы рендерите реже, чем обновляете ввод и игру, то в конце каждой итерации вашего update необходимо также вызывать ImGui::EndFrame:

while (gameIsRunning) {
    while (updateIsNeeded()) {
        updateGame(dt);
        ImGui::SFML::Update(window, dt);
        ImGui::EndFrame();
    }
    renderGame();
}

Виджеты создаются путём вызова соответствующих функций (например, ImGui::InputInt или ImGui::Button). Если вызвать ImGui::ShowTestWindow, то можно увидеть много примеров использования ImGui, весь код можно найти в imgui_demo.cpp.

Полезные перегрузки функций для SFML

Для SFML в биндинге были созданы некоторые перегрузки функций, например в ImGui::Image и ImGui::ImageButton можно кидать sf::Sprite и sf::Texture, также можно легко рисовать линии и прямоугольники вызовом DrawLine, DrawRect и DrawRectFilled.

Заключение

Вот такая библиотека: проста в использовании и настройке, и очень полезна для создания инструментов и дебагинга. Приятного использования!

P.S. Если возникнет интерес, то могу перевести и вторую часть туториала, которая рассказывает про использование ImGui с современным C++ и стандартной библиотекой. Советую обратить на статью внимание тем, кто решит использовать (или уже использует) ImGui: она показывает как просто решать основные проблемы ImGui и делать всё проще и безопаснее, чем это делается в C++03.