Привет, Хабр!


Данная статья — вольный перевод моей статьи на русский с некоторыми небольшими изменениями и улучшениями. Хотелось бы показать как просто и полезно использовать 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(window);
        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.

Комментарии (23)


  1. AllexIn
    13.08.2017 11:02

    Как много же SFML хочет зависимостей. Это просто дичь.
    Пробовал завести на SteamOS проект с SFML — сдался.
    В итоге сделал на SDL, чего всем и желаю.


    1. maaGames
      13.08.2017 11:59
      -5

      Как вы вообще можете сравнивать С++ и С? Или ООП и портировать все зависимые библиотеки, либо С и никакого ООП (или сами пишите вроперы, которые в SFML уже есть).


      1. encyclopedist
        13.08.2017 13:50
        +1

        Для SDL тоже есть C++ обёртки, например libSDL2pp


      1. AllexIn
        13.08.2017 18:22
        +1

        Это API.
        Какие у них интерфейсы наружу торчат — совершенно фиолетово.
        И речь не о том, какие зависимости хочет ООП, речь о том, какие зависимости хочет приложение после компиляции.


  1. SteelRat1
    13.08.2017 11:54
    +2

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

    Вообще-то Qt работает с сигналами-слотами и реакция на то или иное действие с той же кнопкой реализуется так же. Достаточно кликнуть по объекту кнопка правой кнопкой мыши в Qt дизайнере, пункт «перейти к слоту» и из появившегося списка сигналов выбрать нужный. появится уже готовая форма.


    1. eliasdaler Автор
      13.08.2017 12:47
      -1

      Хотел несколько упростить для тех, кто не знаком с Qt. По сути сигналы и слоты — это практически то же самое, что и callback'и. Слот — это callback. В коде связывание кнопки и слота осуществляется путём вызова QObject::connect, который затем генерирует код, который при нажатии на кнопку вызывает данную функцию. В дизайнере происходит почти то же самое, просто код генерируется из xml-файла.

      Здесь же кнопка создаётся путём вызова ImGui::Button внутри условия if, а код внутри по сути является кодом callback'а. Всё же немного проще, чем создание QPushButton, функции и дальнейшего вызова connect.


      1. SteelRat1
        13.08.2017 13:18

        Я не гуру в Qt, и потому решил все-таки еще раз погуглить. Понятно что немного не по теме, но для восстановления справедливости )) — слоты это не callback функции. Они потому и появились как альтернатива callback функциям. Ссылок в инете много по этому поводу, поэтому что-то конкретное предлагать не буду в качестве доказательства. А в целом, спасибо, материал интересный.


    1. shybovycha
      13.08.2017 12:54
      +1

      Подозреваю, что основное отличие заключается не в процессе создания объекта на этапе создания проекта, а именно в процессе рендеринга этого самого контрола — в Qt все элементы создаются в момент создания окна (родительского виджета, если угодно), в то время как в ImGUI, насколько я понял, каждый контрол/виджет создается заново каждый кадр.


      Но это ровно настолько, насколько я понял из статьи и никоим образом не отражает истинное положение вещей.


      Если все же я прав, то это скорее камень в огород ImGUI:


      • если не кешировать созданные контролы/функции-обработчики событий, то чем сложнее GUI, тем более тормознутым будет рендеринг

      • множество вещей менее важных для большинства домашних редакторов игровых уровней, но тем не менее, весьма полезных в некоторых случаях, не получится реализовать на показанном API:

      if (ImGui::Button("Update window title")) {
          // этот код выполняется, когда юзер жмёт на кнопку
      }
      
      // а вот теперь попробуйте реализовать разные обработчики для OnLeftMouseClick и OnRightMouseClick =)


      1. Ryppka
        13.08.2017 13:45

        На самом деле все зависит от конкретного приложения. Если у вас сложная иерархия взаимосвязанных виджитов, которые еще и связаны с какими-то фоновыми источниками данных для этих виджитов, все в интерфейсе происходит в совершенно произвольные моменты времени, то подход кьюта и других «серьезных» фреймворков должен иметь преимущество: вы создаете иерархию оболочек к реальным объектам оконного интерфейса и систематически налаживаете связи между ними.
        Ну а если вся логика интерфейса легко укладывается в одну итерацию главного цикла отрисовки, и, главное, этот явный цикл у вас и так уже есть, то подход ImGui может оказаться намного проще: не нужно делать кучу бойлерплейта для каждой мелочи.
        В любом случае реальная «тяжелая» работа будет где-то там, в window manager'е, windows gui, cocoo или где там...)


        1. shybovycha
          13.08.2017 14:03
          +1

          В том-то и дело, что поскольку интерфейс объявляется и обрабатывается внутри контекста OpenGL, window manager, windows gui, cocoo и что там еще не будет задействован для этого самого GUI. И задача "рендерить заново весь кадр или взять его из кеша и нарисовать курсор поверху" перекладывается либо на разработчика, либо на фреймворк. И теперь только этот самый фреймворк решает, сколько ему понадобится времени, чтобы нарисовать все те же виджеты.


          1. Ryppka
            13.08.2017 14:10
            -1

            Да, но кьют, а особенно система мета-объектов, мне кажутся чудовищными, и если простота задачи позволяет их избежать — это же прекрасно! Не думаю, что тут будут какие-то проблемы с производительностью до достижения критической сложности пропета — кьют достаточно тяжеловесен сам по себе. Начнутся тормоза или придется мудрить в главном цикле, чтобы правильно обрабатывать взаимозависимости — значит пора переходить на более тяжелую артиллерию, я так думаю (С) Винни-Пух.


      1. eliasdaler Автор
        13.08.2017 13:46
        +1

        Да, они создаются заново каждый кадр, но данных содержат мало, так что это всё происходит быстро. Мало того, элементы, которые в данный момент скрыты, не обрабатываются (например когда свёрнуто окно или дерево в TreeView). До их создания просто не доходит код, например:


        if (ImGui::TreeNode("SomeTree")) { // TreeNode возращает true когда дерево развёрнуто
            if (ImGui::TreeNode("Node")) { // код вот здесь не выполняется, если дерево свёрнуто
                ...
            }
        }

        И ImGui позволяет гораздо больше, чем кажется, некоторые делают вот такое:


        И да, некоторые вещи делаются не так просто. Например для реализации OnLeftMouseClick/OnRightMouseClick видимо нужно изменить код ImGui::Button, либо сделать свой виджет, который ведёт себя как кнопка. Но к счастью бОльшая часть виджетов и необходимых поведений присутствует.


        1. shybovycha
          13.08.2017 14:09
          +2

          Картинка крутая, но только в email-нотификации от хабра удалось кой-чего разглядеть (жаль, некликабельна): вот вы говорите нарисовать это все просто. Но давайте рассмотрим конкретно случай изображенный на рисунке выше:


          1) scroll view-панелька слева, куча виджетов
          2) (внутри все той же панельки) listbox с кучей элементов-строк


          Проблема, озвученная мной, звучит примерно так (в данном случае): будут ли рисоваться заново каждый кадр все элементы, которые не попадают во вьюпорт (все, что выше "particle emitter" и все элементы листбокса, ниже "models/editor/camera_icon_3d.msh") или же каким-то образом предусмотрен этот случай и элементы не будут рендериться?


          1. eliasdaler Автор
            13.08.2017 14:24

            Вот ссылка на изображение


            Элементы, которые не попадают во вьюпорт/перекрываются другими виджетами рендериться не будут, ImGui делает проверки, чтобы не добавлять в DrawList невидимые элементы.


            Вот тут есть больше скринов, но пока что Github почему-то не даёт смотреть всем этот тред, автор репозитория сказал, что скоро пофиксит это. :)


        1. OrionGames
          14.08.2017 13:37
          +1

          Кому интересно, это LumixEngine


  1. ZigoRiloo96
    14.08.2017 23:22

    Сам пользуюсь ImGUI + SFML, и скажу что доволен как слон))


    1. eliasdaler Автор
      14.08.2017 23:23
      -1

      Рад слышать. :D